refactor: enhance NewClient to support environment variable fallback and add tests
This commit is contained in:
@@ -7,7 +7,11 @@ Go client library and CLI for the [Mail-in-a-Box](https://mailinabox.email/) adm
|
|||||||
```go
|
```go
|
||||||
import miab "gitea.koszewscy.waw.pl/koszewscy/mailinabox-go"
|
import miab "gitea.koszewscy.waw.pl/koszewscy/mailinabox-go"
|
||||||
|
|
||||||
c := miab.NewClient("box.example.com", "admin@example.com", "password")
|
// Explicit credentials:
|
||||||
|
c, err := miab.NewClient("box.example.com", "admin@example.com", "password")
|
||||||
|
|
||||||
|
// From environment variables (MIAB_* or MAILINABOX_* style):
|
||||||
|
c, err := miab.NewClient("", "", "")
|
||||||
|
|
||||||
c.SetRecord("foo.example.com", "TXT", "v=spf1 ~all")
|
c.SetRecord("foo.example.com", "TXT", "v=spf1 ~all")
|
||||||
c.AddRecord("foo.example.com", "A", "1.2.3.4")
|
c.AddRecord("foo.example.com", "A", "1.2.3.4")
|
||||||
@@ -18,18 +22,30 @@ records, _ := c.ListRecords("TXT")
|
|||||||
|
|
||||||
## CLI
|
## CLI
|
||||||
|
|
||||||
Install:
|
Install from registry:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go install gitea.koszewscy.waw.pl/koszewscy/mailinabox-go/cmd/miab@latest
|
go install gitea.koszewscy.waw.pl/koszewscy/mailinabox-go/cmd/miab@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
Credentials via environment variables:
|
Build locally:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
go build -o miab ./cmd/miab
|
||||||
|
```
|
||||||
|
|
||||||
|
Credentials via environment variables (either style is accepted):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# MIAB style
|
||||||
export MIAB_HOST=box.example.com
|
export MIAB_HOST=box.example.com
|
||||||
export MIAB_USERNAME=admin@example.com
|
export MIAB_USERNAME=admin@example.com
|
||||||
export MIAB_PASSWORD=password
|
export MIAB_PASSWORD=password
|
||||||
|
|
||||||
|
# Mail-in-a-Box style
|
||||||
|
export MAILINABOX_BASE_URL=https://box.example.com
|
||||||
|
export MAILINABOX_EMAIL=admin@example.com
|
||||||
|
export MAILINABOX_PASSWORD=password
|
||||||
```
|
```
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
|
|||||||
+10
-21
@@ -8,6 +8,7 @@ import (
|
|||||||
miab "gitea.koszewscy.waw.pl/koszewscy/mailinabox-go"
|
miab "gitea.koszewscy.waw.pl/koszewscy/mailinabox-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
func usage() {
|
func usage() {
|
||||||
fmt.Fprintf(os.Stderr, `Usage: miab <command> [options]
|
fmt.Fprintf(os.Stderr, `Usage: miab <command> [options]
|
||||||
|
|
||||||
@@ -17,33 +18,21 @@ Commands:
|
|||||||
delete Delete a DNS record
|
delete Delete a DNS record
|
||||||
add Add a DNS record
|
add Add a DNS record
|
||||||
|
|
||||||
Credentials are read from environment variables:
|
Credentials: any empty parameter falls back to environment variables.
|
||||||
MIAB_HOST Mail-in-a-Box hostname (e.g. box.example.com)
|
MIAB_HOST / MAILINABOX_BASE_URL Mail-in-a-Box hostname (MIAB_HOST takes precedence;
|
||||||
MIAB_USERNAME Admin email address
|
hostname is parsed from MAILINABOX_BASE_URL if set)
|
||||||
MIAB_PASSWORD Admin password
|
MIAB_USERNAME / MAILINABOX_EMAIL Admin email address
|
||||||
|
MIAB_PASSWORD / MAILINABOX_PASSWORD Admin password
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustClient() *miab.Client {
|
func mustClient() *miab.Client {
|
||||||
host := os.Getenv("MIAB_HOST")
|
c, err := miab.NewClient("", "", "")
|
||||||
username := os.Getenv("MIAB_USERNAME")
|
if err != nil {
|
||||||
password := os.Getenv("MIAB_PASSWORD")
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
|
||||||
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)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
return miab.NewClient(host, username, password)
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -24,8 +26,51 @@ type DNSRecord struct {
|
|||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient constructs a Client. host should be the bare hostname (e.g. "box.example.com").
|
// NewClient constructs a Client. Any empty parameter is filled from environment variables:
|
||||||
func NewClient(host, username, password string) *Client {
|
//
|
||||||
|
// host: MIAB_HOST or hostname parsed from MAILINABOX_BASE_URL
|
||||||
|
// username: MIAB_USERNAME or MAILINABOX_EMAIL
|
||||||
|
// password: MIAB_PASSWORD or MAILINABOX_PASSWORD
|
||||||
|
//
|
||||||
|
// Returns an error if a value is still empty after the env lookup.
|
||||||
|
func NewClient(host, username, password string) (*Client, error) {
|
||||||
|
if host == "" {
|
||||||
|
host = os.Getenv("MIAB_HOST")
|
||||||
|
if host == "" {
|
||||||
|
if base := os.Getenv("MAILINABOX_BASE_URL"); base != "" {
|
||||||
|
if u, err := url.Parse(base); err == nil {
|
||||||
|
host = u.Hostname()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if username == "" {
|
||||||
|
username = os.Getenv("MIAB_USERNAME")
|
||||||
|
if username == "" {
|
||||||
|
username = os.Getenv("MAILINABOX_EMAIL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if password == "" {
|
||||||
|
password = os.Getenv("MIAB_PASSWORD")
|
||||||
|
if password == "" {
|
||||||
|
password = os.Getenv("MAILINABOX_PASSWORD")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var missing []string
|
||||||
|
if host == "" {
|
||||||
|
missing = append(missing, "MIAB_HOST or MAILINABOX_BASE_URL")
|
||||||
|
}
|
||||||
|
if username == "" {
|
||||||
|
missing = append(missing, "MIAB_USERNAME or MAILINABOX_EMAIL")
|
||||||
|
}
|
||||||
|
if password == "" {
|
||||||
|
missing = append(missing, "MIAB_PASSWORD or MAILINABOX_PASSWORD")
|
||||||
|
}
|
||||||
|
if len(missing) > 0 {
|
||||||
|
return nil, fmt.Errorf("miab: missing required environment variables: %v", missing)
|
||||||
|
}
|
||||||
|
|
||||||
return &Client{
|
return &Client{
|
||||||
host: host,
|
host: host,
|
||||||
username: username,
|
username: username,
|
||||||
@@ -33,7 +78,7 @@ func NewClient(host, username, password string) *Client {
|
|||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
},
|
},
|
||||||
}
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetRecord replaces all existing records of the given type for name with value (HTTP PUT).
|
// SetRecord replaces all existing records of the given type for name with value (HTTP PUT).
|
||||||
|
|||||||
+71
-2
@@ -10,8 +10,74 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestNewClient_miabStyle(t *testing.T) {
|
||||||
|
t.Setenv("MIAB_HOST", "box.example.com")
|
||||||
|
t.Setenv("MIAB_USERNAME", "user@example.com")
|
||||||
|
t.Setenv("MIAB_PASSWORD", "secret")
|
||||||
|
|
||||||
|
c, err := NewClient("", "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if c.host != "box.example.com" || c.username != "user@example.com" || c.password != "secret" {
|
||||||
|
t.Errorf("unexpected client fields: %+v", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewClient_mailinaboxStyle(t *testing.T) {
|
||||||
|
t.Setenv("MAILINABOX_BASE_URL", "https://box.example.com")
|
||||||
|
t.Setenv("MAILINABOX_EMAIL", "user@example.com")
|
||||||
|
t.Setenv("MAILINABOX_PASSWORD", "secret")
|
||||||
|
|
||||||
|
c, err := NewClient("", "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if c.host != "box.example.com" || c.username != "user@example.com" || c.password != "secret" {
|
||||||
|
t.Errorf("unexpected client fields: %+v", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewClient_miabTakesPrecedence(t *testing.T) {
|
||||||
|
t.Setenv("MIAB_HOST", "miab.example.com")
|
||||||
|
t.Setenv("MIAB_USERNAME", "miab@example.com")
|
||||||
|
t.Setenv("MIAB_PASSWORD", "miabpass")
|
||||||
|
t.Setenv("MAILINABOX_BASE_URL", "https://other.example.com")
|
||||||
|
t.Setenv("MAILINABOX_EMAIL", "other@example.com")
|
||||||
|
t.Setenv("MAILINABOX_PASSWORD", "otherpass")
|
||||||
|
|
||||||
|
c, err := NewClient("", "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if c.host != "miab.example.com" || c.username != "miab@example.com" || c.password != "miabpass" {
|
||||||
|
t.Errorf("MIAB_* vars should take precedence, got: %+v", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewClient_missingVars(t *testing.T) {
|
||||||
|
_, err := NewClient("", "", "")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error when no env vars set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewClient_explicitParamsTakePrecedence(t *testing.T) {
|
||||||
|
t.Setenv("MIAB_HOST", "env.example.com")
|
||||||
|
t.Setenv("MIAB_USERNAME", "env@example.com")
|
||||||
|
t.Setenv("MIAB_PASSWORD", "envpass")
|
||||||
|
|
||||||
|
c, err := NewClient("explicit.example.com", "explicit@example.com", "explicitpass")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if c.host != "explicit.example.com" || c.username != "explicit@example.com" || c.password != "explicitpass" {
|
||||||
|
t.Errorf("explicit params should take precedence, got: %+v", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildURL(t *testing.T) {
|
func TestBuildURL(t *testing.T) {
|
||||||
c := NewClient("box.example.com", "user", "pass")
|
c, _ := NewClient("box.example.com", "user", "pass")
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name, recordType, want string
|
name, recordType, want string
|
||||||
@@ -153,7 +219,10 @@ func TestErrorResponse(t *testing.T) {
|
|||||||
|
|
||||||
// testClient builds a Client pointed at the test server's host.
|
// testClient builds a Client pointed at the test server's host.
|
||||||
func testClient(srv *httptest.Server) *Client {
|
func testClient(srv *httptest.Server) *Client {
|
||||||
c := NewClient(strings.TrimPrefix(srv.URL, "http://"), "user@example.com", "secret")
|
c, err := NewClient(strings.TrimPrefix(srv.URL, "http://"), "user@example.com", "secret")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
c.httpClient = srv.Client()
|
c.httpClient = srv.Client()
|
||||||
// Override buildURL to use http instead of https for the test server.
|
// Override buildURL to use http instead of https for the test server.
|
||||||
// We do this by wrapping the httpClient transport (no-op) and patching
|
// We do this by wrapping the httpClient transport (no-op) and patching
|
||||||
|
|||||||
Reference in New Issue
Block a user