Skip to content

Commit

Permalink
Implemented doRequest helper method and added unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ezilber-akamai committed Jul 22, 2024
1 parent 7a5aaec commit bb759a4
Show file tree
Hide file tree
Showing 2 changed files with 231 additions and 0 deletions.
102 changes: 102 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package linodego

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
Expand Down Expand Up @@ -71,6 +74,45 @@ type Client struct {
cachedEntryLock *sync.RWMutex
}

// Client is a wrapper around the Resty client
type HTTPClient struct {
//nolint:unused
httpClient *http.Client
//nolint:unused
userAgent string
//nolint:unused
debug bool
//nolint:unused
retryConditionals []RetryConditional

//nolint:unused
pollInterval time.Duration

//nolint:unused
baseURL string
//nolint:unused
apiVersion string
//nolint:unused
apiProto string
//nolint:unused
selectedProfile string
//nolint:unused
loadedProfile string

//nolint:unused
configProfiles map[string]ConfigProfile

// Fields for caching endpoint responses
//nolint:unused
shouldCache bool
//nolint:unused
cacheExpiration time.Duration
//nolint:unused
cachedEntries map[string]clientCacheEntry
//nolint:unused
cachedEntryLock *sync.RWMutex
}

type EnvDefaults struct {
Token string
Profile string
Expand Down Expand Up @@ -110,6 +152,66 @@ func (c *Client) SetUserAgent(ua string) *Client {
return c
}

type RequestParams struct {
Body map[string]any
Response any
}

// Generic helper to execute HTTP requests using the
//
//nolint:unused
func (c *HTTPClient) doRequest(ctx context.Context, method, url string, params RequestParams, mutators ...func(req *http.Request) error) error {
// Create a new HTTP request
var bodyReader io.Reader
if params.Body != nil {
buf := new(bytes.Buffer)
if err := json.NewEncoder(buf).Encode(params.Body); err != nil {
return fmt.Errorf("failed to encode body: %w", err)
}
bodyReader = buf
}

req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}

// Set default headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
if c.userAgent != "" {
req.Header.Set("User-Agent", c.userAgent)
}

// Apply mutators
for _, mutate := range mutators {
if err := mutate(req); err != nil {
return fmt.Errorf("failed to mutate request: %w", err)
}
}

// Send the request
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()

// Check for HTTP errors
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("received non-2xx status code: %d", resp.StatusCode)
}

// Decode the response body
if params.Response != nil {
if err := json.NewDecoder(resp.Body).Decode(params.Response); err != nil {
return fmt.Errorf("failed to decode response: %w", err)
}
}

return nil
}

// R wraps resty's R method
func (c *Client) R(ctx context.Context) *resty.Request {
return c.resty.R().
Expand Down
129 changes: 129 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package linodego
import (
"bytes"
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
Expand Down Expand Up @@ -198,3 +201,129 @@ func TestDebugLogSanitization(t *testing.T) {
t.Fatalf("actual response does not equal desired response: %s", cmp.Diff(result, testResponse))
}
}

func TestDoRequest_Success(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"message":"success"}`))
}
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()

client := &HTTPClient{
httpClient: server.Client(),
}

params := RequestParams{
Response: &map[string]string{},
}

err := client.doRequest(context.Background(), http.MethodGet, server.URL, params)
if err != nil {
t.Fatal(cmp.Diff(nil, err))
}

expected := "success"
actual := (*params.Response.(*map[string]string))["message"]
if diff := cmp.Diff(expected, actual); diff != "" {
t.Fatalf("response mismatch (-expected +actual):\n%s", diff)
}
}

func TestDoRequest_FailedEncodeBody(t *testing.T) {
client := &HTTPClient{
httpClient: http.DefaultClient,
}

params := RequestParams{
Body: map[string]interface{}{
"invalid": func() {},
},
}

err := client.doRequest(context.Background(), http.MethodPost, "http://example.com", params)
expectedErr := "failed to encode body"
if err == nil || !strings.Contains(err.Error(), expectedErr) {
t.Fatalf("expected error %q, got: %v", expectedErr, err)
}
}

func TestDoRequest_FailedCreateRequest(t *testing.T) {
client := &HTTPClient{
httpClient: http.DefaultClient,
}

// Create a request with an invalid URL to simulate a request creation failure
err := client.doRequest(context.Background(), http.MethodGet, "http://invalid url", RequestParams{})
expectedErr := "failed to create request"
if err == nil || !strings.Contains(err.Error(), expectedErr) {
t.Fatalf("expected error %q, got: %v", expectedErr, err)
}
}

func TestDoRequest_Non2xxStatusCode(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "error", http.StatusInternalServerError)
}
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()

client := &HTTPClient{
httpClient: server.Client(),
}

err := client.doRequest(context.Background(), http.MethodGet, server.URL, RequestParams{})
expectedErr := "received non-2xx status code"
if err == nil || !strings.Contains(err.Error(), expectedErr) {
t.Fatalf("expected error %q, got: %v", expectedErr, err)
}
}

func TestDoRequest_FailedDecodeResponse(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`invalid json`))
}
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()

client := &HTTPClient{
httpClient: server.Client(),
}

params := RequestParams{
Response: &map[string]string{},
}

err := client.doRequest(context.Background(), http.MethodGet, server.URL, params)
expectedErr := "failed to decode response"
if err == nil || !strings.Contains(err.Error(), expectedErr) {
t.Fatalf("expected error %q, got: %v", expectedErr, err)
}
}

func TestDoRequest_MutatorError(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"message":"success"}`))
}
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()

client := &HTTPClient{
httpClient: server.Client(),
}

mutator := func(req *http.Request) error {
return errors.New("mutator error")
}

err := client.doRequest(context.Background(), http.MethodGet, server.URL, RequestParams{}, mutator)
expectedErr := "failed to mutate request"
if err == nil || !strings.Contains(err.Error(), expectedErr) {
t.Fatalf("expected error %q, got: %v", expectedErr, err)
}
}

0 comments on commit bb759a4

Please sign in to comment.