From 473a5d0d5cc1a2abd7e69db0d1b94da69068d535 Mon Sep 17 00:00:00 2001 From: Mateusz Filipowicz Date: Mon, 10 Feb 2025 19:02:30 +0100 Subject: [PATCH] chore: increase test coverage of unifi --- unifi/unifi_test.go | 216 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) diff --git a/unifi/unifi_test.go b/unifi/unifi_test.go index ef43489..13049bc 100644 --- a/unifi/unifi_test.go +++ b/unifi/unifi_test.go @@ -5,11 +5,14 @@ import ( "encoding/json" "errors" "fmt" + "io" "net/http" "net/http/httptest" + "net/url" "reflect" "strings" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -593,3 +596,216 @@ func TestGetSystemInformation(t *testing.T) { }) } } + +func TestParseBaseUrl(t *testing.T) { + t.Parallel() + a := assert.New(t) + + // Valid URL without /api in the path. + base, err := parseBaseUrl("http://localhost") + require.NoError(t, err) + a.Equal("http", base.Scheme) + a.Equal("", base.Path) + + // URL with trailing slash /api/ + _, err = parseBaseUrl("http://localhost/api/") + require.ErrorContains(t, err, "expected a base URL without the `/api`") + + // URL with /api in path (no trailing slash). + _, err = parseBaseUrl("http://localhost/api") + require.ErrorContains(t, err, "expected a base URL without the `/api`") +} + +func TestDetermineApiStyle_InvalidStatus(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Return an unexpected status code. + w.WriteHeader(http.StatusInternalServerError) + })) + defer ts.Close() + + _, err := NewClient(&ClientConfig{ + URL: ts.URL, + APIKey: "test", + VerifySSL: false, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "expected 200 or 302 status code") +} + +func TestRegisterInterceptor(t *testing.T) { + t.Parallel() + // Create a manual client with an empty interceptor slice. + client := &Client{ + interceptors: []ClientInterceptor{}, + } + // Create a dummy interceptor (using TestInterceptor already defined in the file). + var dummy ClientInterceptor = &TestInterceptor{} + initialCount := len(client.interceptors) + client.RegisterInterceptor(&dummy) + assert.Len(t, client.interceptors, initialCount+1) + // Attempt to add the same interceptor again. + client.RegisterInterceptor(&dummy) + assert.Len(t, client.interceptors, initialCount+1) +} + +func TestDoInvalidJsonResponse(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // For API style determination. + if r.URL.Path == "/" { + w.WriteHeader(http.StatusOK) + return + } + // When handling the API call, return an invalid JSON. + if r.URL.Path == NewStyleAPI.ApiPath+"/any" { + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte("invalid json")) + if err != nil { + t.Error(err) + } + return + } + http.NotFound(w, r) + })) + defer ts.Close() + + c, err := NewClient(&ClientConfig{ + URL: ts.URL, + APIKey: "test-key", + VerifySSL: false, + }) + require.Error(t, err) + + var result map[string]interface{} + err = c.Get(context.Background(), "any", nil, &result) + require.ErrorContains(t, err, "unable to decode body") +} + +type failingErrorHandler struct{} + +func (f *failingErrorHandler) HandleError(resp *http.Response) error { + return errors.New("custom error") +} + +func TestErrorHandlerCustom(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // For API style determination. + if r.URL.Path == "/" { + w.WriteHeader(http.StatusOK) + return + } + // For the API call. + if r.URL.Path == NewStyleAPI.ApiPath+"/error" { + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(`{"data":"ok"}`)) + if err != nil { + t.Error(err) + } + return + } + http.NotFound(w, r) + })) + defer ts.Close() + + customErrorHandler := &failingErrorHandler{} + c, err := NewClient(&ClientConfig{ + URL: ts.URL, + APIKey: "test-key", + VerifySSL: false, + ErrorHandler: customErrorHandler, + }) + require.Error(t, err) + + var result map[string]interface{} + err = c.Get(context.Background(), "error", nil, &result) + require.Error(t, err) + assert.Equal(t, "custom error", err.Error()) +} + +func TestCreateRequestURLInvalid(t *testing.T) { + t.Parallel() + c := &Client{ + BaseURL: &url.URL{Scheme: "http", Host: "localhost"}, + apiPaths: &NewStyleAPI, + } + _, err := c.createRequestURL("://bad-url") + require.Error(t, err) + assert.Contains(t, err.Error(), "parse") +} + +func TestCreateRequestURLAbsolute(t *testing.T) { + t.Parallel() + c := &Client{ + BaseURL: &url.URL{Scheme: "http", Host: "localhost"}, + apiPaths: &NewStyleAPI, + } + reqURL, err := c.createRequestURL("http://example.com/test") + require.NoError(t, err) + assert.Equal(t, "http://example.com/test", reqURL.String()) +} + +func TestCreateRequestContextTimeout(t *testing.T) { + t.Parallel() + c := &Client{ + config: &ClientConfig{Timeout: 100 * time.Millisecond}, + } + ctx, cancel := c.createRequestContext() + defer cancel() + _, ok := ctx.Deadline() + require.True(t, ok) + + // Wait for the deadline to expire. + time.Sleep(150 * time.Millisecond) + select { + case <-ctx.Done(): + assert.Equal(t, context.DeadlineExceeded, ctx.Err()) + default: + t.Error("expected context deadline exceeded") + } +} + +func TestMarshalRequestInvalid(t *testing.T) { + t.Parallel() + r, err := marshalRequest(make(chan int)) + require.Error(t, err) + assert.Contains(t, err.Error(), "json") + assert.Nil(t, r) +} + +func TestMarshalRequestValid(t *testing.T) { + t.Parallel() + r, err := marshalRequest(map[string]string{"key": "value"}) + require.NoError(t, err) + data, err := io.ReadAll(r) + require.NoError(t, err) + assert.JSONEq(t, `{"key":"value"}`, string(data)) +} + +func TestLoginWithAPIKeyDirect(t *testing.T) { + t.Parallel() + // Create a client manually with the APIKey set. + c := &Client{ + config: &ClientConfig{ + APIKey: "abc", + }, + } + err := c.Login() + assert.NoError(t, err) +} + +func TestHttpCustomizerError(t *testing.T) { + t.Parallel() + customizer := func(transport *http.Transport) error { + return errors.New("customization failed") + } + _, err := NewClient(&ClientConfig{ + URL: testUrl, + APIKey: "test-key", + VerifySSL: false, + HttpCustomizer: customizer, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed customizing HTTP transport") +}