diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 62704757..3440bcd8 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -1635,9 +1635,8 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Credentials:`) - ew.writeln(` - "LIQUID_WEB_PASSWORD": Storm API Password`) - ew.writeln(` - "LIQUID_WEB_USERNAME": Storm API Username`) - ew.writeln(` - "LIQUID_WEB_ZONE": DNS Zone`) + ew.writeln(` - "LIQUID_WEB_PASSWORD": Liquid Web API Password`) + ew.writeln(` - "LIQUID_WEB_USERNAME": Liquid Web API Username`) ew.writeln() ew.writeln(`Additional Configuration:`) @@ -1645,7 +1644,8 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(` - "LIQUID_WEB_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "LIQUID_WEB_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "LIQUID_WEB_TTL": The TTL of the TXT record used for the DNS challenge`) - ew.writeln(` - "LIQUID_WEB_URL": Storm API endpoint`) + ew.writeln(` - "LIQUID_WEB_URL": Liquid Web API endpoint`) + ew.writeln(` - "LIQUID_WEB_ZONE": DNS Zone`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/liquidweb`) diff --git a/docs/content/dns/zz_gen_liquidweb.md b/docs/content/dns/zz_gen_liquidweb.md index 7c6cb7c1..64721310 100644 --- a/docs/content/dns/zz_gen_liquidweb.md +++ b/docs/content/dns/zz_gen_liquidweb.md @@ -28,7 +28,6 @@ Here is an example bash command using the Liquid Web provider: ```bash LIQUID_WEB_USERNAME=someuser \ LIQUID_WEB_PASSWORD=somepass \ -LIQUID_WEB_ZONE=tacoman.com.net \ lego --email you@example.com --dns liquidweb --domains my.example.org run ``` @@ -39,9 +38,8 @@ lego --email you@example.com --dns liquidweb --domains my.example.org run | Environment Variable Name | Description | |-----------------------|-------------| -| `LIQUID_WEB_PASSWORD` | Storm API Password | -| `LIQUID_WEB_USERNAME` | Storm API Username | -| `LIQUID_WEB_ZONE` | DNS Zone | +| `LIQUID_WEB_PASSWORD` | Liquid Web API Password | +| `LIQUID_WEB_USERNAME` | Liquid Web API Username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). @@ -55,7 +53,8 @@ More information [here]({{< ref "dns#configuration-and-credentials" >}}). | `LIQUID_WEB_POLLING_INTERVAL` | Time between DNS propagation check | | `LIQUID_WEB_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `LIQUID_WEB_TTL` | The TTL of the TXT record used for the DNS challenge | -| `LIQUID_WEB_URL` | Storm API endpoint | +| `LIQUID_WEB_URL` | Liquid Web API endpoint | +| `LIQUID_WEB_ZONE` | DNS Zone | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). @@ -65,7 +64,7 @@ More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information -- [API documentation](https://cart.liquidweb.com/storm/api/docs/v1/) +- [API documentation](https://api.liquidweb.com/docs/) - [Go client](https://github.com/liquidweb/liquidweb-go) diff --git a/go.mod b/go.mod index 649a029a..0225f0f7 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/infobloxopen/infoblox-go-client v1.1.1 github.com/labbsr0x/bindman-dns-webhook v1.0.2 github.com/linode/linodego v1.17.2 - github.com/liquidweb/liquidweb-go v1.6.3 + github.com/liquidweb/liquidweb-go v1.6.4 github.com/mattn/go-isatty v0.0.19 github.com/miekg/dns v1.1.55 github.com/mimuret/golang-iij-dpf v0.9.1 @@ -134,7 +134,6 @@ require ( github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/labbsr0x/goh v1.0.1 // indirect - github.com/liquidweb/go-lwApi v0.0.5 // indirect github.com/liquidweb/liquidweb-cli v0.6.9 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect diff --git a/go.sum b/go.sum index 338b2f8f..b9f24d86 100644 --- a/go.sum +++ b/go.sum @@ -386,12 +386,10 @@ github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmt github.com/linode/linodego v1.17.2 h1:b32dj4662PGG5P9qVa6nBezccWdqgukndlMIuPGq1CQ= github.com/linode/linodego v1.17.2/go.mod h1:C2iyT3Vg2O2sPxkWka4XAQ5WSUtm5LmTZ3Adw43Ra7Q= github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs= -github.com/liquidweb/go-lwApi v0.0.5 h1:CT4cdXzJXmo0bon298kS7NeSk+Gt8/UHpWBBol1NGCA= -github.com/liquidweb/go-lwApi v0.0.5/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs= github.com/liquidweb/liquidweb-cli v0.6.9 h1:acbIvdRauiwbxIsOCEMXGwF75aSJDbDiyAWPjVnwoYM= github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ= -github.com/liquidweb/liquidweb-go v1.6.3 h1:NVHvcnX3eb3BltiIoA+gLYn15nOpkYkdizOEYGSKrk4= -github.com/liquidweb/liquidweb-go v1.6.3/go.mod h1:SuXXp+thr28LnjEw18AYtWwIbWMHSUiajPQs8T9c/Rc= +github.com/liquidweb/liquidweb-go v1.6.4 h1:6S0m3hHSpiLqGD7AFSb7lH/W/qr1wx+tKil9fgIbjMc= +github.com/liquidweb/liquidweb-go v1.6.4/go.mod h1:B934JPIIcdA+uTq2Nz5PgOtG6CuCaEvQKe/Ge/5GgZ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= diff --git a/providers/dns/liquidweb/liquidweb.go b/providers/dns/liquidweb/liquidweb.go index 77381bd9..dc8fa857 100644 --- a/providers/dns/liquidweb/liquidweb.go +++ b/providers/dns/liquidweb/liquidweb.go @@ -4,7 +4,9 @@ package liquidweb import ( "errors" "fmt" + "sort" "strconv" + "strings" "sync" "time" @@ -14,7 +16,7 @@ import ( "github.com/liquidweb/liquidweb-go/network" ) -const defaultBaseURL = "https://api.stormondemand.com" +const defaultBaseURL = "https://api.liquidweb.com" // Environment variables names. const ( @@ -45,15 +47,13 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { - config := &Config{ + return &Config{ BaseURL: defaultBaseURL, TTL: env.GetOrDefaultInt(EnvTTL, 300), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 1*time.Minute), } - - return config } // DNSProvider implements the challenge.Provider interface. @@ -66,7 +66,7 @@ type DNSProvider struct { // NewDNSProvider returns a DNSProvider instance configured for Liquid Web. func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvUsername, EnvPassword, EnvZone) + values, err := env.Get(EnvUsername, EnvPassword) if err != nil { return nil, fmt.Errorf("liquidweb: %w", err) } @@ -75,7 +75,7 @@ func NewDNSProvider() (*DNSProvider, error) { config.BaseURL = env.GetOrFile(EnvURL) config.Username = values[EnvUsername] config.Password = values[EnvPassword] - config.Zone = values[EnvZone] + config.Zone = env.GetOrDefaultString(EnvZone, "") return NewDNSProviderConfig(config) } @@ -90,19 +90,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { config.BaseURL = defaultBaseURL } - if config.Zone == "" { - return nil, errors.New("liquidweb: zone is missing") - } - - if config.Username == "" { - return nil, errors.New("liquidweb: username is missing") - } - - if config.Password == "" { - return nil, errors.New("liquidweb: password is missing") - } - - // Initialize LW client. client, err := lw.NewAPI(config.Username, config.Password, config.BaseURL, int(config.HTTPTimeout.Seconds())) if err != nil { return nil, fmt.Errorf("liquidweb: could not create Liquid Web API client: %w", err) @@ -133,6 +120,15 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { TTL: d.config.TTL, } + if params.Zone == "" { + bestZone, err := d.findZone(params.Name) + if err != nil { + return fmt.Errorf("liquidweb: %w", err) + } + + params.Zone = bestZone + } + dnsEntry, err := d.client.NetworkDNS.Create(params) if err != nil { return fmt.Errorf("liquidweb: could not create TXT record: %w", err) @@ -167,3 +163,31 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return nil } + +func (d *DNSProvider) findZone(domain string) (string, error) { + zones, err := d.client.NetworkDNSZone.ListAll() + if err != nil { + return "", fmt.Errorf("failed to retrieve zones for account: %w", err) + } + + // filter the zones on the account to only ones that match + var zs []network.DNSZone + for _, item := range zones.Items { + if strings.HasSuffix(domain, item.Name) { + zs = append(zs, item) + } + } + + if len(zs) < 1 { + return "", fmt.Errorf("no valid zone in account for certificate '%s'", domain) + } + + // powerdns _only_ looks for records on the longest matching subdomain zone aka, + // for test.sub.example.com if sub.example.com exists, + // it will look there it will not look atexample.com even if it also exists + sort.Slice(zs, func(i, j int) bool { + return len(zs[i].Name) > len(zs[j].Name) + }) + + return zs[0].Name, nil +} diff --git a/providers/dns/liquidweb/liquidweb.toml b/providers/dns/liquidweb/liquidweb.toml index 1da09442..3fc53b8e 100644 --- a/providers/dns/liquidweb/liquidweb.toml +++ b/providers/dns/liquidweb/liquidweb.toml @@ -7,22 +7,21 @@ Since = "v3.1.0" Example = ''' LIQUID_WEB_USERNAME=someuser \ LIQUID_WEB_PASSWORD=somepass \ -LIQUID_WEB_ZONE=tacoman.com.net \ lego --email you@example.com --dns liquidweb --domains my.example.org run ''' [Configuration] [Configuration.Credentials] - LIQUID_WEB_USERNAME = "Storm API Username" - LIQUID_WEB_PASSWORD = "Storm API Password" - LIQUID_WEB_ZONE = "DNS Zone" + LIQUID_WEB_USERNAME = "Liquid Web API Username" + LIQUID_WEB_PASSWORD = "Liquid Web API Password" [Configuration.Additional] - LIQUID_WEB_URL = "Storm API endpoint" + LIQUID_WEB_ZONE = "DNS Zone" + LIQUID_WEB_URL = "Liquid Web API endpoint" LIQUID_WEB_TTL = "The TTL of the TXT record used for the DNS challenge" LIQUID_WEB_POLLING_INTERVAL = "Time between DNS propagation check" LIQUID_WEB_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" LIQUID_WEB_HTTP_TIMEOUT = "Maximum waiting time for the DNS records to be created (not verified)" [Links] - API = "https://cart.liquidweb.com/storm/api/docs/v1/" + API = "https://api.liquidweb.com/docs/" GoClient = "https://github.com/liquidweb/liquidweb-go" diff --git a/providers/dns/liquidweb/liquidweb_test.go b/providers/dns/liquidweb/liquidweb_test.go index 17c02252..47461edd 100644 --- a/providers/dns/liquidweb/liquidweb_test.go +++ b/providers/dns/liquidweb/liquidweb_test.go @@ -1,15 +1,11 @@ package liquidweb import ( - "fmt" - "io" - "net/http" - "net/http/httptest" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/assert" + "github.com/liquidweb/liquidweb-go/network" "github.com/stretchr/testify/require" ) @@ -22,23 +18,20 @@ var envTest = tester.NewEnvTest( EnvZone). WithDomain(envDomain) -func setupTest(t *testing.T) (*DNSProvider, *http.ServeMux) { +func setupTest(t *testing.T, initRecs ...network.DNSRecord) *DNSProvider { t.Helper() - mux := http.NewServeMux() - server := httptest.NewServer(mux) - t.Cleanup(server.Close) + serverURL := mockAPIServer(t, initRecs) config := NewDefaultConfig() config.Username = "blars" config.Password = "tacoman" - config.BaseURL = server.URL - config.Zone = "tacoman.com" + config.BaseURL = serverURL provider, err := NewDNSProviderConfig(config) require.NoError(t, err) - return provider, mux + return provider } func TestNewDNSProvider(t *testing.T) { @@ -48,7 +41,14 @@ func TestNewDNSProvider(t *testing.T) { expected string }{ { - desc: "success", + desc: "minimum-success", + envVars: map[string]string{ + EnvUsername: "blars", + EnvPassword: "tacoman", + }, + }, + { + desc: "set-everything", envVars: map[string]string{ EnvURL: "https://storm.com", EnvUsername: "blars", @@ -59,7 +59,7 @@ func TestNewDNSProvider(t *testing.T) { { desc: "missing credentials", envVars: map[string]string{}, - expected: "liquidweb: some credentials information are missing: LIQUID_WEB_USERNAME,LIQUID_WEB_PASSWORD,LIQUID_WEB_ZONE", + expected: "liquidweb: some credentials information are missing: LIQUID_WEB_USERNAME,LIQUID_WEB_PASSWORD", }, { desc: "missing username", @@ -74,14 +74,8 @@ func TestNewDNSProvider(t *testing.T) { envVars: map[string]string{ EnvUsername: "blars", EnvZone: "blars.com", - }, expected: "liquidweb: some credentials information are missing: LIQUID_WEB_PASSWORD", - }, - { - desc: "missing zone", - envVars: map[string]string{ - EnvUsername: "blars", - EnvPassword: "tacoman", - }, expected: "liquidweb: some credentials information are missing: LIQUID_WEB_ZONE", + }, + expected: "liquidweb: some credentials information are missing: LIQUID_WEB_PASSWORD", }, } @@ -126,28 +120,21 @@ func TestNewDNSProviderConfig(t *testing.T) { username: "", password: "", zone: "", - expected: "liquidweb: zone is missing", + expected: "liquidweb: could not create Liquid Web API client: provided username is empty", }, { desc: "missing username", username: "", password: "secret", zone: "example.com", - expected: "liquidweb: username is missing", + expected: "liquidweb: could not create Liquid Web API client: provided username is empty", }, { desc: "missing password", username: "acme", password: "", zone: "example.com", - expected: "liquidweb: password is missing", - }, - { - desc: "missing zone", - username: "acme", - password: "secret", - zone: "", - expected: "liquidweb: zone is missing", + expected: "liquidweb: could not create Liquid Web API client: provided password is empty", }, } @@ -174,75 +161,102 @@ func TestNewDNSProviderConfig(t *testing.T) { } func TestDNSProvider_Present(t *testing.T) { - provider, mux := setupTest(t) - - mux.HandleFunc("/v1/Network/DNS/Record/create", func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodPost, r.Method) - - username, password, ok := r.BasicAuth() - assert.Equal(t, "blars", username) - assert.Equal(t, "tacoman", password) - assert.True(t, ok) - - reqBody, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - expectedReqBody := ` - { - "params": { - "name": "_acme-challenge.tacoman.com", - "rdata": "\"47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU\"", - "ttl": 300, - "type": "TXT", - "zone": "tacoman.com" - } - }` - assert.JSONEq(t, expectedReqBody, string(reqBody)) - - w.WriteHeader(http.StatusOK) - _, err = fmt.Fprintf(w, `{ - "type": "TXT", - "name": "_acme-challenge.tacoman.com", - "rdata": "\"47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU\"", - "ttl": 300, - "id": 1234567, - "prio": null - }`) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - }) + provider := setupTest(t) err := provider.Present("tacoman.com", "", "") require.NoError(t, err) } func TestDNSProvider_CleanUp(t *testing.T) { - provider, mux := setupTest(t) - - mux.HandleFunc("/v1/Network/DNS/Record/delete", func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodPost, r.Method) - - username, password, ok := r.BasicAuth() - assert.Equal(t, "blars", username) - assert.Equal(t, "tacoman", password) - assert.True(t, ok) - - _, err := fmt.Fprintf(w, `{"deleted": "123"}`) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + provider := setupTest(t, network.DNSRecord{ + Name: "_acme-challenge.tacoman.com", + RData: "123d==", + Type: "TXT", + TTL: 300, + ID: 1234567, + ZoneID: 42, }) - provider.recordIDs["123"] = 1234567 + provider.recordIDs["123d=="] = 1234567 - err := provider.CleanUp("tacoman.com.", "123", "") - require.NoError(t, err, "fail to remove TXT record") + err := provider.CleanUp("tacoman.com.", "123d==", "") + require.NoError(t, err) +} + +func TestDNSProvider(t *testing.T) { + testCases := []struct { + desc string + initRecs []network.DNSRecord + domain string + token string + keyAuth string + present bool + expPresentErr string + cleanup bool + }{ + { + desc: "expected successful", + domain: "tacoman.com", + token: "123", + keyAuth: "456", + present: true, + cleanup: true, + }, + { + desc: "other successful", + domain: "banana.com", + token: "123", + keyAuth: "456", + present: true, + cleanup: true, + }, + { + desc: "zone not on account", + domain: "huckleberry.com", + token: "123", + keyAuth: "456", + present: true, + expPresentErr: "no valid zone in account for certificate '_acme-challenge.huckleberry.com'", + cleanup: false, + }, + { + desc: "ssl for domain", + domain: "sundae.cherry.com", + token: "5847953", + keyAuth: "34872934", + present: true, + cleanup: true, + }, + { + desc: "complicated domain", + domain: "always.money.stand.banana.com", + token: "5847953", + keyAuth: "there is always money in the banana stand", + present: true, + cleanup: true, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + provider := setupTest(t, test.initRecs...) + + if test.present { + err := provider.Present(test.domain, test.token, test.keyAuth) + if test.expPresentErr == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, test.expPresentErr) + } + } + + if test.cleanup { + err := provider.CleanUp(test.domain, test.token, test.keyAuth) + require.NoError(t, err) + } + }) + } } func TestLivePresent(t *testing.T) { diff --git a/providers/dns/liquidweb/servermock_test.go b/providers/dns/liquidweb/servermock_test.go new file mode 100644 index 00000000..8c22595a --- /dev/null +++ b/providers/dns/liquidweb/servermock_test.go @@ -0,0 +1,305 @@ +package liquidweb + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "math/rand" + "net/http" + "net/http/httptest" + "testing" + + "github.com/liquidweb/liquidweb-go/network" + "github.com/liquidweb/liquidweb-go/types" +) + +func mockAPIServer(t *testing.T, initRecs []network.DNSRecord) string { + t.Helper() + + recs := make(map[int]network.DNSRecord) + + for _, rec := range initRecs { + recs[int(rec.ID)] = rec + } + + mux := http.NewServeMux() + mux.Handle("/v1/Network/DNS/Record/delete", mockAPIDelete(recs)) + mux.Handle("/v1/Network/DNS/Record/create", mockAPICreate(recs)) + mux.Handle("/v1/Network/DNS/Zone/list", mockAPIListZones()) + mux.Handle("/bleed/Network/DNS/Record/delete", mockAPIDelete(recs)) + mux.Handle("/bleed/Network/DNS/Record/create", mockAPICreate(recs)) + mux.Handle("/bleed/Network/DNS/Zone/list", mockAPIListZones()) + + server := httptest.NewServer(requireBasicAuth(requireJSON(mux))) + t.Cleanup(server.Close) + + return server.URL +} + +func requireBasicAuth(next http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + username, password, ok := r.BasicAuth() + if ok && username == "blars" && password == "tacoman" { + next.ServeHTTP(w, r) + return + } + + http.Error(w, "invalid auth", http.StatusForbidden) + } +} + +func requireJSON(next http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + buf := &bytes.Buffer{} + + _, err := buf.ReadFrom(r.Body) + if err != nil { + http.Error(w, "malformed request - json required", http.StatusBadRequest) + return + } + + r.Body = io.NopCloser(buf) + next.ServeHTTP(w, r) + } +} + +func mockAPICreate(recs map[int]network.DNSRecord) http.HandlerFunc { + _, mockAPIServerZones := makeMockZones() + + return func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "invalid request", http.StatusInternalServerError) + return + } + + req := struct { + Params network.DNSRecord `json:"params"` + }{} + + if err = json.Unmarshal(body, &req); err != nil { + http.Error(w, makeEncodingError(body), http.StatusBadRequest) + return + } + req.Params.ID = types.FlexInt(rand.Intn(10000000)) + req.Params.ZoneID = types.FlexInt(mockAPIServerZones[req.Params.Name]) + + if _, exists := recs[int(req.Params.ID)]; exists { + http.Error(w, "dns record already exists", http.StatusTeapot) + return + } + recs[int(req.Params.ID)] = req.Params + + resp, err := json.Marshal(req.Params) + if err != nil { + http.Error(w, "", http.StatusInternalServerError) + return + } + http.Error(w, string(resp), http.StatusOK) + } +} + +func mockAPIDelete(recs map[int]network.DNSRecord) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "invalid request", http.StatusInternalServerError) + return + } + + req := struct { + Params struct { + Name string `json:"name"` + ID int `json:"id"` + } `json:"params"` + }{} + + if err := json.Unmarshal(body, &req); err != nil { + http.Error(w, makeEncodingError(body), http.StatusBadRequest) + return + } + + if req.Params.ID == 0 { + http.Error(w, `{"error":"","error_class":"LW::Exception::Input::Multiple","errors":[{"error":"","error_class":"LW::Exception::Input::Required","field":"id","full_message":"The required field 'id' was missing a value.","position":null}],"field":["id"],"full_message":"The following input errors occurred:\nThe required field 'id' was missing a value.","type":null}`, http.StatusOK) + return + } + + if _, ok := recs[req.Params.ID]; !ok { + http.Error(w, fmt.Sprintf(`{"error":"","error_class":"LW::Exception::RecordNotFound","field":"network_dns_rr","full_message":"Record 'network_dns_rr: %d' not found","input":"%d","public_message":null}`, req.Params.ID, req.Params.ID), http.StatusOK) + return + } + delete(recs, req.Params.ID) + http.Error(w, fmt.Sprintf("{\"deleted\":%d}", req.Params.ID), http.StatusOK) + } +} + +func mockAPIListZones() http.HandlerFunc { + mockZones, mockAPIServerZones := makeMockZones() + + return func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "invalid request", http.StatusInternalServerError) + return + } + + req := struct { + Params struct { + PageNum int `json:"page_num"` + } `json:"params"` + }{} + + if err = json.Unmarshal(body, &req); err != nil { + http.Error(w, makeEncodingError(body), http.StatusBadRequest) + return + } + + switch { + case req.Params.PageNum < 1: + req.Params.PageNum = 1 + case req.Params.PageNum > len(mockZones): + req.Params.PageNum = len(mockZones) + } + resp := mockZones[req.Params.PageNum] + resp.ItemTotal = types.FlexInt(len(mockAPIServerZones)) + resp.PageNum = types.FlexInt(req.Params.PageNum) + resp.PageSize = 5 + resp.PageTotal = types.FlexInt(len(mockZones)) + + var respBody []byte + if respBody, err = json.Marshal(resp); err == nil { + http.Error(w, string(respBody), http.StatusOK) + return + } + + http.Error(w, "", http.StatusInternalServerError) + } +} + +func makeEncodingError(buf []byte) string { + return fmt.Sprintf(`{"data":"%q","encoding":"JSON","error":"unexpected end of string while parsing JSON string, at character offset 32 (before \"(end of string)\") at /usr/local/lp/libs/perl/LW/Base/Role/Serializer.pm line 16.\n","error_class":"LW::Exception::Deserialize","full_message":"Could not deserialize \"%q\" from JSON: unexpected end of string while parsing JSON string, at character offset 32 (before \"(end of string)\") at /usr/local/lp/libs/perl/LW/Base/Role/Serializer.pm line 16.\n"}⏎`, string(buf), string(buf)) +} + +func makeMockZones() (map[int]network.DNSZoneList, map[string]int) { + mockZones := map[int]network.DNSZoneList{ + 1: { + Items: []network.DNSZone{ + { + ID: 1, + Name: "blars.com", + Active: 1, + DelegationStatus: "CORRECT", + PrimaryNameserver: "ns.liquidweb.com", + }, + { + ID: 2, + Name: "tacoman.com", + Active: 1, + DelegationStatus: "CORRECT", + PrimaryNameserver: "ns.liquidweb.com", + }, + { + ID: 3, + Name: "storm.com", + Active: 1, + DelegationStatus: "CORRECT", + PrimaryNameserver: "ns.liquidweb.com", + }, + { + ID: 4, + Name: "not-apple.com", + Active: 1, + DelegationStatus: "BAD_NAMESERVERS", + PrimaryNameserver: "ns.liquidweb.com", + }, + { + ID: 5, + Name: "example.com", + Active: 1, + DelegationStatus: "BAD_NAMESERVERS", + PrimaryNameserver: "ns.liquidweb.com", + }, + }, + }, + 2: { + Items: []network.DNSZone{ + { + ID: 6, + Name: "banana.com", + Active: 1, + DelegationStatus: "NXDOMAIN", + PrimaryNameserver: "ns.liquidweb.com", + }, + { + ID: 7, + Name: "cherry.com", + Active: 1, + DelegationStatus: "SERVFAIL", + PrimaryNameserver: "ns.liquidweb.com", + }, + { + ID: 8, + Name: "dates.com", + Active: 1, + DelegationStatus: "SERVFAIL", + PrimaryNameserver: "ns.liquidweb.com", + }, + { + ID: 9, + Name: "eggplant.com", + Active: 1, + DelegationStatus: "SERVFAIL", + PrimaryNameserver: "ns.liquidweb.com", + }, + { + ID: 10, + Name: "fig.com", + Active: 1, + DelegationStatus: "UNKNOWN", + PrimaryNameserver: "ns.liquidweb.com", + }, + }, + }, + 3: { + Items: []network.DNSZone{ + { + ID: 11, + Name: "grapes.com", + Active: 1, + DelegationStatus: "UNKNOWN", + PrimaryNameserver: "ns.liquidweb.com", + }, + { + ID: 12, + Name: "money.banana.com", + Active: 1, + DelegationStatus: "UNKNOWN", + PrimaryNameserver: "ns.liquidweb.com", + }, + { + ID: 13, + Name: "money.stand.banana.com", + Active: 1, + DelegationStatus: "UNKNOWN", + PrimaryNameserver: "ns.liquidweb.com", + }, + { + ID: 14, + Name: "stand.banana.com", + Active: 1, + DelegationStatus: "UNKNOWN", + PrimaryNameserver: "ns.liquidweb.com", + }, + }, + }, + } + + mockAPIServerZones := make(map[string]int) + for _, page := range mockZones { + for _, zone := range page.Items { + mockAPIServerZones[zone.Name] = int(zone.ID) + } + } + return mockZones, mockAPIServerZones +}