feat(notify): release v1.8.0 — Slack/Teams/Google Chat/Telegram/webhook notifications
- Add internal/notify package: best-effort release notifications to Slack, Microsoft Teams, Google Chat, Telegram, and a generic JSON webhook; every target is independently opt-in and a failed notification never fails the release - Add notify config section (slack_webhook_url, teams_webhook_url, google_chat_webhook_url, telegram_bot_token/telegram_chat_id, webhook_url) with matching env var fallbacks; 100% coverage plus FuzzMessagePayloads - Wire notification sending into run() — fires after tag+push (including --no-release), skipped on --no-push/--no-commit since nothing was published yet - Document the notify section in README, Hugo docs, and .releaser.yml template; update CLAUDE.md architecture/fuzzing tables - Also commit the Apache License 2.0 docs-site footer link (docs/hugo.toml geekdocContentLicense), left uncommitted from a prior change Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// googleChatPayload matches the Google Chat Incoming Webhook contract: a
|
||||
// single "text" field.
|
||||
type googleChatPayload struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func newGoogleChatPayload(msg Message) googleChatPayload {
|
||||
return googleChatPayload{Text: fmt.Sprintf("*Released %s*\n\n%s", msg.Version, msg.Notes)}
|
||||
}
|
||||
|
||||
func sendGoogleChat(ctx context.Context, webhookURL string, msg Message) error {
|
||||
return postJSON(ctx, webhookURL, newGoogleChatPayload(msg))
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSendGoogleChat(t *testing.T) {
|
||||
var got googleChatPayload
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewDecoder(r.Body).Decode(&got) //nolint:errcheck
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
if err := sendGoogleChat(context.Background(), srv.URL, Message{Version: "v1.2.3", Notes: "- feat: y"}); err != nil {
|
||||
t.Fatalf("sendGoogleChat: %v", err)
|
||||
}
|
||||
if !strings.Contains(got.Text, "v1.2.3") || !strings.Contains(got.Text, "feat: y") {
|
||||
t.Errorf("payload text = %q", got.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendGoogleChatError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
if err := sendGoogleChat(context.Background(), srv.URL, Message{Version: "v1.0.0"}); err == nil {
|
||||
t.Fatal("expected error for 400 response")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Package notify sends best-effort release notifications to chat and webhook targets.
|
||||
package notify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Message is the content of a release notification, shared across all targets.
|
||||
type Message struct {
|
||||
Version string // tag name, e.g. "v1.2.3"
|
||||
Notes string // release notes body
|
||||
}
|
||||
|
||||
// Config holds the destination for every supported notification target.
|
||||
// Every field is opt-in — a target is skipped when its required fields are empty.
|
||||
type Config struct {
|
||||
SlackWebhookURL string
|
||||
TeamsWebhookURL string
|
||||
GoogleChatWebhookURL string
|
||||
TelegramBotToken string
|
||||
TelegramChatID string
|
||||
WebhookURL string
|
||||
}
|
||||
|
||||
var httpClient = &http.Client{Timeout: 15 * time.Second}
|
||||
|
||||
// SendAll sends msg to every target configured in cfg. Each target is attempted
|
||||
// independently — a failure on one does not prevent the others from being tried.
|
||||
// Returns one error per failed target, or nil if every configured target
|
||||
// succeeded (or none were configured).
|
||||
func SendAll(ctx context.Context, cfg Config, msg Message) []error {
|
||||
var errs []error
|
||||
|
||||
if cfg.SlackWebhookURL != "" {
|
||||
if err := sendSlack(ctx, cfg.SlackWebhookURL, msg); err != nil {
|
||||
errs = append(errs, fmt.Errorf("slack: %w", err))
|
||||
}
|
||||
}
|
||||
if cfg.TeamsWebhookURL != "" {
|
||||
if err := sendTeams(ctx, cfg.TeamsWebhookURL, msg); err != nil {
|
||||
errs = append(errs, fmt.Errorf("teams: %w", err))
|
||||
}
|
||||
}
|
||||
if cfg.GoogleChatWebhookURL != "" {
|
||||
if err := sendGoogleChat(ctx, cfg.GoogleChatWebhookURL, msg); err != nil {
|
||||
errs = append(errs, fmt.Errorf("google chat: %w", err))
|
||||
}
|
||||
}
|
||||
if cfg.TelegramBotToken != "" && cfg.TelegramChatID != "" {
|
||||
if err := sendTelegram(ctx, cfg.TelegramBotToken, cfg.TelegramChatID, msg); err != nil {
|
||||
errs = append(errs, fmt.Errorf("telegram: %w", err))
|
||||
}
|
||||
}
|
||||
if cfg.WebhookURL != "" {
|
||||
if err := sendWebhook(ctx, cfg.WebhookURL, msg); err != nil {
|
||||
errs = append(errs, fmt.Errorf("webhook: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
// postJSON POSTs payload as JSON to url and treats any non-2xx response as an error.
|
||||
func postJSON(ctx context.Context, url string, payload any) error {
|
||||
body, _ := json.Marshal(payload) // payload fields are always plain strings — Marshal cannot fail here
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("returned status %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// FuzzMessagePayloads verifies that building a notification payload never
|
||||
// panics on arbitrary version/notes strings, no matter the target — release
|
||||
// notes are generated from free-form commit messages and end up embedded in
|
||||
// every payload below.
|
||||
func FuzzMessagePayloads(f *testing.F) {
|
||||
f.Add("v1.2.3", "release notes")
|
||||
f.Add("", "")
|
||||
f.Add("v1.0.0", "* markdown _weird_ [chars] `code` <html> & \"quotes\"")
|
||||
f.Add("tag\nwith\nnewline", "notes\x00with\xffbinary")
|
||||
f.Add("v1.0.0", strings.Repeat("x", 10000))
|
||||
|
||||
f.Fuzz(func(t *testing.T, version, notes string) {
|
||||
msg := Message{Version: version, Notes: notes}
|
||||
_, _ = json.Marshal(newSlackPayload(msg))
|
||||
_, _ = json.Marshal(newGoogleChatPayload(msg))
|
||||
_, _ = json.Marshal(newTeamsPayload(msg))
|
||||
_, _ = json.Marshal(newTelegramPayload("chat-id", msg))
|
||||
_, _ = json.Marshal(newWebhookPayload(msg))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// alwaysFailTransport returns an error for every request.
|
||||
type alwaysFailTransport struct{}
|
||||
|
||||
func (alwaysFailTransport) RoundTrip(*http.Request) (*http.Response, error) {
|
||||
return nil, &testTransportError{"connection refused"}
|
||||
}
|
||||
|
||||
type testTransportError struct{ msg string }
|
||||
|
||||
func (e *testTransportError) Error() string { return e.msg }
|
||||
|
||||
func TestPostJSONSuccess(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
if err := postJSON(context.Background(), srv.URL, map[string]string{"a": "b"}); err != nil {
|
||||
t.Fatalf("postJSON: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostJSONBadURL(t *testing.T) {
|
||||
err := postJSON(context.Background(), "http://\x00bad", map[string]string{"a": "b"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostJSONRequestFails(t *testing.T) {
|
||||
orig := httpClient
|
||||
httpClient = &http.Client{Transport: alwaysFailTransport{}}
|
||||
defer func() { httpClient = orig }()
|
||||
|
||||
err := postJSON(context.Background(), "http://127.0.0.1:1", map[string]string{"a": "b"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when HTTP request fails")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostJSONNon2xx(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
err := postJSON(context.Background(), srv.URL, map[string]string{"a": "b"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for 500 response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendAllNoneConfigured(t *testing.T) {
|
||||
errs := SendAll(context.Background(), Config{}, Message{Version: "v1.0.0"})
|
||||
if errs != nil {
|
||||
t.Fatalf("expected no errors, got %v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendAllAllSucceed(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
origBase := telegramAPIBase
|
||||
telegramAPIBase = srv.URL
|
||||
defer func() { telegramAPIBase = origBase }()
|
||||
|
||||
cfg := Config{
|
||||
SlackWebhookURL: srv.URL,
|
||||
TeamsWebhookURL: srv.URL,
|
||||
GoogleChatWebhookURL: srv.URL,
|
||||
TelegramBotToken: "tok",
|
||||
TelegramChatID: "123",
|
||||
WebhookURL: srv.URL,
|
||||
}
|
||||
errs := SendAll(context.Background(), cfg, Message{Version: "v1.0.0", Notes: "notes"})
|
||||
if errs != nil {
|
||||
t.Fatalf("expected no errors, got %v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendAllAllFail(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
origBase := telegramAPIBase
|
||||
telegramAPIBase = srv.URL
|
||||
defer func() { telegramAPIBase = origBase }()
|
||||
|
||||
cfg := Config{
|
||||
SlackWebhookURL: srv.URL,
|
||||
TeamsWebhookURL: srv.URL,
|
||||
GoogleChatWebhookURL: srv.URL,
|
||||
TelegramBotToken: "tok",
|
||||
TelegramChatID: "123",
|
||||
WebhookURL: srv.URL,
|
||||
}
|
||||
errs := SendAll(context.Background(), cfg, Message{Version: "v1.0.0"})
|
||||
if len(errs) != 5 {
|
||||
t.Fatalf("expected 5 errors, got %d: %v", len(errs), errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendAllTelegramRequiresBothFields(t *testing.T) {
|
||||
calls := 0
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
calls++
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
origBase := telegramAPIBase
|
||||
telegramAPIBase = srv.URL
|
||||
defer func() { telegramAPIBase = origBase }()
|
||||
|
||||
// Only bot token set — chat ID missing — Telegram target must be skipped.
|
||||
errs := SendAll(context.Background(), Config{TelegramBotToken: "tok"}, Message{Version: "v1.0.0"})
|
||||
if errs != nil {
|
||||
t.Fatalf("expected no errors, got %v", errs)
|
||||
}
|
||||
if calls != 0 {
|
||||
t.Fatalf("expected telegram to be skipped, got %d calls", calls)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// slackPayload matches the Slack Incoming Webhook contract: a single "text"
|
||||
// field, interpreted as Slack mrkdwn.
|
||||
type slackPayload struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func newSlackPayload(msg Message) slackPayload {
|
||||
return slackPayload{Text: fmt.Sprintf("*Released %s*\n\n%s", msg.Version, msg.Notes)}
|
||||
}
|
||||
|
||||
func sendSlack(ctx context.Context, webhookURL string, msg Message) error {
|
||||
return postJSON(ctx, webhookURL, newSlackPayload(msg))
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSendSlack(t *testing.T) {
|
||||
var got slackPayload
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if ct := r.Header.Get("Content-Type"); ct != "application/json" {
|
||||
t.Errorf("Content-Type = %q", ct)
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&got) //nolint:errcheck
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
if err := sendSlack(context.Background(), srv.URL, Message{Version: "v1.2.3", Notes: "- fix: x"}); err != nil {
|
||||
t.Fatalf("sendSlack: %v", err)
|
||||
}
|
||||
if !strings.Contains(got.Text, "v1.2.3") || !strings.Contains(got.Text, "fix: x") {
|
||||
t.Errorf("payload text = %q", got.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendSlackError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
if err := sendSlack(context.Background(), srv.URL, Message{Version: "v1.0.0"}); err == nil {
|
||||
t.Fatal("expected error for 403 response")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// teamsPayload is a Microsoft Teams "Incoming Webhook" O365 connector card
|
||||
// (MessageCard schema) — the format Teams webhook URLs still accept.
|
||||
type teamsPayload struct {
|
||||
Type string `json:"@type"`
|
||||
Context string `json:"@context"`
|
||||
Summary string `json:"summary"`
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func newTeamsPayload(msg Message) teamsPayload {
|
||||
title := fmt.Sprintf("Released %s", msg.Version)
|
||||
return teamsPayload{
|
||||
Type: "MessageCard",
|
||||
Context: "http://schema.org/extensions",
|
||||
Summary: title,
|
||||
Title: title,
|
||||
Text: msg.Notes,
|
||||
}
|
||||
}
|
||||
|
||||
func sendTeams(ctx context.Context, webhookURL string, msg Message) error {
|
||||
return postJSON(ctx, webhookURL, newTeamsPayload(msg))
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSendTeams(t *testing.T) {
|
||||
var got teamsPayload
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewDecoder(r.Body).Decode(&got) //nolint:errcheck
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
if err := sendTeams(context.Background(), srv.URL, Message{Version: "v1.2.3", Notes: "- fix: z"}); err != nil {
|
||||
t.Fatalf("sendTeams: %v", err)
|
||||
}
|
||||
if got.Type != "MessageCard" {
|
||||
t.Errorf("Type = %q, want MessageCard", got.Type)
|
||||
}
|
||||
if !strings.Contains(got.Title, "v1.2.3") || !strings.Contains(got.Summary, "v1.2.3") {
|
||||
t.Errorf("Title/Summary = %q/%q, want to contain v1.2.3", got.Title, got.Summary)
|
||||
}
|
||||
if !strings.Contains(got.Text, "fix: z") {
|
||||
t.Errorf("Text = %q", got.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendTeamsError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
if err := sendTeams(context.Background(), srv.URL, Message{Version: "v1.0.0"}); err == nil {
|
||||
t.Fatal("expected error for 503 response")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// telegramAPIBase is the Telegram Bot API host. Overridden in tests.
|
||||
var telegramAPIBase = "https://api.telegram.org"
|
||||
|
||||
// telegramPayload matches the Telegram Bot API sendMessage contract. Notes
|
||||
// are sent as plain text (no parse_mode) — release notes come from arbitrary
|
||||
// commit messages and are not guaranteed to be valid Telegram Markdown/HTML,
|
||||
// and a malformed entity makes the whole request fail with a 400.
|
||||
type telegramPayload struct {
|
||||
ChatID string `json:"chat_id"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func newTelegramPayload(chatID string, msg Message) telegramPayload {
|
||||
return telegramPayload{
|
||||
ChatID: chatID,
|
||||
Text: fmt.Sprintf("Released %s\n\n%s", msg.Version, msg.Notes),
|
||||
}
|
||||
}
|
||||
|
||||
func sendTelegram(ctx context.Context, botToken, chatID string, msg Message) error {
|
||||
url := fmt.Sprintf("%s/bot%s/sendMessage", telegramAPIBase, botToken)
|
||||
return postJSON(ctx, url, newTelegramPayload(chatID, msg))
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSendTelegram(t *testing.T) {
|
||||
var got telegramPayload
|
||||
var path string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
path = r.URL.Path
|
||||
json.NewDecoder(r.Body).Decode(&got) //nolint:errcheck
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
orig := telegramAPIBase
|
||||
telegramAPIBase = srv.URL
|
||||
defer func() { telegramAPIBase = orig }()
|
||||
|
||||
if err := sendTelegram(context.Background(), "bot-token", "chat-1", Message{Version: "v1.2.3", Notes: "notes body"}); err != nil {
|
||||
t.Fatalf("sendTelegram: %v", err)
|
||||
}
|
||||
if !strings.Contains(path, "bot-token") {
|
||||
t.Errorf("path = %q, want to contain bot token", path)
|
||||
}
|
||||
if got.ChatID != "chat-1" {
|
||||
t.Errorf("ChatID = %q, want chat-1", got.ChatID)
|
||||
}
|
||||
if !strings.Contains(got.Text, "v1.2.3") || !strings.Contains(got.Text, "notes body") {
|
||||
t.Errorf("Text = %q", got.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendTelegramError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
orig := telegramAPIBase
|
||||
telegramAPIBase = srv.URL
|
||||
defer func() { telegramAPIBase = orig }()
|
||||
|
||||
if err := sendTelegram(context.Background(), "bot-token", "chat-1", Message{Version: "v1.0.0"}); err == nil {
|
||||
t.Fatal("expected error for 400 response")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package notify
|
||||
|
||||
import "context"
|
||||
|
||||
// webhookPayload is a generic JSON envelope for arbitrary API webhooks
|
||||
// (e.g. n8n, Zapier, a custom receiver) that don't follow a chat-app schema.
|
||||
type webhookPayload struct {
|
||||
Version string `json:"version"`
|
||||
Notes string `json:"notes"`
|
||||
}
|
||||
|
||||
func newWebhookPayload(msg Message) webhookPayload {
|
||||
return webhookPayload(msg)
|
||||
}
|
||||
|
||||
func sendWebhook(ctx context.Context, url string, msg Message) error {
|
||||
return postJSON(ctx, url, newWebhookPayload(msg))
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSendWebhook(t *testing.T) {
|
||||
var got webhookPayload
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewDecoder(r.Body).Decode(&got) //nolint:errcheck
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
if err := sendWebhook(context.Background(), srv.URL, Message{Version: "v1.2.3", Notes: "notes body"}); err != nil {
|
||||
t.Fatalf("sendWebhook: %v", err)
|
||||
}
|
||||
if got.Version != "v1.2.3" {
|
||||
t.Errorf("Version = %q, want v1.2.3", got.Version)
|
||||
}
|
||||
if got.Notes != "notes body" {
|
||||
t.Errorf("Notes = %q, want %q", got.Notes, "notes body")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendWebhookError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
if err := sendWebhook(context.Background(), srv.URL, Message{Version: "v1.0.0"}); err == nil {
|
||||
t.Fatal("expected error for 500 response")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user