Files
descrybe/apps/api/internal/auth/session_version_test.go
T
greeneclipse 8580c996c3 Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
2026-08-09 22:47:43 +02:00

193 lines
5.8 KiB
Go

package auth
import (
"os"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestResetPasswordWithTokenBumpsSessionVersion(t *testing.T) {
dsn := strings.TrimSpace(os.Getenv("DATABASE_URL"))
if dsn == "" {
t.Skip("DATABASE_URL not set")
}
ctx := t.Context()
pg, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatalf("postgres: %v", err)
}
t.Cleanup(pg.Close)
var ready bool
if err := pg.QueryRow(ctx, `
SELECT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'users' AND column_name = 'session_version'
)`).Scan(&ready); err != nil || !ready {
t.Skip("users.session_version missing — run goose up for 042_user_session_version")
}
if err := pg.QueryRow(ctx, `
SELECT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'password_reset_tokens'
)`).Scan(&ready); err != nil || !ready {
t.Skip("password_reset_tokens missing — run goose up for 041_password_reset_tokens")
}
svc := &Service{Pool: pg}
userID := uuid.New()
email := "session-ver-" + userID.String()[:8] + "@example.test"
hash, err := HashPassword("OldPassword123!")
if err != nil {
t.Fatalf("hash: %v", err)
}
_, err = pg.Exec(ctx, `
INSERT INTO users (id, email, name, password_hash, must_set_password, is_platform_admin, is_active, session_version)
VALUES ($1, $2, $3, $4, false, false, true, 3)`,
userID, email, "Session Ver", hash)
if err != nil {
t.Fatalf("seed user: %v", err)
}
t.Cleanup(func() {
cleanupCtx := t.Context()
_, _ = pg.Exec(cleanupCtx, `DELETE FROM password_reset_tokens WHERE user_id = $1`, userID)
_, _ = pg.Exec(cleanupCtx, `DELETE FROM users WHERE id = $1`, userID)
})
issue, err := svc.IssuePasswordReset(ctx, email, time.Hour)
if err != nil {
t.Fatalf("IssuePasswordReset: %v", err)
}
if err := svc.ResetPasswordWithToken(ctx, issue.Token, "NewPassword456!"); err != nil {
t.Fatalf("ResetPasswordWithToken: %v", err)
}
st, err := svc.UserSessionState(ctx, userID)
if err != nil {
t.Fatalf("UserSessionState: %v", err)
}
if !st.Active {
t.Fatal("expected active user")
}
if st.Version != 4 {
t.Fatalf("session_version=%d want 4 (bumped from 3)", st.Version)
}
}
func TestChangePasswordBumpsSessionVersion(t *testing.T) {
dsn := strings.TrimSpace(os.Getenv("DATABASE_URL"))
if dsn == "" {
t.Skip("DATABASE_URL not set")
}
ctx := t.Context()
pg, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatalf("postgres: %v", err)
}
t.Cleanup(pg.Close)
var ready bool
if err := pg.QueryRow(ctx, `
SELECT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'users' AND column_name = 'session_version'
)`).Scan(&ready); err != nil || !ready {
t.Skip("users.session_version missing — run goose up for 042_user_session_version")
}
svc := &Service{Pool: pg}
userID := uuid.New()
email := "change-pw-" + userID.String()[:8] + "@example.test"
const oldPassword = "OldPassword123!"
hash, err := HashPassword(oldPassword)
if err != nil {
t.Fatalf("hash: %v", err)
}
_, err = pg.Exec(ctx, `
INSERT INTO users (id, email, name, password_hash, must_set_password, is_platform_admin, is_active, session_version)
VALUES ($1, $2, $3, $4, false, false, true, 2)`,
userID, email, "Change PW", hash)
if err != nil {
t.Fatalf("seed user: %v", err)
}
t.Cleanup(func() {
_, _ = pg.Exec(t.Context(), `DELETE FROM users WHERE id = $1`, userID)
})
if err := svc.ChangePassword(ctx, userID, "wrong-password", "NewPassword456!"); err != ErrInvalidCredentials {
t.Fatalf("wrong current: err=%v want ErrInvalidCredentials", err)
}
if err := svc.ChangePassword(ctx, userID, oldPassword, "short"); err != ErrPasswordTooShort {
t.Fatalf("short password: err=%v want ErrPasswordTooShort", err)
}
if err := svc.ChangePassword(ctx, userID, oldPassword, "NewPassword456!"); err != nil {
t.Fatalf("ChangePassword: %v", err)
}
st, err := svc.UserSessionState(ctx, userID)
if err != nil {
t.Fatalf("UserSessionState: %v", err)
}
if st.Version != 3 {
t.Fatalf("session_version=%d want 3 (bumped from 2)", st.Version)
}
var stored string
if err := pg.QueryRow(ctx, `SELECT password_hash FROM users WHERE id = $1`, userID).Scan(&stored); err != nil {
t.Fatalf("load hash: %v", err)
}
ok, err := VerifyPassword(stored, "NewPassword456!")
if err != nil || !ok {
t.Fatalf("new password verify ok=%v err=%v", ok, err)
}
ok, err = VerifyPassword(stored, oldPassword)
if err != nil || ok {
t.Fatal("old password should no longer verify")
}
}
func TestSetPasswordRejectsWhenAlreadySet(t *testing.T) {
dsn := strings.TrimSpace(os.Getenv("DATABASE_URL"))
if dsn == "" {
t.Skip("DATABASE_URL not set")
}
ctx := t.Context()
pg, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatalf("postgres: %v", err)
}
t.Cleanup(pg.Close)
svc := &Service{Pool: pg}
userID := uuid.New()
email := "set-pw-" + userID.String()[:8] + "@example.test"
hash, err := HashPassword("AlreadySet123!")
if err != nil {
t.Fatalf("hash: %v", err)
}
_, err = pg.Exec(ctx, `
INSERT INTO users (id, email, name, password_hash, must_set_password, is_platform_admin, is_active)
VALUES ($1, $2, $3, $4, false, false, true)`,
userID, email, "Set PW", hash)
if err != nil {
t.Fatalf("seed user: %v", err)
}
t.Cleanup(func() {
_, _ = pg.Exec(t.Context(), `DELETE FROM users WHERE id = $1`, userID)
})
if err := svc.SetPassword(ctx, userID, "AnotherPass123!"); err != ErrPasswordAlreadySet {
t.Fatalf("SetPassword: err=%v want ErrPasswordAlreadySet", err)
}
if err := svc.ChangePassword(ctx, userID, "AlreadySet123!", "short"); err != ErrPasswordTooShort {
// ensure ChangePassword path still works for eligible users after SetPassword rejection
t.Fatalf("ChangePassword short: err=%v", err)
}
}