P0-01: Add ESCAPE clause to LIKE queries in operation_log.go and device.go P0-02: Add atomic Increment to L1Cache and L2Cache interfaces P0-07: Add TOTP verification step after password login P1-01: Sanitize error messages in error.go middleware P1-03: Remove err.Error() from export error messages P1-04: Add error return to CountByResultSince in login_log.go P1-05: Add transactional DeleteCascade to RoleRepository P1-06: Add PasswordChangedAt tracking for JWT token invalidation P1-07: Wrap theme SetDefault in database transaction P1-08: Use config values for database pool parameters P1-09: Add rows.Err() checks in social_account_repo.go P1-10: Validate sortOrder with map in user.go ORDER BY P1-11: Add GORM tags to Announcement struct P1-15: Add pageSize upper limit (100) to device and log handlers
599 lines
18 KiB
Go
599 lines
18 KiB
Go
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", 0)
|
|
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", 0)
|
|
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", 0)
|
|
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", 0)
|
|
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, 0)
|
|
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", 0)
|
|
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", 0)
|
|
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", 0)
|
|
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", 0)
|
|
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", 0)
|
|
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, 0)
|
|
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, 0)
|
|
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", 0)
|
|
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")
|
|
}
|
|
}
|