diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..fadbf74 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,105 @@ +package main + +import ( + "github.com/pkg/errors" + "go-restclient/restclient" + "log" + "net/http" + "net/url" + "time" +) + +type dummyHttpResponse struct { + StatusCode int `json:"status_code"` + Data interface{} `json:"data"` +} + +type dummyBasicAuthenticator struct { + Username, Password string +} + +func NewTestBasicAuthenticator(username, password string) restclient.Authenticator { + return &dummyBasicAuthenticator{ + Username: username, + Password: password, + } +} + +func (b dummyBasicAuthenticator) Apply(request *http.Request) error { + request.SetBasicAuth(b.Username, b.Password) + return nil +} + +func usage1() error { + var response dummyHttpResponse + headers := http.Header{"Content-Type": []string{"application/json"}, "Cookie": []string{"test-1234"}} + queryParams := url.Values{"tenantId": []string{"d90c3101-53bc-4c54-94db-21582bab8e17"}, "vectorId": []string{"1"}} + ri := restclient.NewRequestInfo("https", "ysyesilyurt.com", []string{"tasks", "1"}, &queryParams, &headers, nil) + req, err := restclient.NewRequest(ri) + if err != nil { + return errors.Wrap(err, "Failed to construct http.Request out of RequestInfo") + } + + client := restclient.NewHttpClient(true, 30*time.Second) + cri := restclient.NewDoRequestInfo(req, nil, &response) + err = client.Get(cri) + if err != nil { + return errors.Wrap(err, "Failed to do HTTP GET request") + } + return nil +} + +func usage2() error { + var response dummyHttpResponse + headers := http.Header{"Content-Type": []string{"application/json"}, "Cookie": []string{"test-1234"}} + ri, err := restclient.NewRequestInfoFromRawURL("https://ysyesilyurt.com/tasks/1?tenantId=d90c3101-53bc-4c54-94db-21582bab8e17&vectorId=1", &headers, nil) + if err != nil { + return errors.Wrap(err, "Failed to construct RequestInfo out of Raw URL") + } + + req, err := restclient.NewRequest(ri) + if err != nil { + return errors.Wrap(err, "Failed to construct http.Request out of RequestInfo") + } + + client := restclient.NewHttpClient(true, 0) + auth := NewTestBasicAuthenticator("ysyesilyurt", "0123") + cri := restclient.NewDoRequestInfoWithTimeout(req, auth, &response, 15*time.Second) + err = client.Get(cri) + if err != nil { + return errors.Wrap(err, "Failed to do HTTP GET request") + } + return nil +} + +func usage3() error { + var response dummyHttpResponse + headers := http.Header{"Content-Type": []string{"application/json"}, "Cookie": []string{"test-1234"}} + ri, err := restclient.NewRequestInfoFromRawURL("https://ysyesilyurt.com/tasks/1?tenantId=d90c3101-53bc-4c54-94db-21582bab8e17&vectorId=1", &headers, nil) + if err != nil { + return errors.Wrap(err, "Failed to construct RequestInfo out of Raw URL") + } + + auth := NewTestBasicAuthenticator("ysyesilyurt", "0123") + err = restclient.PerformGetRequest(ri, auth, &response, true, 30*time.Second) + if err != nil { + return errors.Wrap(err, "Failed to perform HTTP GET request") + } + return nil +} + +func main() { + // Below usages will fail with `no such host` when run + err := usage1() + if err != nil { + log.Printf("Example usage1 failed, reason: %v", err) + } + err = usage2() + if err != nil { + log.Printf("Example usage2 failed, reason: %v", err) + } + err = usage3() + if err != nil { + log.Printf("Example usage3 failed, reason: %v", err) + } +} diff --git a/restclient/client_test.go b/restclient/client_test.go new file mode 100644 index 0000000..b1c6362 --- /dev/null +++ b/restclient/client_test.go @@ -0,0 +1,253 @@ +package restclient + +import ( + "encoding/json" + "fmt" + . "github.com/smartystreets/goconvey/convey" + "log" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +const ( + TestDataFormat = "%s - %s" + TestSuccess = "TestSuccess" + TestFailed = "TestFailed" +) + +type testBasicAuthenticator struct { + Username, Password string +} + +func NewTestBasicAuthenticator(username, password string) Authenticator { + return &testBasicAuthenticator{ + Username: username, + Password: password, + } +} + +func (b testBasicAuthenticator) Apply(request *http.Request) error { + request.SetBasicAuth(b.Username, b.Password) + return nil +} + +type testRequestBody struct { + TestId int `json:"test_id"` + TestName string `json:"test_name"` +} + +type testHttpResponse struct { + StatusCode int `json:"status_code"` + Data interface{} `json:"data"` +} + +func TestHttpClientRequests(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var response testHttpResponse + w.Header().Set("Content-Type", "application/json") + + handleRequest := func(reqMethod string, statusCode int, resultString string) { + username, password, ok := r.BasicAuth() + if !ok || username != "ysyesilyurt" || password != "0123" { + statusCode = http.StatusUnauthorized + resultString = TestFailed + } + response.StatusCode = statusCode + response.Data = fmt.Sprintf(TestDataFormat, resultString, reqMethod) + w.WriteHeader(statusCode) + } + + handleRequestWithBody := func(reqMethod string) { + status := http.StatusOK + resultString := TestSuccess + var body testRequestBody + err := UnmarshalRequestBody(r, &body) + if err != nil || body.TestName != "Testing Request Body" || body.TestId != 123 { + status = http.StatusBadRequest + resultString = TestFailed + } + handleRequest(reqMethod, status, resultString) + } + + switch r.Method { + case http.MethodGet: + handleRequest(http.MethodGet, http.StatusOK, TestSuccess) + case http.MethodPost: + handleRequestWithBody(http.MethodPost) + case http.MethodPut: + handleRequestWithBody(http.MethodPut) + case http.MethodPatch: + handleRequestWithBody(http.MethodPatch) + case http.MethodDelete: + handleRequest(http.MethodDelete, http.StatusOK, TestSuccess) + default: + response.StatusCode = http.StatusForbidden + response.Data = fmt.Sprintf(TestDataFormat, TestFailed, r.Method) + } + err := json.NewEncoder(w).Encode(response) + if err != nil { + log.Fatalf("httptest server failed to respond with testHttpResponse %v", err.Error()) + } + })) + defer ts.Close() + // e.g. ts URL: http://127.0.0.1:63316 + splittedURL := strings.Split(ts.URL, "://") + testServerScheme := splittedURL[0] + testServerHost := splittedURL[1] + + Convey("TEST HTTP GET", t, func() { + var testResponse testHttpResponse + testRequestInfo := NewRequestInfo(testServerScheme, testServerHost, nil, nil, nil, nil) + testRequest, err := NewRequest(testRequestInfo) + if err != nil { + log.Fatal("failed to construct testRequest") + } + client := NewHttpClient(true, 30*time.Second) + auth := NewTestBasicAuthenticator("ysyesilyurt", "0123") + cri := NewDoRequestInfo(testRequest, auth, &testResponse) + err = client.Get(cri) + Convey("Response should be fetched without any err and with proper Data", func() { + data, ok := testResponse.Data.(string) + So(err, ShouldBeNil) + So(testResponse.StatusCode, ShouldEqual, http.StatusOK) + So(ok, ShouldBeTrue) + So(data, ShouldEqual, fmt.Sprintf(TestDataFormat, TestSuccess, http.MethodGet)) + }) + }) + + Convey("TEST HTTP GET with failing authentication credentials", t, func() { + var testResponse testHttpResponse + testRequestInfo := NewRequestInfo(testServerScheme, testServerHost, nil, nil, nil, nil) + testRequest, err := NewRequest(testRequestInfo) + if err != nil { + log.Fatal("failed to construct testRequest") + } + client := NewHttpClient(true, 30*time.Second) + auth := NewTestBasicAuthenticator("FAILING", "CREDENTIALS") + cri := NewDoRequestInfo(testRequest, auth, &testResponse) + err = client.Get(cri) + Convey("Response should be fetched without any err and with proper Data", func() { + _, ok := testResponse.Data.(string) + So(ok, ShouldBeFalse) + So(testResponse.StatusCode, ShouldEqual, 0) + So(err, ShouldNotBeNil) + So(err.Error(), ShouldEqual, fmt.Sprintf("{\"status_code\":%d,\"data\":\"%s\"}\n: %s", http.StatusUnauthorized, fmt.Sprintf(TestDataFormat, TestFailed, http.MethodGet), unauthorizedErr)) + }) + }) + + Convey("TEST HTTP POST with correct body", t, func() { + var testResponse testHttpResponse + testBody := testRequestBody{ + TestId: 123, + TestName: "Testing Request Body", + } + testRequestInfo := NewRequestInfo(testServerScheme, testServerHost, nil, nil, nil, testBody) + testRequest, err := NewRequest(testRequestInfo) + if err != nil { + log.Fatal("failed to construct testRequest") + } + client := NewHttpClient(true, 30*time.Second) + auth := NewTestBasicAuthenticator("ysyesilyurt", "0123") + cri := NewDoRequestInfo(testRequest, auth, &testResponse) + err = client.Post(cri) + Convey("Response should be fetched without any err and with proper Data", func() { + data, ok := testResponse.Data.(string) + So(err, ShouldBeNil) + So(testResponse.StatusCode, ShouldEqual, http.StatusOK) + So(ok, ShouldBeTrue) + So(data, ShouldEqual, fmt.Sprintf(TestDataFormat, TestSuccess, http.MethodPost)) + }) + }) + + Convey("TEST HTTP POST with incorrect body", t, func() { + var testResponse testHttpResponse + testBody := "INCORRECT REQUEST BODY" + testRequestInfo := NewRequestInfo(testServerScheme, testServerHost, nil, nil, nil, testBody) + testRequest, err := NewRequest(testRequestInfo) + if err != nil { + log.Fatal("failed to construct testRequest") + } + client := NewHttpClient(true, 30*time.Second) + auth := NewTestBasicAuthenticator("ysyesilyurt", "0123") + cri := NewDoRequestInfo(testRequest, auth, &testResponse) + err = client.Post(cri) + Convey("Response should be fetched without any err and with proper Data", func() { + _, ok := testResponse.Data.(string) + So(ok, ShouldBeFalse) + So(testResponse.StatusCode, ShouldEqual, 0) + So(err, ShouldNotBeNil) + So(err.Error(), ShouldEqual, fmt.Sprintf("{\"status_code\":%d,\"data\":\"%s\"}\n: %s", http.StatusBadRequest, fmt.Sprintf(TestDataFormat, TestFailed, http.MethodPost), badRequestErr)) + }) + }) + + Convey("TEST HTTP PUT with correct body", t, func() { + var testResponse testHttpResponse + testBody := testRequestBody{ + TestId: 123, + TestName: "Testing Request Body", + } + testRequestInfo := NewRequestInfo(testServerScheme, testServerHost, nil, nil, nil, testBody) + testRequest, err := NewRequest(testRequestInfo) + if err != nil { + log.Fatal("failed to construct testRequest") + } + client := NewHttpClient(true, 30*time.Second) + auth := NewTestBasicAuthenticator("ysyesilyurt", "0123") + cri := NewDoRequestInfo(testRequest, auth, &testResponse) + err = client.Put(cri) + Convey("Response should be fetched without any err and with proper Data", func() { + data, ok := testResponse.Data.(string) + So(err, ShouldBeNil) + So(testResponse.StatusCode, ShouldEqual, http.StatusOK) + So(ok, ShouldBeTrue) + So(data, ShouldEqual, fmt.Sprintf(TestDataFormat, TestSuccess, http.MethodPut)) + }) + }) + + Convey("TEST HTTP PATCH with correct body", t, func() { + var testResponse testHttpResponse + testBody := testRequestBody{ + TestId: 123, + TestName: "Testing Request Body", + } + testRequestInfo := NewRequestInfo(testServerScheme, testServerHost, nil, nil, nil, testBody) + testRequest, err := NewRequest(testRequestInfo) + if err != nil { + log.Fatal("failed to construct testRequest") + } + client := NewHttpClient(true, 30*time.Second) + auth := NewTestBasicAuthenticator("ysyesilyurt", "0123") + cri := NewDoRequestInfo(testRequest, auth, &testResponse) + err = client.Patch(cri) + Convey("Response should be fetched without any err and with proper Data", func() { + data, ok := testResponse.Data.(string) + So(err, ShouldBeNil) + So(testResponse.StatusCode, ShouldEqual, http.StatusOK) + So(ok, ShouldBeTrue) + So(data, ShouldEqual, fmt.Sprintf(TestDataFormat, TestSuccess, http.MethodPatch)) + }) + }) + + Convey("TEST HTTP DELETE", t, func() { + var testResponse testHttpResponse + testRequestInfo := NewRequestInfo(testServerScheme, testServerHost, nil, nil, nil, nil) + testRequest, err := NewRequest(testRequestInfo) + if err != nil { + log.Fatal("failed to construct testRequest") + } + client := NewHttpClient(true, 30*time.Second) + auth := NewTestBasicAuthenticator("ysyesilyurt", "0123") + cri := NewDoRequestInfo(testRequest, auth, &testResponse) + err = client.Delete(cri) + Convey("Response should be fetched without any err and with proper Data", func() { + data, ok := testResponse.Data.(string) + So(err, ShouldBeNil) + So(testResponse.StatusCode, ShouldEqual, http.StatusOK) + So(ok, ShouldBeTrue) + So(data, ShouldEqual, fmt.Sprintf(TestDataFormat, TestSuccess, http.MethodDelete)) + }) + }) +} diff --git a/restclient/request_test.go b/restclient/request_test.go new file mode 100644 index 0000000..bf8a1e4 --- /dev/null +++ b/restclient/request_test.go @@ -0,0 +1,128 @@ +package restclient + +import ( + "bytes" + "encoding/json" + "fmt" + . "github.com/smartystreets/goconvey/convey" + "log" + "net/http" + "net/url" + "testing" +) + +type testBodyDto struct { + TestId int `json:"test_id"` + TestString string `json:"test_string"` +} + +func TestNewRequest(t *testing.T) { + tbd := testBodyDto{ + TestId: 123, + TestString: "1234", + } + wanted := createTestNewRequestWantArgs(tbd) + type args struct { + Scheme string + Host string + PathComponents []string + Headers *http.Header + Body interface{} + QueryParams *url.Values + } + tests := []struct { + name string + args args + want *http.Request + }{ + { + name: "Request without Body and pathParams, but has 2 query params", + args: args{ + Scheme: "https", + Host: "ysyesilyurt.com", + PathComponents: []string{"assessments", "scroll"}, + Headers: &http.Header{"Content-Type": []string{"application/json"}, "Cookie": []string{"test-1234", "1234-test"}, "Xsrf-Token": []string{"CSRFToken"}, "X-Xsrf-Token": []string{"ab4f3712-1cd4-4860-9fec-1276866403da"}}, + Body: nil, + QueryParams: &url.Values{"tenantId": []string{"d90c3101-53bc-4c54-94db-21582bab8e17"}, "vectorId": []string{"1"}}, + }, + want: wanted[0], + }, + { + name: "Request with Body and pathParams, but has no query params", + args: args{ + Scheme: "https", + Host: "ysyesilyurt.com", + PathComponents: []string{"assessments", "scroll", "d90c3101-53bc-4c54-94db-21582bab8e17", "1"}, + Headers: &http.Header{"Content-Type": []string{"application/json"}, "Cookie": []string{"test-1234", "1234-test"}}, + Body: tbd, + QueryParams: nil, + }, + want: wanted[1], + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ri := NewRequestInfo(tt.args.Scheme, tt.args.Host, tt.args.PathComponents, tt.args.QueryParams, tt.args.Headers, tt.args.Body) + req, err := NewRequest(ri) + Convey("Requests should be constructed without err and with proper fields", t, func() { + So(err, ShouldBeNil) + So(req.URL.Scheme, ShouldEqual, tt.want.URL.Scheme) + So(req.URL.Host, ShouldEqual, tt.want.URL.Host) + So(req.URL.Path, ShouldEqual, tt.want.URL.Path) + So(req.Header, ShouldResemble, tt.want.Header) + So(req.Body, ShouldResemble, tt.want.Body) + So(req.URL.RawQuery, ShouldEqual, tt.want.URL.RawQuery) + }) + }) + } +} + +func createTestNewRequestWantArgs(tbd testBodyDto) []*http.Request { + // Construct first resulting request + want1, err := http.NewRequest("", "https://ysyesilyurt.com/assessments/scroll?tenantId=d90c3101-53bc-4c54-94db-21582bab8e17&vectorId=1", nil) + if err != nil { + log.Fatalf("Could not construct first request want argument %s", err.Error()) + } + want1.Header.Set("Content-Type", "application/json") + want1.Header.Set("Cookie", "test-1234") + want1.Header.Add("Cookie", "1234-test") + want1.Header.Set("Xsrf-Token", "CSRFToken") + want1.Header.Set("X-Xsrf-Token", "ab4f3712-1cd4-4860-9fec-1276866403da") + + // Construct second resulting request + marshalled, err := json.Marshal(tbd) + if err != nil { + log.Fatalf("Could not marshal request body for the second request want argument %s", err.Error()) + } + want2, err := http.NewRequest("", "https://ysyesilyurt.com/assessments/scroll/d90c3101-53bc-4c54-94db-21582bab8e17/1", bytes.NewReader(marshalled)) + if err != nil { + log.Fatalf("Could not construct second request want argument %s", err.Error()) + } + want2.Header.Set("Content-Type", "application/json") + want2.Header.Set("Cookie", "test-1234") + want2.Header.Add("Cookie", "1234-test") + return []*http.Request{want1, want2} +} + +func TestNewRequestInfoFromURL(t *testing.T) { + Convey("TEST Parse RequestInfo From Raw URL", t, func() { + tbd := testBodyDto{ + TestId: 123, + TestString: "1234", + } + headers := http.Header{"Content-Type": []string{"application/json"}, "Cookie": []string{"test-1234"}} + queryParams := url.Values{"tenantId": []string{"d90c3101-53bc-4c54-94db-21582bab8e17"}, "vectorId": []string{"1"}} + wantRi := NewRequestInfo("https", "ysyesilyurt.com", []string{"assessments", "scroll", "d90c3101-53bc-4c54-94db-21582bab8e17", "1"}, &queryParams, &headers, tbd) + parsedRi, err := NewRequestInfoFromRawURL(fmt.Sprintf("https://ysyesilyurt.com/assessments/scroll/d90c3101-53bc-4c54-94db-21582bab8e17/1?tenantId=%s&vectorId=%s", "d90c3101-53bc-4c54-94db-21582bab8e17", "1"), &headers, tbd) + Convey("Parsed request info should be identical to correct request info", func() { + So(err, ShouldBeNil) + So(parsedRi.Scheme, ShouldEqual, wantRi.Scheme) + So(parsedRi.Host, ShouldEqual, wantRi.Host) + So(parsedRi.PathElements, ShouldResemble, wantRi.PathElements) + So(parsedRi.QueryParams, ShouldResemble, wantRi.QueryParams) + So(parsedRi.Headers, ShouldResemble, wantRi.Headers) + So(parsedRi.Body, ShouldResemble, wantRi.Body) + }) + }) +}