package main import ( "context" "database/sql" "fmt" "net/http" "net/url" "strings" "sync/atomic" "testing" "time" "me-fit/service" "me-fit/types" "github.com/google/uuid" "github.com/stretchr/testify/assert" "golang.org/x/net/html" ) var ( httpClient = http.Client{ // Disable redirect following CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } port atomic.Int32 ) func TestIntegrationSecurityHeader(t *testing.T) { t.Parallel() t.Run("should keep caching for static content", func(t *testing.T) { t.Parallel() _, basePath, ctx := setupIntegrationTest(t) req, err := http.NewRequestWithContext(ctx, "GET", basePath+"/static/favicon.svg", nil) assert.Nil(t, err) resp, err := httpClient.Do(req) assert.Nil(t, err) cacheControl := resp.Header.Get("Cache-Control") assert.Equal(t, "", cacheControl) }) t.Run("should disable caching for dynamic content", func(t *testing.T) { t.Parallel() _, basePath, ctx := setupIntegrationTest(t) req, err := http.NewRequestWithContext(ctx, "GET", basePath, nil) assert.Nil(t, err) resp, err := httpClient.Do(req) assert.Nil(t, err) cacheControl := resp.Header.Get("Cache-Control") assert.Equal(t, "no-cache, no-store, must-revalidate", cacheControl) }) t.Run("should include security headers", func(t *testing.T) { t.Parallel() _, basePath, ctx := setupIntegrationTest(t) req, err := http.NewRequestWithContext(ctx, "GET", basePath, nil) assert.Nil(t, err) resp, err := httpClient.Do(req) assert.Nil(t, err) value := resp.Header.Get("X-Content-Type-Options") assert.Equal(t, "nosniff", value) value = resp.Header.Get("Access-Control-Allow-Origin") assert.Equal(t, basePath, value) value = resp.Header.Get("Access-Control-Allow-Methods") assert.Equal(t, "GET, POST, DELETE", value) value = resp.Header.Get("Content-Security-Policy") assert.Equal(t, "default-src 'none'; "+ "script-src 'self' https://umami.me-fit.eu; "+ "connect-src 'self' https://umami.me-fit.eu; "+ "img-src 'self'; "+ "style-src 'self'; "+ "form-action 'self'; "+ "frame-ancestors 'none';", value) value = resp.Header.Get("Cross-Origin-Resource-Policy") assert.Equal(t, "same-site", value) value = resp.Header.Get("Cross-Origin-Opener-Policy") assert.Equal(t, "same-origin", value) value = resp.Header.Get("Cross-Origin-Embedder-Policy") assert.Equal(t, "require-corp", value) value = resp.Header.Get("Permissions-Policy") assert.Equal(t, "geolocation=(), camera=(), microphone=(), interest-cohort=()", value) value = resp.Header.Get("Referrer-Policy") assert.Equal(t, "strict-origin-when-cross-origin", value) value = resp.Header.Get("Strict-Transport-Security") assert.Equal(t, "max-age=63072000; includeSubDomains; preload", value) }) } func TestIntegrationAuth(t *testing.T) { t.Parallel() t.Run("SignIn", func(t *testing.T) { t.Run(`should redirect to "/" if user is already signed in`, func(t *testing.T) { t.Parallel() db, basePath, ctx := setupIntegrationTest(t) userId := uuid.New() sessionId := "session-id" pass := service.GetHashPassword("password", []byte("salt")) _, err := db.Exec(` INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) assert.Nil(t, err) _, err = db.Exec(` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) assert.Nil(t, err) req, err := http.NewRequestWithContext(ctx, "GET", basePath+"/auth/signin", nil) assert.Nil(t, err) req.Header.Set("Cookie", "id="+sessionId) resp, err := httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusSeeOther, resp.StatusCode) assert.Equal(t, "/", resp.Header.Get("Location")) }) t.Run(`should fail without valid csrf token`, func(t *testing.T) { t.Parallel() db, basePath, ctx := setupIntegrationTest(t) userId := uuid.New() pass := service.GetHashPassword("password", []byte("salt")) _, err := db.Exec(` INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) assert.Nil(t, err) formData := url.Values{ "email": {"mail@mail.de"}, "password": {"password"}, "csrf-token": {"invalid-csrf-token"}, } req, err := http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/signin", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp, err := httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, resp.StatusCode) }) t.Run(`should fail with invalid username`, func(t *testing.T) { t.Parallel() db, basePath, ctx := setupIntegrationTest(t) userId := uuid.New() pass := service.GetHashPassword("password", []byte("salt")) _, err := db.Exec(` INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) assert.Nil(t, err) req, err := http.NewRequestWithContext(ctx, "GET", basePath+"/auth/signin", nil) assert.Nil(t, err) resp, err := httpClient.Do(req) assert.Nil(t, err) html, err := html.Parse(resp.Body) assert.Nil(t, err) csrfToken := findCsrfToken(html) assert.NotEqual(t, "", csrfToken) session := findCookie(resp, "id") formData := url.Values{ "email": {"invalid@mail.de"}, "password": {"password"}, "csrf-token": {csrfToken}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/signin", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+session.Value) req.Header.Set("HX-Request", "true") resp, err = httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) assert.Contains(t, resp.Header.Get("HX-Trigger"), "email or password") }) t.Run(`should fail with invalid password`, func(t *testing.T) { t.Parallel() db, basePath, ctx := setupIntegrationTest(t) userId := uuid.New() pass := service.GetHashPassword("password", []byte("salt")) _, err := db.Exec(` INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) assert.Nil(t, err) req, err := http.NewRequestWithContext(ctx, "GET", basePath+"/auth/signin", nil) assert.Nil(t, err) resp, err := httpClient.Do(req) assert.Nil(t, err) html, err := html.Parse(resp.Body) assert.Nil(t, err) csrfToken := findCsrfToken(html) assert.NotEqual(t, "", csrfToken) session := findCookie(resp, "id") formData := url.Values{ "email": {"mail@mail.de"}, "password": {"invalid-password"}, "csrf-token": {csrfToken}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/signin", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+session.Value) req.Header.Set("HX-Request", "true") resp, err = httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) assert.Contains(t, resp.Header.Get("HX-Trigger"), "email or password") }) t.Run("should return secure cookie with NEW session-id", func(t *testing.T) { t.Parallel() db, basePath, ctx := setupIntegrationTest(t) pass := service.GetHashPassword("password", []byte("salt")) _, err := db.Exec(` INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, uuid.New(), pass, []byte("salt")) assert.Nil(t, err) req, err := http.NewRequestWithContext(ctx, "GET", basePath+"/auth/signin", nil) assert.Nil(t, err) resp, err := httpClient.Do(req) assert.Nil(t, err) html, err := html.Parse(resp.Body) assert.Nil(t, err) anonymousCsrfToken := findCsrfToken(html) assert.NotEqual(t, "", anonymousCsrfToken) anonymousSession := findCookie(resp, "id") assert.NotNil(t, anonymousSession) formData := url.Values{ "email": {"mail@mail.de"}, "password": {"password"}, "csrf-token": {anonymousCsrfToken}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/signin", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+anonymousSession.Value) resp, err = httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusSeeOther, resp.StatusCode) cookie := findCookie(resp, "id") assert.NotNil(t, cookie) assert.Equal(t, http.SameSiteStrictMode, cookie.SameSite, "Cookie is not secure") assert.True(t, cookie.HttpOnly, "Cookie is not secure") assert.True(t, cookie.Secure, "Cookie is not secure") assert.NotEqual(t, anonymousSession.Value, cookie.Value, "Session ID did not change") }) }) t.Run("SignOut", func(t *testing.T) { t.Run("should fail if csrf token is not valid", func(t *testing.T) { t.Parallel() _, basePath, ctx := setupIntegrationTest(t) req, err := http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/sign-out", nil) assert.Nil(t, err) req.Header.Set("csrf-token", "invalid-csrf-token") resp, err := httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, resp.StatusCode) }) t.Run(`should delete current session and redirect to "/"`, func(t *testing.T) { t.Parallel() db, basePath, ctx := setupIntegrationTest(t) userId := uuid.New() sessionId := "session-id" pass := service.GetHashPassword("password", []byte("salt")) _, err := db.Exec(` INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) assert.Nil(t, err) _, err = db.Exec(` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) assert.Nil(t, err) req, err := http.NewRequestWithContext(ctx, "GET", basePath+"/", nil) assert.Nil(t, err) req.Header.Set("Cookie", "id="+sessionId) resp, err := httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) var csrfToken string err = db.QueryRow("SELECT token FROM token WHERE user_id = ? AND type = ?", userId, types.TokenTypeCsrf).Scan(&csrfToken) assert.Nil(t, err) req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/signout", nil) assert.Nil(t, err) req.Header.Set("csrf-token", csrfToken) req.Header.Set("Cookie", "id="+sessionId) resp, err = httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusSeeOther, resp.StatusCode) assert.Equal(t, "/", resp.Header.Get("Location")) cookie := findCookie(resp, "id") assert.NotNil(t, cookie) assert.Equal(t, "", cookie.Value) assert.Equal(t, -1, cookie.MaxAge) var rows int err = db.QueryRow("SELECT COUNT(*) FROM session WHERE user_id = ?", userId).Scan(&rows) assert.Nil(t, err) assert.Equal(t, 0, rows) }) }) t.Run("DeleteAccount", func(t *testing.T) { t.Run(`should redirect to "/" if not signed in`, func(t *testing.T) { t.Parallel() _, basePath, ctx := setupIntegrationTest(t) req, err := http.NewRequestWithContext(ctx, "GET", basePath+"/auth/delete-account", nil) assert.Nil(t, err) resp, err := httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusSeeOther, resp.StatusCode) assert.Equal(t, "/auth/signin", resp.Header.Get("Location")) }) t.Run("should fail if not signed in", func(t *testing.T) { t.Parallel() _, basePath, ctx := setupIntegrationTest(t) req, err := http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/delete-account", nil) assert.Nil(t, err) resp, err := httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, resp.StatusCode) }) t.Run("should fail if password is incorrect", func(t *testing.T) { t.Parallel() db, basePath, ctx := setupIntegrationTest(t) userId := uuid.New() pass := service.GetHashPassword("password", []byte("salt")) _, err := db.Exec(` INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) sessionId := "session-id" assert.Nil(t, err) _, err = db.Exec(` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) assert.Nil(t, err) req, err := http.NewRequestWithContext(ctx, "GET", basePath+"/auth/delete-account", nil) assert.Nil(t, err) req.Header.Set("Cookie", "id="+sessionId) resp, err := httpClient.Do(req) assert.Nil(t, err) html, err := html.Parse(resp.Body) assert.Nil(t, err) csrfToken := findCsrfToken(html) assert.NotEqual(t, "", csrfToken) formData := url.Values{ "password": {"wrong-password"}, "csrf-token": {csrfToken}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/delete-account", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("HX-Request", "true") resp, err = httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, resp.StatusCode) }) t.Run("should fail if csrf-token is incorrect", func(t *testing.T) { t.Parallel() db, basePath, ctx := setupIntegrationTest(t) userId := uuid.New() pass := service.GetHashPassword("password", []byte("salt")) _, err := db.Exec(` INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) sessionId := "session-id" assert.Nil(t, err) _, err = db.Exec(` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) assert.Nil(t, err) formData := url.Values{ "password": {"password"}, "csrf-token": {"wrong-csrf-token"}, } req, err := http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/delete-account", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("HX-Request", "true") resp, err := httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, resp.StatusCode) }) t.Run("should delete all user related data", func(t *testing.T) { t.Parallel() db, basePath, ctx := setupIntegrationTest(t) userId := uuid.New() pass := service.GetHashPassword("password", []byte("salt")) _, err := db.Exec(` INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) sessionId := "session-id" assert.Nil(t, err) _, err = db.Exec(` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) assert.Nil(t, err) req, err := http.NewRequestWithContext(ctx, "GET", basePath+"/auth/delete-account", nil) assert.Nil(t, err) req.Header.Set("Cookie", "id="+sessionId) resp, err := httpClient.Do(req) assert.Nil(t, err) html, err := html.Parse(resp.Body) assert.Nil(t, err) csrfToken := findCsrfToken(html) assert.NotEqual(t, "", csrfToken) formData := url.Values{ "password": {"password"}, "csrf-token": {csrfToken}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/delete-account", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("HX-Request", "true") resp, err = httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) var rows int err = db.QueryRow("SELECT COUNT(*) FROM session WHERE user_id = ?", userId).Scan(&rows) assert.Nil(t, err) assert.Equal(t, 0, rows) err = db.QueryRow("SELECT COUNT(*) FROM token WHERE user_id = ?", userId).Scan(&rows) assert.Nil(t, err) assert.Equal(t, 0, rows) err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ?", userId).Scan(&rows) assert.Nil(t, err) assert.Equal(t, 0, rows) err = db.QueryRow("SELECT COUNT(*) FROM workout WHERE user_id = ?", userId).Scan(&rows) assert.Nil(t, err) assert.Equal(t, 0, rows) }) }) t.Run("ChangePassword", func(t *testing.T) { t.Run("should change password and invalidate all other user sessions", func(t *testing.T) { t.Parallel() db, basePath, ctx := setupIntegrationTest(t) userId := uuid.New() userIdOther := uuid.New() pass := service.GetHashPassword("password", []byte("salt")) _, err := db.Exec(` INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) sessionId := "session-id" assert.Nil(t, err) _, err = db.Exec(` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) assert.Nil(t, err) _, err = db.Exec(` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES ("second", ?, datetime(), datetime("now", "+1 day"))`, userId) assert.Nil(t, err) _, err = db.Exec(` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES ("other", ?, datetime(), datetime("now", "+1 day"))`, userIdOther) assert.Nil(t, err) req, err := http.NewRequestWithContext(ctx, "GET", basePath+"/auth/change-password", nil) assert.Nil(t, err) req.Header.Set("Cookie", "id="+sessionId) resp, err := httpClient.Do(req) assert.Nil(t, err) html, err := html.Parse(resp.Body) assert.Nil(t, err) csrfToken := findCsrfToken(html) assert.NotEqual(t, "", csrfToken) formData := url.Values{ "current-password": {"password"}, "new-password": {"MyNewSecurePassword1!"}, "csrf-token": {csrfToken}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/change-password", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("HX-Request", "true") resp, err = httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) var sessionIds []string sessions, err := db.Query(`SELECT session_id FROM session WHERE NOT user_id = ? ORDER BY session_id`, uuid.Nil) assert.Nil(t, err) for sessions.Next() { var sessionId string err = sessions.Scan(&sessionId) assert.Nil(t, err) sessionIds = append(sessionIds, sessionId) } assert.Equal(t, 2, len(sessionIds)) assert.Equal(t, "other", sessionIds[0]) assert.Equal(t, "session-id", sessionIds[1]) }) }) t.Run("ForgotPassword", func(t *testing.T) { t.Run("should change password and invalidate ALL sessions", func(t *testing.T) { t.Parallel() d, basePath, ctx := setupIntegrationTest(t) userId := uuid.New() pass := service.GetHashPassword("password", []byte("salt")) _, err := d.Exec(` INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) assert.Nil(t, err) _, err = d.Exec(` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES ("session-id", ?, datetime(), datetime("now", "+1 day"))`, userId) assert.Nil(t, err) req, err := http.NewRequestWithContext(ctx, "GET", basePath+"/auth/forgot-password", nil) assert.Nil(t, err) resp, err := httpClient.Do(req) assert.Nil(t, err) sessionId := findCookie(resp, "id").Value html, err := html.Parse(resp.Body) assert.Nil(t, err) csrfToken := findCsrfToken(html) assert.NotEqual(t, "", csrfToken) formData := url.Values{ "email": {"mail@mail.de"}, "csrf-token": {csrfToken}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/forgot-password", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("HX-Request", "true") resp, err = httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) var token string err = d.QueryRow("SELECT token FROM token WHERE type = ?", types.TokenTypePasswordReset).Scan(&token) assert.Nil(t, err) formData = url.Values{ "new-password": {"MyNewSecurePassword1!"}, "csrf-token": {csrfToken}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/forgot-password-actual", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("HX-Request", "true") req.Header.Set("HX-Current-URL", basePath+"/auth/change-password?token="+url.QueryEscape(token)) resp, err = httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) sessions, err := d.Query("SELECT session_id FROM session WHERE user_id = ?", userId) assert.Nil(t, err) assert.False(t, sessions.Next()) }) }) t.Run("Session", func(t *testing.T) { t.Run("should create new anonymous session if current session gets outdated", func(t *testing.T) { t.Parallel() d, basePath, ctx := setupIntegrationTest(t) userId := uuid.New() sessionId := "session-id" _, err := d.Exec(` INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt")) assert.Nil(t, err) _, err = d.Exec(` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES (?, ?, datetime("now", "-8 hour"), datetime("now", "-1 minute"))`, sessionId, userId) assert.Nil(t, err) req, err := http.NewRequestWithContext(ctx, "GET", basePath, nil) assert.Nil(t, err) req.Header.Set("Cookie", "id="+sessionId) resp, err := httpClient.Do(req) assert.Nil(t, err) newSession := findCookie(resp, "id") assert.NotNil(t, newSession) assert.NotEqual(t, sessionId, newSession.Value) var rows int err = d.QueryRow("SELECT COUNT(*) FROM session WHERE user_id = ?", userId).Scan(&rows) assert.Nil(t, err) assert.Equal(t, 0, rows) }) t.Run("should create anonymous session", func(t *testing.T) { t.Parallel() _, basePath, ctx := setupIntegrationTest(t) req, err := http.NewRequestWithContext(ctx, "GET", basePath, nil) assert.Nil(t, err) resp, err := httpClient.Do(req) assert.Nil(t, err) newSession := findCookie(resp, "id") assert.NotNil(t, newSession) assert.NotEqual(t, "", newSession.Value) }) t.Run("should not have access to user information with outdated session", func(t *testing.T) { t.Parallel() d, basePath, ctx := setupIntegrationTest(t) userId := uuid.New() sessionId := "session-id" _, err := d.Exec(` INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt")) assert.Nil(t, err) _, err = d.Exec(` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES (?, ?, datetime("now", "-8 hour"), datetime("now", "-1 minute"))`, sessionId, userId) assert.Nil(t, err) req, err := http.NewRequestWithContext(ctx, "GET", basePath+"/workout", nil) assert.Nil(t, err) req.Header.Set("Cookie", "id="+sessionId) resp, err := httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusSeeOther, resp.StatusCode) assert.Equal(t, "/auth/signin", resp.Header.Get("Location")) }) }) } func findCookie(resp *http.Response, name string) *http.Cookie { for _, cookie := range resp.Cookies() { if cookie.Name == name { return cookie } } return nil } func setupIntegrationTest(t *testing.T) (db *sql.DB, basePath string, ctx context.Context) { ctx, done := context.WithCancel(context.Background()) t.Cleanup(done) db, err := sql.Open("sqlite3", ":memory:") if err != nil { t.Fatalf("Could not open Database data.db: %v", err) } t.Cleanup(func() { db.Close() }) testPort := port.Add(1) testPort += 1024 go run(ctx, db, getEnv(testPort)) basePath = "http://localhost:" + fmt.Sprint(testPort) err = waitForReady(ctx, 5*time.Second, basePath, t) assert.Nil(t, err) return db, basePath, ctx } func getEnv(port int32) func(string) string { return func(key string) string { if key == "PORT" { return fmt.Sprint(port) } else if key == "SMTP_ENABLED" { return "false" } else if key == "PROMETHEUS_ENABLED" { return "false" } else if key == "BASE_URL" { return "http://localhost:" + fmt.Sprint(port) } else if key == "ENVIRONMENT" { return "test" } else { return "" } } } // waitForReady calls the specified endpoint until it gets a 200 // response or until the context is cancelled or the timeout is // reached. func waitForReady( ctx context.Context, timeout time.Duration, endpoint string, t *testing.T, ) error { client := http.Client{} startTime := time.Now() for { req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) assert.Nil(t, err) resp, err := client.Do(req) if err == nil && resp.StatusCode == http.StatusOK { resp.Body.Close() return nil } else if err == nil { resp.Body.Close() } select { case <-ctx.Done(): return ctx.Err() default: if time.Since(startTime) >= timeout { t.Fatal("timeout reached while waiting for endpoint") return types.ErrInternal } // wait a little while between checks time.Sleep(250 * time.Millisecond) } } } func findCsrfToken(data *html.Node) string { attr := getTokenAttribute(data) if attr != nil { return attr.Val } if data.FirstChild != nil { if token := findCsrfToken(data.FirstChild); token != "" { return token } } if data.NextSibling != nil { if token := findCsrfToken(data.NextSibling); token != "" { return token } } return "" } func getTokenAttribute(data *html.Node) *html.Attribute { returnValue := false for _, attr := range data.Attr { if attr.Key == "name" && attr.Val == "csrf-token" { returnValue = true } } if !returnValue { return nil } for _, attr := range data.Attr { if attr.Key == "value" { return &attr } } return nil }