From b9251201c66470f29512281d592fc9d1751e795c Mon Sep 17 00:00:00 2001 From: Neil Vohra Date: Thu, 12 Mar 2026 10:05:59 +0000 Subject: [PATCH] Correct Slack notification URLs and add manual trigger test file --- .../pkg/webhook/manual_trigger_test.go | 115 ++++++++++++++++++ workers/webhook/pkg/webhook/sender_test.go | 41 ++++++- workers/webhook/pkg/webhook/slack.go | 13 +- workers/webhook/pkg/webhook/slack_test.go | 8 +- 4 files changed, 170 insertions(+), 7 deletions(-) create mode 100644 workers/webhook/pkg/webhook/manual_trigger_test.go diff --git a/workers/webhook/pkg/webhook/manual_trigger_test.go b/workers/webhook/pkg/webhook/manual_trigger_test.go new file mode 100644 index 000000000..514d38f3d --- /dev/null +++ b/workers/webhook/pkg/webhook/manual_trigger_test.go @@ -0,0 +1,115 @@ +//go:build manual + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package webhook + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "github.com/GoogleChrome/webstatus.dev/lib/workertypes" +) + +// TestManualSlackTrigger is a helper "test" that sends a real Slack message. +// To run this test, fill in a valid slackURL below and run: +// go test -v -tags=manual -run TestManualSlackTrigger ./workers/webhook/pkg/webhook +func TestManualSlackTrigger(t *testing.T) { + slackURL := "" // <--- FILL THIS IN + if slackURL == "" { + t.Skip("slackURL not set, skipping manual trigger test") + } + + testCases := []struct { + name string + query string + text string + }{ + { + name: "Search Query", + query: "baseline_status:newly", + text: "Manual Test: Search update for 'baseline_status:newly'", + }, + { + name: "Feature Query", + query: "id:\"anchor-positioning\"", + text: "Manual Test: Feature update for 'Anchor Positioning'", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + summary := workertypes.EventSummary{ + SchemaVersion: "v1", + Text: tc.text, + Categories: workertypes.SummaryCategories{Added: 1}, + Highlights: []workertypes.SummaryHighlight{ + { + Type: workertypes.SummaryHighlightTypeAdded, + FeatureID: "anchor-positioning", + FeatureName: "Anchor Positioning", + }, + }, + } + summaryRaw, _ := json.Marshal(summary) + + job := workertypes.IncomingWebhookDeliveryJob{ + WebhookDeliveryJob: workertypes.WebhookDeliveryJob{ + WebhookURL: slackURL, + SummaryRaw: summaryRaw, + Metadata: workertypes.DeliveryMetadata{ + EventID: "manual-trigger-event", + SearchID: "manual-search-id", + SearchName: tc.name, + Query: tc.query, + Frequency: workertypes.FrequencyImmediate, + GeneratedAt: time.Now(), + }, + Triggers: []workertypes.JobTrigger{workertypes.FeaturePromotedToNewly}, + ChannelID: "manual-channel-id", + WebhookType: workertypes.WebhookTypeSlack, + }, + WebhookEventID: "manual-webhook-event-id", + } + + mgr := &slackManager{ + frontendBaseURL: "http://localhost:5555", + httpClient: &http.Client{}, + stateManager: &noopStateManager{}, // Don't try to write to Spanner + job: job, + } + + err := mgr.Send(context.Background()) + if err != nil { + t.Errorf("Failed to send Slack message for %s: %v", tc.name, err) + } else { + fmt.Printf("Slack message sent successfully for %s!\n", tc.name) + } + }) + } +} + +type noopStateManager struct{} + +func (n *noopStateManager) RecordSuccess(ctx context.Context, channelID string, sentAt time.Time, webhookEventID string) error { + return nil +} +func (n *noopStateManager) RecordFailure(ctx context.Context, channelID string, err error, failedAt time.Time, isPermanent bool, webhookEventID string) error { + return nil +} diff --git a/workers/webhook/pkg/webhook/sender_test.go b/workers/webhook/pkg/webhook/sender_test.go index 8c32f8b21..f7e64ef97 100644 --- a/workers/webhook/pkg/webhook/sender_test.go +++ b/workers/webhook/pkg/webhook/sender_test.go @@ -17,6 +17,7 @@ package webhook import ( "context" "errors" + "io" "net/http" "strings" "testing" @@ -27,7 +28,13 @@ import ( func TestSender_SendWebhook_Success(t *testing.T) { mockHTTP := &mockHTTPClient{ - doFunc: func(_ *http.Request) (*http.Response, error) { + doFunc: func(req *http.Request) (*http.Response, error) { + body, _ := io.ReadAll(req.Body) + expectedLink := "View Results: https://webstatus.dev/?q=group%3Acss" + if !strings.Contains(string(body), expectedLink) { + t.Errorf("expected link %s not found in body", expectedLink) + } + return newTestResponse(http.StatusOK, "ok"), nil }, } @@ -83,6 +90,38 @@ func TestSender_SendWebhook_TransientFailure(t *testing.T) { } } +func TestSender_SendWebhook_FeatureDeepLink_Success(t *testing.T) { + mockHTTP := &mockHTTPClient{ + doFunc: func(req *http.Request) (*http.Response, error) { + body, _ := io.ReadAll(req.Body) + expectedLink := "View Results: https://webstatus.dev/features/anchor-positioning" + if !strings.Contains(string(body), expectedLink) { + t.Errorf("expected link %s not found in body", expectedLink) + } + + return newTestResponse(http.StatusOK, "ok"), nil + }, + } + + mockState := &mockChannelStateManager{ + successCalls: nil, + failureCalls: nil, + recordErr: nil, + } + sender := NewSender(mockHTTP, mockState, "https://webstatus.dev") + + job := newTestIncomingWebhookDeliveryJob( + "https://hooks.slack.com/services/123", workertypes.WebhookTypeSlack, + "id:\"anchor-positioning\"", []byte(`{"text":"Test Body"}`)) + + err := sender.SendWebhook(context.Background(), job) + if err != nil { + t.Fatalf("SendWebhook failed: %v", err) + } + + verifySuccess(t, mockState) +} + func TestSender_SendWebhook_HTTPFailure(t *testing.T) { mockHTTP := &mockHTTPClient{ doFunc: func(_ *http.Request) (*http.Response, error) { diff --git a/workers/webhook/pkg/webhook/slack.go b/workers/webhook/pkg/webhook/slack.go index 34e785b25..7b7beb528 100644 --- a/workers/webhook/pkg/webhook/slack.go +++ b/workers/webhook/pkg/webhook/slack.go @@ -22,6 +22,7 @@ import ( "fmt" "net/http" "net/url" + "strings" "github.com/GoogleChrome/webstatus.dev/lib/httputils" "github.com/GoogleChrome/webstatus.dev/lib/workertypes" @@ -56,9 +57,17 @@ func (s *slackSender) Send(ctx context.Context) error { return fmt.Errorf("%w: failed to unmarshal summary: %w", ErrPermanentWebhook, err) } + // Determine the correct results URL. + // 1. Check if it's a feature-specific query (id:"...") query := s.job.Metadata.Query - // Default search results page - resultsURL := fmt.Sprintf("%s/features?q=%s", s.frontendBaseURL, url.QueryEscape(query)) + var resultsURL string + if strings.HasPrefix(query, "id:\"") && strings.HasSuffix(query, "\"") { + featureKey := strings.TrimSuffix(strings.TrimPrefix(query, "id:\""), "\"") + resultsURL = fmt.Sprintf("%s/features/%s", s.frontendBaseURL, featureKey) + } else { + // 2. Default search results page (at the root) + resultsURL = fmt.Sprintf("%s/?q=%s", s.frontendBaseURL, url.QueryEscape(query)) + } payload := SlackPayload{ Text: fmt.Sprintf("WebStatus.dev Notification: %s\nQuery: %s\nView Results: %s", diff --git a/workers/webhook/pkg/webhook/slack_test.go b/workers/webhook/pkg/webhook/slack_test.go index 51df83b25..0bb561bb8 100644 --- a/workers/webhook/pkg/webhook/slack_test.go +++ b/workers/webhook/pkg/webhook/slack_test.go @@ -41,7 +41,7 @@ func TestSlackSender_Send(t *testing.T) { expectedPayload: &SlackPayload{ Text: "WebStatus.dev Notification: New feature landed\n" + "Query: group:css\n" + - "View Results: https://webstatus.dev/features?q=group%3Acss", + "View Results: https://webstatus.dev/?q=group%3Acss", }, expectedErr: nil, }, @@ -58,7 +58,7 @@ func TestSlackSender_Send(t *testing.T) { expectedPayload: &SlackPayload{ Text: "WebStatus.dev Notification: Test Body\n" + "Query: id:\"anchor-positioning\"\n" + - "View Results: https://webstatus.dev/features?q=id%3A%22anchor-positioning%22", + "View Results: https://webstatus.dev/features/anchor-positioning", }, expectedErr: nil, }, @@ -88,7 +88,7 @@ func TestSlackSender_Send(t *testing.T) { expectedPayload: &SlackPayload{ Text: "WebStatus.dev Notification: fail\n" + "Query: \n" + - "View Results: https://webstatus.dev/features?q=", + "View Results: https://webstatus.dev/?q=", }, expectedErr: ErrPermanentWebhook, }, @@ -105,7 +105,7 @@ func TestSlackSender_Send(t *testing.T) { expectedPayload: &SlackPayload{ Text: "WebStatus.dev Notification: retry\n" + "Query: \n" + - "View Results: https://webstatus.dev/features?q=", + "View Results: https://webstatus.dev/?q=", }, expectedErr: ErrTransientWebhook, },