initial commit: add Mail-in-a-Box Go client library and CLI with DNS record management
This commit is contained in:
@@ -0,0 +1,42 @@
|
|||||||
|
# mailinabox-go
|
||||||
|
|
||||||
|
Go client library and CLI for the [Mail-in-a-Box](https://mailinabox.email/) admin DNS API.
|
||||||
|
|
||||||
|
## Library
|
||||||
|
|
||||||
|
```go
|
||||||
|
import miab "gitea.koszewscy.waw.pl/koszewscy/mailinabox-go"
|
||||||
|
|
||||||
|
c := miab.NewClient("box.example.com", "admin@example.com", "password")
|
||||||
|
|
||||||
|
c.SetRecord("foo.example.com", "TXT", "v=spf1 ~all")
|
||||||
|
c.AddRecord("foo.example.com", "A", "1.2.3.4")
|
||||||
|
c.DeleteRecord("foo.example.com", "A", "1.2.3.4")
|
||||||
|
|
||||||
|
records, _ := c.ListRecords("TXT")
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
Install:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go install gitea.koszewscy.waw.pl/koszewscy/mailinabox-go/cmd/miab@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Credentials via environment variables:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export MIAB_HOST=box.example.com
|
||||||
|
export MIAB_USERNAME=admin@example.com
|
||||||
|
export MIAB_PASSWORD=password
|
||||||
|
```
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
miab list [--type TXT]
|
||||||
|
miab set --name foo.example.com --type TXT --value "hello"
|
||||||
|
miab add --name foo.example.com --type A --value 1.2.3.4
|
||||||
|
miab delete --name foo.example.com --type TXT [--value "hello"]
|
||||||
|
```
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
miab "gitea.koszewscy.waw.pl/koszewscy/mailinabox-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
fmt.Fprintf(os.Stderr, `Usage: miab <command> [options]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
list List custom DNS records
|
||||||
|
set Set (replace) a DNS record
|
||||||
|
delete Delete a DNS record
|
||||||
|
add Add a DNS record
|
||||||
|
|
||||||
|
Credentials are read from environment variables:
|
||||||
|
MIAB_HOST Mail-in-a-Box hostname (e.g. box.example.com)
|
||||||
|
MIAB_USERNAME Admin email address
|
||||||
|
MIAB_PASSWORD Admin password
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustClient() *miab.Client {
|
||||||
|
host := os.Getenv("MIAB_HOST")
|
||||||
|
username := os.Getenv("MIAB_USERNAME")
|
||||||
|
password := os.Getenv("MIAB_PASSWORD")
|
||||||
|
|
||||||
|
var missing []string
|
||||||
|
if host == "" {
|
||||||
|
missing = append(missing, "MIAB_HOST")
|
||||||
|
}
|
||||||
|
if username == "" {
|
||||||
|
missing = append(missing, "MIAB_USERNAME")
|
||||||
|
}
|
||||||
|
if password == "" {
|
||||||
|
missing = append(missing, "MIAB_PASSWORD")
|
||||||
|
}
|
||||||
|
if len(missing) > 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "miab: missing required environment variables: %v\n", missing)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return miab.NewClient(host, username, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Usage = usage
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if flag.NArg() < 1 {
|
||||||
|
usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := flag.Arg(0)
|
||||||
|
args := flag.Args()[1:]
|
||||||
|
|
||||||
|
switch cmd {
|
||||||
|
case "list":
|
||||||
|
runList(args)
|
||||||
|
case "set":
|
||||||
|
runSet(args)
|
||||||
|
case "delete":
|
||||||
|
runDelete(args)
|
||||||
|
case "add":
|
||||||
|
runAdd(args)
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "miab: unknown command %q\n", cmd)
|
||||||
|
usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runList(args []string) {
|
||||||
|
fs := flag.NewFlagSet("list", flag.ExitOnError)
|
||||||
|
recordType := fs.String("type", "", "Filter by record type (e.g. A, TXT, MX)")
|
||||||
|
fs.Parse(args)
|
||||||
|
|
||||||
|
client := mustClient()
|
||||||
|
records, err := client.ListRecords(*recordType)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "miab: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *recordType != "" {
|
||||||
|
fmt.Printf("Custom %s records:\n", *recordType)
|
||||||
|
} else {
|
||||||
|
fmt.Println("Custom DNS records:")
|
||||||
|
}
|
||||||
|
for _, r := range records {
|
||||||
|
fmt.Printf(" - %s (%s): %s\n", r.Name, r.Type, r.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSet(args []string) {
|
||||||
|
fs := flag.NewFlagSet("set", flag.ExitOnError)
|
||||||
|
name := fs.String("name", "", "Domain name")
|
||||||
|
recordType := fs.String("type", "", "Record type (e.g. A, TXT)")
|
||||||
|
value := fs.String("value", "", "Record value")
|
||||||
|
fs.Parse(args)
|
||||||
|
|
||||||
|
if *name == "" || *recordType == "" || *value == "" {
|
||||||
|
fmt.Fprintln(os.Stderr, "miab set: --name, --type and --value are required")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := mustClient()
|
||||||
|
if err := client.SetRecord(*name, *recordType, *value); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "miab: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDelete(args []string) {
|
||||||
|
fs := flag.NewFlagSet("delete", flag.ExitOnError)
|
||||||
|
name := fs.String("name", "", "Domain name")
|
||||||
|
recordType := fs.String("type", "", "Record type (e.g. A, TXT)")
|
||||||
|
value := fs.String("value", "", "Record value (optional)")
|
||||||
|
fs.Parse(args)
|
||||||
|
|
||||||
|
if *name == "" || *recordType == "" {
|
||||||
|
fmt.Fprintln(os.Stderr, "miab delete: --name and --type are required")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := mustClient()
|
||||||
|
if err := client.DeleteRecord(*name, *recordType, *value); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "miab: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runAdd(args []string) {
|
||||||
|
fs := flag.NewFlagSet("add", flag.ExitOnError)
|
||||||
|
name := fs.String("name", "", "Domain name")
|
||||||
|
recordType := fs.String("type", "", "Record type (e.g. A, TXT)")
|
||||||
|
value := fs.String("value", "", "Record value")
|
||||||
|
fs.Parse(args)
|
||||||
|
|
||||||
|
if *name == "" || *recordType == "" || *value == "" {
|
||||||
|
fmt.Fprintln(os.Stderr, "miab add: --name, --type and --value are required")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := mustClient()
|
||||||
|
if err := client.AddRecord(*name, *recordType, *value); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "miab: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package miab
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client holds connection configuration for the Mail-in-a-Box admin DNS API.
|
||||||
|
type Client struct {
|
||||||
|
host string
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNSRecord represents a single custom DNS record returned by the MIAB API.
|
||||||
|
type DNSRecord struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient constructs a Client. host should be the bare hostname (e.g. "box.example.com").
|
||||||
|
func NewClient(host, username, password string) *Client {
|
||||||
|
return &Client{
|
||||||
|
host: host,
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRecord replaces all existing records of the given type for name with value (HTTP PUT).
|
||||||
|
func (c *Client) SetRecord(name, recordType, value string) error {
|
||||||
|
_, err := c.do(http.MethodPut, c.buildURL(name, recordType), value)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRecord removes the record matching name, recordType and value (HTTP DELETE).
|
||||||
|
// Pass an empty value to remove all records of that type for the name.
|
||||||
|
func (c *Client) DeleteRecord(name, recordType, value string) error {
|
||||||
|
_, err := c.do(http.MethodDelete, c.buildURL(name, recordType), value)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRecord adds a record without replacing existing ones (HTTP POST).
|
||||||
|
func (c *Client) AddRecord(name, recordType, value string) error {
|
||||||
|
_, err := c.do(http.MethodPost, c.buildURL(name, recordType), value)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListRecords returns all custom DNS records. If recordType is non-empty, only
|
||||||
|
// records of that type are returned.
|
||||||
|
func (c *Client) ListRecords(recordType string) ([]DNSRecord, error) {
|
||||||
|
body, err := c.do(http.MethodGet, fmt.Sprintf("https://%s/admin/dns/custom", c.host), "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MIAB returns an array of objects with fields: qname, rtype, value
|
||||||
|
var raw []struct {
|
||||||
|
QName string `json:"qname"`
|
||||||
|
RType string `json:"rtype"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(body), &raw); err != nil {
|
||||||
|
return nil, fmt.Errorf("miab: failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var records []DNSRecord
|
||||||
|
upper := strings.ToUpper(recordType)
|
||||||
|
for _, r := range raw {
|
||||||
|
if recordType == "" || r.RType == upper {
|
||||||
|
records = append(records, DNSRecord{
|
||||||
|
Name: r.QName,
|
||||||
|
Type: r.RType,
|
||||||
|
Value: r.Value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildURL mirrors miab.py's get_miab_url: omits the record type segment for "A" records.
|
||||||
|
func (c *Client) buildURL(name, recordType string) string {
|
||||||
|
if strings.ToUpper(recordType) == "A" {
|
||||||
|
return fmt.Sprintf("https://%s/admin/dns/custom/%s", c.host, name)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("https://%s/admin/dns/custom/%s/%s", c.host, name, strings.ToUpper(recordType))
|
||||||
|
}
|
||||||
|
|
||||||
|
// do executes an HTTP request with Basic Auth and returns the response body as a string.
|
||||||
|
func (c *Client) do(method, url, body string) (string, error) {
|
||||||
|
var bodyReader io.Reader
|
||||||
|
if body != "" {
|
||||||
|
bodyReader = strings.NewReader(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, url, bodyReader)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("miab: failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
req.SetBasicAuth(c.username, c.password)
|
||||||
|
if body != "" {
|
||||||
|
req.Header.Set("Content-Type", "text/plain")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("miab: request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("miab: failed to read response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("miab: unexpected status %d: %s", resp.StatusCode, strings.TrimSpace(string(respBody)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(respBody), nil
|
||||||
|
}
|
||||||
+201
@@ -0,0 +1,201 @@
|
|||||||
|
package miab
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildURL(t *testing.T) {
|
||||||
|
c := NewClient("box.example.com", "user", "pass")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name, recordType, want string
|
||||||
|
}{
|
||||||
|
{"example.com", "A", "https://box.example.com/admin/dns/custom/example.com"},
|
||||||
|
{"example.com", "a", "https://box.example.com/admin/dns/custom/example.com"},
|
||||||
|
{"example.com", "TXT", "https://box.example.com/admin/dns/custom/example.com/TXT"},
|
||||||
|
{"example.com", "txt", "https://box.example.com/admin/dns/custom/example.com/TXT"},
|
||||||
|
{"example.com", "MX", "https://box.example.com/admin/dns/custom/example.com/MX"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := c.buildURL(tt.name, tt.recordType)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("buildURL(%q, %q) = %q; want %q", tt.name, tt.recordType, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetRecord(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPut {
|
||||||
|
t.Errorf("expected PUT, got %s", r.Method)
|
||||||
|
}
|
||||||
|
if r.URL.Path != "/admin/dns/custom/test.example.com/TXT" {
|
||||||
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
assertBasicAuth(t, r, "user@example.com", "secret")
|
||||||
|
body, _ := io.ReadAll(r.Body)
|
||||||
|
if string(body) != "hello" {
|
||||||
|
t.Errorf("unexpected body: %s", body)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(w, "OK")
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
c := testClient(srv)
|
||||||
|
if err := c.SetRecord("test.example.com", "TXT", "hello"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteRecord(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodDelete {
|
||||||
|
t.Errorf("expected DELETE, got %s", r.Method)
|
||||||
|
}
|
||||||
|
if r.URL.Path != "/admin/dns/custom/test.example.com/TXT" {
|
||||||
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
assertBasicAuth(t, r, "user@example.com", "secret")
|
||||||
|
body, _ := io.ReadAll(r.Body)
|
||||||
|
if string(body) != "hello" {
|
||||||
|
t.Errorf("unexpected body: %s", body)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(w, "OK")
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
c := testClient(srv)
|
||||||
|
if err := c.DeleteRecord("test.example.com", "TXT", "hello"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddRecord(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
t.Errorf("expected POST, got %s", r.Method)
|
||||||
|
}
|
||||||
|
if r.URL.Path != "/admin/dns/custom/test.example.com" {
|
||||||
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
assertBasicAuth(t, r, "user@example.com", "secret")
|
||||||
|
fmt.Fprintln(w, "OK")
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
c := testClient(srv)
|
||||||
|
if err := c.AddRecord("test.example.com", "A", "1.2.3.4"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListRecords_all(t *testing.T) {
|
||||||
|
raw := []map[string]string{
|
||||||
|
{"qname": "a.example.com", "rtype": "A", "value": "1.2.3.4"},
|
||||||
|
{"qname": "b.example.com", "rtype": "TXT", "value": "v=spf1"},
|
||||||
|
}
|
||||||
|
srv := jsonServer(t, raw)
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
c := testClient(srv)
|
||||||
|
records, err := c.ListRecords("")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(records) != 2 {
|
||||||
|
t.Fatalf("expected 2 records, got %d", len(records))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListRecords_filtered(t *testing.T) {
|
||||||
|
raw := []map[string]string{
|
||||||
|
{"qname": "a.example.com", "rtype": "A", "value": "1.2.3.4"},
|
||||||
|
{"qname": "b.example.com", "rtype": "TXT", "value": "v=spf1"},
|
||||||
|
}
|
||||||
|
srv := jsonServer(t, raw)
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
c := testClient(srv)
|
||||||
|
records, err := c.ListRecords("TXT")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(records) != 1 {
|
||||||
|
t.Fatalf("expected 1 TXT record, got %d", len(records))
|
||||||
|
}
|
||||||
|
if records[0].Name != "b.example.com" {
|
||||||
|
t.Errorf("unexpected record name: %s", records[0].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrorResponse(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
c := testClient(srv)
|
||||||
|
err := c.SetRecord("test.example.com", "TXT", "val")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "401") {
|
||||||
|
t.Errorf("expected 401 in error, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// testClient builds a Client pointed at the test server's host.
|
||||||
|
func testClient(srv *httptest.Server) *Client {
|
||||||
|
c := NewClient(strings.TrimPrefix(srv.URL, "http://"), "user@example.com", "secret")
|
||||||
|
c.httpClient = srv.Client()
|
||||||
|
// Override buildURL to use http instead of https for the test server.
|
||||||
|
// We do this by wrapping the httpClient transport (no-op) and patching
|
||||||
|
// the host so the URL function produces http:// URLs.
|
||||||
|
// Simplest approach: override the host to include the scheme directly.
|
||||||
|
// Since buildURL always prepends "https://", we use a custom transport
|
||||||
|
// that rewrites the scheme.
|
||||||
|
c.httpClient.Transport = &schemeRewriter{underlying: http.DefaultTransport, targetURL: srv.URL}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// schemeRewriter is a RoundTripper that rewrites the request URL scheme+host
|
||||||
|
// to point at the test server, allowing testClient to use the real buildURL logic.
|
||||||
|
type schemeRewriter struct {
|
||||||
|
underlying http.RoundTripper
|
||||||
|
targetURL string // e.g. "http://127.0.0.1:PORT"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *schemeRewriter) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
clone := req.Clone(req.Context())
|
||||||
|
target := strings.TrimPrefix(s.targetURL, "http://")
|
||||||
|
clone.URL.Scheme = "http"
|
||||||
|
clone.URL.Host = target
|
||||||
|
return s.underlying.RoundTrip(clone)
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonServer(t *testing.T, payload any) *httptest.Server {
|
||||||
|
t.Helper()
|
||||||
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assertBasicAuth(t, r, "user@example.com", "secret")
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(payload)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertBasicAuth(t *testing.T, r *http.Request, username, password string) {
|
||||||
|
t.Helper()
|
||||||
|
u, p, ok := r.BasicAuth()
|
||||||
|
if !ok {
|
||||||
|
t.Error("expected Basic Auth header")
|
||||||
|
}
|
||||||
|
if u != username || p != password {
|
||||||
|
t.Errorf("auth mismatch: got %s/%s, want %s/%s", u, p, username, password)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user