github.com/instill-ai/component@v0.16.0-beta/pkg/connector/util/httpclient/httpclient_test.go (about)

     1  package httpclient
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"net/http"
     7  	"net/http/httptest"
     8  	"testing"
     9  
    10  	qt "github.com/frankban/quicktest"
    11  	"go.uber.org/zap"
    12  	"go.uber.org/zap/zaptest/observer"
    13  
    14  	"github.com/instill-ai/x/errmsg"
    15  )
    16  
    17  func TestClient_SendReqAndUnmarshal(t *testing.T) {
    18  	c := qt.New(t)
    19  
    20  	const testName = "Pokédex"
    21  	const path = "/137"
    22  	data := struct{ Name string }{Name: "Porygon"}
    23  
    24  	c.Run("ok - with default headers", func(c *qt.C) {
    25  		h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    26  			c.Check(r.URL.Path, qt.Equals, path)
    27  
    28  			c.Check(r.Header.Get("Content-Type"), qt.Equals, MIMETypeJSON)
    29  			c.Check(r.Header.Get("Accept"), qt.Equals, MIMETypeJSON)
    30  
    31  			c.Assert(r.Body, qt.IsNotNil)
    32  			defer r.Body.Close()
    33  
    34  			body, err := io.ReadAll(r.Body)
    35  			c.Assert(err, qt.IsNil)
    36  			c.Check(body, qt.JSONEquals, data)
    37  
    38  			w.Header().Set("Content-Type", "application/json")
    39  			fmt.Fprintln(w, `{"added": 1}`)
    40  		})
    41  
    42  		srv := httptest.NewServer(h)
    43  		c.Cleanup(srv.Close)
    44  
    45  		client := New(testName, srv.URL)
    46  
    47  		var got okBody
    48  		resp, err := client.R().
    49  			SetBody(data).
    50  			SetResult(&got).
    51  			Post(path)
    52  
    53  		c.Assert(err, qt.IsNil)
    54  		c.Check(resp.IsError(), qt.IsFalse)
    55  		c.Check(got.Added, qt.Equals, 1)
    56  	})
    57  
    58  	c.Run("nok - client error", func(c *qt.C) {
    59  		zCore, zLogs := observer.New(zap.InfoLevel)
    60  		host := "https://uninitialized.server.zz"
    61  
    62  		client := New(testName, host, WithLogger(zap.New(zCore)))
    63  
    64  		_, err := client.R().Post(path)
    65  		c.Check(err, qt.ErrorMatches, ".*no such host*")
    66  
    67  		logs := zLogs.All()
    68  		c.Assert(logs, qt.HasLen, 1)
    69  
    70  		entry := logs[0].ContextMap()
    71  		c.Check(err, qt.ErrorMatches, fmt.Sprintf(".*%s", entry["error"]))
    72  		c.Check(entry["url"], qt.Equals, host+path)
    73  	})
    74  
    75  	testcases := []struct {
    76  		name           string
    77  		gotStatus      int
    78  		gotBody        string
    79  		gotContentType string
    80  		wantIssue      string
    81  		wantLogFields  []string
    82  	}{
    83  		{
    84  			name:           "nok - 401 (unexpected response body)",
    85  			gotStatus:      http.StatusUnauthorized,
    86  			gotContentType: "plain/text",
    87  			gotBody:        `Incorrect API key`,
    88  			wantIssue:      fmt.Sprintf("%s responded with a 401 status code. Incorrect API key", testName),
    89  			wantLogFields:  []string{"url", "body", "status"},
    90  		},
    91  		{
    92  			name:          "nok - 401 (no response body)",
    93  			gotStatus:     http.StatusUnauthorized,
    94  			wantIssue:     fmt.Sprintf("%s responded with a 401 status code. Please refer to %s's API reference for more information.", testName, testName),
    95  			wantLogFields: []string{"url", "body", "status"},
    96  		},
    97  		{
    98  			name:           "nok - 401",
    99  			gotStatus:      http.StatusUnauthorized,
   100  			gotContentType: "application/json",
   101  			gotBody:        `{ "message": "Incorrect API key provided." }`,
   102  			wantIssue:      fmt.Sprintf("%s responded with a 401 status code. Incorrect API key provided.", testName),
   103  			wantLogFields:  []string{"url", "body", "status"},
   104  		},
   105  		{
   106  			name:           "nok - JSON error",
   107  			gotStatus:      http.StatusOK,
   108  			gotContentType: "application/json",
   109  			gotBody:        `{ `,
   110  			wantLogFields:  []string{"url", "body", "error"},
   111  		},
   112  	}
   113  
   114  	for _, tc := range testcases {
   115  		c.Run(tc.name, func(c *qt.C) {
   116  			h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   117  				w.Header().Set("Content-Type", tc.gotContentType)
   118  				w.WriteHeader(tc.gotStatus)
   119  				fmt.Fprintln(w, tc.gotBody)
   120  			})
   121  
   122  			srv := httptest.NewServer(h)
   123  			c.Cleanup(srv.Close)
   124  
   125  			var errResp errBody
   126  			zCore, zLogs := observer.New(zap.InfoLevel)
   127  			client := New(testName, srv.URL,
   128  				WithLogger(zap.New(zCore)),
   129  				WithEndUserError(errResp),
   130  			)
   131  
   132  			_, err := client.R().SetResult(new(okBody)).Post(path)
   133  			c.Check(err, qt.IsNotNil)
   134  			c.Check(errmsg.Message(err), qt.Equals, tc.wantIssue)
   135  
   136  			// Error log contains desired keys.
   137  			for _, k := range tc.wantLogFields {
   138  				logs := zLogs.FilterFieldKey(k)
   139  				c.Check(logs.Len(), qt.Equals, 1, qt.Commentf("missing field in log: %s", k))
   140  			}
   141  
   142  			// All logs contain the "name" key. Sometimes (e.g. on
   143  			// unmarshalling error) we'll have > 1 log so the assertion above
   144  			// is too particular.
   145  			logs := zLogs.FilterFieldKey("name")
   146  			c.Assert(logs.Len(), qt.Not(qt.Equals), 0)
   147  			c.Check(logs.All()[0].ContextMap()["name"], qt.Equals, testName)
   148  		})
   149  	}
   150  }
   151  
   152  type okBody struct {
   153  	Added int `json:"added"`
   154  }
   155  
   156  // errBody is the error paylaod of the test API.
   157  type errBody struct {
   158  	Msg string `json:"message"`
   159  }
   160  
   161  // Message is a way to access the error message from the error payload.
   162  func (e errBody) Message() string {
   163  	return e.Msg
   164  }