Files
uptimemonitor/test/test_case.go
2025-08-01 18:01:55 +02:00

350 lines
8.2 KiB
Go

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
}