A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
368 lines
8.5 KiB
Go
368 lines
8.5 KiB
Go
package services
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/breakpilot/consent-service/internal/models"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// TestHashPassword tests password hashing
|
|
func TestHashPassword(t *testing.T) {
|
|
// Create service without DB for unit tests
|
|
s := &AuthService{}
|
|
|
|
password := "testPassword123!"
|
|
hash, err := s.HashPassword(password)
|
|
|
|
if err != nil {
|
|
t.Fatalf("HashPassword failed: %v", err)
|
|
}
|
|
|
|
if hash == "" {
|
|
t.Error("Hash should not be empty")
|
|
}
|
|
|
|
if hash == password {
|
|
t.Error("Hash should not equal the original password")
|
|
}
|
|
|
|
// Hash should be different each time (bcrypt uses random salt)
|
|
hash2, _ := s.HashPassword(password)
|
|
if hash == hash2 {
|
|
t.Error("Same password should produce different hashes due to salt")
|
|
}
|
|
}
|
|
|
|
// TestVerifyPassword tests password verification
|
|
func TestVerifyPassword(t *testing.T) {
|
|
s := &AuthService{}
|
|
|
|
password := "testPassword123!"
|
|
hash, _ := s.HashPassword(password)
|
|
|
|
// Should verify correct password
|
|
if !s.VerifyPassword(password, hash) {
|
|
t.Error("VerifyPassword should return true for correct password")
|
|
}
|
|
|
|
// Should reject incorrect password
|
|
if s.VerifyPassword("wrongPassword", hash) {
|
|
t.Error("VerifyPassword should return false for incorrect password")
|
|
}
|
|
|
|
// Should reject empty password
|
|
if s.VerifyPassword("", hash) {
|
|
t.Error("VerifyPassword should return false for empty password")
|
|
}
|
|
}
|
|
|
|
// TestGenerateSecureToken tests token generation
|
|
func TestGenerateSecureToken(t *testing.T) {
|
|
s := &AuthService{}
|
|
|
|
tests := []struct {
|
|
name string
|
|
length int
|
|
}{
|
|
{"short token", 16},
|
|
{"standard token", 32},
|
|
{"long token", 64},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
token, err := s.GenerateSecureToken(tt.length)
|
|
if err != nil {
|
|
t.Fatalf("GenerateSecureToken failed: %v", err)
|
|
}
|
|
|
|
if token == "" {
|
|
t.Error("Token should not be empty")
|
|
}
|
|
|
|
// Tokens should be unique
|
|
token2, _ := s.GenerateSecureToken(tt.length)
|
|
if token == token2 {
|
|
t.Error("Generated tokens should be unique")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestHashToken tests token hashing for storage
|
|
func TestHashToken(t *testing.T) {
|
|
s := &AuthService{}
|
|
|
|
token := "test-token-123"
|
|
hash := s.HashToken(token)
|
|
|
|
if hash == "" {
|
|
t.Error("Hash should not be empty")
|
|
}
|
|
|
|
if hash == token {
|
|
t.Error("Hash should not equal the original token")
|
|
}
|
|
|
|
// Same token should produce same hash (deterministic)
|
|
hash2 := s.HashToken(token)
|
|
if hash != hash2 {
|
|
t.Error("Same token should produce same hash")
|
|
}
|
|
|
|
// Different tokens should produce different hashes
|
|
differentHash := s.HashToken("different-token")
|
|
if hash == differentHash {
|
|
t.Error("Different tokens should produce different hashes")
|
|
}
|
|
}
|
|
|
|
// TestGenerateAccessToken tests JWT access token generation
|
|
func TestGenerateAccessToken(t *testing.T) {
|
|
s := &AuthService{
|
|
jwtSecret: "test-secret-key-for-testing-purposes",
|
|
accessTokenExp: time.Hour,
|
|
}
|
|
|
|
user := &models.User{
|
|
ID: uuid.New(),
|
|
Email: "test@example.com",
|
|
Role: "user",
|
|
AccountStatus: "active",
|
|
}
|
|
|
|
token, err := s.GenerateAccessToken(user)
|
|
if err != nil {
|
|
t.Fatalf("GenerateAccessToken failed: %v", err)
|
|
}
|
|
|
|
if token == "" {
|
|
t.Error("Token should not be empty")
|
|
}
|
|
|
|
// Token should have three parts (header.payload.signature)
|
|
parts := 0
|
|
for _, c := range token {
|
|
if c == '.' {
|
|
parts++
|
|
}
|
|
}
|
|
if parts != 2 {
|
|
t.Errorf("JWT token should have 3 parts, got %d dots", parts)
|
|
}
|
|
}
|
|
|
|
// TestValidateAccessToken tests JWT token validation
|
|
func TestValidateAccessToken(t *testing.T) {
|
|
secret := "test-secret-key-for-testing-purposes"
|
|
s := &AuthService{
|
|
jwtSecret: secret,
|
|
accessTokenExp: time.Hour,
|
|
}
|
|
|
|
user := &models.User{
|
|
ID: uuid.New(),
|
|
Email: "test@example.com",
|
|
Role: "admin",
|
|
AccountStatus: "active",
|
|
}
|
|
|
|
token, _ := s.GenerateAccessToken(user)
|
|
|
|
// Should validate valid token
|
|
claims, err := s.ValidateAccessToken(token)
|
|
if err != nil {
|
|
t.Fatalf("ValidateAccessToken failed: %v", err)
|
|
}
|
|
|
|
if claims.UserID != user.ID.String() {
|
|
t.Errorf("Expected UserID %s, got %s", user.ID.String(), claims.UserID)
|
|
}
|
|
|
|
if claims.Email != user.Email {
|
|
t.Errorf("Expected Email %s, got %s", user.Email, claims.Email)
|
|
}
|
|
|
|
if claims.Role != user.Role {
|
|
t.Errorf("Expected Role %s, got %s", user.Role, claims.Role)
|
|
}
|
|
}
|
|
|
|
// TestValidateAccessToken_Invalid tests invalid token scenarios
|
|
func TestValidateAccessToken_Invalid(t *testing.T) {
|
|
s := &AuthService{
|
|
jwtSecret: "test-secret-key-for-testing-purposes",
|
|
accessTokenExp: time.Hour,
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
token string
|
|
}{
|
|
{"empty token", ""},
|
|
{"invalid format", "not-a-jwt-token"},
|
|
{"invalid signature", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzIn0.invalidsignature"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, err := s.ValidateAccessToken(tt.token)
|
|
if err == nil {
|
|
t.Error("ValidateAccessToken should fail for invalid token")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestValidateAccessToken_WrongSecret tests token with wrong secret
|
|
func TestValidateAccessToken_WrongSecret(t *testing.T) {
|
|
s1 := &AuthService{
|
|
jwtSecret: "secret-one",
|
|
accessTokenExp: time.Hour,
|
|
}
|
|
|
|
s2 := &AuthService{
|
|
jwtSecret: "secret-two",
|
|
accessTokenExp: time.Hour,
|
|
}
|
|
|
|
user := &models.User{
|
|
ID: uuid.New(),
|
|
Email: "test@example.com",
|
|
Role: "user",
|
|
AccountStatus: "active",
|
|
}
|
|
|
|
// Generate token with first secret
|
|
token, _ := s1.GenerateAccessToken(user)
|
|
|
|
// Try to validate with second secret (should fail)
|
|
_, err := s2.ValidateAccessToken(token)
|
|
if err == nil {
|
|
t.Error("ValidateAccessToken should fail when using wrong secret")
|
|
}
|
|
}
|
|
|
|
// TestGenerateRefreshToken tests refresh token generation
|
|
func TestGenerateRefreshToken(t *testing.T) {
|
|
s := &AuthService{}
|
|
|
|
token, hash, err := s.GenerateRefreshToken()
|
|
if err != nil {
|
|
t.Fatalf("GenerateRefreshToken failed: %v", err)
|
|
}
|
|
|
|
if token == "" {
|
|
t.Error("Token should not be empty")
|
|
}
|
|
|
|
if hash == "" {
|
|
t.Error("Hash should not be empty")
|
|
}
|
|
|
|
// Verify hash matches token
|
|
expectedHash := s.HashToken(token)
|
|
if hash != expectedHash {
|
|
t.Error("Returned hash should match hashed token")
|
|
}
|
|
|
|
// Tokens should be unique
|
|
token2, hash2, _ := s.GenerateRefreshToken()
|
|
if token == token2 {
|
|
t.Error("Generated tokens should be unique")
|
|
}
|
|
if hash == hash2 {
|
|
t.Error("Generated hashes should be unique")
|
|
}
|
|
}
|
|
|
|
// TestPasswordStrength tests various password scenarios
|
|
func TestPasswordStrength(t *testing.T) {
|
|
s := &AuthService{}
|
|
|
|
passwords := []struct {
|
|
password string
|
|
valid bool
|
|
}{
|
|
{"short", true}, // bcrypt accepts any length
|
|
{"12345678", true}, // numbers only
|
|
{"password", true}, // letters only
|
|
{"Pass123!", true}, // mixed
|
|
{"", true}, // empty (bcrypt allows)
|
|
{string(make([]byte, 72)), true}, // max bcrypt length
|
|
}
|
|
|
|
for _, p := range passwords {
|
|
hash, err := s.HashPassword(p.password)
|
|
if p.valid && err != nil {
|
|
t.Errorf("HashPassword failed for valid password %q: %v", p.password, err)
|
|
}
|
|
if p.valid && !s.VerifyPassword(p.password, hash) {
|
|
t.Errorf("VerifyPassword failed for password %q", p.password)
|
|
}
|
|
}
|
|
}
|
|
|
|
// BenchmarkHashPassword benchmarks password hashing
|
|
func BenchmarkHashPassword(b *testing.B) {
|
|
s := &AuthService{}
|
|
password := "testPassword123!"
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
s.HashPassword(password)
|
|
}
|
|
}
|
|
|
|
// BenchmarkVerifyPassword benchmarks password verification
|
|
func BenchmarkVerifyPassword(b *testing.B) {
|
|
s := &AuthService{}
|
|
password := "testPassword123!"
|
|
hash, _ := s.HashPassword(password)
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
s.VerifyPassword(password, hash)
|
|
}
|
|
}
|
|
|
|
// BenchmarkGenerateAccessToken benchmarks JWT token generation
|
|
func BenchmarkGenerateAccessToken(b *testing.B) {
|
|
s := &AuthService{
|
|
jwtSecret: "test-secret-key-for-testing-purposes",
|
|
accessTokenExp: time.Hour,
|
|
}
|
|
|
|
user := &models.User{
|
|
ID: uuid.New(),
|
|
Email: "test@example.com",
|
|
Role: "user",
|
|
AccountStatus: "active",
|
|
}
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
s.GenerateAccessToken(user)
|
|
}
|
|
}
|
|
|
|
// BenchmarkValidateAccessToken benchmarks JWT token validation
|
|
func BenchmarkValidateAccessToken(b *testing.B) {
|
|
s := &AuthService{
|
|
jwtSecret: "test-secret-key-for-testing-purposes",
|
|
accessTokenExp: time.Hour,
|
|
}
|
|
|
|
user := &models.User{
|
|
ID: uuid.New(),
|
|
Email: "test@example.com",
|
|
Role: "user",
|
|
AccountStatus: "active",
|
|
}
|
|
|
|
token, _ := s.GenerateAccessToken(user)
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
s.ValidateAccessToken(token)
|
|
}
|
|
}
|