Skip to content

Commit

Permalink
refactor curl job structure
Browse files Browse the repository at this point in the history
  • Loading branch information
reugn committed Aug 6, 2023
1 parent 30f5d6a commit ab9d7b2
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 60 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
18 changes: 15 additions & 3 deletions examples/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"io"
"net/http"
"sync"
"time"
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down
95 changes: 44 additions & 51 deletions quartz/job.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package quartz

import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"os/exec"
"strings"
"sync/atomic"
)

Expand Down Expand Up @@ -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 {
Expand Down
117 changes: 117 additions & 0 deletions quartz/job_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ package quartz_test
import (
"context"
"errors"
"fmt"
"math/rand"
"net/http"
"runtime"
"strings"
"sync/atomic"
"testing"
"time"
Expand Down Expand Up @@ -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)
})
}
}
14 changes: 9 additions & 5 deletions quartz/scheduler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,19 @@ 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()

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()

Expand All @@ -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 {
Expand All @@ -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)
}
Expand Down

0 comments on commit ab9d7b2

Please sign in to comment.