diff --git a/CHANGELOG.md b/CHANGELOG.md index 6447721..4475dc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added +- Add tests suite for the Webhook sender package (#19) - Refactor ProcessWebhooks for clarity, safety, and testability (#3 ) - Implement Payload Signing with webhook-signature Header in SendWebhook (#6) - Implement File Logging Module for Enhanced Error Handling (#7) diff --git a/webhook/logging/logging.go b/webhook/logging/logging.go index 682b5d4..ea7199c 100644 --- a/webhook/logging/logging.go +++ b/webhook/logging/logging.go @@ -25,9 +25,9 @@ func currentDateTime() string { return time.Now().Format("2006-01-02 15:04:05") } -func WebhookLogger(errorType string, errorMessage error) error { +var WebhookLogger = func(errorType string, errorMessage error) error { logFileDate := currentDate() - logFileName := fmt.Sprintf("../logs/%s.log", logFileDate) + logFileName := fmt.Sprintf("%s.log", logFileDate) file, err := os.OpenFile(logFileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { diff --git a/webhook/queue/worker.go b/webhook/queue/worker.go index 65a5a14..2ce2e6d 100644 --- a/webhook/queue/worker.go +++ b/webhook/queue/worker.go @@ -62,7 +62,7 @@ func retryWithExponentialBackoff(payload redisClient.WebhookPayload) error { time.Sleep(backoffTime) } - logging.WebhookLogger(logging.WarningType, fmt.Errorf("maximum retries reached: %s", maxRetries)) + logging.WebhookLogger(logging.WarningType, fmt.Errorf("maximum retries reached: %d", maxRetries)) return nil } diff --git a/webhook/sender/sender_test.go b/webhook/sender/sender_test.go new file mode 100644 index 0000000..84f61a3 --- /dev/null +++ b/webhook/sender/sender_test.go @@ -0,0 +1,81 @@ +package sender + +/* +This package contains mostly test functions for the utils used to send webhooks. If these utils works, +we can ensure that the send webhook function will function normally too. For a whole test suite for the sender function, +check out the webhook_test.go file. +*/ + +import ( + "bytes" + "io" + "net/http" + "testing" +) + +type MockClient struct { + MockDo func(req *http.Request) (*http.Response, error) +} + +func (m *MockClient) Do(req *http.Request) (*http.Response, error) { + return m.MockDo(req) +} + +// Testing the MarshalJSON method +func TestMarshalJSON(t *testing.T) { + data := map[string]string{ + "key": "value", + } + expectedJSON := `{"key":"value"}` + + jsonBytes, err := marshalJSON(data) + if err != nil { + t.Fatalf("Expected no error, but got: %v", err) + } + + if string(jsonBytes) != expectedJSON { + t.Fatalf("Expected %s, but got %s", expectedJSON, string(jsonBytes)) + } +} + +// Test for prepareRequest +func TestPrepareRequest(t *testing.T) { + url := "http://example.com/webhook" + jsonBytes := []byte(`{"key":"value"}`) + secretHash := "secret123" + + req, err := prepareRequest(url, jsonBytes, secretHash) + if err != nil { + t.Fatalf("Expected no error, but got: %v", err) + } + + if req.Header.Get("Content-Type") != "application/json" { + t.Fatalf("Expected header Content-Type to be application/json but got %s", req.Header.Get("Content-Type")) + } + + if req.Header.Get("X-Secret-Hash") != secretHash { + t.Fatalf("Expected header X-Secret-Hash to be %s but got %s", secretHash, req.Header.Get("X-Secret-Hash")) + } +} + +func TestSendRequest(t *testing.T) { + HTTPClient = &MockClient{ + MockDo: func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString("OK")), + }, nil + }, + } + + req, _ := http.NewRequest("GET", "http://example.com", nil) + resp, err := sendRequest(req) + if err != nil { + t.Fatalf("Expected no error, but got: %v", err) + } + + body, _ := io.ReadAll(resp.Body) + if string(body) != "OK" { + t.Fatalf("Expected body to be OK but got %s", string(body)) + } +} diff --git a/webhook/sender/utils.go b/webhook/sender/utils.go index aa7a770..9172ca5 100644 --- a/webhook/sender/utils.go +++ b/webhook/sender/utils.go @@ -9,9 +9,13 @@ import ( "webhook/logging" ) -var HTTPClient = &http.Client{} +type HTTPDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +var HTTPClient HTTPDoer = &http.Client{} -func marshalJSON(data interface{}) ([]byte, error) { +var marshalJSON = func(data interface{}) ([]byte, error) { jsonBytes, err := json.Marshal(data) if err != nil { logging.WebhookLogger(logging.ErrorType, fmt.Errorf("error marshaling JSON: %s", err)) @@ -20,7 +24,7 @@ func marshalJSON(data interface{}) ([]byte, error) { return jsonBytes, nil } -func prepareRequest(url string, jsonBytes []byte, secretHash string) (*http.Request, error) { +var prepareRequest = func(url string, jsonBytes []byte, secretHash string) (*http.Request, error) { req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBytes)) if err != nil { logging.WebhookLogger(logging.ErrorType, fmt.Errorf("error during the webhook request preparation")) @@ -36,7 +40,7 @@ func prepareRequest(url string, jsonBytes []byte, secretHash string) (*http.Requ return req, nil } -func sendRequest(req *http.Request) (*http.Response, error) { +var sendRequest = func(req *http.Request) (*http.Response, error) { resp, err := HTTPClient.Do(req) if err != nil { return nil, err @@ -44,13 +48,13 @@ func sendRequest(req *http.Request) (*http.Response, error) { return resp, nil } -func closeResponse(body io.ReadCloser) { +var closeResponse = func(body io.ReadCloser) { if err := body.Close(); err != nil { logging.WebhookLogger(logging.ErrorType, fmt.Errorf("error closing response body: %s", err)) } } -func processResponse(resp *http.Response) (string, []byte, error) { +var processResponse = func(resp *http.Response) (string, []byte, error) { respBody, err := io.ReadAll(resp.Body) if err != nil { logging.WebhookLogger(logging.ErrorType, fmt.Errorf("error reading response body: %s", err)) diff --git a/webhook/sender/webhook_test.go b/webhook/sender/webhook_test.go new file mode 100644 index 0000000..1d9bbfa --- /dev/null +++ b/webhook/sender/webhook_test.go @@ -0,0 +1,92 @@ +package sender + +import ( + "errors" + "net/http" + "testing" + "webhook/logging" +) + +var ( + marshalJSONOrig = marshalJSON + prepareRequestOrig = prepareRequest + sendRequestOrig = sendRequest + processResponseOrig = processResponse + webhookLoggerInvoked = false +) + +func TestSendWebhook(t *testing.T) { + logging.WebhookLogger = func(errorType string, errorMessage error) error { + webhookLoggerInvoked = true + return nil + } + + t.Run("Successful webhook sending", func(t *testing.T) { + resetMocks() // Reset all mocks to original functions + + err := SendWebhook(nil, "http://dummy.com", "webhookId", "secretHash") + if err != nil { + t.Fatalf("Expected no error, but got: %v", err) + } + }) + + t.Run("Failed webhook due to marshaling errors", func(t *testing.T) { + resetMocks() + marshalJSON = func(data interface{}) ([]byte, error) { + return nil, errors.New("marshaling error") + } + + err := SendWebhook(nil, "http://dummy.com", "webhookId", "secretHash") + if err == nil || err.Error() != "marshaling error" { + t.Fatalf("Expected marshaling error, but got: %v", err) + } + }) + + t.Run("Failed webhook due to request preparation errors", func(t *testing.T) { + resetMocks() + prepareRequest = func(url string, jsonBytes []byte, secretHash string) (*http.Request, error) { + return nil, errors.New("request preparation error") + } + + err := SendWebhook(nil, "http://dummy.com", "webhookId", "secretHash") + if err == nil || err.Error() != "request preparation error" { + t.Fatalf("Expected request preparation error, but got: %v", err) + } + }) + + t.Run("Failed webhook due to response processing errors", func(t *testing.T) { + resetMocks() + processResponse = func(resp *http.Response) (string, []byte, error) { + return "failed", nil, errors.New("response processing error") + } + + err := SendWebhook(nil, "http://dummy.com", "webhookId", "secretHash") + if err == nil || err.Error() != "response processing error" { + t.Fatalf("Expected response processing error, but got: %v", err) + } + }) + + t.Run("Logging on failed webhook delivery", func(t *testing.T) { + resetMocks() + processResponse = func(resp *http.Response) (string, []byte, error) { + return "failed", []byte("error body"), nil + } + + webhookLoggerInvoked = false + err := SendWebhook(nil, "http://dummy.com", "webhookId", "secretHash") + if err == nil || err.Error() != "failed" { + t.Fatalf("Expected failed status, but got: %v", err) + } + + if !webhookLoggerInvoked { + t.Fatalf("Expected WebhookLogger to be invoked") + } + }) +} + +func resetMocks() { + marshalJSON = marshalJSONOrig + prepareRequest = prepareRequestOrig + sendRequest = sendRequestOrig + processResponse = processResponseOrig +}