diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 2621c79b1d..957ab7fa21 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -335,7 +335,7 @@ func displayDNSHelp(w io.Writer, name string) error { // generated from: providers/dns/beget/beget.toml ew.writeln(`Configuration for Beget.com.`) ew.writeln(`Code: 'beget'`) - ew.writeln(`Since: 'v1.0.0'`) + ew.writeln(`Since: 'v4.12.0'`) ew.writeln() ew.writeln(`Credentials:`) diff --git a/docs/content/dns/zz_gen_beget.md b/docs/content/dns/zz_gen_beget.md index a7212ae5d0..c2d78adc0d 100644 --- a/docs/content/dns/zz_gen_beget.md +++ b/docs/content/dns/zz_gen_beget.md @@ -4,7 +4,7 @@ date: 2019-03-03T16:39:46+01:00 draft: false slug: beget dnsprovider: - since: "v1.0.0" + since: "v4.12.0" code: "beget" url: "https://beget.com/" --- @@ -20,7 +20,7 @@ Configuration for [Beget.com](https://beget.com/). - Code: `beget` -- Since: v1.0.0 +- Since: v4.12.0 Here is an example bash command using the Beget.com provider: diff --git a/providers/dns/beget/beget.go b/providers/dns/beget/beget.go index 728e0da0d6..9d1002a0dc 100644 --- a/providers/dns/beget/beget.go +++ b/providers/dns/beget/beget.go @@ -1,142 +1,143 @@ -// Package beget implements a DNS provider for solving the DNS-01 challenge using beget.com DNS. -package beget - -import ( - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/beget/internal" -) - -// Environment variables names. -const ( - envNamespace = "BEGET_" - - EnvUsername = envNamespace + "USERNAME" - EnvPassword = envNamespace + "PASSWORD" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - Username string - Password string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, 300), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout*2), //2m - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for beget.com. -// Credentials must be passed in the environment variables: -// BEGET_USERNAME and BEGET_PASSWORD. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvUsername, EnvPassword) - if err != nil { - return nil, fmt.Errorf("beget: %w", err) - } - - config := NewDefaultConfig() - config.Username = values[EnvUsername] - config.Password = values[EnvPassword] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for beget.com. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("beget: the configuration of the DNS provider is nil") - } - - if config.Username == "" || config.Password == "" { - return nil, errors.New("beget: incomplete credentials, missing username and/or password") - } - - client := internal.NewClient(config.Username, config.Password) - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - return &DNSProvider{config: config, client: client}, nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("beget: could not find zone for domain %q and fqdn %q : %w", domain, info.EffectiveFQDN, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("beget: %w", err) - } - - err = d.client.AddTXTRecord(dns01.UnFqdn(authZone), subDomain, info.Value) - if err != nil { - return fmt.Errorf("beget: failed to create TXT records [domain: %s, sub domain: %s]: %w", - dns01.UnFqdn(authZone), subDomain, err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("beget: could not find zone for domain %q and fqdn %q : %w", domain, info.EffectiveFQDN, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("beget: %w", err) - } - - err = d.client.RemoveTxtRecord(dns01.UnFqdn(authZone), subDomain) - if err != nil { - return fmt.Errorf("beget: failed to remove TXT records [domain: %s, sub domain: %s]: %w", - dns01.UnFqdn(authZone), subDomain, err) - } - - return nil -} +// Package beget implements a DNS provider for solving the DNS-01 challenge using beget.com DNS. +package beget + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/go-acme/lego/v4/providers/dns/beget/internal" +) + +// Environment variables names. +const ( + envNamespace = "BEGET_" + + EnvUsername = envNamespace + "USERNAME" + EnvPassword = envNamespace + "PASSWORD" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + Username string + Password string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, 300), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout*2), // 2m + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for beget.com. +// Credentials must be passed in the environment variables: +// BEGET_USERNAME and BEGET_PASSWORD. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvUsername, EnvPassword) + if err != nil { + return nil, fmt.Errorf("beget: %w", err) + } + + config := NewDefaultConfig() + config.Username = values[EnvUsername] + config.Password = values[EnvPassword] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for beget.com. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("beget: the configuration of the DNS provider is nil") + } + + if config.Username == "" || config.Password == "" { + return nil, errors.New("beget: incomplete credentials, missing username and/or password") + } + + client := internal.NewClient(config.Username, config.Password) + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + return &DNSProvider{config: config, client: client}, nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("beget: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("beget: %w", err) + } + + err = d.client.AddTXTRecord(context.Background(), dns01.UnFqdn(authZone), subDomain, info.Value) + if err != nil { + return fmt.Errorf("beget: failed to create TXT records [domain: %s, sub domain: %s]: %w", + dns01.UnFqdn(authZone), subDomain, err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("beget: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("beget: %w", err) + } + + err = d.client.RemoveTxtRecord(context.Background(), dns01.UnFqdn(authZone), subDomain) + if err != nil { + return fmt.Errorf("beget: failed to remove TXT records [domain: %s, sub domain: %s]: %w", + dns01.UnFqdn(authZone), subDomain, err) + } + + return nil +} diff --git a/providers/dns/beget/beget.toml b/providers/dns/beget/beget.toml index 988429f8d6..86f2ae98fd 100644 --- a/providers/dns/beget/beget.toml +++ b/providers/dns/beget/beget.toml @@ -2,7 +2,7 @@ Name = "Beget.com" Description = '''''' URL = "https://beget.com/" Code = "beget" -Since = "v1.0.0" +Since = "v4.12.0" Example = ''' BEGET_USERNAME=xxxxxx \ diff --git a/providers/dns/beget/beget_test.go b/providers/dns/beget/beget_test.go index c3192766f4..fc65e42141 100644 --- a/providers/dns/beget/beget_test.go +++ b/providers/dns/beget/beget_test.go @@ -1,152 +1,149 @@ -package beget - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest( - EnvUsername, - EnvPassword). - WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvUsername: "123", - EnvPassword: "456", - }, - }, - { - desc: "missing credentials", - envVars: map[string]string{ - EnvUsername: "", - EnvPassword: "", - }, - expected: "beget: some credentials information are missing: BEGET_USERNAME,BEGET_PASSWORD", - }, - { - desc: "missing username", - envVars: map[string]string{ - EnvUsername: "", - EnvPassword: "456", - }, - expected: "beget: some credentials information are missing: BEGET_USERNAME", - }, - { - desc: "missing password", - envVars: map[string]string{ - EnvUsername: "123", - EnvPassword: "", - }, - expected: "beget: some credentials information are missing: BEGET_PASSWORD", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - username string - password string - expected string - }{ - { - desc: "success", - username: "123", - password: "456", - }, - { - desc: "missing credentials", - username: "", - password: "", - expected: "beget: incomplete credentials, missing username and/or password", - }, - { - desc: "missing username", - username: "", - password: "456", - expected: "beget: incomplete credentials, missing username and/or password", - }, - { - desc: "missing password", - username: "123", - password: "", - expected: "beget: incomplete credentials, missing username and/or password", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.Username = test.username - config.Password = test.password - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - provider, err := NewDNSProvider() - assert.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - assert.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - provider, err := NewDNSProvider() - assert.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - assert.NoError(t, err) -} +package beget + +import ( + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvUsername, EnvPassword).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvUsername: "123", + EnvPassword: "456", + }, + }, + { + desc: "missing credentials", + envVars: map[string]string{ + EnvUsername: "", + EnvPassword: "", + }, + expected: "beget: some credentials information are missing: BEGET_USERNAME,BEGET_PASSWORD", + }, + { + desc: "missing username", + envVars: map[string]string{ + EnvUsername: "", + EnvPassword: "456", + }, + expected: "beget: some credentials information are missing: BEGET_USERNAME", + }, + { + desc: "missing password", + envVars: map[string]string{ + EnvUsername: "123", + EnvPassword: "", + }, + expected: "beget: some credentials information are missing: BEGET_PASSWORD", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + defer envTest.RestoreEnv() + envTest.ClearEnv() + + envTest.Apply(test.envVars) + + p, err := NewDNSProvider() + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + username string + password string + expected string + }{ + { + desc: "success", + username: "123", + password: "456", + }, + { + desc: "missing credentials", + username: "", + password: "", + expected: "beget: incomplete credentials, missing username and/or password", + }, + { + desc: "missing username", + username: "", + password: "456", + expected: "beget: incomplete credentials, missing username and/or password", + }, + { + desc: "missing password", + username: "123", + password: "", + expected: "beget: incomplete credentials, missing username and/or password", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.Username = test.username + config.Password = test.password + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + assert.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + assert.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + assert.NoError(t, err) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + assert.NoError(t, err) +} diff --git a/providers/dns/beget/internal/client.go b/providers/dns/beget/internal/client.go index 728917ec10..21f6c0f0b9 100644 --- a/providers/dns/beget/internal/client.go +++ b/providers/dns/beget/internal/client.go @@ -1,130 +1,130 @@ -package internal - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" -) - -const defaultBaseURL = "https://api.beget.com/api/" - -// Client the beget.com client. -type Client struct { - Login string - Passwd string - - BaseURL string - HTTPClient *http.Client -} - -// NewClient Creates a beget.com client. -func NewClient(login, passwd string) *Client { - return &Client{ - Login: login, - Passwd: passwd, - BaseURL: defaultBaseURL, - HTTPClient: http.DefaultClient, - } -} - -// RemoveTxtRecord removes a TXT record. -// https://beget.com/ru/kb/api/funkczii-upravleniya-dns -func (c Client) RemoveTxtRecord(domain, subDomain string) error { - request := ChangeRecordsRequest{ - Fqdn: fmt.Sprintf("%s.%s", subDomain, domain), - Records: RecordList{}, - } - - resp, err := c.do(request, "dns", "changeRecords") - if err != nil { - return err - } - - return resp.HasError() -} - -// AddTXTRecord adds a TXT record. -// https://beget.com/ru/kb/api/funkczii-upravleniya-dns -func (c Client) AddTXTRecord(domain, subDomain, content string) error { - request := ChangeRecordsRequest{ - Fqdn: fmt.Sprintf("%s.%s", subDomain, domain), - Records: RecordList{ - TXT: []TxtRecord{ - {Priority: 10, Value: content}, - }, - }, - } - - resp, err := c.do(request, "dns", "changeRecords") - if err != nil { - return err - } - - return resp.HasError() -} - -func (c Client) do(request interface{}, fragments ...string) (*APIResponse, error) { - endpoint, err := c.createEndpoint(fragments...) - if err != nil { - return nil, err - } - - inputData, err := json.Marshal(request) - if err != nil { - return nil, err - } - - query := endpoint.Query() - query.Add("input_data", string(inputData)) - query.Add("login", c.Login) - query.Add("passwd", c.Passwd) - query.Add("input_format", "json") - query.Add("output_format", "json") - endpoint.RawQuery = query.Encode() - - resp, err := http.Get(endpoint.String()) - if err != nil { - return nil, err - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - all, errB := io.ReadAll(resp.Body) - if errB != nil { - return nil, fmt.Errorf("API error, status code: %d", resp.StatusCode) - } - - var apiResp APIResponse - errB = json.Unmarshal(all, &apiResp) - if errB != nil { - return nil, fmt.Errorf("API error, status code: %d, %s", resp.StatusCode, string(all)) - } - - return nil, fmt.Errorf("%w, status code: %d", apiResp, resp.StatusCode) - } - - all, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - var apiResp APIResponse - err = json.Unmarshal(all, &apiResp) - if err != nil { - return nil, err - } - - return &apiResp, nil -} - -func (c Client) createEndpoint(fragments ...string) (*url.URL, error) { - baseURL, err := url.Parse(c.BaseURL) - if err != nil { - return nil, err - } - - return baseURL.JoinPath(fragments...), nil -} +package internal + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" +) + +const defaultBaseURL = "https://api.beget.com/api/" + +// Client the beget.com client. +type Client struct { + login string + password string + + baseURL *url.URL + HTTPClient *http.Client +} + +// NewClient Creates a beget.com client. +func NewClient(login, password string) *Client { + baseURL, _ := url.Parse(defaultBaseURL) + + return &Client{ + login: login, + password: password, + baseURL: baseURL, + HTTPClient: &http.Client{Timeout: 5 * time.Second}, + } +} + +// AddTXTRecord adds a TXT record. +// https://beget.com/ru/kb/api/funkczii-upravleniya-dns +func (c Client) AddTXTRecord(ctx context.Context, domain, subDomain, content string) error { + request := ChangeRecordsRequest{ + Fqdn: fmt.Sprintf("%s.%s", subDomain, domain), + Records: RecordList{ + TXT: []Record{ + {Priority: 10, Value: content}, + }, + }, + } + + resp, err := c.doRequest(ctx, request, "dns", "changeRecords") + if err != nil { + return err + } + + return resp.HasError() +} + +// RemoveTxtRecord removes a TXT record. +// https://beget.com/ru/kb/api/funkczii-upravleniya-dns +func (c Client) RemoveTxtRecord(ctx context.Context, domain, subDomain string) error { + request := ChangeRecordsRequest{ + Fqdn: fmt.Sprintf("%s.%s", subDomain, domain), + Records: RecordList{}, + } + + resp, err := c.doRequest(ctx, request, "dns", "changeRecords") + if err != nil { + return err + } + + return resp.HasError() +} + +func (c Client) doRequest(ctx context.Context, data any, fragments ...string) (*APIResponse, error) { + endpoint := c.baseURL.JoinPath(fragments...) + + inputData, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("failed to mashall input data: %w", err) + } + + query := endpoint.Query() + query.Add("input_data", string(inputData)) + query.Add("login", c.login) + query.Add("passwd", c.password) + query.Add("input_format", "json") + query.Add("output_format", "json") + endpoint.RawQuery = query.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return nil, errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + return nil, parseError(req, resp) + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + var apiResp APIResponse + err = json.Unmarshal(raw, &apiResp) + if err != nil { + return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return &apiResp, nil +} + +func parseError(req *http.Request, resp *http.Response) error { + raw, _ := io.ReadAll(resp.Body) + + var apiResp APIResponse + err := json.Unmarshal(raw, &apiResp) + if err != nil { + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + return fmt.Errorf("[status code %d] %w", resp.StatusCode, apiResp) +} diff --git a/providers/dns/beget/internal/client_test.go b/providers/dns/beget/internal/client_test.go new file mode 100644 index 0000000000..c36d91043b --- /dev/null +++ b/providers/dns/beget/internal/client_test.go @@ -0,0 +1,99 @@ +package internal + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func setupTest(t *testing.T, handler http.HandlerFunc) *Client { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc("/dns/changeRecords", handler) + + client := NewClient("user", "secret") + client.HTTPClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client +} + +func writeFixtureHandler(filename string) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + query := req.URL.Query() + + if query.Get("login") != "user" { + http.Error(rw, fmt.Sprintf("invalid login: %q", query.Get("login")), http.StatusUnauthorized) + return + } + if query.Get("passwd") != "secret" { + http.Error(rw, fmt.Sprintf("invalid password: %q", query.Get("passwd")), http.StatusUnauthorized) + return + } + + if filename == "" { + return + } + + file, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, _ = io.Copy(rw, file) + } +} + +func TestClient_AddTXTRecord(t *testing.T) { + client := setupTest(t, writeFixtureHandler("changeRecords.json")) + + err := client.AddTXTRecord(context.Background(), "example.com", "sub", "txtTXTtxt") + require.NoError(t, err) +} + +func TestClient_AddTXTRecord_error(t *testing.T) { + client := setupTest(t, writeFixtureHandler("error.json")) + + err := client.AddTXTRecord(context.Background(), "example.com", "sub", "txtTXTtxt") + require.Error(t, err) +} + +func TestClient_AddTXTRecord_answer_error(t *testing.T) { + client := setupTest(t, writeFixtureHandler("answer_error.json")) + + err := client.AddTXTRecord(context.Background(), "example.com", "sub", "txtTXTtxt") + require.Error(t, err) +} + +func TestClient_RemoveTxtRecord(t *testing.T) { + client := setupTest(t, writeFixtureHandler("changeRecords.json")) + + err := client.RemoveTxtRecord(context.Background(), "example.com", "sub") + require.NoError(t, err) +} + +func TestClient_RemoveTxtRecord_error(t *testing.T) { + client := setupTest(t, writeFixtureHandler("error.json")) + + err := client.RemoveTxtRecord(context.Background(), "example.com", "sub") + require.Error(t, err) +} diff --git a/providers/dns/beget/internal/fixtures/answer_error.json b/providers/dns/beget/internal/fixtures/answer_error.json new file mode 100644 index 0000000000..12f5fdda76 --- /dev/null +++ b/providers/dns/beget/internal/fixtures/answer_error.json @@ -0,0 +1,12 @@ +{ + "status": "success", + "answer": { + "status": "error", + "errors": [ + { + "error_code": "INVALID_DATA", + "error_text": "Login length cannot be greater than 12 characters" + } + ] + } +} diff --git a/providers/dns/beget/internal/fixtures/changeRecords.json b/providers/dns/beget/internal/fixtures/changeRecords.json new file mode 100644 index 0000000000..4c182d4e6d --- /dev/null +++ b/providers/dns/beget/internal/fixtures/changeRecords.json @@ -0,0 +1,31 @@ +{ + "status": "success", + "answer": { + "status": "success", + "result": { + "A": [ + { + "priority": 10, + "value": "127.0.0.1" + } + ], + "MX": [ + { + "priority": 10, + "value": "mx1.beget.ru" + }, + { + "priority": 20, + "value": "mx2.beget.ru" + } + ], + "TXT": [ + { + "priority": 10, + "value": "TXT record" + } + ] + } + } +} + diff --git a/providers/dns/beget/internal/fixtures/error.json b/providers/dns/beget/internal/fixtures/error.json new file mode 100644 index 0000000000..1dd2a111e7 --- /dev/null +++ b/providers/dns/beget/internal/fixtures/error.json @@ -0,0 +1,5 @@ +{ + "status": "error", + "error_text": "No such method", + "error_code": "NO_SUCH_METHOD" +} diff --git a/providers/dns/beget/internal/model.go b/providers/dns/beget/internal/types.go similarity index 76% rename from providers/dns/beget/internal/model.go rename to providers/dns/beget/internal/types.go index 7dbbffc8dd..61f8a5c5e3 100644 --- a/providers/dns/beget/internal/model.go +++ b/providers/dns/beget/internal/types.go @@ -1,62 +1,65 @@ -package internal - -import "fmt" - -const successResult = "success" - -// APIResponse is the representation of an API response. -type APIResponse struct { - Status string `json:"status"` - - Answer *Answer `json:"answer,omitempty"` - - ErrorCode string `json:"error_code,omitempty"` - ErrorText string `json:"error_text,omitempty"` -} - -func (a APIResponse) Error() string { - return fmt.Sprintf("API %s: %s: %s", a.Status, a.ErrorCode, a.ErrorText) -} - -func (a Answer) Error() string { - return fmt.Sprintf("API answer %s: %s: %s", a.Status, a.ErrorCode, a.ErrorText) -} - -// HasError returns an error is the response contains an error. -func (a APIResponse) HasError() error { - if a.Status != successResult { - return a - } - - if a.Answer == nil || a.Status != successResult || !a.Answer.Result { - return a.Answer - } - - return nil -} - -// Answer is the representation of an API response answer. -type Answer struct { - Status string `json:"status,omitempty"` - Result bool `json:"result,omitempty"` - - ErrorCode string `json:"error_code,omitempty"` - ErrorText string `json:"error_text,omitempty"` -} - -// ChangeRecordsRequest - data representation for data change request. -type ChangeRecordsRequest struct { - Fqdn string `json:"fqdn,omitempty"` - Records RecordList `json:"records,omitempty"` -} - -// List of entries (in this case only described TXT) -type RecordList struct { - TXT []TxtRecord `json:"TXT,omitempty"` -} - -//Txt record structure -type TxtRecord struct { - Priority int `json:"priority,omitempty"` - Value string `json:"value,omitempty"` -} +package internal + +import ( + "encoding/json" + "fmt" +) + +const successResult = "success" + +// APIResponse is the representation of an API response. +type APIResponse struct { + Status string `json:"status"` + + Answer *Answer `json:"answer,omitempty"` + + ErrorCode string `json:"error_code,omitempty"` + ErrorText string `json:"error_text,omitempty"` +} + +func (a APIResponse) Error() string { + return fmt.Sprintf("API %s: %s: %s", a.Status, a.ErrorCode, a.ErrorText) +} + +// HasError returns an error is the response contains an error. +func (a APIResponse) HasError() error { + if a.Status != successResult { + return a + } + + if a.Answer == nil || a.Status != successResult || a.Answer.Status != successResult { + return a.Answer + } + + return nil +} + +// Answer is the representation of an API response answer. +type Answer struct { + Status string `json:"status,omitempty"` + Result json.RawMessage `json:"result,omitempty"` + + ErrorCode string `json:"error_code,omitempty"` + ErrorText string `json:"error_text,omitempty"` +} + +func (a Answer) Error() string { + return fmt.Sprintf("API answer %s: %s: %s", a.Status, a.ErrorCode, a.ErrorText) +} + +// ChangeRecordsRequest - data representation for data change request. +type ChangeRecordsRequest struct { + Fqdn string `json:"fqdn,omitempty"` + Records RecordList `json:"records,omitempty"` +} + +// RecordList List of entries (in this case only described TXT). +type RecordList struct { + TXT []Record `json:"TXT,omitempty"` +} + +// Record record structure. +type Record struct { + Priority int `json:"priority,omitempty"` + Value string `json:"value,omitempty"` +}