From ab9d7b286310ebde01471b1dd3a2c96ad02b354a Mon Sep 17 00:00:00 2001 From: reugn Date: Sun, 6 Aug 2023 10:42:36 +0300 Subject: [PATCH] refactor curl job structure --- README.md | 3 +- examples/main.go | 18 +++++- quartz/job.go | 95 +++++++++++++++---------------- quartz/job_test.go | 117 +++++++++++++++++++++++++++++++++++++++ quartz/scheduler_test.go | 14 +++-- 5 files changed, 187 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 56d2563..418b0de 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,8 @@ sched := quartz.NewStdScheduler() sched.Start(ctx) cronTrigger, _ := quartz.NewCronTrigger("1/5 * * * * *") shellJob := quartz.NewShellJob("ls -la") -curlJob, _ := quartz.NewCurlJob(http.MethodGet, "http://worldclockapi.com/api/json/est/now", "", nil) +request, _ := http.NewRequest(http.MethodGet, "https://worldtimeapi.org/api/timezone/utc", nil) +curlJob, _ := quartz.NewCurlJob(request) functionJob := quartz.NewFunctionJob(func(_ context.Context) (int, error) { return 42, nil }) sched.ScheduleJob(shellJob, cronTrigger) sched.ScheduleJob(curlJob, quartz.NewSimpleTrigger(time.Second*7)) diff --git a/examples/main.go b/examples/main.go index 1199dee..09c13d5 100644 --- a/examples/main.go +++ b/examples/main.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "io" "net/http" "sync" "time" @@ -71,7 +72,12 @@ func sampleJobs(ctx context.Context, wg *sync.WaitGroup) { } shellJob := quartz.NewShellJob("ls -la") - curlJob, err := quartz.NewCurlJob(http.MethodGet, "http://worldclockapi.com/api/json/est/now", "", nil) + request, err := http.NewRequest(http.MethodGet, "https://worldtimeapi.org/api/timezone/utc", nil) + if err != nil { + fmt.Println(err) + return + } + curlJob, err := quartz.NewCurlJob(request) if err != nil { fmt.Println(err) return @@ -86,8 +92,14 @@ func sampleJobs(ctx context.Context, wg *sync.WaitGroup) { fmt.Println(sched.GetJobKeys()) fmt.Println(shellJob.Result) - fmt.Println(curlJob.Response) - fmt.Println(functionJob.Result) + + responseBody, err := io.ReadAll(curlJob.Response.Body) + if err != nil { + fmt.Println(err) + } else { + fmt.Printf("%s\n%s\n", curlJob.Response.Status, string(responseBody)) + } + fmt.Printf("Function job result: %v\n", *functionJob.Result) time.Sleep(time.Second * 2) sched.Stop() diff --git a/quartz/job.go b/quartz/job.go index 88444c9..8979211 100644 --- a/quartz/job.go +++ b/quartz/job.go @@ -1,12 +1,11 @@ package quartz import ( - "bytes" "context" "fmt" - "io" "net/http" "os/exec" + "strings" "sync/atomic" ) @@ -80,77 +79,71 @@ func (sh *ShellJob) Execute(ctx context.Context) { // CurlJob represents a cURL command Job, implements the quartz.Job interface. // cURL is a command-line tool for getting or sending data including files using URL syntax. type CurlJob struct { - RequestMethod string - URL string - Body string - Headers map[string]string - Response string - StatusCode int - JobStatus JobStatus - request *http.Request -} - -// NewCurlJob returns a new CurlJob. -func NewCurlJob( - method string, - url string, - body string, - headers map[string]string, -) (*CurlJob, error) { - _body := bytes.NewBuffer([]byte(body)) - req, err := http.NewRequest(method, url, _body) - if err != nil { - return nil, err - } + httpClient HTTPHandler + request *http.Request + Response *http.Response + JobStatus JobStatus + description string +} - for k, v := range headers { - req.Header.Set(k, v) - } +// HTTPHandler sends an HTTP request and returns an HTTP response, +// following policy (such as redirects, cookies, auth) as configured +// on the implementing HTTP client. +type HTTPHandler interface { + Do(req *http.Request) (*http.Response, error) +} + +// NewCurlJob returns a new CurlJob using the default HTTP client. +func NewCurlJob(request *http.Request) (*CurlJob, error) { + return NewCurlJobWithHTTPClient(request, http.DefaultClient) +} +// NewCurlJobWithHTTPClient returns a new CurlJob using a custom HTTP client. +func NewCurlJobWithHTTPClient(request *http.Request, httpClient HTTPHandler) (*CurlJob, error) { return &CurlJob{ - RequestMethod: method, - URL: url, - Body: body, - Headers: headers, - Response: "", - StatusCode: -1, - JobStatus: NA, - request: req, + httpClient: httpClient, + request: request, + JobStatus: NA, + description: formatRequest(request), }, nil } // Description returns the description of the CurlJob. func (cu *CurlJob) Description() string { - return fmt.Sprintf("CurlJob: %s %s %s", cu.RequestMethod, cu.URL, cu.Body) + return fmt.Sprintf("CurlJob:\n%s", cu.description) } // Key returns the unique CurlJob key. func (cu *CurlJob) Key() int { - return HashCode(cu.Description()) + return HashCode(cu.description) +} + +func formatRequest(r *http.Request) string { + var request []string + url := fmt.Sprintf("%v %v %v", r.Method, r.URL, r.Proto) + request = append(request, url) + for name, headers := range r.Header { + for _, h := range headers { + request = append(request, fmt.Sprintf("%v: %v", name, h)) + } + } + if r.ContentLength > 0 { + request = append(request, fmt.Sprintf("Content Length: %d", r.ContentLength)) + } + return strings.Join(request, "\n") } // Execute is called by a Scheduler when the Trigger associated with this job fires. func (cu *CurlJob) Execute(ctx context.Context) { - client := &http.Client{} cu.request = cu.request.WithContext(ctx) - resp, err := client.Do(cu.request) - if err != nil { - cu.JobStatus = FAILURE - cu.StatusCode = -1 - cu.Response = err.Error() - return - } + var err error + cu.Response, err = cu.httpClient.Do(cu.request) - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - if resp.StatusCode >= 200 && resp.StatusCode < 400 { + if err == nil && cu.Response.StatusCode >= 200 && cu.Response.StatusCode < 400 { cu.JobStatus = OK } else { cu.JobStatus = FAILURE } - - cu.StatusCode = resp.StatusCode - cu.Response = string(body) } type isolatedJob struct { diff --git a/quartz/job_test.go b/quartz/job_test.go index f046941..fe9b1ea 100644 --- a/quartz/job_test.go +++ b/quartz/job_test.go @@ -3,8 +3,11 @@ package quartz_test import ( "context" "errors" + "fmt" "math/rand" + "net/http" "runtime" + "strings" "sync/atomic" "testing" "time" @@ -82,3 +85,117 @@ func TestMultipleExecution(t *testing.T) { t.Error("only one job should run") } } + +type httpHandlerMock struct { + doFunc func(req *http.Request) (*http.Response, error) +} + +func (m httpHandlerMock) Do(req *http.Request) (*http.Response, error) { + return m.doFunc(req) +} + +var worldtimeapiURL = "https://worldtimeapi.org/api/timezone/utc" + +func TestCurlJob(t *testing.T) { + request, err := http.NewRequest(http.MethodGet, worldtimeapiURL, nil) + if err != nil { + t.Error(err) + } + handlerOk := struct{ httpHandlerMock }{} + handlerOk.doFunc = func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Request: request, + }, nil + } + handlerErr := struct{ httpHandlerMock }{} + handlerErr.doFunc = func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 500, + Request: request, + }, nil + } + + tests := []struct { + name string + request *http.Request + client quartz.HTTPHandler + expectedStatus quartz.JobStatus + }{ + { + name: "HTTP 200 OK", + request: request, + client: handlerOk, + expectedStatus: quartz.OK, + }, + { + name: "HTTP 500 Internal Server Error", + request: request, + client: handlerErr, + expectedStatus: quartz.FAILURE, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + httpJob, err := quartz.NewCurlJobWithHTTPClient(tt.request, tt.client) + if err != nil { + t.Error(err) + } + httpJob.Execute(context.Background()) + assertEqual(t, httpJob.JobStatus, tt.expectedStatus) + }) + } +} + +func TestCurlJobDescription(t *testing.T) { + postRequest, err := http.NewRequest( + http.MethodPost, + worldtimeapiURL, + strings.NewReader("{\"a\":1}"), + ) + if err != nil { + t.Error(err) + } + postRequest.Header = http.Header{ + "Content-Type": {"application/json"}, + } + getRequest, err := http.NewRequest( + http.MethodGet, + worldtimeapiURL, + nil, + ) + if err != nil { + t.Error(err) + } + + tests := []struct { + name string + request *http.Request + expectedDescription string + }{ + { + name: "POST with headers and body", + request: postRequest, + expectedDescription: "CurlJob:\n" + + fmt.Sprintf("POST %s HTTP/1.1\n", worldtimeapiURL) + + "Content-Type: application/json\n" + + "Content Length: 7", + }, + { + name: "Get request", + request: getRequest, + expectedDescription: "CurlJob:\n" + + fmt.Sprintf("GET %s HTTP/1.1", worldtimeapiURL), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + httpJob, err := quartz.NewCurlJobWithHTTPClient(tt.request, http.DefaultClient) + if err != nil { + t.Error(err) + } + assertEqual(t, httpJob.Description(), tt.expectedDescription) + }) + } +} diff --git a/quartz/scheduler_test.go b/quartz/scheduler_test.go index 1b16d88..e145307 100644 --- a/quartz/scheduler_test.go +++ b/quartz/scheduler_test.go @@ -22,7 +22,9 @@ func TestScheduler(t *testing.T) { shellJob.Description() jobKeys[0] = shellJob.Key() - curlJob, err := quartz.NewCurlJob(http.MethodGet, "http://worldclockapi.com/api/json/est/now", "", nil) + request, err := http.NewRequest(http.MethodGet, "https://worldtimeapi.org/api/timezone/utc", nil) + assertEqual(t, err, nil) + curlJob, err := quartz.NewCurlJob(request) assertEqual(t, err, nil) curlJob.Description() jobKeys[1] = curlJob.Key() @@ -30,7 +32,9 @@ func TestScheduler(t *testing.T) { errShellJob := quartz.NewShellJob("ls -z") jobKeys[2] = errShellJob.Key() - errCurlJob, err := quartz.NewCurlJob(http.MethodGet, "http://", "", nil) + request, err = http.NewRequest(http.MethodGet, "http://", nil) + assertEqual(t, err, nil) + errCurlJob, err := quartz.NewCurlJob(request) assertEqual(t, err, nil) jobKeys[3] = errCurlJob.Key() @@ -42,7 +46,7 @@ func TestScheduler(t *testing.T) { time.Sleep(time.Second) scheduledJobKeys := sched.GetJobKeys() - assertEqual(t, scheduledJobKeys, []int{3668896347, 328790344}) + assertEqual(t, scheduledJobKeys, []int{3668896347, 2787962474}) _, err = sched.GetScheduledJob(jobKeys[0]) if err != nil { @@ -56,12 +60,12 @@ func TestScheduler(t *testing.T) { scheduledJobKeys = sched.GetJobKeys() assertEqual(t, len(scheduledJobKeys), 1) - assertEqual(t, scheduledJobKeys, []int{328790344}) + assertEqual(t, scheduledJobKeys, []int{2787962474}) sched.Clear() sched.Stop() assertEqual(t, shellJob.JobStatus, quartz.OK) - // assertEqual(t, curlJob.JobStatus, quartz.OK) + assertEqual(t, curlJob.JobStatus, quartz.OK) assertEqual(t, errShellJob.JobStatus, quartz.FAILURE) assertEqual(t, errCurlJob.JobStatus, quartz.FAILURE) }