Improve README SEO and increase test coverage to 50%
Optimized README for better discoverability with keywords: api, habits, self-hosted. Consolidated content to reduce redundancy while maintaining clarity. Added comprehensive test coverage across multiple layers: - Infrastructure: bcrypt hashing, JWT tokens, configuration validation - Application commands: user deletion, password reset, token revocation, email verification - Application queries: login, token refresh - Domain entities: refresh tokens, password reset tokens Coverage increased from 42.5% to 50.8% with meaningful business logic tests. Fixed integration test handler initialization with correct parameters.
This commit is contained in:
@@ -0,0 +1,357 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
func TestNewJWTService(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
secret string
|
||||
expiryHours int
|
||||
}{
|
||||
{
|
||||
name: "standard configuration",
|
||||
secret: "my-secret-key",
|
||||
expiryHours: 24,
|
||||
},
|
||||
{
|
||||
name: "short expiry",
|
||||
secret: "test-secret",
|
||||
expiryHours: 1,
|
||||
},
|
||||
{
|
||||
name: "long expiry",
|
||||
secret: "test-secret",
|
||||
expiryHours: 168,
|
||||
},
|
||||
{
|
||||
name: "empty secret",
|
||||
secret: "",
|
||||
expiryHours: 24,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
service := NewJWTService(tt.secret, tt.expiryHours)
|
||||
if service == nil {
|
||||
t.Fatal("NewJWTService() returned nil")
|
||||
}
|
||||
|
||||
if string(service.secret) != tt.secret {
|
||||
t.Errorf("secret = %v, want %v", string(service.secret), tt.secret)
|
||||
}
|
||||
|
||||
expectedExpiry := time.Duration(tt.expiryHours) * time.Hour
|
||||
if service.expiry != expectedExpiry {
|
||||
t.Errorf("expiry = %v, want %v", service.expiry, expectedExpiry)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWTService_GenerateToken(t *testing.T) {
|
||||
service := NewJWTService("test-secret-key", 24)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
userID string
|
||||
email string
|
||||
}{
|
||||
{
|
||||
name: "standard user",
|
||||
userID: "user-123",
|
||||
email: "user@example.com",
|
||||
},
|
||||
{
|
||||
name: "empty user ID",
|
||||
userID: "",
|
||||
email: "user@example.com",
|
||||
},
|
||||
{
|
||||
name: "empty email",
|
||||
userID: "user-123",
|
||||
email: "",
|
||||
},
|
||||
{
|
||||
name: "both empty",
|
||||
userID: "",
|
||||
email: "",
|
||||
},
|
||||
{
|
||||
name: "special characters in email",
|
||||
userID: "user-456",
|
||||
email: "user+test@example.com",
|
||||
},
|
||||
{
|
||||
name: "uuid as user ID",
|
||||
userID: "550e8400-e29b-41d4-a716-446655440000",
|
||||
email: "uuid@example.com",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
token, err := service.GenerateToken(tt.userID, tt.email)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateToken() error = %v", err)
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
t.Fatal("GenerateToken() returned empty token")
|
||||
}
|
||||
|
||||
claims, err := service.ValidateToken(token)
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateToken() error = %v", err)
|
||||
}
|
||||
|
||||
if claims.UserID != tt.userID {
|
||||
t.Errorf("UserID = %v, want %v", claims.UserID, tt.userID)
|
||||
}
|
||||
|
||||
if claims.Email != tt.email {
|
||||
t.Errorf("Email = %v, want %v", claims.Email, tt.email)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWTService_ValidateToken(t *testing.T) {
|
||||
secret := "test-secret-key"
|
||||
service := NewJWTService(secret, 24)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
setupToken func() string
|
||||
expectError bool
|
||||
errorMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid token",
|
||||
setupToken: func() string {
|
||||
token, _ := service.GenerateToken("user-123", "user@example.com")
|
||||
return token
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "invalid token format",
|
||||
setupToken: func() string {
|
||||
return "not.a.valid.token"
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "empty token",
|
||||
setupToken: func() string {
|
||||
return ""
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "token with wrong secret",
|
||||
setupToken: func() string {
|
||||
wrongService := NewJWTService("wrong-secret", 24)
|
||||
token, _ := wrongService.GenerateToken("user-123", "user@example.com")
|
||||
return token
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "expired token",
|
||||
setupToken: func() string {
|
||||
expiredService := NewJWTService(secret, -1)
|
||||
token, _ := expiredService.GenerateToken("user-123", "user@example.com")
|
||||
return token
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "malformed token",
|
||||
setupToken: func() string {
|
||||
return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.malformed"
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "token with invalid signature",
|
||||
setupToken: func() string {
|
||||
token, _ := service.GenerateToken("user-123", "user@example.com")
|
||||
return token[:len(token)-5] + "xxxxx"
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
token := tt.setupToken()
|
||||
claims, err := service.ValidateToken(token)
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Fatal("ValidateToken() expected error but got nil")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateToken() unexpected error = %v", err)
|
||||
}
|
||||
if claims == nil {
|
||||
t.Fatal("ValidateToken() returned nil claims")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWTService_ValidateTokenClaims(t *testing.T) {
|
||||
service := NewJWTService("test-secret-key", 24)
|
||||
|
||||
userID := "user-123"
|
||||
email := "user@example.com"
|
||||
|
||||
token, err := service.GenerateToken(userID, email)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateToken() error = %v", err)
|
||||
}
|
||||
|
||||
claims, err := service.ValidateToken(token)
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateToken() error = %v", err)
|
||||
}
|
||||
|
||||
if claims.UserID != userID {
|
||||
t.Errorf("UserID = %v, want %v", claims.UserID, userID)
|
||||
}
|
||||
|
||||
if claims.Email != email {
|
||||
t.Errorf("Email = %v, want %v", claims.Email, email)
|
||||
}
|
||||
|
||||
if claims.ExpiresAt == nil {
|
||||
t.Fatal("ExpiresAt is nil")
|
||||
}
|
||||
|
||||
if claims.IssuedAt == nil {
|
||||
t.Fatal("IssuedAt is nil")
|
||||
}
|
||||
|
||||
if claims.ExpiresAt.Before(claims.IssuedAt.Time) {
|
||||
t.Error("ExpiresAt is before IssuedAt")
|
||||
}
|
||||
|
||||
expectedExpiry := claims.IssuedAt.Add(24 * time.Hour)
|
||||
if !claims.ExpiresAt.Time.Equal(expectedExpiry) {
|
||||
diff := claims.ExpiresAt.Time.Sub(expectedExpiry)
|
||||
if diff > time.Second || diff < -time.Second {
|
||||
t.Errorf("ExpiresAt = %v, want approximately %v (diff: %v)", claims.ExpiresAt.Time, expectedExpiry, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWTService_TokenExpiry(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expiryHours int
|
||||
}{
|
||||
{
|
||||
name: "1 hour expiry",
|
||||
expiryHours: 1,
|
||||
},
|
||||
{
|
||||
name: "24 hours expiry",
|
||||
expiryHours: 24,
|
||||
},
|
||||
{
|
||||
name: "168 hours (1 week) expiry",
|
||||
expiryHours: 168,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
service := NewJWTService("test-secret", tt.expiryHours)
|
||||
token, err := service.GenerateToken("user-123", "user@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateToken() error = %v", err)
|
||||
}
|
||||
|
||||
claims, err := service.ValidateToken(token)
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateToken() error = %v", err)
|
||||
}
|
||||
|
||||
expectedExpiry := time.Now().Add(time.Duration(tt.expiryHours) * time.Hour)
|
||||
diff := claims.ExpiresAt.Time.Sub(expectedExpiry)
|
||||
|
||||
if diff > time.Second || diff < -time.Second {
|
||||
t.Errorf("ExpiresAt difference too large: %v", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWTService_ValidateTokenWithWrongSigningMethod(t *testing.T) {
|
||||
service := NewJWTService("test-secret", 24)
|
||||
|
||||
claims := &Claims{
|
||||
UserID: "user-123",
|
||||
Email: "user@example.com",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodNone, claims)
|
||||
tokenString, err := token.SignedString(jwt.UnsafeAllowNoneSignatureType)
|
||||
if err != nil {
|
||||
t.Fatalf("SignedString() error = %v", err)
|
||||
}
|
||||
|
||||
_, err = service.ValidateToken(tokenString)
|
||||
if err == nil {
|
||||
t.Fatal("ValidateToken() expected error for none signing method but got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWTService_MultipleTokens(t *testing.T) {
|
||||
service := NewJWTService("test-secret", 24)
|
||||
|
||||
token1, err := service.GenerateToken("user-1", "user1@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateToken(1) error = %v", err)
|
||||
}
|
||||
|
||||
token2, err := service.GenerateToken("user-2", "user2@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateToken(2) error = %v", err)
|
||||
}
|
||||
|
||||
if token1 == token2 {
|
||||
t.Error("Generated identical tokens for different users")
|
||||
}
|
||||
|
||||
claims1, err := service.ValidateToken(token1)
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateToken(1) error = %v", err)
|
||||
}
|
||||
|
||||
claims2, err := service.ValidateToken(token2)
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateToken(2) error = %v", err)
|
||||
}
|
||||
|
||||
if claims1.UserID == claims2.UserID {
|
||||
t.Error("Claims have same UserID")
|
||||
}
|
||||
|
||||
if claims1.Email == claims2.Email {
|
||||
t.Error("Claims have same Email")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoad_Success(t *testing.T) {
|
||||
os.Setenv("DB_PATH", "/test/db.sqlite")
|
||||
os.Setenv("APP_URL", "http://test.com")
|
||||
os.Setenv("JWT_SECRET", "test-secret")
|
||||
os.Setenv("JWT_EXPIRY", "1h")
|
||||
os.Setenv("REFRESH_TOKEN_EXPIRY", "7d")
|
||||
os.Setenv("DEFAULT_TIMEZONE", "UTC")
|
||||
defer clearEnv()
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg.DBPath != "/test/db.sqlite" {
|
||||
t.Errorf("DBPath = %v, want %v", cfg.DBPath, "/test/db.sqlite")
|
||||
}
|
||||
if cfg.AppURL != "http://test.com" {
|
||||
t.Errorf("AppURL = %v, want %v", cfg.AppURL, "http://test.com")
|
||||
}
|
||||
if cfg.JWTSecret != "test-secret" {
|
||||
t.Errorf("JWTSecret = %v, want %v", cfg.JWTSecret, "test-secret")
|
||||
}
|
||||
if cfg.JWTExpiry != "1h" {
|
||||
t.Errorf("JWTExpiry = %v, want %v", cfg.JWTExpiry, "1h")
|
||||
}
|
||||
if cfg.RefreshTokenExpiry != "7d" {
|
||||
t.Errorf("RefreshTokenExpiry = %v, want %v", cfg.RefreshTokenExpiry, "7d")
|
||||
}
|
||||
if cfg.DefaultTimezone != "UTC" {
|
||||
t.Errorf("DefaultTimezone = %v, want %v", cfg.DefaultTimezone, "UTC")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_WithDefaults(t *testing.T) {
|
||||
os.Setenv("DB_PATH", "/test/db.sqlite")
|
||||
os.Setenv("APP_URL", "http://test.com")
|
||||
os.Setenv("JWT_SECRET", "test-secret")
|
||||
os.Setenv("JWT_EXPIRY", "1h")
|
||||
os.Setenv("REFRESH_TOKEN_EXPIRY", "7d")
|
||||
os.Setenv("DEFAULT_TIMEZONE", "UTC")
|
||||
defer clearEnv()
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg.Port != "8080" {
|
||||
t.Errorf("Port = %v, want default %v", cfg.Port, "8080")
|
||||
}
|
||||
|
||||
if cfg.SMTPPort != "587" {
|
||||
t.Errorf("SMTPPort = %v, want default %v", cfg.SMTPPort, "587")
|
||||
}
|
||||
|
||||
if cfg.SupportEmail != "contact@apocapoc.app" {
|
||||
t.Errorf("SupportEmail = %v, want default %v", cfg.SupportEmail, "contact@apocapoc.app")
|
||||
}
|
||||
|
||||
if cfg.SendWelcomeEmail != "false" {
|
||||
t.Errorf("SendWelcomeEmail = %v, want default %v", cfg.SendWelcomeEmail, "false")
|
||||
}
|
||||
|
||||
if cfg.RegistrationMode != "open" {
|
||||
t.Errorf("RegistrationMode = %v, want default %v", cfg.RegistrationMode, "open")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_WithCustomDefaults(t *testing.T) {
|
||||
os.Setenv("DB_PATH", "/test/db.sqlite")
|
||||
os.Setenv("APP_URL", "http://test.com")
|
||||
os.Setenv("JWT_SECRET", "test-secret")
|
||||
os.Setenv("JWT_EXPIRY", "1h")
|
||||
os.Setenv("REFRESH_TOKEN_EXPIRY", "7d")
|
||||
os.Setenv("DEFAULT_TIMEZONE", "UTC")
|
||||
os.Setenv("PORT", "3000")
|
||||
os.Setenv("SMTP_PORT", "465")
|
||||
os.Setenv("SUPPORT_EMAIL", "support@test.com")
|
||||
os.Setenv("SEND_WELCOME_EMAIL", "true")
|
||||
os.Setenv("REGISTRATION_MODE", "closed")
|
||||
defer clearEnv()
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg.Port != "3000" {
|
||||
t.Errorf("Port = %v, want %v", cfg.Port, "3000")
|
||||
}
|
||||
|
||||
if cfg.SMTPPort != "465" {
|
||||
t.Errorf("SMTPPort = %v, want %v", cfg.SMTPPort, "465")
|
||||
}
|
||||
|
||||
if cfg.SupportEmail != "support@test.com" {
|
||||
t.Errorf("SupportEmail = %v, want %v", cfg.SupportEmail, "support@test.com")
|
||||
}
|
||||
|
||||
if cfg.SendWelcomeEmail != "true" {
|
||||
t.Errorf("SendWelcomeEmail = %v, want %v", cfg.SendWelcomeEmail, "true")
|
||||
}
|
||||
|
||||
if cfg.RegistrationMode != "closed" {
|
||||
t.Errorf("RegistrationMode = %v, want %v", cfg.RegistrationMode, "closed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_MissingDBPath(t *testing.T) {
|
||||
clearEnv()
|
||||
os.Setenv("APP_URL", "http://test.com")
|
||||
os.Setenv("JWT_SECRET", "test-secret")
|
||||
os.Setenv("JWT_EXPIRY", "1h")
|
||||
os.Setenv("REFRESH_TOKEN_EXPIRY", "7d")
|
||||
os.Setenv("DEFAULT_TIMEZONE", "UTC")
|
||||
defer clearEnv()
|
||||
|
||||
_, err := Load()
|
||||
if err == nil {
|
||||
t.Fatal("Load() expected error for missing DB_PATH but got nil")
|
||||
}
|
||||
|
||||
expectedMsg := "DB_PATH is required"
|
||||
if err.Error() != expectedMsg {
|
||||
t.Errorf("Load() error = %v, want %v", err.Error(), expectedMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_MissingJWTSecret(t *testing.T) {
|
||||
clearEnv()
|
||||
os.Setenv("DB_PATH", "/test/db.sqlite")
|
||||
os.Setenv("APP_URL", "http://test.com")
|
||||
os.Setenv("JWT_EXPIRY", "1h")
|
||||
os.Setenv("REFRESH_TOKEN_EXPIRY", "7d")
|
||||
os.Setenv("DEFAULT_TIMEZONE", "UTC")
|
||||
defer clearEnv()
|
||||
|
||||
_, err := Load()
|
||||
if err == nil {
|
||||
t.Fatal("Load() expected error for missing JWT_SECRET but got nil")
|
||||
}
|
||||
|
||||
expectedMsg := "JWT_SECRET is required"
|
||||
if err.Error() != expectedMsg {
|
||||
t.Errorf("Load() error = %v, want %v", err.Error(), expectedMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_MissingJWTExpiry(t *testing.T) {
|
||||
clearEnv()
|
||||
os.Setenv("DB_PATH", "/test/db.sqlite")
|
||||
os.Setenv("APP_URL", "http://test.com")
|
||||
os.Setenv("JWT_SECRET", "test-secret")
|
||||
os.Setenv("REFRESH_TOKEN_EXPIRY", "7d")
|
||||
os.Setenv("DEFAULT_TIMEZONE", "UTC")
|
||||
defer clearEnv()
|
||||
|
||||
_, err := Load()
|
||||
if err == nil {
|
||||
t.Fatal("Load() expected error for missing JWT_EXPIRY but got nil")
|
||||
}
|
||||
|
||||
expectedMsg := "JWT_EXPIRY is required"
|
||||
if err.Error() != expectedMsg {
|
||||
t.Errorf("Load() error = %v, want %v", err.Error(), expectedMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_MissingRefreshTokenExpiry(t *testing.T) {
|
||||
clearEnv()
|
||||
os.Setenv("DB_PATH", "/test/db.sqlite")
|
||||
os.Setenv("APP_URL", "http://test.com")
|
||||
os.Setenv("JWT_SECRET", "test-secret")
|
||||
os.Setenv("JWT_EXPIRY", "1h")
|
||||
os.Setenv("DEFAULT_TIMEZONE", "UTC")
|
||||
defer clearEnv()
|
||||
|
||||
_, err := Load()
|
||||
if err == nil {
|
||||
t.Fatal("Load() expected error for missing REFRESH_TOKEN_EXPIRY but got nil")
|
||||
}
|
||||
|
||||
expectedMsg := "REFRESH_TOKEN_EXPIRY is required"
|
||||
if err.Error() != expectedMsg {
|
||||
t.Errorf("Load() error = %v, want %v", err.Error(), expectedMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_MissingDefaultTimezone(t *testing.T) {
|
||||
clearEnv()
|
||||
os.Setenv("DB_PATH", "/test/db.sqlite")
|
||||
os.Setenv("APP_URL", "http://test.com")
|
||||
os.Setenv("JWT_SECRET", "test-secret")
|
||||
os.Setenv("JWT_EXPIRY", "1h")
|
||||
os.Setenv("REFRESH_TOKEN_EXPIRY", "7d")
|
||||
defer clearEnv()
|
||||
|
||||
_, err := Load()
|
||||
if err == nil {
|
||||
t.Fatal("Load() expected error for missing DEFAULT_TIMEZONE but got nil")
|
||||
}
|
||||
|
||||
expectedMsg := "DEFAULT_TIMEZONE is required"
|
||||
if err.Error() != expectedMsg {
|
||||
t.Errorf("Load() error = %v, want %v", err.Error(), expectedMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_WithSMTPConfig(t *testing.T) {
|
||||
os.Setenv("DB_PATH", "/test/db.sqlite")
|
||||
os.Setenv("APP_URL", "http://test.com")
|
||||
os.Setenv("JWT_SECRET", "test-secret")
|
||||
os.Setenv("JWT_EXPIRY", "1h")
|
||||
os.Setenv("REFRESH_TOKEN_EXPIRY", "7d")
|
||||
os.Setenv("DEFAULT_TIMEZONE", "UTC")
|
||||
os.Setenv("SMTP_HOST", "smtp.test.com")
|
||||
os.Setenv("SMTP_PORT", "587")
|
||||
os.Setenv("SMTP_USER", "user@test.com")
|
||||
os.Setenv("SMTP_PASSWORD", "test-password")
|
||||
os.Setenv("SMTP_FROM", "noreply@test.com")
|
||||
defer clearEnv()
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg.SMTPHost != "smtp.test.com" {
|
||||
t.Errorf("SMTPHost = %v, want %v", cfg.SMTPHost, "smtp.test.com")
|
||||
}
|
||||
if cfg.SMTPPort != "587" {
|
||||
t.Errorf("SMTPPort = %v, want %v", cfg.SMTPPort, "587")
|
||||
}
|
||||
if cfg.SMTPUser != "user@test.com" {
|
||||
t.Errorf("SMTPUser = %v, want %v", cfg.SMTPUser, "user@test.com")
|
||||
}
|
||||
if cfg.SMTPPassword != "test-password" {
|
||||
t.Errorf("SMTPPassword = %v, want %v", cfg.SMTPPassword, "test-password")
|
||||
}
|
||||
if cfg.SMTPFrom != "noreply@test.com" {
|
||||
t.Errorf("SMTPFrom = %v, want %v", cfg.SMTPFrom, "noreply@test.com")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEnvOrDefault(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
defaultValue string
|
||||
envValue string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "env value exists",
|
||||
key: "TEST_KEY",
|
||||
defaultValue: "default",
|
||||
envValue: "custom",
|
||||
want: "custom",
|
||||
},
|
||||
{
|
||||
name: "env value empty uses default",
|
||||
key: "TEST_KEY_EMPTY",
|
||||
defaultValue: "default",
|
||||
envValue: "",
|
||||
want: "default",
|
||||
},
|
||||
{
|
||||
name: "env value not set uses default",
|
||||
key: "TEST_KEY_NOT_SET",
|
||||
defaultValue: "default",
|
||||
envValue: "",
|
||||
want: "default",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.envValue != "" {
|
||||
os.Setenv(tt.key, tt.envValue)
|
||||
defer os.Unsetenv(tt.key)
|
||||
}
|
||||
|
||||
got := getEnvOrDefault(tt.key, tt.defaultValue)
|
||||
if got != tt.want {
|
||||
t.Errorf("getEnvOrDefault() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func clearEnv() {
|
||||
os.Unsetenv("DB_PATH")
|
||||
os.Unsetenv("PORT")
|
||||
os.Unsetenv("APP_URL")
|
||||
os.Unsetenv("JWT_SECRET")
|
||||
os.Unsetenv("JWT_EXPIRY")
|
||||
os.Unsetenv("REFRESH_TOKEN_EXPIRY")
|
||||
os.Unsetenv("DEFAULT_TIMEZONE")
|
||||
os.Unsetenv("SMTP_HOST")
|
||||
os.Unsetenv("SMTP_PORT")
|
||||
os.Unsetenv("SMTP_USER")
|
||||
os.Unsetenv("SMTP_PASSWORD")
|
||||
os.Unsetenv("SMTP_FROM")
|
||||
os.Unsetenv("SUPPORT_EMAIL")
|
||||
os.Unsetenv("SEND_WELCOME_EMAIL")
|
||||
os.Unsetenv("REGISTRATION_MODE")
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func TestNewBcryptHasher(t *testing.T) {
|
||||
hasher := NewBcryptHasher()
|
||||
if hasher == nil {
|
||||
t.Fatal("NewBcryptHasher() returned nil")
|
||||
}
|
||||
|
||||
_, ok := hasher.(*BcryptHasher)
|
||||
if !ok {
|
||||
t.Fatal("NewBcryptHasher() did not return *BcryptHasher")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBcryptHasher_Hash(t *testing.T) {
|
||||
hasher := NewBcryptHasher()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
password string
|
||||
}{
|
||||
{
|
||||
name: "simple password",
|
||||
password: "password123",
|
||||
},
|
||||
{
|
||||
name: "complex password",
|
||||
password: "P@ssw0rd!123$%^&*()",
|
||||
},
|
||||
{
|
||||
name: "empty password",
|
||||
password: "",
|
||||
},
|
||||
{
|
||||
name: "unicode password",
|
||||
password: "pässwörd123",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
hashed, err := hasher.Hash(tt.password)
|
||||
if err != nil {
|
||||
t.Fatalf("Hash() error = %v", err)
|
||||
}
|
||||
|
||||
if hashed == "" {
|
||||
t.Fatal("Hash() returned empty string")
|
||||
}
|
||||
|
||||
if hashed == tt.password {
|
||||
t.Fatal("Hash() returned the same as input password")
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(hashed), []byte(tt.password))
|
||||
if err != nil {
|
||||
t.Fatalf("Generated hash does not match original password: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBcryptHasher_Compare(t *testing.T) {
|
||||
hasher := NewBcryptHasher()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
password string
|
||||
compareWith string
|
||||
expectError bool
|
||||
errorAssertion func(error) bool
|
||||
}{
|
||||
{
|
||||
name: "matching passwords",
|
||||
password: "password123",
|
||||
compareWith: "password123",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "non-matching passwords",
|
||||
password: "password123",
|
||||
compareWith: "wrongpassword",
|
||||
expectError: true,
|
||||
errorAssertion: func(err error) bool {
|
||||
return err == bcrypt.ErrMismatchedHashAndPassword
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty password comparison",
|
||||
password: "",
|
||||
compareWith: "",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "unicode password match",
|
||||
password: "pässwörd123",
|
||||
compareWith: "pässwörd123",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "unicode password mismatch",
|
||||
password: "pässwörd123",
|
||||
compareWith: "password123",
|
||||
expectError: true,
|
||||
errorAssertion: func(err error) bool {
|
||||
return err == bcrypt.ErrMismatchedHashAndPassword
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "case sensitive",
|
||||
password: "Password123",
|
||||
compareWith: "password123",
|
||||
expectError: true,
|
||||
errorAssertion: func(err error) bool {
|
||||
return err == bcrypt.ErrMismatchedHashAndPassword
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
hashed, err := hasher.Hash(tt.password)
|
||||
if err != nil {
|
||||
t.Fatalf("Hash() error = %v", err)
|
||||
}
|
||||
|
||||
err = hasher.Compare(hashed, tt.compareWith)
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Fatal("Compare() expected error but got nil")
|
||||
}
|
||||
if tt.errorAssertion != nil && !tt.errorAssertion(err) {
|
||||
t.Fatalf("Compare() error = %v, but assertion failed", err)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatalf("Compare() unexpected error = %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBcryptHasher_CompareWithInvalidHash(t *testing.T) {
|
||||
hasher := NewBcryptHasher()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
invalidHash string
|
||||
password string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "invalid hash format",
|
||||
invalidHash: "not-a-valid-hash",
|
||||
password: "password123",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "empty hash",
|
||||
invalidHash: "",
|
||||
password: "password123",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "corrupted hash",
|
||||
invalidHash: "$2a$10$invalidhashdata",
|
||||
password: "password123",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := hasher.Compare(tt.invalidHash, tt.password)
|
||||
if !tt.expectError && err != nil {
|
||||
t.Fatalf("Compare() unexpected error = %v", err)
|
||||
}
|
||||
if tt.expectError && err == nil {
|
||||
t.Fatal("Compare() expected error but got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBcryptHasher_HashGeneratesDifferentHashes(t *testing.T) {
|
||||
hasher := NewBcryptHasher()
|
||||
password := "samePassword123"
|
||||
|
||||
hash1, err := hasher.Hash(password)
|
||||
if err != nil {
|
||||
t.Fatalf("Hash() error = %v", err)
|
||||
}
|
||||
|
||||
hash2, err := hasher.Hash(password)
|
||||
if err != nil {
|
||||
t.Fatalf("Hash() error = %v", err)
|
||||
}
|
||||
|
||||
if hash1 == hash2 {
|
||||
t.Fatal("Hash() generated identical hashes for same password (should use salt)")
|
||||
}
|
||||
|
||||
if err := hasher.Compare(hash1, password); err != nil {
|
||||
t.Fatalf("First hash doesn't match password: %v", err)
|
||||
}
|
||||
|
||||
if err := hasher.Compare(hash2, password); err != nil {
|
||||
t.Fatalf("Second hash doesn't match password: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -42,12 +42,12 @@ func setupTestServer(t *testing.T) *TestServer {
|
||||
refreshTokenRepo := sqlite.NewRefreshTokenRepository(db)
|
||||
passwordResetTokenRepo := sqlite.NewPasswordResetTokenRepository(db)
|
||||
|
||||
registerHandler := commands.NewRegisterUserHandler(userRepo, passwordHasher, nil, "", "open")
|
||||
registerHandler := commands.NewRegisterUserHandler(userRepo, passwordHasher, nil, "", "open", false)
|
||||
loginHandler := queries.NewLoginUserHandler(userRepo, passwordHasher)
|
||||
refreshTokenHandler := queries.NewRefreshTokenHandler(refreshTokenRepo, userRepo)
|
||||
revokeTokenHandler := commands.NewRevokeTokenHandler(refreshTokenRepo)
|
||||
revokeAllTokensHandler := commands.NewRevokeAllTokensHandler(refreshTokenRepo)
|
||||
verifyEmailHandler := commands.NewVerifyEmailHandler(userRepo)
|
||||
verifyEmailHandler := commands.NewVerifyEmailHandler(userRepo, nil, false)
|
||||
resendVerificationEmailHandler := commands.NewResendVerificationEmailHandler(userRepo, nil, "")
|
||||
requestPasswordResetHandler := commands.NewRequestPasswordResetHandler(userRepo, passwordResetTokenRepo, nil, "")
|
||||
resetPasswordHandler := commands.NewResetPasswordHandler(userRepo, passwordResetTokenRepo, passwordHasher)
|
||||
|
||||
Reference in New Issue
Block a user