feat: add logging and support for custom logger (#36)

* feat: add support for logging

* fix linting

* chore: remove old APIError in favor of ServerError
This commit is contained in:
Mateusz Filipowicz
2025-02-23 12:59:46 +01:00
committed by GitHub
parent 95a4ff87ea
commit e79dcb13f0
13 changed files with 260 additions and 63 deletions

View File

@@ -6,6 +6,7 @@ linters:
- 'mnd' - 'mnd'
- 'nlreturn' - 'nlreturn'
- 'tagliatelle' - 'tagliatelle'
- 'ireturn'
# Temporary # Temporary
- 'cyclop' - 'cyclop'

View File

@@ -10,6 +10,8 @@ import (
type {{ .Name }} interface { type {{ .Name }} interface {
Logger
{{- range $k, $v := .Functions }} {{- range $k, $v := .Functions }}
{{ if $v.Comment }}// {{ $v.Comment }}{{ end }} {{ if $v.Comment }}// {{ $v.Comment }}{{ end }}

View File

@@ -40,7 +40,7 @@ type defaultUnifiVersionProvider struct {
firmwareUpdateApi string firmwareUpdateApi string
} }
func NewUnifiVersionProvider(firmwareUpdateApi string) UnifiVersionProvider { //nolint:ireturn func NewUnifiVersionProvider(firmwareUpdateApi string) UnifiVersionProvider {
return &defaultUnifiVersionProvider{ return &defaultUnifiVersionProvider{
firmwareUpdateApi: firmwareUpdateApi, firmwareUpdateApi: firmwareUpdateApi,
} }

View File

@@ -94,8 +94,76 @@ if err != nil {
## Debugging and Logging ## Debugging and Logging
For troubleshooting, it might be useful to enable verbose logging. You can implement an interceptor to log additional The SDK provides flexible logging capabilities through the `Logger` interface. You can either use the default logger or implement your own custom logger.
details like headers, body content, and timings. This can be enabled conditionally in your application's debug mode.
### Using the Default Logger
The SDK includes a default logger based on [logrus](https://github.com/sirupsen/logrus). You can configure it with different logging levels:
```go
// Configure client with default logger at Debug level
config := &unifi.ClientConfig{
URL: "https://unifi.localdomain",
APIKey: "your-api-key",
Logger: unifi.NewDefaultLogger(unifi.DebugLevel),
}
client, err := unifi.NewClient(config)
```
Available logging levels are:
- `unifi.DisabledLevel` - no logging
- `unifi.TraceLevel` - most verbose level
- `unifi.DebugLevel` - debug information
- `unifi.InfoLevel` - default level, informational messages
- `unifi.WarnLevel` - warning messages
- `unifi.ErrorLevel` - error messages only
Then `Logger` methods are available to be used within the client:
```go
client.Logger.Trace("Trace message")
client.Logger.Tracef("Trace message with %s", "formatting")
client.Logger.Debug("Debug message")
client.Logger.Debugf("Debug message with %s", "formatting")
client.Logger.Info("Info message")
client.Logger.Infof("Info message with %s", "formatting")
client.Logger.Warn("Warn message")
client.Logger.Warnf("Warn message with %s", "formatting")
client.Logger.Error("Error message")
client.Logger.Errorf("Error message with %s", "formatting")
```
### Custom Logger Implementation
You can implement your own logger by implementing the `Logger` interface:
```go
type MyCustomLogger struct {
// your logger fields
}
// Implement all required methods
func (l *MyCustomLogger) Trace(msg string) { /* implementation */ }
func (l *MyCustomLogger) Debug(msg string) { /* implementation */ }
func (l *MyCustomLogger) Info(msg string) { /* implementation */ }
func (l *MyCustomLogger) Error(msg string) { /* implementation */ }
func (l *MyCustomLogger) Warn(msg string) { /* implementation */ }
func (l *MyCustomLogger) Tracef(format string, args ...interface{}) { /* implementation */ }
func (l *MyCustomLogger) Debugf(format string, args ...interface{}) { /* implementation */ }
func (l *MyCustomLogger) Infof(format string, args ...interface{}) { /* implementation */ }
func (l *MyCustomLogger) Errorf(format string, args ...interface{}) { /* implementation */ }
func (l *MyCustomLogger) Warnf(format string, args ...interface{}) { /* implementation */ }
// Use custom logger in client configuration
config := &unifi.ClientConfig{
URL: "https://unifi.localdomain",
APIKey: "your-api-key",
Logger: &MyCustomLogger{},
}
client, err := unifi.NewClient(config)
```
If no logger is specified in the configuration, the SDK will use the default logger with `Info` level.
## Advanced Error Handling ## Advanced Error Handling

View File

@@ -71,6 +71,10 @@ if err != nil {
- `HttpTransportCustomizer` for transport-level customization - `HttpTransportCustomizer` for transport-level customization
- `HttpRoundTripperProvider` for complete HTTP client control - `HttpRoundTripperProvider` for complete HTTP client control
4. **Removed unifi.APIError**:
- Old: `unifi.APIError` struct for API errors
- New: Standard `unifi.ServerError` struct for API errors
5. **Additional Features in filipowm/go-unifi**: 5. **Additional Features in filipowm/go-unifi**:
- Validation modes (Soft, Hard, Disabled) - Validation modes (Soft, Hard, Disabled)
- Request/Response interceptors - Request/Response interceptors
@@ -90,6 +94,7 @@ if err != nil {
}) })
``` ```
4. Remove explicit `Login()` calls as they are now handled automatically, unless you use [bare client initialization](./getting_started.md#BareClientInitialization) 4. Remove explicit `Login()` calls as they are now handled automatically, unless you use [bare client initialization](./getting_started.md#BareClientInitialization)
5. Replace usage of `unifi.APIError` with `unifi.ServerError`
The rest of your code using the client methods should continue to work as before, as the API methods remain the same. The rest of your code using the client methods should continue to work as before, as the API methods remain the same.

View File

@@ -59,6 +59,7 @@ var (
// determineApiStyle checks the base URL to decide which API style to use and sets the apiPaths accordingly. // determineApiStyle checks the base URL to decide which API style to use and sets the apiPaths accordingly.
func (c *client) determineApiStyle() error { func (c *client) determineApiStyle() error {
c.Debug("Determining API style")
ctx, cancel := c.newRequestContext() ctx, cancel := c.newRequestContext()
defer cancel() defer cancel()
@@ -85,8 +86,10 @@ func (c *client) determineApiStyle() error {
switch resp.StatusCode { switch resp.StatusCode {
case http.StatusOK: case http.StatusOK:
c.Debug("Using new style API")
c.apiPaths = &NewStyleAPI c.apiPaths = &NewStyleAPI
case http.StatusFound: case http.StatusFound:
c.Debug("Using old style API")
c.apiPaths = &OldStyleAPI c.apiPaths = &OldStyleAPI
default: default:
return fmt.Errorf("expected 200 or 302 status code, but got: %d", resp.StatusCode) return fmt.Errorf("expected 200 or 302 status code, but got: %d", resp.StatusCode)

View File

@@ -8,6 +8,7 @@ import (
) )
type Client interface { type Client interface {
Logger
// BaseURL returns the base URL of the controller. // BaseURL returns the base URL of the controller.
BaseURL() string BaseURL() string

View File

@@ -14,20 +14,17 @@ import (
"golang.org/x/net/publicsuffix" "golang.org/x/net/publicsuffix"
) )
// validationMode represents the mode for request validation. // ValidationMode represents the mode for request validation.
// It may be set to "soft", "hard", or "disable". The default is "soft". // It may be set to "soft", "hard", or "disable". The default is "soft".
type validationMode string type ValidationMode int
const ( const (
// SoftValidation indicates that validation errors are logged as warnings but do not prevent the request from proceeding. // SoftValidation indicates that validation errors are logged as warnings but do not prevent the request from proceeding.
SoftValidation validationMode = "soft" SoftValidation ValidationMode = iota
// HardValidation indicates that validation errors are treated as fatal and will cause the request to be rejected. // HardValidation indicates that validation errors are treated as fatal and will cause the request to be rejected.
HardValidation validationMode = "hard" HardValidation
// DisableValidation indicates that no validation is performed on the request body. // DisableValidation indicates that no validation is performed on the request body.
DisableValidation validationMode = "disable" DisableValidation
// DefaultValidation is the default validation mode used if none is specified.
// Currently set to SoftValidation, but may change to HardValidation in a future major version.
DefaultValidation validationMode = SoftValidation // TODO: change to hard in next major version
) )
// HttpTransportCustomizer is a function type for customizing the HTTP transport. // HttpTransportCustomizer is a function type for customizing the HTTP transport.
@@ -73,7 +70,8 @@ type ClientConfig struct {
UserAgent string UserAgent string
ErrorHandler ResponseErrorHandler ErrorHandler ResponseErrorHandler
UseLocking bool UseLocking bool
ValidationMode validationMode `validate:"omitempty,oneof=soft hard disable"` ValidationMode ValidationMode
Logger Logger
} }
// Credentials abstracts authentication credentials. // Credentials abstracts authentication credentials.
@@ -112,12 +110,13 @@ func (u UserPassCredentials) GetPass() string { return u.Password }
// client represents a UniFi client. // client represents a UniFi client.
type client struct { type client struct {
Logger
baseURL *url.URL baseURL *url.URL
sysInfo *SysInfo sysInfo *SysInfo
apiPaths *APIPaths apiPaths *APIPaths
timeout time.Duration timeout time.Duration
credentials Credentials credentials Credentials
validationMode validationMode validationMode ValidationMode
useLocking bool useLocking bool
http *http.Client http *http.Client
@@ -168,10 +167,19 @@ func (c *client) Version() string {
} }
func newClientFromConfig(config *ClientConfig, v *validator) (*client, error) { func newClientFromConfig(config *ClientConfig, v *validator) (*client, error) {
var log Logger
if config.Logger != nil {
log = config.Logger
} else {
log = NewDefaultLogger(InfoLevel)
}
log.Info("Initializing new UniFi client")
var rt http.RoundTripper var rt http.RoundTripper
var err error var err error
config.URL = strings.TrimRight(config.URL, "/") config.URL = strings.TrimRight(config.URL, "/")
log.Debugf("Connecting to UniFi controller at %s", config.URL)
if config.HttpRoundTripperProvider != nil { if config.HttpRoundTripperProvider != nil {
log.Debug("Using custom HTTP round tripper provider")
rt = config.HttpRoundTripperProvider() rt = config.HttpRoundTripperProvider()
} }
if rt == nil { if rt == nil {
@@ -180,6 +188,7 @@ func newClientFromConfig(config *ClientConfig, v *validator) (*client, error) {
TLSClientConfig: &tls.Config{InsecureSkipVerify: !config.VerifySSL}, TLSClientConfig: &tls.Config{InsecureSkipVerify: !config.VerifySSL},
} }
if config.HttpTransportCustomizer != nil { if config.HttpTransportCustomizer != nil {
log.Debug("Customizing HTTP transport")
if transport, err = config.HttpTransportCustomizer(transport); err != nil { if transport, err = config.HttpTransportCustomizer(transport); err != nil {
return nil, fmt.Errorf("failed customizing HTTP transport: %w", err) return nil, fmt.Errorf("failed customizing HTTP transport: %w", err)
} }
@@ -205,14 +214,18 @@ func newClientFromConfig(config *ClientConfig, v *validator) (*client, error) {
var credentials Credentials var credentials Credentials
if config.APIKey != "" { if config.APIKey != "" {
log.Debug("Using API key authentication")
credentials = APIKeyCredentials{APIKey: config.APIKey} credentials = APIKeyCredentials{APIKey: config.APIKey}
interceptors = append(interceptors, &APIKeyAuthInterceptor{apiKey: config.APIKey}) interceptors = append(interceptors, &APIKeyAuthInterceptor{apiKey: config.APIKey})
} else { } else {
log.Debug("Using user/pass authentication")
credentials = UserPassCredentials{User: config.User, Password: config.Password} credentials = UserPassCredentials{User: config.User, Password: config.Password}
interceptors = append(interceptors, &CSRFInterceptor{}) interceptors = append(interceptors, &CSRFInterceptor{})
} }
if len(config.UserAgent) == 0 { if len(config.UserAgent) == 0 {
config.UserAgent = defaultUserAgent config.UserAgent = defaultUserAgent
} else {
log.Debugf("Using custom User-Agent header: %s", config.UserAgent)
} }
interceptors = append(interceptors, &DefaultHeadersInterceptor{headers: map[string]string{ interceptors = append(interceptors, &DefaultHeadersInterceptor{headers: map[string]string{
UserAgentHeader: config.UserAgent, UserAgentHeader: config.UserAgent,
@@ -221,13 +234,13 @@ func newClientFromConfig(config *ClientConfig, v *validator) (*client, error) {
}}) }})
var errorHandler ResponseErrorHandler var errorHandler ResponseErrorHandler
if config.ErrorHandler != nil { if config.ErrorHandler != nil {
log.Debug("Using custom response error handler")
errorHandler = config.ErrorHandler errorHandler = config.ErrorHandler
} else { } else {
log.Debug("Using default response error handler")
errorHandler = &DefaultResponseErrorHandler{} errorHandler = &DefaultResponseErrorHandler{}
} }
if config.ValidationMode == "" { log.Tracef("Validation mode: %d", config.ValidationMode)
config.ValidationMode = DefaultValidation
}
u := &client{ u := &client{
baseURL: baseURL, baseURL: baseURL,
timeout: config.Timeout, timeout: config.Timeout,
@@ -239,6 +252,7 @@ func newClientFromConfig(config *ClientConfig, v *validator) (*client, error) {
errorHandler: errorHandler, errorHandler: errorHandler,
lock: sync.Mutex{}, lock: sync.Mutex{},
validator: v, validator: v,
Logger: log,
} }
for _, interceptor := range config.Interceptors { for _, interceptor := range config.Interceptors {
u.AddInterceptor(&interceptor) u.AddInterceptor(&interceptor)
@@ -262,6 +276,7 @@ func NewClient(config *ClientConfig) (Client, error) { //nolint: ireturn
return c, fmt.Errorf("failed getting server info: %w", err) return c, fmt.Errorf("failed getting server info: %w", err)
} else { } else {
c.sysInfo = sysInfo c.sysInfo = sysInfo
c.Debugf("Connected to UniFi controller\nversion: %s; name: %s; build: %s; hostname: %s", sysInfo.Version, sysInfo.Name, sysInfo.Build, sysInfo.Hostname)
} }
return c, nil return c, nil
} }
@@ -296,8 +311,10 @@ func newBareClient(config *ClientConfig) (*client, error) {
// It returns an error if the authentication process fails. // It returns an error if the authentication process fails.
func (c *client) Login() error { func (c *client) Login() error {
if c.credentials.IsAPIKey() { if c.credentials.IsAPIKey() {
c.Trace("API key authentication; skipping login")
return nil return nil
} }
c.Trace("Logging in with user/pass credentials")
ctx, cancel := c.newRequestContext() ctx, cancel := c.newRequestContext()
defer cancel() defer cancel()

115
unifi/logging.go Normal file
View File

@@ -0,0 +1,115 @@
package unifi
import (
"github.com/sirupsen/logrus"
)
type Logger interface {
Trace(format string)
Debug(format string)
Info(format string)
Error(format string)
Warn(format string)
Tracef(format string, args ...interface{})
Debugf(format string, args ...interface{})
Infof(format string, args ...interface{})
Errorf(format string, args ...interface{})
Warnf(format string, args ...interface{})
}
type LoggingLevel int
const (
DisabledLevel LoggingLevel = iota
TraceLevel
DebugLevel
InfoLevel
WarnLevel
ErrorLevel
)
func NewDefaultLogger(level LoggingLevel) Logger {
l := logrus.New()
var logrusLevel logrus.Level
switch level {
case DisabledLevel:
return &noopLogger{}
case TraceLevel:
logrusLevel = logrus.TraceLevel
case DebugLevel:
logrusLevel = logrus.DebugLevel
case InfoLevel:
logrusLevel = logrus.InfoLevel
case WarnLevel:
logrusLevel = logrus.WarnLevel
case ErrorLevel:
logrusLevel = logrus.ErrorLevel
default:
logrusLevel = logrus.InfoLevel
}
l.SetLevel(logrusLevel)
l.SetFormatter(&logrus.TextFormatter{
DisableTimestamp: true,
DisableLevelTruncation: true,
FullTimestamp: false,
ForceColors: true,
})
return &defaultLogger{l}
}
type noopLogger struct{}
func (l *noopLogger) Trace(msg string) {}
func (l *noopLogger) Debug(msg string) {}
func (l *noopLogger) Info(msg string) {}
func (l *noopLogger) Error(msg string) {}
func (l *noopLogger) Warn(msg string) {}
func (l *noopLogger) Tracef(format string, args ...interface{}) {}
func (l *noopLogger) Debugf(format string, args ...interface{}) {}
func (l *noopLogger) Infof(format string, args ...interface{}) {}
func (l *noopLogger) Errorf(format string, args ...interface{}) {}
func (l *noopLogger) Warnf(format string, args ...interface{}) {}
type defaultLogger struct {
*logrus.Logger
}
func (l *defaultLogger) Trace(msg string) {
l.Logger.Trace(msg)
}
func (l *defaultLogger) Debug(msg string) {
l.Logger.Debug(msg)
}
func (l *defaultLogger) Info(msg string) {
l.Logger.Info(msg)
}
func (l *defaultLogger) Error(msg string) {
l.Logger.Error(msg)
}
func (l *defaultLogger) Warn(msg string) {
l.Logger.Warn(msg)
}
func (l *defaultLogger) Tracef(format string, args ...interface{}) {
l.Logger.Tracef(format, args...)
}
func (l *defaultLogger) Debugf(format string, args ...interface{}) {
l.Logger.Debugf(format, args...)
}
func (l *defaultLogger) Infof(format string, args ...interface{}) {
l.Logger.Infof(format, args...)
}
func (l *defaultLogger) Errorf(format string, args ...interface{}) {
l.Logger.Errorf(format, args...)
}
func (l *defaultLogger) Warnf(format string, args ...interface{}) {
l.Logger.Warnf(format, args...)
}

View File

@@ -39,12 +39,12 @@ func (c *client) buildRequestURL(apiPath string) (*url.URL, error) {
// validateRequestBody validates the request body if validation is enabled. // validateRequestBody validates the request body if validation is enabled.
func (c *client) validateRequestBody(reqBody interface{}) error { func (c *client) validateRequestBody(reqBody interface{}) error {
if reqBody != nil && c.validationMode != DisableValidation { if reqBody != nil && c.validationMode != DisableValidation {
c.Trace("Validating request body")
if err := c.validator.Validate(reqBody); err != nil { if err := c.validator.Validate(reqBody); err != nil {
err = fmt.Errorf("failed validating request body: %w", err)
if c.validationMode == HardValidation { if c.validationMode == HardValidation {
return err return fmt.Errorf("failed validating request body: %w", err)
} else { } else {
fmt.Println(err) c.Warnf("failed validating request body: %s", err)
} }
} }
} }
@@ -64,6 +64,7 @@ func (c *client) newRequestContext() (context.Context, context.CancelFunc) {
// It validates the request body, applies interceptors, and decodes the HTTP response into respBody if provided. // It validates the request body, applies interceptors, and decodes the HTTP response into respBody if provided.
// It returns an error if the request or response handling fails. // It returns an error if the request or response handling fails.
func (c *client) Do(ctx context.Context, method, apiPath string, reqBody interface{}, respBody interface{}) error { func (c *client) Do(ctx context.Context, method, apiPath string, reqBody interface{}, respBody interface{}) error {
c.Tracef("Performing request: %s %s", method, apiPath)
if err := c.validateRequestBody(reqBody); err != nil { if err := c.validateRequestBody(reqBody); err != nil {
return err return err
} }
@@ -76,6 +77,7 @@ func (c *client) Do(ctx context.Context, method, apiPath string, reqBody interfa
if err != nil { if err != nil {
return fmt.Errorf("unable to create request URL: %w", err) return fmt.Errorf("unable to create request URL: %w", err)
} }
c.Debugf("Executing request: %s %s", method, url.String())
req, err := http.NewRequestWithContext(ctx, method, url.String(), reqReader) req, err := http.NewRequestWithContext(ctx, method, url.String(), reqReader)
if err != nil { if err != nil {
@@ -84,8 +86,10 @@ func (c *client) Do(ctx context.Context, method, apiPath string, reqBody interfa
if c.useLocking { if c.useLocking {
c.lock.Lock() c.lock.Lock()
c.Trace("Acquired lock fo request")
defer c.lock.Unlock() defer c.lock.Unlock()
} }
c.Trace("Executing request interceptors")
for _, interceptor := range c.interceptors { for _, interceptor := range c.interceptors {
if err := interceptor.InterceptRequest(req); err != nil { if err := interceptor.InterceptRequest(req); err != nil {
return err return err
@@ -98,20 +102,24 @@ func (c *client) Do(ctx context.Context, method, apiPath string, reqBody interfa
} }
defer resp.Body.Close() defer resp.Body.Close()
c.Trace("Executing response interceptors")
for _, interceptor := range c.interceptors { for _, interceptor := range c.interceptors {
if err := interceptor.InterceptResponse(resp); err != nil { if err := interceptor.InterceptResponse(resp); err != nil {
return err return err
} }
} }
c.Trace("Checking for errors in response")
if err := c.errorHandler.HandleError(resp); err != nil { if err := c.errorHandler.HandleError(resp); err != nil {
return err return err
} }
if respBody == nil || resp.ContentLength == 0 { if respBody == nil || resp.ContentLength == 0 {
c.Trace("No response body to decode")
return nil return nil
} }
c.Trace("Decoding response body")
err = json.NewDecoder(resp.Body).Decode(respBody) err = json.NewDecoder(resp.Body).Decode(respBody)
if err != nil { if err != nil {
return fmt.Errorf("unable to decode body: %s %s %w", method, apiPath, err) return fmt.Errorf("unable to decode body: %s %s %w", method, apiPath, err)

View File

@@ -117,6 +117,7 @@ func (c *client) getOldSysInfo(ctx context.Context) (*SysInfo, error) {
// GetSystemInformation retrieves system information, trying the new API first and falling back to the old API if necessary. // GetSystemInformation retrieves system information, trying the new API first and falling back to the old API if necessary.
func (c *client) GetSystemInformation() (*SysInfo, error) { func (c *client) GetSystemInformation() (*SysInfo, error) {
c.Trace("Reading system information")
ctx, cancel := c.newRequestContext() ctx, cancel := c.newRequestContext()
defer cancel() defer cancel()

View File

@@ -10,26 +10,6 @@ import (
var ErrNotFound = errors.New("not found") var ErrNotFound = errors.New("not found")
// TODO old-style error handling to be removed in future versions.
type APIError struct {
RC string
Message string
}
func (err *APIError) Error() string {
return err.Message
}
func (err *APIError) Is(target error) bool {
var apiError *APIError
if errors.As(target, &apiError) {
if err.RC == apiError.RC && err.Message == apiError.Message {
return true
}
}
return false
}
type Meta struct { type Meta struct {
RC string `json:"rc"` RC string `json:"rc"`
Message string `json:"msg"` Message string `json:"msg"`
@@ -37,8 +17,8 @@ type Meta struct {
func (m *Meta) error() error { func (m *Meta) error() error {
if m.RC != "ok" { if m.RC != "ok" {
return &APIError{ return &ServerError{
RC: m.RC, ErrorCode: m.RC,
Message: m.Message, Message: m.Message,
} }
} }

View File

@@ -488,17 +488,16 @@ func TestUrlValidation(t *testing.T) {
func TestValidationModeValidation(t *testing.T) { func TestValidationModeValidation(t *testing.T) {
t.Parallel() t.Parallel()
testCases := []struct { testCases := []struct {
validationMode validationMode validationMode ValidationMode
expectedError string
}{ }{
{SoftValidation, ""}, {SoftValidation},
{HardValidation, ""}, {HardValidation},
{DisableValidation, ""}, {DisableValidation},
{"invalid", "must be one of"}, {99},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(string(tc.validationMode), func(t *testing.T) { t.Run(fmt.Sprintf("%d", tc.validationMode), func(t *testing.T) {
t.Parallel() t.Parallel()
// given // given
cc := &ClientConfig{ cc := &ClientConfig{
@@ -511,12 +510,6 @@ func TestValidationModeValidation(t *testing.T) {
// when // when
err = v.Validate(cc) err = v.Validate(cc)
// then
if tc.expectedError != "" {
require.ErrorContains(t, err, tc.expectedError)
return
}
require.NoError(t, err) require.NoError(t, err)
}) })
} }
@@ -541,7 +534,7 @@ type validateableBody struct {
func TestValidationModes(t *testing.T) { func TestValidationModes(t *testing.T) {
t.Parallel() t.Parallel()
testCases := []struct { testCases := []struct {
validationMode validationMode validationMode ValidationMode
expectedError string expectedError string
expectRequest bool expectRequest bool
}{ }{
@@ -551,7 +544,7 @@ func TestValidationModes(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(string(tc.validationMode), func(t *testing.T) { t.Run(fmt.Sprintf("%d", tc.validationMode), func(t *testing.T) {
t.Parallel() t.Parallel()
a := assert.New(t) a := assert.New(t)
// given // given
@@ -843,11 +836,14 @@ func TestMarshalRequestValid(t *testing.T) {
func TestLoginWithAPIKeyDirect(t *testing.T) { func TestLoginWithAPIKeyDirect(t *testing.T) {
t.Parallel() t.Parallel()
// Create a client manually with the APIKey set. // Create a client manually with the APIKey set.
c := &client{
credentials: APIKeyCredentials{APIKey: "abc"}, c, err := newBareClient(&ClientConfig{
} APIKey: "abc",
err := c.Login() URL: testUrl,
assert.NoError(t, err) })
require.Error(t, err)
err = c.Login()
require.NoError(t, err)
} }
func TestHttpTransportCustomizerError(t *testing.T) { func TestHttpTransportCustomizerError(t *testing.T) {