initial commit

This commit is contained in:
Krzysztof
2025-08-01 18:01:55 +02:00
commit 9af1af7f92
73 changed files with 6531 additions and 0 deletions

201
test/check_test.go Normal file
View File

@@ -0,0 +1,201 @@
package test
import (
"net/http"
"testing"
"time"
"uptimemonitor"
"uptimemonitor/service"
)
func TestCheck_ListChecks(t *testing.T) {
t.Run("setup is required to load checks", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Get("/monitors/1/checks").
AssertRedirect(http.StatusSeeOther, "/setup")
})
t.Run("guests cannot load checks", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.CreateTestUser("test@example.com", "password")
tc.Get("/monitors/1/checks").
AssertRedirect(http.StatusSeeOther, "/login")
})
t.Run("monitor has to exist", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.LogIn().
Get("/monitors/1/checks").
AssertStatusCode(http.StatusNotFound)
})
t.Run("latest checks are returned", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
monitor, _ := tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: "https://example.com",
})
tc.Store.CreateCheck(t.Context(), uptimemonitor.Check{
MonitorID: monitor.ID,
Monitor: monitor,
})
tc.LogIn().
Get("/monitors/1/checks").
AssertStatusCode(http.StatusOK).
AssertElementVisible(`div[id="monitors-1-checks-1"]`)
})
}
func TestCheck_PeriodicChecks(t *testing.T) {
t.Run("it does not create checks if no monitors are defined", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
service := service.New(tc.Store)
ch := service.StartCheck()
service.RunCheck(t.Context(), ch)
tc.AssertDatabaseCount("checks", 0)
service.RunCheck(t.Context(), ch)
tc.AssertDatabaseCount("checks", 0)
})
t.Run("it creates checks every minute", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: "https://example.com",
})
service := service.CheckService{
Store: tc.Store,
}
ch := service.StartCheck()
service.RunCheck(t.Context(), ch)
service.RunCheck(t.Context(), ch)
time.Sleep(3 * time.Second)
// tc.AssertDatabaseCount("checks", 2) // todo: fix
})
t.Run("checks can use different http methods", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{Url: tc.Server.URL + "/test/post", HttpMethod: http.MethodPost})
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{Url: tc.Server.URL + "/test/patch", HttpMethod: http.MethodPatch})
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{Url: tc.Server.URL + "/test/put", HttpMethod: http.MethodPut})
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{Url: tc.Server.URL + "/test/delete", HttpMethod: http.MethodDelete})
service := service.CheckService{Store: tc.Store}
ch := service.StartCheck()
service.RunCheck(t.Context(), ch)
time.Sleep(1 * time.Second)
tc.AssertDatabaseCount("checks", 4)
tc.AssertDatabaseCount("incidents", 0)
first, err := tc.Store.GetCheckByID(t.Context(), 1)
tc.AssertNoError(err)
second, err := tc.Store.GetCheckByID(t.Context(), 2)
tc.AssertNoError(err)
third, err := tc.Store.GetCheckByID(t.Context(), 3)
tc.AssertNoError(err)
fifth, err := tc.Store.GetCheckByID(t.Context(), 4)
tc.AssertNoError(err)
tc.AssertEqual(http.StatusOK, first.StatusCode)
tc.AssertEqual(http.StatusOK, second.StatusCode)
tc.AssertEqual(http.StatusOK, third.StatusCode)
tc.AssertEqual(http.StatusOK, fifth.StatusCode)
})
t.Run("checks can send custom body", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{Url: tc.Server.URL + "/test/body", HttpMethod: http.MethodPost, HttpBody: `{"test":123}`})
service := service.CheckService{Store: tc.Store}
ch := service.StartCheck()
service.RunCheck(t.Context(), ch)
time.Sleep(1 * time.Second)
check, _ := tc.Store.GetCheckByID(t.Context(), 1)
tc.AssertEqual(http.StatusOK, check.StatusCode)
})
t.Run("checks can send custom headers", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{Url: tc.Server.URL + "/test/headers", HttpMethod: http.MethodPost, HttpHeaders: `{"test":"abc"}`})
service := service.CheckService{Store: tc.Store}
ch := service.StartCheck()
service.RunCheck(t.Context(), ch)
time.Sleep(1 * time.Second)
check, _ := tc.Store.GetCheckByID(t.Context(), 1)
tc.AssertEqual(http.StatusOK, check.StatusCode)
})
}
func TestCheck_Cleanup(t *testing.T) {
t.Run("old cleanups are removed", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{Url: tc.Server.URL + "/test/200", HttpMethod: http.MethodGet})
tc.Store.CreateCheck(t.Context(), uptimemonitor.Check{
MonitorID: 1,
CreatedAt: time.Now().Add(-time.Hour).Add(-15 * time.Minute),
})
tc.Store.CreateCheck(t.Context(), uptimemonitor.Check{
MonitorID: 1,
})
tc.Store.CreateIncident(t.Context(), uptimemonitor.Incident{
MonitorID: 1,
CreatedAt: time.Now().Add(-time.Hour * 24 * 8),
})
tc.Store.CreateIncident(t.Context(), uptimemonitor.Incident{
MonitorID: 1,
})
tc.AssertDatabaseCount("checks", 2)
tc.AssertDatabaseCount("incidents", 2)
service := service.CheckService{Store: tc.Store}
ch := service.StartCheck()
service.RunCheck(t.Context(), ch)
time.Sleep(1 * time.Second)
tc.AssertDatabaseCount("checks", 2) // one new, old one deleted
tc.AssertDatabaseCount("incidents", 1)
service.RunCheck(t.Context(), ch)
time.Sleep(1 * time.Second)
tc.AssertDatabaseCount("checks", 3)
})
}

62
test/home_test.go Normal file
View File

@@ -0,0 +1,62 @@
package test
import (
"net/http"
"testing"
"uptimemonitor"
)
func TestHome(t *testing.T) {
t.Run("setup is required to access home page", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Get("/").AssertRedirect(http.StatusSeeOther, "/setup")
})
t.Run("guests cannot access home page", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.CreateTestUser("test@example.com", "password")
tc.Get("/").AssertRedirect(http.StatusSeeOther, "/login")
})
t.Run("monitors are displayed on home page", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: "https://example.com",
})
tc.LogIn().
Get("/").
AssertStatusCode(http.StatusOK).
AssertElementVisible(`[hx-get="/monitors"]`)
})
t.Run("incidents are displayed on home page", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: "https://example.com",
})
tc.LogIn().
Get("/").
AssertStatusCode(http.StatusOK).
AssertElementVisible(`[hx-get="/incidents"]`)
})
t.Run("if there are no monitors, user is redirected to the new page", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.LogIn().
Get("/").
AssertRedirect(http.StatusSeeOther, "/new")
})
}

357
test/incident_test.go Normal file
View File

@@ -0,0 +1,357 @@
package test
import (
"fmt"
"net/http"
"testing"
"time"
"uptimemonitor"
"uptimemonitor/service"
)
func TestIncident(t *testing.T) {
t.Run("no incident is created when check succeeds", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
service := service.New(tc.Store)
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: tc.Server.URL + "/test/200",
})
ch := service.StartCheck()
service.RunCheck(t.Context(), ch)
time.Sleep(time.Second)
tc.AssertDatabaseCount("incidents", 0)
tc.AssertDatabaseCount("checks", 1)
})
t.Run("incident is created when check fails", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
service := service.New(tc.Store)
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: tc.Server.URL + "/test/404",
})
ch := service.StartCheck()
service.RunCheck(t.Context(), ch)
time.Sleep(time.Second)
tc.AssertDatabaseCount("incidents", 1)
tc.AssertDatabaseCount("checks", 1)
})
t.Run("new incident is not created for the same monitor if it already exists", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
service := service.New(tc.Store)
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: tc.Server.URL + "/test/500",
})
ch := service.StartCheck()
service.RunCheck(t.Context(), ch)
time.Sleep(time.Second)
tc.AssertDatabaseCount("incidents", 1)
tc.AssertDatabaseCount("checks", 1)
ch = service.StartCheck()
service.RunCheck(t.Context(), ch)
time.Sleep(time.Second)
tc.AssertDatabaseCount("incidents", 1)
})
t.Run("incident is created when check fails with different status code", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
service := service.New(tc.Store)
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: tc.Server.URL + "/test/500",
})
tc.Store.CreateIncident(t.Context(), uptimemonitor.Incident{
MonitorID: 1,
StatusCode: 404,
ResponseTimeMs: 100,
Body: "not found",
Headers: "Content-Type: text/plain",
})
ch := service.StartCheck()
service.RunCheck(t.Context(), ch)
time.Sleep(time.Second)
tc.AssertDatabaseCount("incidents", 2)
})
t.Run("invalid domains create incidents", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
service := service.New(tc.Store)
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: "http://invalid-url",
})
ch := service.StartCheck()
service.RunCheck(t.Context(), ch)
time.Sleep(time.Second)
tc.AssertDatabaseCount("incidents", 1)
})
t.Run("incidents get resolved", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
service := service.New(tc.Store)
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: tc.Server.URL + "/test/even",
})
ch := service.StartCheck()
service.RunCheck(t.Context(), ch)
time.Sleep(time.Second)
tc.AssertDatabaseCount("incidents", 1)
service.RunCheck(t.Context(), ch)
time.Sleep(time.Second)
tc.AssertDatabaseCount("incidents", 1)
incident, _ := tc.Store.LastOpenIncident(t.Context(), 1)
if incident.ID != 0 {
t.Fatalf("expected not to found any incidents,found: %v", incident)
}
})
}
func TestIncident_ListIncidents(t *testing.T) {
t.Run("setup is required", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Get("/incidents").AssertRedirect(http.StatusSeeOther, "/setup")
})
t.Run("guests cannot list incidents", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.CreateTestUser("test@example.com", "password")
tc.Get("/incidents").AssertRedirect(http.StatusSeeOther, "/login")
})
t.Run("users can list incidents", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: "http://example.com",
})
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: "http://example.com",
})
tc.Store.CreateIncident(t.Context(), uptimemonitor.Incident{
MonitorID: 1,
StatusCode: 404,
ResponseTimeMs: 100,
})
tc.Store.CreateIncident(t.Context(), uptimemonitor.Incident{
MonitorID: 2,
StatusCode: 500,
ResponseTimeMs: 100,
})
tc.LogIn().Get("/incidents").
AssertStatusCode(http.StatusOK).
AssertElementVisible(`[id="incidents-1"]`).
AssertElementVisible(`[id="incidents-2"]`)
})
}
func TestIncident_ListMonitorIncidents(t *testing.T) {
t.Run("setup is required", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Get("/monitors/1/incidents").AssertRedirect(http.StatusSeeOther, "/setup")
})
t.Run("guests cannot list incidents", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.CreateTestUser("test@example.com", "password")
tc.Get("/monitors/1/incidents").AssertRedirect(http.StatusSeeOther, "/login")
})
t.Run("users can list incidents", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: "http://example.com",
})
tc.Store.CreateIncident(t.Context(), uptimemonitor.Incident{
MonitorID: 1,
StatusCode: 404,
ResponseTimeMs: 100,
})
tc.LogIn().Get("/monitors/1/incidents").
AssertStatusCode(http.StatusOK).
AssertElementVisible(`[id="incidents-1"]`)
})
}
func TestIncident_RemoveIncidents(t *testing.T) {
t.Run("setup is required", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Delete("/incidents/1").AssertStatusCode(http.StatusForbidden)
})
t.Run("guests cannot remove incidents", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.CreateTestUser("test@example.com", "password")
tc.Delete("/incidents/1").AssertRedirect(http.StatusSeeOther, "/login")
})
t.Run("remove incident form is visible", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
m, _ := tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: "http://example.com",
})
i, _ := tc.Store.CreateIncident(t.Context(), uptimemonitor.Incident{
MonitorID: 1,
StatusCode: 404,
ResponseTimeMs: 100,
})
tc.LogIn().
Get(fmt.Sprintf("/m/%s/i/%s", m.Uuid, i.Uuid)).
AssertStatusCode(http.StatusOK).
AssertElementVisible(`form[hx-delete="/incidents/1"]`)
})
t.Run("users can remove incidents", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
m, _ := tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: "http://example.com",
})
tc.Store.CreateIncident(t.Context(), uptimemonitor.Incident{
MonitorID: 1,
StatusCode: 404,
ResponseTimeMs: 100,
})
tc.LogIn().
Delete("/incidents/1").
AssertStatusCode(http.StatusOK).
AssertHeader("HX-Redirect", fmt.Sprintf("/m/%s", m.Uuid))
tc.AssertDatabaseCount("incidents", 0)
})
}
func TestIncident_IncidentPage(t *testing.T) {
t.Run("setup is required", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Get("/m/uuid/i/uuid").AssertRedirect(http.StatusSeeOther, "/setup")
})
t.Run("guests cannot view incidents", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.CreateTestUser("test@example.com", "password")
tc.Get("/m/uuid/i/uuid").AssertRedirect(http.StatusSeeOther, "/login")
})
t.Run("incident has to exist", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
m, _ := tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: "http://example.com",
})
tc.LogIn().Get(fmt.Sprintf("/m/%s/i/uuid", m.Uuid)).
AssertStatusCode(http.StatusNotFound)
})
t.Run("monitor has to exist", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: "http://example.com",
})
i, _ := tc.Store.CreateIncident(t.Context(), uptimemonitor.Incident{
MonitorID: 1,
StatusCode: 404,
ResponseTimeMs: 100,
})
tc.LogIn().Get(fmt.Sprintf("/m/uuid/i/%s", i.Uuid)).
AssertStatusCode(http.StatusNotFound)
})
t.Run("incident is visible", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
m, _ := tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: "http://example.com",
})
i, _ := tc.Store.CreateIncident(t.Context(), uptimemonitor.Incident{
MonitorID: 1,
StatusCode: 404,
ResponseTimeMs: 100,
})
tc.LogIn().
Get(fmt.Sprintf("/m/%s/i/%s", m.Uuid, i.Uuid)).
AssertStatusCode(http.StatusOK).
AssertSeeText("404 Not Found")
})
}

115
test/login_test.go Normal file
View File

@@ -0,0 +1,115 @@
package test
import (
"net/http"
"net/url"
"testing"
)
func TestLogin(t *testing.T) {
t.Run("setup is required before user can log in", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Get("/login").AssertRedirect(http.StatusSeeOther, "/setup")
tc.Post("/login", nil).AssertStatusCode(http.StatusForbidden)
})
t.Run("shows a login form", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.CreateTestUser("test@example.com", "password")
tc.Get("/login").
AssertStatusCode(http.StatusOK).
AssertElementVisible(`form[hx-post="/login"]`)
})
t.Run("validates form", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.CreateTestUser("test@example.com", "password")
tc.Post("/login", nil).
AssertStatusCode(http.StatusBadRequest).
AssertSeeText("The email is required").
AssertSeeText("The password is required")
tc.Post("/login", url.Values{
"email": []string{"invalid"},
}).
AssertStatusCode(http.StatusBadRequest).
AssertSeeText("The email format is invalid")
})
t.Run("user has to exist", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.CreateTestUser("test@example.com", "password")
tc.Post("/login", url.Values{
"email": []string{"other@example.com"},
"password": []string{"password"},
}).
AssertStatusCode(http.StatusBadRequest).
AssertSeeText("The credentials do not match our records")
})
t.Run("password has to be valid", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.CreateTestUser("test@example.com", "password")
tc.Post("/login", url.Values{
"email": []string{"test@example.com"},
"password": []string{"invalid"},
}).
AssertStatusCode(http.StatusBadRequest).
AssertSeeText("The credentials do not match our records")
})
t.Run("user can log in", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.CreateTestUser("test@example.com", "password")
tc.Get("/").AssertRedirect(http.StatusSeeOther, "/login")
ar := tc.Post("/login", url.Values{
"email": []string{"test@example.com"},
"password": []string{"password"},
}).
AssertStatusCode(http.StatusOK).
AssertCookieSet("session").
AssertHeader("HX-Redirect", "/")
tc.AssertDatabaseCount("sessions", 1)
cookies := ar.Response.Cookies()
var cookie *http.Cookie
for _, c := range cookies {
if c.Name == "session" {
cookie = c
}
}
tc.WithCookie(cookie).
Get("/new").
AssertNoRedirect().
AssertStatusCode(http.StatusOK)
})
t.Run("logged in users are redirected to a new page", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.LogIn().
Get("/login").
AssertRedirect(http.StatusSeeOther, "/new")
})
}

22
test/logout_test.go Normal file
View File

@@ -0,0 +1,22 @@
package test
import (
"net/http"
"testing"
)
func TestLogout(t *testing.T) {
t.Run("user can log out", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.LogIn().
Get("/logout").
AssertStatusCode(http.StatusOK).
AssertRedirect(http.StatusSeeOther, "/login")
tc.AssertDatabaseCount("sessions", 0)
tc.Get("/").AssertRedirect(http.StatusSeeOther, "/login")
})
}

27
test/middleware_test.go Normal file
View File

@@ -0,0 +1,27 @@
package test
import (
"net/http"
"testing"
)
func TestMiddleware(t *testing.T) {
t.Run("panic recoverer", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Get("/test/panic").AssertStatusCode(http.StatusInternalServerError)
})
t.Run("cache test", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.LogIn().
Get("/").
AssertStatusCode(http.StatusOK).
AssertHeader("Cache-Control", "no-cache, no-store, must-revalidate").
AssertHeader("Pragma", "no-cache").
AssertHeader("Expires", "0")
})
}

285
test/monitor_test.go Normal file
View File

@@ -0,0 +1,285 @@
package test
import (
"fmt"
"net/http"
"net/url"
"testing"
"time"
"uptimemonitor"
"uptimemonitor/service"
)
func TestMonitor_ListMonitors(t *testing.T) {
t.Run("setup is required", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Get("/monitors").
AssertRedirect(http.StatusSeeOther, "/setup")
})
t.Run("logged user is required", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.CreateTestUser("test@example.com", "password")
tc.Get("/monitors").
AssertRedirect(http.StatusSeeOther, "/login")
})
t.Run("monitors table is visible on home page", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{Url: "https://example.com"})
tc.LogIn().
Get("/").
AssertStatusCode(http.StatusOK).
AssertElementVisible(`div[hx-get="/monitors"]`)
})
t.Run("empty monitors list", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.LogIn().
Get("/monitors").
AssertNoRedirect().
AssertStatusCode(http.StatusOK)
})
t.Run("list monitors", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{Url: "https://example.com"})
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{Url: "https://example.com/123"})
tc.LogIn().
Get("/monitors").
AssertSeeText("example.com").
AssertSeeText("example.com/123")
})
}
func TestMonitor_CreateMonitor(t *testing.T) {
t.Run("setup is required", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Post("/monitors", url.Values{}).
AssertStatusCode(http.StatusForbidden)
})
t.Run("user has to be logged in", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.CreateTestUser("test@example.com", "password")
tc.Post("/monitors", url.Values{}).
AssertRedirect(http.StatusSeeOther, "/login")
})
t.Run("monitor form is visible", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.LogIn().
Get("/new").
AssertNoRedirect().
AssertStatusCode(http.StatusOK).
AssertElementVisible(`form[hx-post="/monitors"]`).
AssertElementVisible(`select[name="http_method"]`).
AssertElementVisible(`input[name="url"]`)
})
t.Run("url is required", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.LogIn().
Post("/monitors", url.Values{}).
AssertNoRedirect().
AssertStatusCode(http.StatusBadRequest).
AssertSeeText("The url is required")
})
t.Run("the url has to be a valid url", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.LogIn().
Post("/monitors", url.Values{
"url": []string{"invalid"},
}).
AssertStatusCode(http.StatusBadRequest).
AssertSeeText("The url is invalid")
})
t.Run("the url can be created", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
res := tc.LogIn().
Post("/monitors", url.Values{
"http_method": []string{"GET"},
"has_custom_headers": []string{"on"},
"http_headers": []string{`{"test":"abc"}`},
"has_custom_body": []string{"on"},
"http_body": []string{`{"test":"123"}`},
"url": []string{"https://example.com"},
}).
AssertStatusCode(http.StatusOK)
m, _ := tc.Store.GetMonitorByID(t.Context(), 1)
res.AssertHeader("HX-Redirect", fmt.Sprintf("/m/%s", m.Uuid))
tc.AssertDatabaseCount("monitors", 1)
tc.Get("/monitors").AssertSeeText("example.com")
tc.AssertEqual(m.Url, "https://example.com")
tc.AssertEqual(m.HttpMethod, "GET")
tc.AssertEqual(m.HttpHeaders, `{"test":"abc"}`)
tc.AssertEqual(m.HttpBody, `{"test":"123"}`)
})
t.Run("custom headers are validated when present", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.LogIn().
Post("/monitors", url.Values{
"http_method": []string{"GET"},
"url": []string{"https://example.com"},
}).
AssertStatusCode(http.StatusOK)
tc.Post("/monitors", url.Values{
"http_method": []string{"GET"},
"url": []string{"https://example.com"},
"has_custom_headers": []string{"on"},
"http_headers": []string{`INVALID JSON`},
}).
AssertStatusCode(http.StatusBadRequest).
AssertSeeText("The http headers should be a valid JSON")
})
}
func TestMonitor_MonitorPage(t *testing.T) {
t.Run("setup is required", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Get("/m/123").AssertRedirect(http.StatusSeeOther, "/setup")
})
t.Run("guests cannot view monitors", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.CreateTestUser("test@example.com", "password")
tc.Get("/m/123").AssertRedirect(http.StatusSeeOther, "/login")
})
t.Run("monitor has to exist", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.LogIn().Get("/m/123").AssertStatusCode(http.StatusNotFound)
})
t.Run("monitor can be viewed", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
m, _ := tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: "http://example.com",
})
tc.LogIn().Get(fmt.Sprintf("/m/%s", m.Uuid)).AssertStatusCode(http.StatusOK)
})
}
func TestMonitor_RemoveMonitor(t *testing.T) {
t.Run("guests cannot remove monitors", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Get("/m/uuid/delete").AssertRedirect(http.StatusSeeOther, "/setup")
tc.Delete("/monitors/1").AssertStatusCode(http.StatusForbidden)
})
t.Run("monitor has to exist", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.LogIn()
tc.Get("/m/uuid/delete").AssertStatusCode(http.StatusNotFound)
tc.Delete("/monitors/1").AssertStatusCode(http.StatusNotFound)
})
t.Run("delete monitor form is present", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
m, _ := tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
HttpMethod: http.MethodGet,
Url: "http://example.com",
})
tc.LogIn().
Get(fmt.Sprintf("/m/%s/delete", m.Uuid)).
AssertStatusCode(http.StatusOK).
AssertElementVisible(fmt.Sprintf(`form[hx-delete="/monitors/%d"]`, m.ID))
})
t.Run("monitor can be removed", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
HttpMethod: http.MethodGet,
Url: "http://example.com",
})
tc.LogIn().
Delete("/monitors/1").
AssertStatusCode(http.StatusOK).
AssertHeader("HX-Redirect", "/")
tc.AssertDatabaseCount("monitors", 0)
})
t.Run("whe monitor is removed, checks and incidents are also removed", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
HttpMethod: http.MethodGet,
Url: tc.Server.URL + "/test/500",
})
service := service.New(tc.Store)
ch := service.StartCheck()
service.RunCheck(t.Context(), ch)
time.Sleep(time.Second)
tc.AssertDatabaseCount("checks", 1)
tc.AssertDatabaseCount("incidents", 1)
tc.LogIn().
Delete("/monitors/1").
AssertStatusCode(http.StatusOK).
AssertHeader("HX-Redirect", "/")
tc.AssertDatabaseCount("checks", 0)
tc.AssertDatabaseCount("incidents", 0)
})
}

82
test/setup_test.go Normal file
View File

@@ -0,0 +1,82 @@
package test
import (
"net/http"
"net/url"
"testing"
"uptimemonitor"
)
func TestSetup(t *testing.T) {
t.Run("redirects to setup when no users are found", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Get("/").
AssertRedirect(http.StatusSeeOther, "/setup")
})
t.Run("redirects to login page when users are found", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Store.CreateUser(t.Context(), uptimemonitor.User{
Name: "Test User",
Email: "test@example.com",
})
tc.Get("/setup").
AssertRedirect(http.StatusSeeOther, "/login")
})
t.Run("shows a setup form when no users are found", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Get("/setup").
AssertStatusCode(http.StatusOK).
AssertElementVisible(`form[hx-post="/setup"]`).
AssertElementVisible(`input[name="name"]`).
AssertElementVisible(`input[name="email"]`).
AssertElementVisible(`input[name="password"]`).
AssertElementVisible(`button[type="submit"]`)
})
t.Run("validates a form", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Post("/setup", url.Values{}).
AssertStatusCode(http.StatusBadRequest).
AssertElementVisible(`form[hx-swap="outerHTML"]`).
AssertSeeText("The name is required").
AssertSeeText("The email is required").
AssertSeeText("The password is required")
res := tc.Post("/setup", url.Values{
"email": []string{"invalid"},
})
res.AssertStatusCode(http.StatusBadRequest).
AssertElementVisible(`form[hx-swap="outerHTML"]`).
AssertSeeText("The email format is invalid")
})
t.Run("setup", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.AssertDatabaseCount("users", 0)
res := tc.Post("/setup", url.Values{
"name": []string{"Test"},
"email": []string{"test@example.com"},
"password": []string{"password"},
})
res.AssertHeader("HX-Redirect", "/")
tc.AssertDatabaseCount("users", 1)
tc.Get("/setup").AssertRedirect(http.StatusSeeOther, "/new")
})
}

26
test/sponsor_test.go Normal file
View File

@@ -0,0 +1,26 @@
package test
import (
"net/http"
"testing"
)
func TestSponsor(t *testing.T) {
t.Run("sponsors are lazy loaded", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Get("/sponsors").
AssertStatusCode(http.StatusOK).
AssertElementVisible(`div[hx-get="/sponsors"]`)
})
t.Run("sponsors are loaded via api", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.WithHeader("HX-Request", "true").
Get("/sponsors").
AssertSeeText("AIR Labs")
})
}

349
test/test_case.go Normal file
View File

@@ -0,0 +1,349 @@
package test
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strings"
"testing"
"time"
"uptimemonitor"
"uptimemonitor/handler"
"uptimemonitor/pkg/testutil"
"uptimemonitor/router"
"uptimemonitor/service"
"uptimemonitor/store"
"golang.org/x/crypto/bcrypt"
)
var TestWebhookCalledCount int64
var TestWebhookBody string
var ExpectedWebhookBody string
var ExpectedWebhookHeaderKey string
var ExpectedWebhookHeaderValue string
type TestCase struct {
T *testing.T
Server *httptest.Server
Client *http.Client
Store *store.Store
User *uptimemonitor.User
Headers map[string]string
Cookies []*http.Cookie
}
func NewTestCase(t *testing.T) *TestCase {
store := store.New(":memory:")
service := service.New(store)
handler := handler.New(store, service, false)
router := router.New(handler, registerRoutes)
server := httptest.NewServer(router)
TestWebhookBody = ""
TestWebhookCalledCount = 0
ExpectedWebhookBody = ""
ExpectedWebhookHeaderKey = ""
ExpectedWebhookHeaderValue = ""
return &TestCase{
T: t,
Server: server,
Client: server.Client(),
Store: store,
Headers: map[string]string{},
Cookies: []*http.Cookie{},
}
}
func registerRoutes(router *http.ServeMux) {
router.HandleFunc("GET /test/200", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
router.HandleFunc("GET /test/404", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
})
router.HandleFunc("GET /test/500", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
})
router.HandleFunc("GET /test/timeout", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(30 * time.Second)
w.WriteHeader(http.StatusOK)
})
router.HandleFunc("GET /test/panic", func(w http.ResponseWriter, r *http.Request) {
panic("test")
})
i := 0
router.HandleFunc("GET /test/even", func(w http.ResponseWriter, r *http.Request) {
defer func() {
i++
}()
if i%2 == 0 {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
})
router.HandleFunc("POST /test/post", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
router.HandleFunc("PATCH /test/patch", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
router.HandleFunc("PUT /test/put", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
router.HandleFunc("DELETE /test/delete", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
router.HandleFunc("POST /test/body", func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
defer r.Body.Close()
if string(body) != `{"test":123}` {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
})
router.HandleFunc("POST /test/headers", func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("test") != "abc" {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
})
router.HandleFunc("POST /test/webhook", func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
defer r.Body.Close()
if ExpectedWebhookBody != "" && string(body) != ExpectedWebhookBody {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if ExpectedWebhookHeaderKey != "" && r.Header.Get(ExpectedWebhookHeaderKey) != ExpectedWebhookHeaderValue {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
TestWebhookCalledCount++
TestWebhookBody = string(body)
w.WriteHeader(http.StatusOK)
})
}
func (tc *TestCase) Close() {
tc.Server.Close()
}
func (tc *TestCase) WithHeader(key, value string) *TestCase {
tc.Headers[key] = value
return tc
}
func (tc *TestCase) WithCookie(c *http.Cookie) *TestCase {
tc.Cookies = append(tc.Cookies, c)
return tc
}
func (tc *TestCase) Get(url string) *testutil.AssertableResponse {
req, err := http.NewRequest(http.MethodGet, tc.Server.URL+url, nil)
if err != nil {
tc.T.Fatalf("unexpected error: %v", err)
}
if len(tc.Cookies) > 0 {
for _, c := range tc.Cookies {
req.AddCookie(c)
}
}
if len(tc.Headers) > 0 {
for k, v := range tc.Headers {
req.Header.Set(k, v)
}
}
res, err := tc.Client.Do(req)
if err != nil {
tc.T.Fatalf("failed to get %s: %v", url, err)
}
return testutil.NewAssertableResponse(tc.T, res)
}
func (tc *TestCase) Post(url string, data url.Values) *testutil.AssertableResponse {
req, err := http.NewRequest(http.MethodPost, tc.Server.URL+url, strings.NewReader(data.Encode()))
if err != nil {
tc.T.Fatalf("unexpected error: %v", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if len(tc.Cookies) > 0 {
for _, c := range tc.Cookies {
req.AddCookie(c)
}
}
res, err := tc.Client.Do(req)
if err != nil {
tc.T.Fatalf("failed to post %s: %v", url, err)
}
for _, c := range res.Cookies() {
if c.Name == "session" {
tc.Cookies = append(tc.Cookies, c)
}
}
return testutil.NewAssertableResponse(tc.T, res)
}
func (tc *TestCase) Patch(url string, data url.Values) *testutil.AssertableResponse {
req, err := http.NewRequest(http.MethodPatch, tc.Server.URL+url, strings.NewReader(data.Encode()))
if err != nil {
tc.T.Fatalf("unexpected error: %v", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if len(tc.Cookies) > 0 {
for _, c := range tc.Cookies {
req.AddCookie(c)
}
}
res, err := tc.Client.Do(req)
if err != nil {
tc.T.Fatalf("failed to post %s: %v", url, err)
}
return testutil.NewAssertableResponse(tc.T, res)
}
func (tc *TestCase) Delete(url string) *testutil.AssertableResponse {
req, err := http.NewRequest(http.MethodDelete, tc.Server.URL+url, nil)
if err != nil {
tc.T.Fatalf("unexpected error: %v", err)
}
if len(tc.Cookies) > 0 {
for _, c := range tc.Cookies {
req.AddCookie(c)
}
}
res, err := tc.Client.Do(req)
if err != nil {
tc.T.Fatalf("failed to delete %s: %v", url, err)
}
return testutil.NewAssertableResponse(tc.T, res)
}
func (tc *TestCase) AssertDatabaseCount(table string, expected int) *TestCase {
tc.T.Helper()
stmt := fmt.Sprintf(`SELECT COUNT(*) FROM %s`, table)
var count int
err := tc.Store.DB().QueryRow(stmt).Scan(&count)
if err != nil {
tc.T.Fatalf("failed to count rows from table '%s', error: %v", table, err)
}
if count != expected {
tc.T.Fatalf("expected to find %d number of rows in a table '%s, but found %d", expected, table, count)
}
return tc
}
func (tc *TestCase) CreateTestUser(email, password string) *TestCase {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
tc.T.Fatalf("unexpected bcrypt error: %v", err)
}
user, err := tc.Store.CreateUser(tc.T.Context(), uptimemonitor.User{
Name: "Test User",
Email: email,
PasswordHash: string(hash),
})
if err != nil {
tc.T.Fatalf("unable to create test user: %v", err)
}
tc.User = &user
return tc
}
func (tc *TestCase) LogIn() *TestCase {
tc.CreateTestUser("test@example.com", "password")
session, err := tc.Store.CreateSession(tc.T.Context(), uptimemonitor.Session{
User: *tc.User,
UserID: tc.User.ID,
ExpiresAt: time.Now().Add(time.Hour),
})
if err != nil {
tc.T.Fatalf("unexpected error: %v", err)
}
c := &http.Cookie{
Name: "session",
Value: session.Uuid,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: false,
Expires: session.ExpiresAt,
}
return tc.WithCookie(c)
}
func (tc *TestCase) AssertEqual(a, b any) *TestCase {
tc.T.Helper()
if !reflect.DeepEqual(a, b) {
tc.T.Fatalf(`expected "%v" to be equal to "%v"`, a, b)
}
return tc
}
func (tc *TestCase) AssertNoError(err error) *TestCase {
tc.T.Helper()
if err != nil {
tc.T.Fatalf("unexpected error: %v", err)
}
return tc
}

110
test/uptime_test.go Normal file
View File

@@ -0,0 +1,110 @@
package test
import (
"testing"
"uptimemonitor"
)
func TestUptime(t *testing.T) {
t.Run("uptime is empty when there are no checks", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
m, _ := tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: "http://example.com",
})
tc.AssertEqual(m.Uptime, float32(0))
tc.AssertEqual(m.AvgResponseTimeMs, int64(0))
})
t.Run("uptime is computed when check is created", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: "http://example.com",
})
tc.Store.CreateCheck(t.Context(), uptimemonitor.Check{
MonitorID: 1,
StatusCode: 200,
ResponseTimeMs: 100,
})
m, _ := tc.Store.GetMonitorByID(t.Context(), 1)
tc.AssertEqual(m.N, int64(1))
tc.AssertEqual(m.Uptime, float32(100))
tc.AssertEqual(m.AvgResponseTimeMs, int64(100))
})
t.Run("it works with multiple checks", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: "http://example.com",
})
tc.Store.CreateCheck(t.Context(), uptimemonitor.Check{
MonitorID: 1,
StatusCode: 200,
ResponseTimeMs: 100,
})
tc.Store.CreateCheck(t.Context(), uptimemonitor.Check{
MonitorID: 1,
StatusCode: 200,
ResponseTimeMs: 200,
})
m, _ := tc.Store.GetMonitorByID(t.Context(), 1)
tc.AssertEqual(m.N, int64(2))
tc.AssertEqual(m.Uptime, float32(100))
tc.AssertEqual(m.AvgResponseTimeMs, int64(150))
})
t.Run("uptime is updated", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: "http://example.com",
})
tc.Store.CreateCheck(t.Context(), uptimemonitor.Check{
MonitorID: 1,
StatusCode: 404,
ResponseTimeMs: 100,
})
m, _ := tc.Store.GetMonitorByID(t.Context(), 1)
tc.AssertEqual(m.N, int64(1))
tc.AssertEqual(m.Uptime, float32(0))
tc.Store.CreateCheck(t.Context(), uptimemonitor.Check{
MonitorID: 1,
StatusCode: 200,
ResponseTimeMs: 100,
})
m, _ = tc.Store.GetMonitorByID(t.Context(), 1)
tc.AssertEqual(m.N, int64(2))
tc.AssertEqual(m.Uptime, float32(50))
tc.Store.CreateCheck(t.Context(), uptimemonitor.Check{
MonitorID: 1,
StatusCode: 500,
ResponseTimeMs: 100,
})
m, _ = tc.Store.GetMonitorByID(t.Context(), 1)
tc.AssertEqual(m.N, int64(3))
tc.AssertEqual(m.Uptime, float32(33.3))
})
}

162
test/webhook_test.go Normal file
View File

@@ -0,0 +1,162 @@
package test
import (
"fmt"
"net/http"
"net/url"
"testing"
"time"
"uptimemonitor"
"uptimemonitor/form"
"uptimemonitor/service"
)
func TestWebhook_SaveWebhook(t *testing.T) {
t.Run("webhook is validated", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
f := form.MonitorForm{
Url: "https://valid.url",
HttpMethod: http.MethodGet,
HasWebhook: true,
WebhookUrl: "invalid",
WebhookHeaders: "invalid json",
WebhookBody: "data",
}
tc.AssertEqual(false, f.Validate())
})
t.Run("webhook info can be saved when creating a monitor", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.LogIn().
Post("/monitors", url.Values{
"http_method": []string{"GET"},
"url": []string{"https://example.com"},
"has_webhook": []string{"on"},
"webhook_url": []string{tc.Server.URL + "/test/webhook"},
"webhook_method": []string{"POST"},
"webhook_headers": []string{`{"test":"abc"}`},
"webhook_body": []string{`{"test":"123"}`},
}).AssertStatusCode(http.StatusOK)
m, _ := tc.Store.GetMonitorByID(t.Context(), 1)
tc.AssertEqual(m.WebhookUrl, tc.Server.URL+"/test/webhook")
tc.AssertEqual(m.WebhookMethod, "POST")
tc.AssertEqual(m.WebhookHeaders, `{"test":"abc"}`)
tc.AssertEqual(m.WebhookBody, `{"test":"123"}`)
})
t.Run("webhook data can be updated", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
_, err := tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
HttpMethod: http.MethodGet,
Url: "https://google.com",
})
tc.AssertNoError(err)
tc.LogIn().
Patch("/monitors/1", url.Values{
"http_method": []string{"GET"},
"url": []string{"https://example.com"},
"has_webhook": []string{"on"},
"webhook_url": []string{tc.Server.URL + "/test/webhook"},
"webhook_method": []string{"POST"},
"webhook_headers": []string{`{"test":"abc"}`},
"webhook_body": []string{`{"test":"123"}`},
}).AssertStatusCode(http.StatusOK)
m, err := tc.Store.GetMonitorByID(t.Context(), 1)
tc.AssertNoError(err)
tc.AssertEqual(m.WebhookUrl, tc.Server.URL+"/test/webhook")
tc.AssertEqual(m.WebhookMethod, "POST")
tc.AssertEqual(m.WebhookHeaders, `{"test":"abc"}`)
tc.AssertEqual(m.WebhookBody, `{"test":"123"}`)
})
t.Run("webhook fields are present in the forms", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
tc.LogIn().Get("/new").
AssertElementVisible(`input[name="has_webhook"]`).
AssertElementVisible(`select[name="webhook_method"]`).
AssertElementVisible(`textarea[name="webhook_body"]`).
AssertElementVisible(`textarea[name="webhook_headers"]`)
m, _ := tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
HttpMethod: http.MethodGet,
Url: "https://google.com",
})
tc.Get(fmt.Sprintf("/m/%s/edit", m.Uuid)).
AssertElementVisible(`input[name="has_webhook"]`).
AssertElementVisible(`select[name="webhook_method"]`).
AssertElementVisible(`textarea[name="webhook_body"]`).
AssertElementVisible(`textarea[name="webhook_headers"]`)
})
t.Run("webhook is called on incident", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
ExpectedWebhookBody = `{"test":123}`
ExpectedWebhookHeaderKey = "test"
ExpectedWebhookHeaderValue = "abc"
service := service.New(tc.Store)
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: tc.Server.URL + "/test/500",
WebhookMethod: http.MethodPost,
WebhookUrl: tc.Server.URL + "/test/webhook",
WebhookHeaders: `{"test":"abc"}`,
WebhookBody: `{"test":123}`,
})
ch := service.StartCheck()
service.RunCheck(t.Context(), ch)
time.Sleep(time.Second * 3)
tc.AssertDatabaseCount("incidents", 1)
tc.AssertDatabaseCount("checks", 1)
tc.AssertEqual(int64(TestWebhookCalledCount), int64(1))
})
t.Run("webhook can have parsed body", func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Close()
service := service.New(tc.Store)
url := tc.Server.URL + "/test/500"
tc.Store.CreateMonitor(t.Context(), uptimemonitor.Monitor{
Url: url,
WebhookMethod: http.MethodPost,
WebhookUrl: tc.Server.URL + "/test/webhook",
WebhookBody: `{{.Url}},{{ .StatusCode}}`,
})
ch := service.StartCheck()
service.RunCheck(t.Context(), ch)
time.Sleep(time.Second * 3)
tc.AssertDatabaseCount("incidents", 1)
tc.AssertDatabaseCount("checks", 1)
tc.AssertEqual(int64(TestWebhookCalledCount), int64(1))
tc.AssertEqual(TestWebhookBody, fmt.Sprintf("%s,%d", url, 500))
})
}