Skip to content

Commit

Permalink
Add tests and correcto for no-alerts
Browse files Browse the repository at this point in the history
  • Loading branch information
academo committed Jan 3, 2025
1 parent 3f66fec commit 45cd7f0
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 5 deletions.
25 changes: 25 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Go Build and Test

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"

- name: Build
run: go build -v ./...

- name: Test
run: go test -v ./...
8 changes: 6 additions & 2 deletions Magefile.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func buildBinary() error {
env := map[string]string{
"CGO_ENABLED": "0",
"GO111MODULE": "on",
"GOOS": getOrDefault("GOOS", "linux"),
"GOOS": getOrDefault("GOOS", "linux"),
}

if err := sh.RunWith(env, "go", "build", "-o", filepath.Join(distDir, "grafana-ntfy"), "./pkg"); err != nil {
Expand All @@ -45,7 +45,11 @@ func RunLocal() error {
env := map[string]string{
"DEBUG": "1",
}
return sh.RunWith(env, filepath.Join(distDir, "grafana-ntfy"), "-ntfy-url", "https://ntfy.sh/mytopic")
return sh.RunWith(env, filepath.Join(distDir, "grafana-ntfy"), "-ntfy-url", "https://ntfy.sh/academo")
}

func Test() error {
return sh.RunV("go", "test", "./...")
}

func Deploy() error {
Expand Down
19 changes: 16 additions & 3 deletions pkg/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ var (
debug = flag.Bool("debug", false, "print extra debug information")
)

type HttpClient interface {
Do(*http.Request) (*http.Response, error)
}

var urlRe = regexp.MustCompile(`(https?://.*?)/([-a-zA-Z0-9()@:%_\+.~#?&=]+)$`)
var topic string
var serverUrl string
Expand Down Expand Up @@ -130,7 +134,7 @@ func handleRequest(response http.ResponseWriter, request *http.Request) {
}

notificationPayload := prepareNotification(payload)
err = sendNotification(notificationPayload, request.Header.Get("Authorization"))
err = sendNotification(notificationPayload, request.Header.Get("Authorization"), http.DefaultClient)
if err != nil {
slog.Error("Error sending notification", "err", err)
http.Error(response, "Error sending notification", http.StatusInternalServerError)
Expand All @@ -149,6 +153,15 @@ func handleRequest(response http.ResponseWriter, request *http.Request) {
}

func prepareNotification(alertPayload AlertsPayload) NtfyNotification {
// edge case with a non-alert
if len(alertPayload.Alerts) == 0 {
return NtfyNotification{
Message: alertPayload.Message,
Title: alertPayload.Title,
Topic: topic,
}
}

firstAlert := alertPayload.Alerts[0]
actions := []NtfyAction{
{
Expand Down Expand Up @@ -176,7 +189,7 @@ func prepareNotification(alertPayload AlertsPayload) NtfyNotification {
return payload
}

func sendNotification(payload NtfyNotification, authHeader string) error {
func sendNotification(payload NtfyNotification, authHeader string, client HttpClient) error {
// Marshal the payload
message, err := json.Marshal(payload)
if err != nil {
Expand Down Expand Up @@ -205,7 +218,7 @@ func sendNotification(payload NtfyNotification, authHeader string) error {

// Send the request
defer req.Body.Close()
resp, err := http.DefaultClient.Do(req)
resp, err := client.Do(req)
if err != nil {
return err
}
Expand Down
194 changes: 194 additions & 0 deletions pkg/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package main

import (
"bytes"
"io"
"net/http"
"reflect"
"testing"
)

func TestValidateFlags(t *testing.T) {
tests := map[string]struct {
url string
wantErr bool
}{
"valid url": {"https://ntfy.sh/topic", false},
"empty url": {"", true},
"invalid url": {"not-a-url", true},
"no topic": {"https://ntfy.sh/", true},
"invalid chars": {"https://ntfy.sh/topic$%^", true},
"custom server": {"https://custom.ntfy/mytopic", false},
"http url": {"http://ntfy.sh/topic", false},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
*ntfyUrl = tc.url
err := validateFlags()
if (err != nil) != tc.wantErr {
t.Errorf("validateFlags() error = %v, wantErr %v", err, tc.wantErr)
}
})
}
}

func TestPrepareNotification(t *testing.T) {
tests := map[string]struct {
input AlertsPayload
want NtfyNotification
}{
"basic notification": {
input: AlertsPayload{
Message: "test message",
Title: "test title",
ExternalURL: "http://grafana/alert",
Alerts: []Alert{{SilenceURL: "http://grafana/silence"}},
},
want: NtfyNotification{
Message: "test message",
Title: "test title",
Topic: "test-topic",
Actions: []NtfyAction{
{Action: "view", Label: "Open in Grafana", Url: "http://grafana/alert", Clear: true},
{Action: "view", Label: "Silence", Url: "http://grafana/silence", Clear: false},
},
},
},
"empty alerts": {
input: AlertsPayload{
Message: "msg",
Alerts: []Alert{},
},
want: NtfyNotification{
Message: "msg",
Topic: "test-topic",
},
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
topic = "test-topic"
got := prepareNotification(tc.input)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("prepareNotification() = %v, want %v", got, tc.want)
}
})
}
}

func TestSendNotification(t *testing.T) {
originalUsername := *username
originalPassword := *password

tests := map[string]struct {
client *mockHttpClient
authHeader string
username string
password string
wantErr bool
checkReq func(*testing.T, *http.Request)
}{
"success": {
client: &mockHttpClient{
response: &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBuffer(nil)),
},
},
checkReq: func(t *testing.T, req *http.Request) {
if auth := req.Header.Get("Authorization"); auth != "" {
t.Errorf("unexpected Authorization header: %v", auth)
}
},
},
"with auth header": {
client: &mockHttpClient{
response: &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBuffer(nil)),
},
},
authHeader: "Bearer token",
checkReq: func(t *testing.T, req *http.Request) {
if auth := req.Header.Get("Authorization"); auth != "Bearer token" {
t.Errorf("expected Authorization header %q, got %q", "Bearer token", auth)
}
},
},
"with basic auth": {
client: &mockHttpClient{
response: &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBuffer(nil)),
},
},
username: "user",
password: "pass",
checkReq: func(t *testing.T, req *http.Request) {
username, password, ok := req.BasicAuth()
if !ok {
t.Error("expected basic auth, got none")
}
if username != "user" || password != "pass" {
t.Errorf("expected basic auth user/pass, got %q/%q", username, password)
}
},
},
"non-200 response": {
client: &mockHttpClient{
response: &http.Response{
StatusCode: http.StatusUnauthorized,
Body: io.NopCloser(bytes.NewBuffer(nil)),
},
},
wantErr: true,
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
*username = tc.username
*password = tc.password

var capturedReq *http.Request
mockClient := &mockHttpClient{
response: tc.client.response,
err: tc.client.err,
onDo: func(req *http.Request) {
capturedReq = req
},
}

payload := NtfyNotification{Message: "test"}
err := sendNotification(payload, tc.authHeader, mockClient)

if (err != nil) != tc.wantErr {
t.Errorf("sendNotification() error = %v, wantErr %v", err, tc.wantErr)
}

if tc.checkReq != nil && capturedReq != nil {
tc.checkReq(t, capturedReq)
}
})
}

// Restore original values
*username = originalUsername
*password = originalPassword
}

// Update mockHttpClient to capture request
type mockHttpClient struct {
response *http.Response
err error
onDo func(*http.Request)
}

func (m *mockHttpClient) Do(req *http.Request) (*http.Response, error) {
if m.onDo != nil {
m.onDo(req)
}
return m.response, m.err
}

0 comments on commit 45cd7f0

Please sign in to comment.