Passwort und Reset Passwort tests

This commit is contained in:
2025-08-13 08:37:53 +02:00
parent 9b7059884b
commit 68922033ea
3 changed files with 1050 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
# Password Reset Tests Documentation
Diese Dokumentation beschreibt die umfassenden Tests für das Password-Reset-System.
## Test-Übersicht
Das Password-Reset-System wurde mit 16 umfassenden Tests abgedeckt:
### 1. API-Handler-Tests
#### RequestPasswordReset Tests:
- `TestRequestPasswordReset_Success` - Erfolgreiche Password-Reset-Anfrage mit `redirect_url`
- `TestRequestPasswordReset_WithoutRedirectURL` - Erfolgreich ohne `redirect_url` (Fallback wird verwendet)
- `TestRequestPasswordReset_NonExistentEmail` - Nicht existierende E-Mail (Sicherheitsverhalten: Keine Information preisgeben)
- `TestRequestPasswordReset_InvalidEmail` - Ungültiges E-Mail-Format (Validation-Fehler)
#### ValidateResetToken Tests:
- `TestValidateResetToken_Success` - Gültiger Token wird korrekt validiert
- `TestValidateResetToken_InvalidToken` - Ungültiger Token wird abgelehnt
- `TestValidateResetToken_ExpiredToken` - Abgelaufener Token wird abgelehnt
#### ResetPassword Tests:
- `TestResetPassword_Success` - Erfolgreiches Zurücksetzen des Passworts
- `TestResetPassword_InvalidToken` - Ungültiger Token wird abgelehnt
- `TestResetPassword_ShortPassword` - Zu kurzes Passwort wird abgelehnt (< 6 Zeichen)
- `TestResetPassword_ExpiredToken` - Abgelaufener Token wird abgelehnt
### 2. User-Model-Tests
#### Password-Reset-Hash Management:
- `TestUser_SetPasswordResetHash` - Setzt Reset-Hash und Ablaufzeit (14 Tage)
- `TestUser_ClearPasswordResetHash` - Entfernt Reset-Hash und Ablaufzeit
- `TestUser_IsResetHashValid` - Validiert Reset-Hash gegen Zeit und Wert
#### Database-Query Tests:
- `TestUser_FindByResetHash` - Findet User anhand gültigem Reset-Hash
- `TestUser_FindByEmail` - Findet User anhand E-Mail-Adresse
## Test-Setup
### Database Migration
- Jeder Test führt automatisch `MigrateStructure()` aus um sicherzustellen, dass die `reset_pw_hash` und `reset_pw_hash_expires` Felder existieren
### Test-Isolation
- Eindeutige E-Mail-Adressen per Test über `rand.Intn(100000)` um UNIQUE-Constraint-Konflikte zu vermeiden
- SQLite In-Memory-Datenbank für schnelle Test-Ausführung
### Test-Daten
- Generierte Test-User mit MD5-gehashten Passwörtern (wie im echten System)
- Realistische Reset-Token-Generierung und Zeitstempel
## Sicherheits-Tests
### E-Mail-Enumeration-Schutz
- Tests stellen sicher, dass bei nicht-existierenden E-Mail-Adressen dieselbe Erfolgsmeldung zurückgegeben wird
- Keine Information wird preisgegeben, ob ein Account existiert oder nicht
### Token-Sicherheit
- Token-Ablauf wird korrekt geprüft (14 Tage)
- Ungültige Tokens werden sicher abgelehnt
- Token werden nach erfolgreichem Reset automatisch gelöscht
### Input-Validation
- E-Mail-Format-Validierung
- Passwort-Mindestlänge (6 Zeichen)
- Token-Presence-Validation
## Test-Ausführung
```bash
# Alle Password-Reset-Tests ausführen
go test ./user -v -run ".*Reset.*"
# Einzelnen Test ausführen
go test ./user -v -run "TestRequestPasswordReset_Success"
# Tests mit frischer Database bei jedem Lauf
go test ./user -v -run ".*Reset.*" -count=1
```
## Test-Ergebnisse
Alle 16 Tests bestehen erfolgreich:
- ✅ API-Handler-Tests (11 Tests)
- ✅ User-Model-Tests (5 Tests)
- ✅ Sicherheits-Tests (integriert)
- ✅ Database-Integration-Tests (integriert)
## Abgedeckte Szenarien
1. **Happy Path**: Normale Password-Reset-Workflows
2. **Error Cases**: Ungültige Eingaben, abgelaufene Token
3. **Security Cases**: E-Mail-Enumeration-Schutz, Token-Sicherheit
4. **Edge Cases**: Fehlende Parameter, Fallback-Verhalten
5. **Database Integration**: Migration, CRUD-Operationen, Constraints
Die Test-Suite gewährleistet eine vollständige Abdeckung aller Password-Reset-Funktionen mit fokus auf Sicherheit und Benutzerfreundlichkeit.
+453
View File
@@ -0,0 +1,453 @@
package user
import (
"bamort/database"
"bytes"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"math/rand"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func setupTestUser(t *testing.T) User {
database.SetupTestDB()
// Migrate User structure to ensure reset password fields exist
err := MigrateStructure()
require.NoError(t, err, "Failed to migrate user structure")
// Generate unique email for each test to avoid conflicts
randomSuffix := rand.Intn(100000)
user := User{
Username: fmt.Sprintf("testuser_reset_%d", randomSuffix),
PasswordHash: "testpassword123",
Email: fmt.Sprintf("test.reset.%d@example.com", randomSuffix),
}
// Hash password like in RegisterUser
hashedPassword := md5.Sum([]byte(user.PasswordHash))
user.PasswordHash = hex.EncodeToString(hashedPassword[:])
err = user.Create()
require.NoError(t, err, "Failed to create test user")
return user
}
func TestRequestPasswordReset_Success(t *testing.T) {
user := setupTestUser(t)
// Setup Gin router
gin.SetMode(gin.TestMode)
router := gin.New()
router.POST("/password-reset/request", RequestPasswordReset)
// Test data
requestData := map[string]interface{}{
"email": user.Email,
"redirect_url": "http://localhost:3000",
}
jsonData, _ := json.Marshal(requestData)
// Create request
req, _ := http.NewRequest("POST", "/password-reset/request", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
// Execute request
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Assertions
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Contains(t, response["message"], "Falls ein Account mit dieser E-Mail-Adresse existiert")
// Check that user has reset hash set in database
var dbUser User
err = dbUser.FindByEmail(user.Email)
require.NoError(t, err)
assert.NotNil(t, dbUser.ResetPwHash, "Reset hash should be set")
assert.NotNil(t, dbUser.ResetPwHashExpires, "Reset expiry should be set")
assert.True(t, dbUser.ResetPwHashExpires.After(time.Now()), "Reset expiry should be in future")
}
func TestRequestPasswordReset_WithoutRedirectURL(t *testing.T) {
user := setupTestUser(t)
gin.SetMode(gin.TestMode)
router := gin.New()
router.POST("/password-reset/request", RequestPasswordReset)
// Test data without redirect_url
requestData := map[string]interface{}{
"email": user.Email,
}
jsonData, _ := json.Marshal(requestData)
req, _ := http.NewRequest("POST", "/password-reset/request", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Should still work with fallback URL
assert.Equal(t, http.StatusOK, w.Code)
}
func TestRequestPasswordReset_NonExistentEmail(t *testing.T) {
database.SetupTestDB()
gin.SetMode(gin.TestMode)
router := gin.New()
router.POST("/password-reset/request", RequestPasswordReset)
requestData := map[string]interface{}{
"email": "nonexistent@example.com",
"redirect_url": "http://localhost:3000",
}
jsonData, _ := json.Marshal(requestData)
req, _ := http.NewRequest("POST", "/password-reset/request", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Should return success to prevent email enumeration
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Contains(t, response["message"], "Falls ein Account mit dieser E-Mail-Adresse existiert")
}
func TestRequestPasswordReset_InvalidEmail(t *testing.T) {
database.SetupTestDB()
gin.SetMode(gin.TestMode)
router := gin.New()
router.POST("/password-reset/request", RequestPasswordReset)
requestData := map[string]interface{}{
"email": "invalid-email-format",
"redirect_url": "http://localhost:3000",
}
jsonData, _ := json.Marshal(requestData)
req, _ := http.NewRequest("POST", "/password-reset/request", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Should return 400 for invalid email format
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestValidateResetToken_Success(t *testing.T) {
user := setupTestUser(t)
// Set reset hash
resetHash := "test_reset_hash_123456789"
err := user.SetPasswordResetHash(resetHash)
require.NoError(t, err)
gin.SetMode(gin.TestMode)
router := gin.New()
router.GET("/password-reset/validate/:token", ValidateResetToken)
req, _ := http.NewRequest("GET", "/password-reset/validate/"+resetHash, nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.True(t, response["valid"].(bool))
assert.Equal(t, user.Username, response["username"])
assert.NotNil(t, response["expires"])
}
func TestValidateResetToken_InvalidToken(t *testing.T) {
database.SetupTestDB()
gin.SetMode(gin.TestMode)
router := gin.New()
router.GET("/password-reset/validate/:token", ValidateResetToken)
req, _ := http.NewRequest("GET", "/password-reset/validate/invalid_token", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestValidateResetToken_ExpiredToken(t *testing.T) {
user := setupTestUser(t)
// Set expired reset hash
resetHash := "expired_reset_hash_123456789"
expiredTime := time.Now().Add(-1 * time.Hour) // 1 hour ago
user.ResetPwHash = &resetHash
user.ResetPwHashExpires = &expiredTime
err := user.Save()
require.NoError(t, err)
gin.SetMode(gin.TestMode)
router := gin.New()
router.GET("/password-reset/validate/:token", ValidateResetToken)
req, _ := http.NewRequest("GET", "/password-reset/validate/"+resetHash, nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestResetPassword_Success(t *testing.T) {
user := setupTestUser(t)
originalPassword := user.PasswordHash
// Set reset hash
resetHash := "test_reset_hash_for_password_change"
err := user.SetPasswordResetHash(resetHash)
require.NoError(t, err)
gin.SetMode(gin.TestMode)
router := gin.New()
router.POST("/password-reset/reset", ResetPassword)
requestData := map[string]interface{}{
"token": resetHash,
"new_password": "new_secure_password123",
}
jsonData, _ := json.Marshal(requestData)
req, _ := http.NewRequest("POST", "/password-reset/reset", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Equal(t, "Passwort erfolgreich zurückgesetzt", response["message"])
// Verify password was changed and reset hash was cleared
var dbUser User
err = dbUser.FindByEmail(user.Email)
require.NoError(t, err)
assert.NotEqual(t, originalPassword, dbUser.PasswordHash, "Password should have changed")
assert.Nil(t, dbUser.ResetPwHash, "Reset hash should be cleared")
assert.Nil(t, dbUser.ResetPwHashExpires, "Reset expiry should be cleared")
// Verify new password hash
expectedHash := md5.Sum([]byte("new_secure_password123"))
expectedHashString := hex.EncodeToString(expectedHash[:])
assert.Equal(t, expectedHashString, dbUser.PasswordHash, "New password hash should match")
}
func TestResetPassword_InvalidToken(t *testing.T) {
database.SetupTestDB()
gin.SetMode(gin.TestMode)
router := gin.New()
router.POST("/password-reset/reset", ResetPassword)
requestData := map[string]interface{}{
"token": "invalid_token",
"new_password": "new_secure_password123",
}
jsonData, _ := json.Marshal(requestData)
req, _ := http.NewRequest("POST", "/password-reset/reset", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestResetPassword_ShortPassword(t *testing.T) {
user := setupTestUser(t)
resetHash := "test_reset_hash_short_password"
err := user.SetPasswordResetHash(resetHash)
require.NoError(t, err)
gin.SetMode(gin.TestMode)
router := gin.New()
router.POST("/password-reset/reset", ResetPassword)
requestData := map[string]interface{}{
"token": resetHash,
"new_password": "123", // Too short
}
jsonData, _ := json.Marshal(requestData)
req, _ := http.NewRequest("POST", "/password-reset/reset", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestResetPassword_ExpiredToken(t *testing.T) {
user := setupTestUser(t)
// Set expired reset hash
resetHash := "expired_reset_hash_for_reset"
expiredTime := time.Now().Add(-1 * time.Hour) // 1 hour ago
user.ResetPwHash = &resetHash
user.ResetPwHashExpires = &expiredTime
err := user.Save()
require.NoError(t, err)
gin.SetMode(gin.TestMode)
router := gin.New()
router.POST("/password-reset/reset", ResetPassword)
requestData := map[string]interface{}{
"token": resetHash,
"new_password": "new_secure_password123",
}
jsonData, _ := json.Marshal(requestData)
req, _ := http.NewRequest("POST", "/password-reset/reset", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
// Test User model methods
func TestUser_SetPasswordResetHash(t *testing.T) {
user := setupTestUser(t)
resetHash := "test_hash_123456789"
err := user.SetPasswordResetHash(resetHash)
assert.NoError(t, err)
assert.NotNil(t, user.ResetPwHash)
assert.Equal(t, resetHash, *user.ResetPwHash)
assert.NotNil(t, user.ResetPwHashExpires)
assert.True(t, user.ResetPwHashExpires.After(time.Now()))
assert.True(t, user.ResetPwHashExpires.Before(time.Now().Add(15*24*time.Hour))) // Should be ~14 days
}
func TestUser_ClearPasswordResetHash(t *testing.T) {
user := setupTestUser(t)
// First set a reset hash
resetHash := "test_hash_to_clear"
err := user.SetPasswordResetHash(resetHash)
require.NoError(t, err)
require.NotNil(t, user.ResetPwHash)
// Then clear it
err = user.ClearPasswordResetHash()
assert.NoError(t, err)
assert.Nil(t, user.ResetPwHash)
assert.Nil(t, user.ResetPwHashExpires)
}
func TestUser_IsResetHashValid(t *testing.T) {
user := setupTestUser(t)
resetHash := "valid_test_hash_123"
err := user.SetPasswordResetHash(resetHash)
require.NoError(t, err)
// Test valid hash
assert.True(t, user.IsResetHashValid(resetHash))
// Test invalid hash
assert.False(t, user.IsResetHashValid("wrong_hash"))
// Test expired hash
expiredTime := time.Now().Add(-1 * time.Hour)
user.ResetPwHashExpires = &expiredTime
assert.False(t, user.IsResetHashValid(resetHash))
// Test nil hash
user.ResetPwHash = nil
user.ResetPwHashExpires = nil
assert.False(t, user.IsResetHashValid(resetHash))
}
func TestUser_FindByResetHash(t *testing.T) {
user := setupTestUser(t)
resetHash := "find_by_hash_test_123"
err := user.SetPasswordResetHash(resetHash)
require.NoError(t, err)
// Test finding valid hash
var foundUser User
err = foundUser.FindByResetHash(resetHash)
assert.NoError(t, err)
assert.Equal(t, user.UserID, foundUser.UserID)
assert.Equal(t, user.Email, foundUser.Email)
// Test finding invalid hash
var notFoundUser User
err = notFoundUser.FindByResetHash("invalid_hash")
assert.Error(t, err)
// Test finding expired hash
expiredTime := time.Now().Add(-1 * time.Hour)
user.ResetPwHashExpires = &expiredTime
err = user.Save()
require.NoError(t, err)
var expiredUser User
err = expiredUser.FindByResetHash(resetHash)
assert.Error(t, err) // Should not find expired token
}
func TestUser_FindByEmail(t *testing.T) {
user := setupTestUser(t)
// Test finding existing email
var foundUser User
err := foundUser.FindByEmail(user.Email)
assert.NoError(t, err)
assert.Equal(t, user.UserID, foundUser.UserID)
assert.Equal(t, user.Username, foundUser.Username)
// Test finding non-existent email
var notFoundUser User
err = notFoundUser.FindByEmail("nonexistent@example.com")
assert.Error(t, err)
}