package auth import ( "path/filepath" "strings" "testing" "time" ) func TestHashPassword_UsesArgon2id(t *testing.T) { hashed, err := HashPassword("StrongPass1!") if err != nil { t.Fatalf("hash password failed: %v", err) } if !strings.HasPrefix(hashed, "$argon2id$") { t.Fatalf("expected argon2id hash, got %q", hashed) } if !VerifyPassword(hashed, "StrongPass1!") { t.Fatal("expected argon2id password verification to succeed") } } func TestVerifyPassword_SupportsLegacyBcrypt(t *testing.T) { hashed, err := BcryptHash("LegacyPass1!") if err != nil { t.Fatalf("hash legacy bcrypt password failed: %v", err) } if !VerifyPassword(hashed, "LegacyPass1!") { t.Fatal("expected bcrypt compatibility verification to succeed") } } func TestNewJWTWithOptions_RS256(t *testing.T) { dir := t.TempDir() jwtManager, err := NewJWTWithOptions(JWTOptions{ Algorithm: jwtAlgorithmRS256, RSAPrivateKeyPath: filepath.Join(dir, "private.pem"), RSAPublicKeyPath: filepath.Join(dir, "public.pem"), AccessTokenExpire: 2 * time.Hour, RefreshTokenExpire: 24 * time.Hour, }) if err != nil { t.Fatalf("create rs256 jwt manager failed: %v", err) } accessToken, refreshToken, err := jwtManager.GenerateTokenPair(42, "rs256-user") if err != nil { t.Fatalf("generate token pair failed: %v", err) } if jwtManager.GetAlgorithm() != jwtAlgorithmRS256 { t.Fatalf("unexpected algorithm: %s", jwtManager.GetAlgorithm()) } accessClaims, err := jwtManager.ValidateAccessToken(accessToken) if err != nil { t.Fatalf("validate access token failed: %v", err) } if accessClaims.UserID != 42 || accessClaims.Username != "rs256-user" { t.Fatalf("unexpected access claims: %+v", accessClaims) } refreshClaims, err := jwtManager.ValidateRefreshToken(refreshToken) if err != nil { t.Fatalf("validate refresh token failed: %v", err) } if refreshClaims.Type != "refresh" { t.Fatalf("unexpected refresh claims: %+v", refreshClaims) } } func TestNewJWTWithOptions_RS256_RequiresKeyMaterial(t *testing.T) { _, err := NewJWTWithOptions(JWTOptions{ Algorithm: jwtAlgorithmRS256, AccessTokenExpire: 2 * time.Hour, RefreshTokenExpire: 24 * time.Hour, }) if err == nil { t.Fatal("expected RS256 without key material to fail") } } func TestNewJWTWithOptions_RS256_RequireExistingKeysRejectsMissingFiles(t *testing.T) { dir := t.TempDir() _, err := NewJWTWithOptions(JWTOptions{ Algorithm: jwtAlgorithmRS256, RSAPrivateKeyPath: filepath.Join(dir, "missing-private.pem"), RSAPublicKeyPath: filepath.Join(dir, "missing-public.pem"), RequireExistingRSAKeys: true, AccessTokenExpire: 2 * time.Hour, RefreshTokenExpire: 24 * time.Hour, }) if err == nil { t.Fatal("expected RS256 strict mode to reject missing key files") } } func TestNewJWTWithOptions_RS256_RequireExistingKeysAllowsExistingFiles(t *testing.T) { dir := t.TempDir() privatePath := filepath.Join(dir, "private.pem") publicPath := filepath.Join(dir, "public.pem") if _, err := NewJWTWithOptions(JWTOptions{ Algorithm: jwtAlgorithmRS256, RSAPrivateKeyPath: privatePath, RSAPublicKeyPath: publicPath, AccessTokenExpire: 2 * time.Hour, RefreshTokenExpire: 24 * time.Hour, }); err != nil { t.Fatalf("prepare key files failed: %v", err) } jwtManager, err := NewJWTWithOptions(JWTOptions{ Algorithm: jwtAlgorithmRS256, RSAPrivateKeyPath: privatePath, RSAPublicKeyPath: publicPath, RequireExistingRSAKeys: true, AccessTokenExpire: 2 * time.Hour, RefreshTokenExpire: 24 * time.Hour, }) if err != nil { t.Fatalf("expected strict mode to accept existing key files, got: %v", err) } if jwtManager.GetAlgorithm() != jwtAlgorithmRS256 { t.Fatalf("unexpected algorithm: %s", jwtManager.GetAlgorithm()) } } func TestGenerateAccessToken_Success(t *testing.T) { jwtManager, err := NewJWTWithOptions(JWTOptions{ Algorithm: jwtAlgorithmHS256, HS256Secret: "test-secret-key-for-jwt-at-least-32-chars", AccessTokenExpire: 15 * time.Minute, RefreshTokenExpire: 7 * 24 * time.Hour, }) if err != nil { t.Fatalf("create jwt manager failed: %v", err) } token, err := jwtManager.GenerateAccessToken(123, "testuser") if err != nil { t.Fatalf("generate access token failed: %v", err) } if token == "" { t.Fatal("expected non-empty token") } claims, err := jwtManager.ValidateAccessToken(token) if err != nil { t.Fatalf("validate access token failed: %v", err) } if claims.UserID != 123 { t.Errorf("UserID = %d, want 123", claims.UserID) } if claims.Username != "testuser" { t.Errorf("Username = %s, want testuser", claims.Username) } if claims.Type != "access" { t.Errorf("Type = %s, want access", claims.Type) } } func TestGenerateRefreshToken_Success(t *testing.T) { jwtManager, err := NewJWTWithOptions(JWTOptions{ Algorithm: jwtAlgorithmHS256, HS256Secret: "test-secret-key-for-jwt-at-least-32-chars", AccessTokenExpire: 15 * time.Minute, RefreshTokenExpire: 7 * 24 * time.Hour, }) if err != nil { t.Fatalf("create jwt manager failed: %v", err) } token, err := jwtManager.GenerateRefreshToken(456, "refreshuser") if err != nil { t.Fatalf("generate refresh token failed: %v", err) } if token == "" { t.Fatal("expected non-empty token") } claims, err := jwtManager.ValidateRefreshToken(token) if err != nil { t.Fatalf("validate refresh token failed: %v", err) } if claims.UserID != 456 { t.Errorf("UserID = %d, want 456", claims.UserID) } if claims.Type != "refresh" { t.Errorf("Type = %s, want refresh", claims.Type) } } func TestGenerateTokenPair_Success(t *testing.T) { jwtManager, err := NewJWTWithOptions(JWTOptions{ Algorithm: jwtAlgorithmHS256, HS256Secret: "test-secret-key-for-jwt-at-least-32-chars", AccessTokenExpire: 15 * time.Minute, RefreshTokenExpire: 7 * 24 * time.Hour, }) if err != nil { t.Fatalf("create jwt manager failed: %v", err) } accessToken, refreshToken, err := jwtManager.GenerateTokenPair(789, "pairuser") if err != nil { t.Fatalf("generate token pair failed: %v", err) } if accessToken == "" || refreshToken == "" { t.Fatal("expected non-empty tokens") } accessClaims, err := jwtManager.ValidateAccessToken(accessToken) if err != nil { t.Fatalf("validate access token failed: %v", err) } if accessClaims.UserID != 789 { t.Errorf("UserID = %d, want 789", accessClaims.UserID) } refreshClaims, err := jwtManager.ValidateRefreshToken(refreshToken) if err != nil { t.Fatalf("validate refresh token failed: %v", err) } if refreshClaims.UserID != 789 { t.Errorf("UserID = %d, want 789", refreshClaims.UserID) } } func TestGenerateTokenPairWithRemember_Success(t *testing.T) { jwtManager, err := NewJWTWithOptions(JWTOptions{ Algorithm: jwtAlgorithmHS256, HS256Secret: "test-secret-key-for-jwt-at-least-32-chars", AccessTokenExpire: 15 * time.Minute, RefreshTokenExpire: 7 * 24 * time.Hour, RememberLoginExpire: 30 * 24 * time.Hour, }) if err != nil { t.Fatalf("create jwt manager failed: %v", err) } accessToken, refreshToken, err := jwtManager.GenerateTokenPairWithRemember(999, "rememberuser", true) if err != nil { t.Fatalf("generate token pair with remember failed: %v", err) } if accessToken == "" || refreshToken == "" { t.Fatal("expected non-empty tokens") } accessClaims, err := jwtManager.ValidateAccessToken(accessToken) if err != nil { t.Fatalf("validate access token failed: %v", err) } if accessClaims.Remember { t.Error("access token should not have Remember flag") } refreshClaims, err := jwtManager.ValidateRefreshToken(refreshToken) if err != nil { t.Fatalf("validate refresh token failed: %v", err) } if !refreshClaims.Remember { t.Error("refresh token should have Remember flag set to true") } } func TestValidateAccessToken_WrongType(t *testing.T) { jwtManager, err := NewJWTWithOptions(JWTOptions{ Algorithm: jwtAlgorithmHS256, HS256Secret: "test-secret-key-for-jwt-at-least-32-chars", AccessTokenExpire: 15 * time.Minute, RefreshTokenExpire: 7 * 24 * time.Hour, }) if err != nil { t.Fatalf("create jwt manager failed: %v", err) } // Use a refresh token as if it were an access token refreshToken, err := jwtManager.GenerateRefreshToken(123, "testuser") if err != nil { t.Fatalf("generate refresh token failed: %v", err) } _, err = jwtManager.ValidateAccessToken(refreshToken) if err == nil { t.Fatal("expected error when validating refresh token as access token") } } func TestValidateRefreshToken_WrongType(t *testing.T) { jwtManager, err := NewJWTWithOptions(JWTOptions{ Algorithm: jwtAlgorithmHS256, HS256Secret: "test-secret-key-for-jwt-at-least-32-chars", AccessTokenExpire: 15 * time.Minute, RefreshTokenExpire: 7 * 24 * time.Hour, }) if err != nil { t.Fatalf("create jwt manager failed: %v", err) } // Use an access token as if it were a refresh token accessToken, err := jwtManager.GenerateAccessToken(123, "testuser") if err != nil { t.Fatalf("generate access token failed: %v", err) } _, err = jwtManager.ValidateRefreshToken(accessToken) if err == nil { t.Fatal("expected error when validating access token as refresh token") } } func TestValidateAccessToken_InvalidToken(t *testing.T) { jwtManager, err := NewJWTWithOptions(JWTOptions{ Algorithm: jwtAlgorithmHS256, HS256Secret: "test-secret-key-for-jwt-at-least-32-chars", AccessTokenExpire: 15 * time.Minute, RefreshTokenExpire: 7 * 24 * time.Hour, }) if err != nil { t.Fatalf("create jwt manager failed: %v", err) } _, err = jwtManager.ValidateAccessToken("invalid-token") if err == nil { t.Fatal("expected error for invalid token") } } func TestGetAccessTokenExpire(t *testing.T) { jwtManager, err := NewJWTWithOptions(JWTOptions{ Algorithm: jwtAlgorithmHS256, HS256Secret: "test-secret-key-for-jwt-at-least-32-chars", AccessTokenExpire: 30 * time.Minute, RefreshTokenExpire: 7 * 24 * time.Hour, }) if err != nil { t.Fatalf("create jwt manager failed: %v", err) } expire := jwtManager.GetAccessTokenExpire() if expire != 30*time.Minute { t.Errorf("GetAccessTokenExpire() = %v, want 30m", expire) } } func TestGetRefreshTokenExpire(t *testing.T) { jwtManager, err := NewJWTWithOptions(JWTOptions{ Algorithm: jwtAlgorithmHS256, HS256Secret: "test-secret-key-for-jwt-at-least-32-chars", AccessTokenExpire: 15 * time.Minute, RefreshTokenExpire: 14 * 24 * time.Hour, }) if err != nil { t.Fatalf("create jwt manager failed: %v", err) } expire := jwtManager.GetRefreshTokenExpire() if expire != 14*24*time.Hour { t.Errorf("GetRefreshTokenExpire() = %v, want 14d", expire) } } func TestParseToken_Invalid(t *testing.T) { jwtManager, err := NewJWTWithOptions(JWTOptions{ Algorithm: jwtAlgorithmHS256, HS256Secret: "test-secret-key-for-jwt-at-least-32-chars", AccessTokenExpire: 15 * time.Minute, RefreshTokenExpire: 7 * 24 * time.Hour, }) if err != nil { t.Fatalf("create jwt manager failed: %v", err) } _, err = jwtManager.ParseToken("not-a-valid-jwt-token") if err == nil { t.Fatal("expected error for invalid token") } } func TestGenerateLongLivedRefreshToken_Success(t *testing.T) { jwtManager, err := NewJWTWithOptions(JWTOptions{ Algorithm: jwtAlgorithmHS256, HS256Secret: "test-secret-key-for-jwt-at-least-32-chars", AccessTokenExpire: 15 * time.Minute, RefreshTokenExpire: 7 * 24 * time.Hour, RememberLoginExpire: 30 * 24 * time.Hour, }) if err != nil { t.Fatalf("create jwt manager failed: %v", err) } token, err := jwtManager.GenerateLongLivedRefreshToken(123, "longliveuser") if err != nil { t.Fatalf("generate long lived refresh token failed: %v", err) } if token == "" { t.Fatal("expected non-empty token") } claims, err := jwtManager.ValidateRefreshToken(token) if err != nil { t.Fatalf("validate refresh token failed: %v", err) } if claims.UserID != 123 { t.Errorf("UserID = %d, want 123", claims.UserID) } if !claims.Remember { t.Error("expected Remember flag to be set") } } func TestParseRSAPrivateKey_InvalidPEM(t *testing.T) { _, err := parseRSAPrivateKey("not-a-valid-pem-block") if err == nil { t.Fatal("expected error for invalid PEM") } } func TestParseRSAPublicKey_InvalidPEM(t *testing.T) { _, err := parseRSAPublicKey("not-a-valid-pem-block") if err == nil { t.Fatal("expected error for invalid PEM") } } func TestGenerateAndPersistRSAKeyPair_EmptyPath(t *testing.T) { _, _, err := generateAndPersistRSAKeyPair("", "public.pem") if err == nil { t.Fatal("expected error for empty private path") } _, _, err = generateAndPersistRSAKeyPair("private.pem", "") if err == nil { t.Fatal("expected error for empty public path") } } func TestRefreshAccessToken_Success(t *testing.T) { jwtManager, err := NewJWTWithOptions(JWTOptions{ Algorithm: jwtAlgorithmHS256, HS256Secret: "test-secret-key-for-jwt-at-least-32-chars", AccessTokenExpire: 15 * time.Minute, RefreshTokenExpire: 7 * 24 * time.Hour, }) if err != nil { t.Fatalf("create jwt manager failed: %v", err) } // Generate a valid refresh token first refreshToken, err := jwtManager.GenerateRefreshToken(123, "testuser") if err != nil { t.Fatalf("generate refresh token failed: %v", err) } // Use refresh to get new access token newAccessToken, err := jwtManager.RefreshAccessToken(refreshToken) if err != nil { t.Fatalf("refresh access token failed: %v", err) } if newAccessToken == "" { t.Fatal("expected non-empty access token") } claims, err := jwtManager.ValidateAccessToken(newAccessToken) if err != nil { t.Fatalf("validate new access token failed: %v", err) } if claims.UserID != 123 { t.Errorf("UserID = %d, want 123", claims.UserID) } } func TestRefreshAccessToken_InvalidRefreshToken(t *testing.T) { jwtManager, err := NewJWTWithOptions(JWTOptions{ Algorithm: jwtAlgorithmHS256, HS256Secret: "test-secret-key-for-jwt-at-least-32-chars", AccessTokenExpire: 15 * time.Minute, RefreshTokenExpire: 7 * 24 * time.Hour, }) if err != nil { t.Fatalf("create jwt manager failed: %v", err) } _, err = jwtManager.RefreshAccessToken("invalid-refresh-token") if err == nil { t.Fatal("expected error for invalid refresh token") } } func TestRefreshAccessToken_AccessTokenProvided(t *testing.T) { jwtManager, err := NewJWTWithOptions(JWTOptions{ Algorithm: jwtAlgorithmHS256, HS256Secret: "test-secret-key-for-jwt-at-least-32-chars", AccessTokenExpire: 15 * time.Minute, RefreshTokenExpire: 7 * 24 * time.Hour, }) if err != nil { t.Fatalf("create jwt manager failed: %v", err) } // Generate an access token and try to use it as refresh accessToken, err := jwtManager.GenerateAccessToken(123, "testuser") if err != nil { t.Fatalf("generate access token failed: %v", err) } _, err = jwtManager.RefreshAccessToken(accessToken) if err == nil { t.Fatal("expected error when using access token as refresh token") } } func TestGenerateTokenPairWithRemember_RememberFalse(t *testing.T) { jwtManager, err := NewJWTWithOptions(JWTOptions{ Algorithm: jwtAlgorithmHS256, HS256Secret: "test-secret-key-for-jwt-at-least-32-chars", AccessTokenExpire: 15 * time.Minute, RefreshTokenExpire: 7 * 24 * time.Hour, RememberLoginExpire: 30 * 24 * time.Hour, }) if err != nil { t.Fatalf("create jwt manager failed: %v", err) } accessToken, refreshToken, err := jwtManager.GenerateTokenPairWithRemember(123, "testuser", false) if err != nil { t.Fatalf("GenerateTokenPairWithRemember failed: %v", err) } if accessToken == "" || refreshToken == "" { t.Fatal("Expected non-empty tokens") } // Verify refresh token does NOT have Remember flag claims, err := jwtManager.ValidateRefreshToken(refreshToken) if err != nil { t.Fatalf("ValidateRefreshToken failed: %v", err) } if claims.Remember { t.Error("Refresh token should NOT have Remember flag when remember=false") } } func TestGenerateTokenPairWithRemember_NoRememberExpireConfig(t *testing.T) { jwtManager, err := NewJWTWithOptions(JWTOptions{ Algorithm: jwtAlgorithmHS256, HS256Secret: "test-secret-key-for-jwt-at-least-32-chars", AccessTokenExpire: 15 * time.Minute, RefreshTokenExpire: 7 * 24 * time.Hour, // RememberLoginExpire not set }) if err != nil { t.Fatalf("create jwt manager failed: %v", err) } // Should use RefreshTokenExpire when RememberLoginExpire is not set accessToken, refreshToken, err := jwtManager.GenerateTokenPairWithRemember(123, "testuser", true) if err != nil { t.Fatalf("GenerateTokenPairWithRemember failed: %v", err) } if accessToken == "" || refreshToken == "" { t.Fatal("Expected non-empty tokens") } claims, err := jwtManager.ValidateRefreshToken(refreshToken) if err != nil { t.Fatalf("ValidateRefreshToken failed: %v", err) } if !claims.Remember { t.Error("Refresh token should have Remember flag") } } func TestGenerateLongLivedRefreshToken_NoRememberExpire(t *testing.T) { jwtManager, err := NewJWTWithOptions(JWTOptions{ Algorithm: jwtAlgorithmHS256, HS256Secret: "test-secret-key-for-jwt-at-least-32-chars", AccessTokenExpire: 15 * time.Minute, RefreshTokenExpire: 7 * 24 * time.Hour, // RememberLoginExpire not set - should use RefreshTokenExpire }) if err != nil { t.Fatalf("create jwt manager failed: %v", err) } token, err := jwtManager.GenerateLongLivedRefreshToken(123, "testuser") if err != nil { t.Fatalf("GenerateLongLivedRefreshToken failed: %v", err) } claims, err := jwtManager.ValidateRefreshToken(token) if err != nil { t.Fatalf("ValidateRefreshToken failed: %v", err) } if !claims.Remember { t.Error("Long-lived refresh token should have Remember flag") } }