package test_test import ( "net/http" "net/url" "spend-sparrow/internal/service" "spend-sparrow/internal/types" "strings" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/net/html" ) 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, http.MethodGet, basePath+"/static/favicon.svg", nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() cacheControl := resp.Header.Get("Cache-Control") assert.Empty(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, http.MethodGet, basePath, nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() 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, http.MethodGet, basePath, nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() 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'; "+ "font-src 'self'; "+ "connect-src 'self'; "+ "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-origin", 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.Parallel() 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")) require.NoError(t, err) _, err = db.Exec(` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/signin", nil) require.NoError(t, err) req.Header.Set("Cookie", "id="+sessionId) resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() 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")) require.NoError(t, err) formData := url.Values{ "email": {"mail@mail.de"}, "password": {"password"}, } req, err := http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/signin", strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Csrf-Token", "invalid-csrf-token") resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() 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")) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/signin", nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) html, err := html.Parse(resp.Body) require.NoError(t, err) _ = resp.Body.Close() csrfToken := findCsrfToken(t, html) assert.NotEmpty(t, csrfToken) session := findCookie(t, resp) formData := url.Values{ "email": {"invalid@mail.de"}, "password": {"password"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/signin", strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+session.Value) req.Header.Set("Csrf-Token", csrfToken) req.Header.Set("Hx-Request", "true") resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() 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")) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/signin", nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) html, err := html.Parse(resp.Body) require.NoError(t, err) _ = resp.Body.Close() csrfToken := findCsrfToken(t, html) assert.NotEmpty(t, csrfToken) session := findCookie(t, resp) formData := url.Values{ "email": {"mail@mail.de"}, "password": {"invalid-password"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/signin", strings.NewReader(formData.Encode())) require.NoError(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") req.Header.Set("Csrf-Token", csrfToken) resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() 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")) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/signin", nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) html, err := html.Parse(resp.Body) require.NoError(t, err) _ = resp.Body.Close() anonymousCsrfToken := findCsrfToken(t, html) assert.NotEmpty(t, anonymousCsrfToken) anonymousSession := findCookie(t, resp) assert.NotNil(t, anonymousSession) formData := url.Values{ "email": {"mail@mail.de"}, "password": {"password"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/signin", strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Csrf-Token", anonymousCsrfToken) req.Header.Set("Cookie", "id="+anonymousSession.Value) resp, err = httpClient.Do(req) require.NoError(t, err) assert.Equal(t, http.StatusSeeOther, resp.StatusCode) _ = resp.Body.Close() cookie := findCookie(t, resp) 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("should return in ~250 ms in all cases", 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")) require.NoError(t, err) // Everythings correct req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/signin", nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) body, err := html.Parse(resp.Body) require.NoError(t, err) _ = resp.Body.Close() anonymousCsrfToken := findCsrfToken(t, body) assert.NotEmpty(t, anonymousCsrfToken) anonymousSession := findCookie(t, resp) assert.NotNil(t, anonymousSession) formData := url.Values{ "email": {"mail@mail.de"}, "password": {"password"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/signin", strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+anonymousSession.Value) req.Header.Set("Csrf-Token", anonymousCsrfToken) req.Header.Set("Hx-Request", "true") timeStart := time.Now() resp, err = httpClient.Do(req) timeEnd := time.Now() require.NoError(t, err) if timeEnd.Sub(timeStart) > 260*time.Millisecond || timeEnd.Sub(timeStart) < 250*time.Millisecond { t.Fail() t.Logf("Time did not match: %v", timeEnd.Sub(timeStart)) } assert.Equal(t, http.StatusOK, resp.StatusCode) _ = resp.Body.Close() // Wrong password req, err = http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/signin", nil) require.NoError(t, err) resp, err = httpClient.Do(req) require.NoError(t, err) body, err = html.Parse(resp.Body) require.NoError(t, err) _ = resp.Body.Close() anonymousCsrfToken = findCsrfToken(t, body) assert.NotEmpty(t, anonymousCsrfToken) anonymousSession = findCookie(t, resp) assert.NotNil(t, anonymousSession) formData = url.Values{ "email": {"mail@mail.de"}, "password": {"wrong-password"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/signin", strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+anonymousSession.Value) req.Header.Set("Hx-Request", "true") req.Header.Set("Csrf-Token", anonymousCsrfToken) timeStart = time.Now() resp, err = httpClient.Do(req) timeEnd = time.Now() require.NoError(t, err) if timeEnd.Sub(timeStart) > 260*time.Millisecond || timeEnd.Sub(timeStart) <= 250*time.Millisecond { t.Fail() t.Logf("Time did not match: %v", timeEnd.Sub(timeStart)) } assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) _ = resp.Body.Close() // Wrong username req, err = http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/signin", nil) require.NoError(t, err) resp, err = httpClient.Do(req) require.NoError(t, err) body, err = html.Parse(resp.Body) require.NoError(t, err) _ = resp.Body.Close() anonymousCsrfToken = findCsrfToken(t, body) assert.NotEmpty(t, anonymousCsrfToken) anonymousSession = findCookie(t, resp) assert.NotNil(t, anonymousSession) formData = url.Values{ "email": {"invalid-mail@mail.de"}, "password": {"password"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/signin", strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+anonymousSession.Value) req.Header.Set("Hx-Request", "true") req.Header.Set("Csrf-Token", anonymousCsrfToken) timeStart = time.Now() resp, err = httpClient.Do(req) timeEnd = time.Now() require.NoError(t, err) if timeEnd.Sub(timeStart) > 260*time.Millisecond || timeEnd.Sub(timeStart) <= 250*time.Millisecond { t.Fail() t.Logf("Time did not match: %v", timeEnd.Sub(timeStart)) } assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) _ = resp.Body.Close() }) t.Run("should create new session and invalidate old one (session fixation prevention)", 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")) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/signin", nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) html, err := html.Parse(resp.Body) require.NoError(t, err) _ = resp.Body.Close() anonymousCsrfToken := findCsrfToken(t, html) assert.NotEmpty(t, anonymousCsrfToken) anonymousSession := findCookie(t, resp) assert.NotNil(t, anonymousSession) formData := url.Values{ "email": {"mail@mail.de"}, "password": {"password"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/signin", strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+anonymousSession.Value) req.Header.Set("Hx-Request", "true") req.Header.Set("Csrf-Token", anonymousCsrfToken) resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) var rows int err = db.QueryRow("SELECT COUNT(*) FROM session WHERE session_id = ?", anonymousSession.Value).Scan(&rows) require.NoError(t, err) assert.Equal(t, 0, rows) err = db.QueryRow("SELECT COUNT(*) FROM token WHERE token = ?", anonymousCsrfToken).Scan(&rows) require.NoError(t, err) assert.Equal(t, 0, rows) }) }) t.Run("SignUp", func(t *testing.T) { t.Run(`should redirect to "/" if 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")) require.NoError(t, err) _, err = db.Exec(` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/signin", nil) require.NoError(t, err) req.Header.Set("Cookie", "id="+sessionId) resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusSeeOther, resp.StatusCode) assert.Equal(t, "/", resp.Header.Get("Location")) }) t.Run(`should fail if csrf token is invalid`, func(t *testing.T) { t.Parallel() _, basePath, ctx := setupIntegrationTest(t) formData := url.Values{ "email": {"mail@mail.de"}, "password": {"password"}, } req, err := http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/signin", strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Hx-Request", "true") req.Header.Set("Csrf-Token", "invalid-csrf-token") resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusBadRequest, resp.StatusCode) assert.Contains(t, resp.Header.Get("Hx-Trigger"), "CSRF") }) t.Run(`should fail if password is insecure`, func(t *testing.T) { t.Parallel() _, basePath, ctx := setupIntegrationTest(t) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/signup", nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) body, err := html.Parse(resp.Body) require.NoError(t, err) _ = resp.Body.Close() anonymousCsrfToken := findCsrfToken(t, body) assert.NotEmpty(t, anonymousCsrfToken) anonymousSession := findCookie(t, resp) assert.NotNil(t, anonymousSession) formData := url.Values{ "email": {"mail@mail.de"}, "password": {"insecure-password"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/signup", strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Hx-Request", "true") req.Header.Set("Cookie", "id="+anonymousSession.Value) req.Header.Set("Csrf-Token", anonymousCsrfToken) resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusBadRequest, resp.StatusCode) assert.Contains(t, resp.Header.Get("Hx-Trigger"), "password") }) t.Run(`should say "verification mail send" if user already exists within ~250 ms`, func(t *testing.T) { t.Parallel() db, basePath, ctx := setupIntegrationTest(t) _, err := db.Exec(` INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, uuid.New(), service.GetHashPassword("password", []byte("salt")), []byte("salt")) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/signup", nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) body, err := html.Parse(resp.Body) require.NoError(t, err) _ = resp.Body.Close() anonymousCsrfToken := findCsrfToken(t, body) assert.NotEmpty(t, anonymousCsrfToken) anonymousSession := findCookie(t, resp) assert.NotNil(t, anonymousSession) formData := url.Values{ "email": {"mail@mail.de"}, "password": {"secure-Password!1"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/signup", strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Hx-Request", "true") req.Header.Set("Cookie", "id="+anonymousSession.Value) req.Header.Set("Csrf-Token", anonymousCsrfToken) timeStart := time.Now() resp, err = httpClient.Do(req) timeEnd := time.Now() require.NoError(t, err) _ = resp.Body.Close() timeTaken := timeEnd.Sub(timeStart) assert.LessOrEqual(t, timeTaken, 260*time.Millisecond) assert.GreaterOrEqual(t, timeTaken, 250*time.Millisecond) assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Contains(t, resp.Header.Get("Hx-Trigger"), "An activation link has been send to your email") }) t.Run(`should say "verification mail send" within ~250 ms`, func(t *testing.T) { t.Parallel() db, basePath, ctx := setupIntegrationTest(t) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/signup", nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) body, err := html.Parse(resp.Body) require.NoError(t, err) _ = resp.Body.Close() anonymousCsrfToken := findCsrfToken(t, body) assert.NotEmpty(t, anonymousCsrfToken) anonymousSession := findCookie(t, resp) assert.NotNil(t, anonymousSession) formData := url.Values{ "email": {"mail@mail.de"}, "password": {"secure-Password!1"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/signup", strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Hx-Request", "true") req.Header.Set("Cookie", "id="+anonymousSession.Value) req.Header.Set("Csrf-Token", anonymousCsrfToken) timeStart := time.Now() resp, err = httpClient.Do(req) timeEnd := time.Now() require.NoError(t, err) _ = resp.Body.Close() timeTaken := timeEnd.Sub(timeStart) assert.LessOrEqual(t, timeTaken, 260*time.Millisecond) assert.GreaterOrEqual(t, timeTaken, 250*time.Millisecond) assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Contains(t, resp.Header.Get("Hx-Trigger"), "An activation link has been send to your email") var rows int err = db.QueryRow("SELECT COUNT(*) FROM user WHERE email = ? AND email_verified = FALSE", "mail@mail.de").Scan(&rows) require.NoError(t, err) assert.Equal(t, 1, rows) var token string err = db.QueryRow("SELECT t.token FROM token t INNER JOIN user u ON u.user_id = t.user_id WHERE u.email = ? AND t.type = ?", "mail@mail.de", types.TokenTypeEmailVerify).Scan(&token) require.NoError(t, err) assert.NotEmpty(t, token) }) }) t.Run("SignUpVerification", func(t *testing.T) { t.Run(`should fail verifying email with non existent token`, func(t *testing.T) { t.Parallel() db, basePath, ctx := setupIntegrationTest(t) userId := uuid.New() _, 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, []byte("pass"), []byte("salt")) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/verify-email?token=invalid-token", nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusBadRequest, resp.StatusCode) var rows int err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND email_verified = FALSE", userId).Scan(&rows) require.NoError(t, err) assert.Equal(t, 1, rows) }) t.Run(`should fail verifying email with outdated token`, func(t *testing.T) { t.Parallel() db, basePath, ctx := setupIntegrationTest(t) userId := uuid.New() token := "my-outdated-verifying-token" _, 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, []byte("pass"), []byte("salt")) require.NoError(t, err) _, err = db.Exec(` INSERT INTO token (token, user_id, type, created_at, expires_at) VALUES (?, ?, ?, datetime("now", "-16 minute"), datetime("now", "-1 minute"))`, token, userId, types.TokenTypeEmailVerify) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/verify-email?token="+token, nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusBadRequest, resp.StatusCode) var rows int err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND email_verified = FALSE", userId).Scan(&rows) require.NoError(t, err) assert.Equal(t, 1, rows) }) t.Run(`should verify email with correct token`, func(t *testing.T) { t.Parallel() db, basePath, ctx := setupIntegrationTest(t) userId := uuid.New() token := "my-verifying-token" _, 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, []byte("pass"), []byte("salt")) require.NoError(t, err) _, err = db.Exec(` INSERT INTO token (token, user_id, session_id, type, created_at, expires_at) VALUES (?, ?, "", ?, datetime("now"), datetime("now", "+15 minute"))`, token, userId, types.TokenTypeEmailVerify) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/verify-email?token="+token, nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) var rows int err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND email_verified = TRUE", userId).Scan(&rows) require.NoError(t, err) assert.Equal(t, 1, rows) }) }) 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, http.MethodPost, basePath+"/api/auth/sign-out", nil) require.NoError(t, err) req.Header.Set("Csrf-Token", "invalid-csrf-token") resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() 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")) require.NoError(t, err) _, err = db.Exec(` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/", nil) require.NoError(t, err) req.Header.Set("Cookie", "id="+sessionId) resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() 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) require.NoError(t, err) req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/signout", nil) require.NoError(t, err) req.Header.Set("Csrf-Token", csrfToken) req.Header.Set("Cookie", "id="+sessionId) resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusSeeOther, resp.StatusCode) assert.Equal(t, "/", resp.Header.Get("Location")) cookie := findCookie(t, resp) assert.NotNil(t, cookie) assert.Empty(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) require.NoError(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, http.MethodGet, basePath+"/auth/delete-account", nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() 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, http.MethodPost, basePath+"/api/auth/delete-account", nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() 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" require.NoError(t, err) _, err = db.Exec(` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/delete-account", nil) require.NoError(t, err) req.Header.Set("Cookie", "id="+sessionId) resp, err := httpClient.Do(req) require.NoError(t, err) html, err := html.Parse(resp.Body) require.NoError(t, err) _ = resp.Body.Close() csrfToken := findCsrfToken(t, html) assert.NotEmpty(t, csrfToken) formData := url.Values{ "password": {"wrong-password"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/delete-account", strings.NewReader(formData.Encode())) require.NoError(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("Csrf-Token", csrfToken) resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() 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" require.NoError(t, err) _, err = db.Exec(` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) require.NoError(t, err) formData := url.Values{ "password": {"password"}, } req, err := http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/delete-account", strings.NewReader(formData.Encode())) require.NoError(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("Csrf-Token", "wrong-csrf-token") resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() 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, csrfToken, sessionId := createValidUserSession(t, db, "") formData := url.Values{ "name": {"Name"}, } req, err := http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/account/new", strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("Csrf-Token", csrfToken) req.Header.Set("Hx-Request", "true") resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) formData = url.Values{ "name": {"Name"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/treasurechest/new", strings.NewReader(formData.Encode())) require.NoError(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("Csrf-Token", csrfToken) resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) formData = url.Values{ "timestamp": {"2006-01-02"}, "value": {"100.00"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/transaction/new", strings.NewReader(formData.Encode())) require.NoError(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("Csrf-Token", csrfToken) resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) formData = url.Values{ "password": {"password"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/delete-account", strings.NewReader(formData.Encode())) require.NoError(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("Csrf-Token", csrfToken) resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) var rows int err = db.QueryRow("SELECT COUNT(*) FROM session WHERE user_id = ?", userId).Scan(&rows) require.NoError(t, err) assert.Equal(t, 0, rows) err = db.QueryRow("SELECT COUNT(*) FROM token WHERE user_id = ?", userId).Scan(&rows) require.NoError(t, err) assert.Equal(t, 0, rows) err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ?", userId).Scan(&rows) require.NoError(t, err) assert.Equal(t, 0, rows) err = db.QueryRow("SELECT COUNT(*) FROM account WHERE user_id = ?", userId).Scan(&rows) require.NoError(t, err) assert.Equal(t, 0, rows) err = db.QueryRow("SELECT COUNT(*) FROM treasure_chest WHERE user_id = ?", userId).Scan(&rows) require.NoError(t, err) assert.Equal(t, 0, rows) err = db.QueryRow("SELECT COUNT(*) FROM \"transaction\" WHERE user_id = ?", userId).Scan(&rows) require.NoError(t, err) assert.Equal(t, 0, rows) }) }) t.Run("ChangePassword", 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, http.MethodGet, basePath+"/auth/change-password", nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusSeeOther, resp.StatusCode) assert.Equal(t, "/auth/signin", resp.Header.Get("Location")) }) t.Run(`should throw unautohorized if not signed in`, func(t *testing.T) { t.Parallel() _, basePath, ctx := setupIntegrationTest(t) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/signin", nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) html, err := html.Parse(resp.Body) require.NoError(t, err) _ = resp.Body.Close() anonymousCsrfToken := findCsrfToken(t, html) assert.NotEmpty(t, anonymousCsrfToken) anonymousSessionId := findCookie(t, resp).Value assert.NotEmpty(t, anonymousSessionId) formData := url.Values{ "current-password": {"password"}, "new-password": {"MyNewSecurePassword1!"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/change-password", strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+anonymousSessionId) req.Header.Set("Hx-Request", "true") req.Header.Set("Csrf-Token", anonymousCsrfToken) resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) }) t.Run(`should fail if csrf token is invalid`, 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" require.NoError(t, err) _, err = db.Exec(` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) require.NoError(t, err) formData := url.Values{ "current-password": {"password"}, "new-password": {"MyNewSecurePassword1!"}, } req, err := http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/change-password", strings.NewReader(formData.Encode())) require.NoError(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("Csrf-Token", "invalid-csrf-token") resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusBadRequest, resp.StatusCode) var rows int err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows) require.NoError(t, err) assert.Equal(t, 1, rows) }) t.Run("should fail if current password does not match", 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" require.NoError(t, err) _, err = db.Exec(` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/change-password", nil) require.NoError(t, err) req.Header.Set("Cookie", "id="+sessionId) resp, err := httpClient.Do(req) require.NoError(t, err) html, err := html.Parse(resp.Body) require.NoError(t, err) _ = resp.Body.Close() csrfToken := findCsrfToken(t, html) assert.NotEmpty(t, csrfToken) formData := url.Values{ "current-password": {"wrong-password"}, "new-password": {"MyNewSecurePassword1!"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/change-password", strings.NewReader(formData.Encode())) require.NoError(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("Csrf-Token", csrfToken) resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusBadRequest, resp.StatusCode) var rows int err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows) require.NoError(t, err) assert.Equal(t, 1, rows) }) t.Run("should fail if new password is insecure", 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" require.NoError(t, err) _, err = db.Exec(` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/change-password", nil) require.NoError(t, err) req.Header.Set("Cookie", "id="+sessionId) resp, err := httpClient.Do(req) require.NoError(t, err) html, err := html.Parse(resp.Body) require.NoError(t, err) _ = resp.Body.Close() csrfToken := findCsrfToken(t, html) assert.NotEmpty(t, csrfToken) formData := url.Values{ "current-password": {"password"}, "new-password": {"insecure-password"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/change-password", strings.NewReader(formData.Encode())) require.NoError(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("Csrf-Token", csrfToken) resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusBadRequest, resp.StatusCode) var rows int err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows) require.NoError(t, err) assert.Equal(t, 1, rows) }) 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" require.NoError(t, err) _, err = db.Exec(` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) require.NoError(t, err) _, err = db.Exec(` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES ("second", ?, datetime(), datetime("now", "+1 day"))`, userId) require.NoError(t, err) _, err = db.Exec(` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES ("other", ?, datetime(), datetime("now", "+1 day"))`, userIdOther) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/change-password", nil) require.NoError(t, err) req.Header.Set("Cookie", "id="+sessionId) resp, err := httpClient.Do(req) require.NoError(t, err) html, err := html.Parse(resp.Body) require.NoError(t, err) _ = resp.Body.Close() csrfToken := findCsrfToken(t, html) assert.NotEmpty(t, csrfToken) formData := url.Values{ "current-password": {"password"}, "new-password": {"MyNewSecurePassword1!"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/change-password", strings.NewReader(formData.Encode())) require.NoError(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("Csrf-Token", csrfToken) resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) pass = service.GetHashPassword("MyNewSecurePassword1!", []byte("salt")) var rows int err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows) require.NoError(t, err) assert.Equal(t, 1, rows) var sessionIds []string sessions, err := db.Query(`SELECT session_id FROM session WHERE NOT user_id = ? ORDER BY session_id`, uuid.Nil) require.NoError(t, err) for sessions.Next() { var sessionId string err = sessions.Scan(&sessionId) require.NoError(t, err) sessionIds = append(sessionIds, sessionId) } assert.Len(t, sessionIds, 2) assert.Equal(t, "other", sessionIds[0]) assert.Equal(t, "session-id", sessionIds[1]) }) }) t.Run("ForgotPasswordMail", func(t *testing.T) { t.Run(`should redirect to "/" if signed in`, 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")) require.NoError(t, err) sessionId := "session-id" _, err = d.Exec(` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES ("session-id", ?, datetime(), datetime("now", "+1 day"))`, userId) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/forgot-password", nil) require.NoError(t, err) req.Header.Set("Cookie", "id="+sessionId) resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusSeeOther, resp.StatusCode) assert.Equal(t, "/", resp.Header.Get("Location")) }) t.Run(`should fail if csrf token is invalid`, 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")) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/forgot-password", nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) anonymousSessionId := findCookie(t, resp).Value assert.NotEmpty(t, anonymousSessionId) formData := url.Values{ "email": {"mail@mail.de"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/forgot-password", strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Hx-Request", "true") req.Header.Set("Csrf-Token", "invalid-csrf-token") resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusBadRequest, resp.StatusCode) var rows int err = d.QueryRow("SELECT COUNT(*) FROM token WHERE user_id = ? AND type = ?", userId, types.TokenTypePasswordReset).Scan(&rows) require.NoError(t, err) assert.Equal(t, 0, rows) }) t.Run(`should fail but respond with uniform message`, func(t *testing.T) { t.Parallel() _, basePath, ctx := setupIntegrationTest(t) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/forgot-password", nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) anonymousSessionId := findCookie(t, resp).Value assert.NotEmpty(t, anonymousSessionId) body, err := html.Parse(resp.Body) require.NoError(t, err) _ = resp.Body.Close() anonymousCsrfToken := findCsrfToken(t, body) formData := url.Values{ "email": {"non-existent@mail.de"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/forgot-password", strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Hx-Request", "true") req.Header.Set("Cookie", "id="+anonymousSessionId) req.Header.Set("Csrf-Token", anonymousCsrfToken) resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) msg := "If the address exists, an email has been sent." assert.Contains(t, resp.Header.Get("Hx-Trigger"), msg) }) t.Run(`should generate token and respond with uniform message`, 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")) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/forgot-password", nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) anonymousSessionId := findCookie(t, resp).Value assert.NotEmpty(t, anonymousSessionId) body, err := html.Parse(resp.Body) require.NoError(t, err) _ = resp.Body.Close() anonymousCsrfToken := findCsrfToken(t, body) formData := url.Values{ "email": {"mail@mail.de"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/forgot-password", strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Hx-Request", "true") req.Header.Set("Cookie", "id="+anonymousSessionId) req.Header.Set("Csrf-Token", anonymousCsrfToken) resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) msg := "If the address exists, an email has been sent." assert.Contains(t, resp.Header.Get("Hx-Trigger"), msg) var rows int err = db.QueryRow("SELECT COUNT(*) FROM token WHERE user_id = ? AND type = ?", userId, types.TokenTypePasswordReset).Scan(&rows) require.NoError(t, err) assert.Equal(t, 1, rows) }) }) t.Run("ForgotPasswordResponse", func(t *testing.T) { t.Run(`should fail if token does not exist`, 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")) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/forgot-password", nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) anonymousSessionId := findCookie(t, resp).Value html, err := html.Parse(resp.Body) require.NoError(t, err) _ = resp.Body.Close() anonymousCsrfToken := findCsrfToken(t, html) assert.NotEmpty(t, anonymousCsrfToken) formData := url.Values{ "new-password": {"MyNewSecurePassword1!"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/forgot-password-actual", strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+anonymousSessionId) req.Header.Set("Hx-Request", "true") req.Header.Set("Hx-Current-Url", basePath+"/auth/change-password?token=invalidToken") req.Header.Set("Csrf-Token", anonymousCsrfToken) resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusBadRequest, resp.StatusCode) var rows int err = d.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows) require.NoError(t, err) assert.Equal(t, 1, rows) }) t.Run(`should fail if token is outdated`, 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")) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/forgot-password", nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) anonymousSessionId := findCookie(t, resp).Value html, err := html.Parse(resp.Body) require.NoError(t, err) _ = resp.Body.Close() anonymousCsrfToken := findCsrfToken(t, html) assert.NotEmpty(t, anonymousCsrfToken) token := "password-reset-token" _, err = d.Exec(` INSERT INTO token (token, user_id, session_id, type, created_at, expires_at) VALUES (?, ?, ?, ?, datetime("now", "-16 minute"), datetime("now", "-1 minute"))`, token, userId, "", types.TokenTypePasswordReset) require.NoError(t, err) formData := url.Values{ "new-password": {"MyNewSecurePassword1!"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/forgot-password-actual", strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+anonymousSessionId) req.Header.Set("Hx-Request", "true") req.Header.Set("Hx-Current-Url", basePath+"/auth/change-password?token="+url.QueryEscape(token)) req.Header.Set("Csrf-Token", anonymousCsrfToken) resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusBadRequest, resp.StatusCode) var rows int err = d.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows) require.NoError(t, err) assert.Equal(t, 1, rows) }) t.Run(`should fail if password is insecure`, 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")) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/forgot-password", nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) anonymousSessionId := findCookie(t, resp).Value html, err := html.Parse(resp.Body) require.NoError(t, err) _ = resp.Body.Close() anonymousCsrfToken := findCsrfToken(t, html) assert.NotEmpty(t, anonymousCsrfToken) token := "password-reset-token" _, err = d.Exec(` INSERT INTO token (token, user_id, session_id, type, created_at, expires_at) VALUES (?, ?, ?, ?, datetime("now"), datetime("now", "+15 minute"))`, token, userId, "", types.TokenTypePasswordReset) require.NoError(t, err) formData := url.Values{ "new-password": {"insecure-password"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/forgot-password-actual", strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+anonymousSessionId) req.Header.Set("Hx-Request", "true") req.Header.Set("Hx-Current-Url", basePath+"/auth/change-password?token="+url.QueryEscape(token)) req.Header.Set("Csrf-Token", anonymousCsrfToken) resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusBadRequest, resp.StatusCode) var rows int err = d.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows) require.NoError(t, err) assert.Equal(t, 1, rows) }) 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")) require.NoError(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) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/auth/forgot-password", nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) sessionId := findCookie(t, resp).Value html, err := html.Parse(resp.Body) require.NoError(t, err) _ = resp.Body.Close() csrfToken := findCsrfToken(t, html) assert.NotEmpty(t, csrfToken) formData := url.Values{ "email": {"mail@mail.de"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/forgot-password", strings.NewReader(formData.Encode())) require.NoError(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("Csrf-Token", csrfToken) resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) var token string err = d.QueryRow("SELECT token FROM token WHERE type = ?", types.TokenTypePasswordReset).Scan(&token) require.NoError(t, err) formData = url.Values{ "new-password": {"MyNewSecurePassword1!"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/forgot-password-actual", strings.NewReader(formData.Encode())) require.NoError(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("Csrf-Token", csrfToken) req.Header.Set("Hx-Current-Url", basePath+"/auth/change-password?token="+url.QueryEscape(token)) resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) sessions, err := d.Query("SELECT session_id FROM session WHERE user_id = ?", userId) require.NoError(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")) require.NoError(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) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath, nil) require.NoError(t, err) req.Header.Set("Cookie", "id="+sessionId) resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() newSession := findCookie(t, resp) 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) require.NoError(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, http.MethodGet, basePath, nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() newSession := findCookie(t, resp) assert.NotNil(t, newSession) assert.NotEmpty(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")) require.NoError(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) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/account", nil) require.NoError(t, err) req.Header.Set("Cookie", "id="+sessionId) resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusSeeOther, resp.StatusCode) assert.Equal(t, "/auth/signin", resp.Header.Get("Location")) }) }) } func TestIntegrationAccount(t *testing.T) { t.Parallel() t.Run("SignIn", func(t *testing.T) { t.Parallel() t.Run(`should throw unauthorized if try to getAll, get, edit, insert or delete`, func(t *testing.T) { t.Parallel() _, basePath, ctx := setupIntegrationTest(t) csrfToken, sessionId := createAnonymousSession(t, ctx, basePath) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/account", nil) require.NoError(t, err) resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusSeeOther, resp.StatusCode) assert.Equal(t, "/auth/signin", resp.Header.Get("Location")) req, err = http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/account/some-id", nil) require.NoError(t, err) req.Header.Set("Cookie", "id="+sessionId) resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusSeeOther, resp.StatusCode) assert.Equal(t, "/auth/signin", resp.Header.Get("Location")) formData := url.Values{ "name": {"name"}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/account/some-id", strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("Csrf-Token", csrfToken) resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusSeeOther, resp.StatusCode) assert.Equal(t, "/auth/signin", resp.Header.Get("Location")) req, err = http.NewRequestWithContext(ctx, http.MethodDelete, basePath+"/account/some-id", nil) require.NoError(t, err) req.Header.Set("Csrf-Token", csrfToken) req.Header.Set("Cookie", "id="+sessionId) resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusSeeOther, resp.StatusCode) assert.Equal(t, "/auth/signin", resp.Header.Get("Location")) }) t.Run(`should be able to insert, get, delete and update`, func(t *testing.T) { t.Parallel() db, basePath, ctx := setupIntegrationTest(t) _, csrfToken, sessionId := createValidUserSession(t, db, "") // Insert expectedName := "My great Account" formData := url.Values{ "name": {expectedName}, } req, err := http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/account/new", strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("Csrf-Token", csrfToken) resp, err := httpClient.Do(req) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Contains(t, readBody(t, resp.Body), expectedName) _ = resp.Body.Close() var id uuid.UUID err = db.Get(&id, "SELECT id FROM account") require.NoError(t, err) // Update expectedNewName := "My new Account" formData = url.Values{ "name": {expectedNewName}, } req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/account/"+id.String(), strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("Csrf-Token", csrfToken) resp, err = httpClient.Do(req) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Contains(t, readBody(t, resp.Body), expectedNewName) _ = resp.Body.Close() // Get req, err = http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/account/"+id.String(), nil) require.NoError(t, err) req.Header.Set("Cookie", "id="+sessionId) resp, err = httpClient.Do(req) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Contains(t, readBody(t, resp.Body), expectedNewName) _ = resp.Body.Close() // Delete req, err = http.NewRequestWithContext(ctx, http.MethodDelete, basePath+"/account/"+id.String(), strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("Csrf-Token", csrfToken) resp, err = httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) // Get (not found) req, err = http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/account/"+id.String(), nil) require.NoError(t, err) req.Header.Set("Cookie", "id="+sessionId) resp, err = httpClient.Do(req) require.NoError(t, err) assert.Equal(t, http.StatusNotFound, resp.StatusCode) assert.NotContains(t, readBody(t, resp.Body), expectedNewName) _ = resp.Body.Close() }) t.Run(`should not be able to see other users content`, func(t *testing.T) { t.Parallel() db, basePath, ctx := setupIntegrationTest(t) _, csrfToken1, sessionId1 := createValidUserSession(t, db, "1") _, _, sessionId2 := createValidUserSession(t, db, "2") expectedName1 := "Account 1" formData := url.Values{ "name": {expectedName1}, } req, err := http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/account/new", strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId1) req.Header.Set("Csrf-Token", csrfToken1) resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) req, err = http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/account", nil) require.NoError(t, err) req.Header.Set("Cookie", "id="+sessionId2) resp, err = httpClient.Do(req) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) assert.NotContains(t, expectedName1, readBody(t, resp.Body)) _ = resp.Body.Close() }) t.Run(`should prohibit special characters in name`, func(t *testing.T) { t.Parallel() db, basePath, ctx := setupIntegrationTest(t) _, csrfToken, sessionId := createValidUserSession(t, db, "") data := map[string]int{ "<": 400, ">": 400, "/": 400, "\\": 400, "?": 400, ":": 400, "*": 400, "|": 400, "Account": 200, } for name, status := range data { formData := url.Values{ "name": {name}, } req, err := http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/account/new", strings.NewReader(formData.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("Csrf-Token", csrfToken) resp, err := httpClient.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, status, resp.StatusCode, "for name: "+name) } }) }) }