package webhook import ( "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "io" "net/http" "net/http/httptest" "sync/atomic" "testing" "time" ) func TestSendWebhookRequestSetsHeaders(test *testing.T) { var capturedRequest *http.Request var capturedBody []byte testServer := httptest.NewServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { capturedRequest = request capturedBody, _ = io.ReadAll(request.Body) responseWriter.WriteHeader(http.StatusOK) })) defer testServer.Close() dispatcher := &Dispatcher{ httpClient: testServer.Client(), } subscriber := webhookSubscriber{ UserIdentifier: "user-1", WebhookURL: testServer.URL, WebhookSecret: nil, } payload := []byte(`{"event":"entries.created"}`) deliveryError := dispatcher.sendWebhookRequest(context.Background(), subscriber, payload) require.NoError(test, deliveryError) assert.Equal(test, "application/json", capturedRequest.Header.Get("Content-Type")) assert.Equal(test, "asa.news Webhook/1.0", capturedRequest.Header.Get("User-Agent")) assert.Empty(test, capturedRequest.Header.Get("X-Asa-Signature-256")) assert.Equal(test, `{"event":"entries.created"}`, string(capturedBody)) } func TestSendWebhookRequestHMACSignature(test *testing.T) { var capturedSignatureHeader string testServer := httptest.NewServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { capturedSignatureHeader = request.Header.Get("X-Asa-Signature-256") responseWriter.WriteHeader(http.StatusOK) })) defer testServer.Close() dispatcher := &Dispatcher{ httpClient: testServer.Client(), } webhookSecret := "my-super-secret-key" subscriber := webhookSubscriber{ UserIdentifier: "user-1", WebhookURL: testServer.URL, WebhookSecret: &webhookSecret, } payload := []byte(`{"event":"entries.created","entries":[]}`) deliveryError := dispatcher.sendWebhookRequest(context.Background(), subscriber, payload) require.NoError(test, deliveryError) expectedMAC := hmac.New(sha256.New, []byte(webhookSecret)) expectedMAC.Write(payload) expectedSignature := "sha256=" + hex.EncodeToString(expectedMAC.Sum(nil)) assert.Equal(test, expectedSignature, capturedSignatureHeader) } func TestSendWebhookRequestEmptySecretNoSignature(test *testing.T) { var capturedSignatureHeader string testServer := httptest.NewServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { capturedSignatureHeader = request.Header.Get("X-Asa-Signature-256") responseWriter.WriteHeader(http.StatusOK) })) defer testServer.Close() dispatcher := &Dispatcher{ httpClient: testServer.Client(), } emptySecret := "" subscriber := webhookSubscriber{ UserIdentifier: "user-1", WebhookURL: testServer.URL, WebhookSecret: &emptySecret, } deliveryError := dispatcher.sendWebhookRequest(context.Background(), subscriber, []byte(`{}`)) require.NoError(test, deliveryError) assert.Empty(test, capturedSignatureHeader) } func TestSendWebhookRequestReturnsErrorOnServerError(test *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { responseWriter.WriteHeader(http.StatusInternalServerError) })) defer testServer.Close() dispatcher := &Dispatcher{ httpClient: testServer.Client(), } subscriber := webhookSubscriber{ UserIdentifier: "user-1", WebhookURL: testServer.URL, WebhookSecret: nil, } deliveryError := dispatcher.sendWebhookRequest(context.Background(), subscriber, []byte(`{}`)) assert.Error(test, deliveryError) assert.Contains(test, deliveryError.Error(), "500") } func TestSendWebhookRequestAccepts2xxStatuses(test *testing.T) { successCodes := []int{200, 201, 202, 204} for _, statusCode := range successCodes { testServer := httptest.NewServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { responseWriter.WriteHeader(statusCode) })) dispatcher := &Dispatcher{ httpClient: testServer.Client(), } subscriber := webhookSubscriber{ UserIdentifier: "user-1", WebhookURL: testServer.URL, WebhookSecret: nil, } deliveryError := dispatcher.sendWebhookRequest(context.Background(), subscriber, []byte(`{}`)) assert.NoError(test, deliveryError, "expected status %d to succeed", statusCode) testServer.Close() } } func TestSendWebhookRequestReturnsErrorOn4xx(test *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { responseWriter.WriteHeader(http.StatusBadRequest) })) defer testServer.Close() dispatcher := &Dispatcher{ httpClient: testServer.Client(), } subscriber := webhookSubscriber{ UserIdentifier: "user-1", WebhookURL: testServer.URL, WebhookSecret: nil, } deliveryError := dispatcher.sendWebhookRequest(context.Background(), subscriber, []byte(`{}`)) assert.Error(test, deliveryError) assert.Contains(test, deliveryError.Error(), "400") } func TestWebhookPayloadSerialization(test *testing.T) { title := "Test Entry" entryURL := "https://example.com/1" payload := WebhookPayload{ Event: "entries.created", Timestamp: "2024-01-01T12:00:00Z", Entries: []EntryPayload{ { EntryIdentifier: "entry-1", FeedIdentifier: "feed-1", GUID: "guid-1", URL: &entryURL, Title: &title, Author: nil, Summary: nil, PublishedAt: nil, EnclosureURL: nil, EnclosureType: nil, }, }, } assert.Equal(test, "entries.created", payload.Event) assert.Len(test, payload.Entries, 1) assert.Equal(test, "entry-1", payload.Entries[0].EntryIdentifier) assert.Equal(test, "Test Entry", *payload.Entries[0].Title) assert.Nil(test, payload.Entries[0].Author) } func TestSendWebhookRequestTimeout(test *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { time.Sleep(2 * time.Second) responseWriter.WriteHeader(http.StatusOK) })) defer testServer.Close() dispatcher := &Dispatcher{ httpClient: &http.Client{ Timeout: 50 * time.Millisecond, }, } subscriber := webhookSubscriber{ UserIdentifier: "user-1", WebhookURL: testServer.URL, WebhookSecret: nil, } deliveryError := dispatcher.sendWebhookRequest(context.Background(), subscriber, []byte(`{}`)) assert.Error(test, deliveryError) } func TestSendWebhookRequestRetryCount(test *testing.T) { var requestCount atomic.Int32 testServer := httptest.NewServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { requestCount.Add(1) responseWriter.WriteHeader(http.StatusInternalServerError) })) defer testServer.Close() dispatcher := &Dispatcher{ httpClient: testServer.Client(), } subscriber := webhookSubscriber{ UserIdentifier: "user-1", WebhookURL: testServer.URL, WebhookSecret: nil, } for range 4 { dispatcher.sendWebhookRequest(context.Background(), subscriber, []byte(`{}`)) } assert.Equal(test, int32(4), requestCount.Load()) }