@@ -2851,3 +2851,142 @@ def __init__(self, name: str):
28512851 team_with_slug .slug = "workspace-alice-example-com"
28522852 db .set_email_team_slug (None , None , team_with_slug )
28532853 assert team_with_slug .slug == "workspace-alice-example-com"
2854+
2855+
2856+ def test_password_reset_is_expired_naive_expired ():
2857+ """Naive datetime in the past is detected as expired."""
2858+ with patch ("mcpgateway.db.utc_now" , return_value = datetime (2026 , 3 , 9 , 12 , 0 , 0 , tzinfo = timezone .utc )):
2859+ token = db .PasswordResetToken (
2860+ user_email = "user@example.com" ,
2861+ token_hash = "a" * 64 ,
2862+ expires_at = datetime (2026 , 3 , 8 , 12 , 0 , 0 ),
2863+ )
2864+ assert token .is_expired () is True
2865+
2866+
2867+ def test_password_reset_is_expired_naive_not_expired ():
2868+ """Naive datetime in the future is detected as not expired."""
2869+ with patch ("mcpgateway.db.utc_now" , return_value = datetime (2026 , 3 , 9 , 12 , 0 , 0 , tzinfo = timezone .utc )):
2870+ token = db .PasswordResetToken (
2871+ user_email = "user@example.com" ,
2872+ token_hash = "a" * 64 ,
2873+ expires_at = datetime (2026 , 3 , 10 , 12 , 0 , 0 ),
2874+ )
2875+ assert token .is_expired () is False
2876+
2877+
2878+ def test_password_reset_is_expired_aware ():
2879+ """Aware datetime comparison works without normalization."""
2880+ with patch ("mcpgateway.db.utc_now" , return_value = datetime (2026 , 3 , 9 , 12 , 0 , 0 , tzinfo = timezone .utc )):
2881+ token = db .PasswordResetToken (
2882+ user_email = "user@example.com" ,
2883+ token_hash = "a" * 64 ,
2884+ expires_at = datetime (2026 , 3 , 8 , 12 , 0 , 0 , tzinfo = timezone .utc ),
2885+ )
2886+ assert token .is_expired () is True
2887+
2888+
2889+ def test_password_reset_is_expired_none ():
2890+ """None expires_at is treated as not expired."""
2891+ token = db .PasswordResetToken (
2892+ user_email = "user@example.com" ,
2893+ token_hash = "a" * 64 ,
2894+ expires_at = None ,
2895+ )
2896+ assert token .is_expired () is False
2897+
2898+
2899+ def test_user_role_is_expired_naive_expired ():
2900+ """Naive datetime in the past is detected as expired."""
2901+ with patch ("mcpgateway.db.utc_now" , return_value = datetime (2026 , 3 , 9 , 12 , 0 , 0 , tzinfo = timezone .utc )):
2902+ user_role = db .UserRole (
2903+ user_email = "user@example.com" ,
2904+ role_id = "role_id" ,
2905+ scope = "scope" ,
2906+ granted_by = "user1@example.com" ,
2907+ granted_at = datetime (2026 , 3 , 7 , 12 , 0 , 0 ),
2908+ expires_at = datetime (2026 , 3 , 8 , 12 , 0 , 0 ),
2909+ )
2910+ assert user_role .is_expired () is True
2911+
2912+
2913+ def test_user_role_is_expired_aware_not_expired ():
2914+ """Aware datetime in the future is detected as not expired."""
2915+ with patch ("mcpgateway.db.utc_now" , return_value = datetime (2026 , 3 , 9 , 12 , 0 , 0 , tzinfo = timezone .utc )):
2916+ user_role = db .UserRole (
2917+ user_email = "user@example.com" ,
2918+ role_id = "role_id" ,
2919+ scope = "scope" ,
2920+ granted_by = "user1@example.com" ,
2921+ granted_at = datetime (2026 , 3 , 7 , 12 , 0 , 0 ),
2922+ expires_at = datetime (2026 , 3 , 10 , 12 , 0 , 0 , tzinfo = timezone .utc ),
2923+ )
2924+ assert user_role .is_expired () is False
2925+
2926+
2927+ def test_user_role_is_expired_none ():
2928+ """None expires_at means role never expires."""
2929+ user_role = db .UserRole (
2930+ user_email = "user@example.com" ,
2931+ role_id = "role_id" ,
2932+ scope = "scope" ,
2933+ granted_by = "user1@example.com" ,
2934+ granted_at = datetime (2026 , 3 , 7 , 12 , 0 , 0 ),
2935+ expires_at = None ,
2936+ )
2937+ assert user_role .is_expired () is False
2938+
2939+
2940+ def test_email_api_token_is_expired_naive_expired ():
2941+ """Naive datetime in the past is detected as expired."""
2942+ with patch ("mcpgateway.db.utc_now" , return_value = datetime (2026 , 3 , 9 , 12 , 0 , 0 , tzinfo = timezone .utc )):
2943+ token = db .EmailApiToken (
2944+ user_email = "alice@example.com" ,
2945+ name = "Production API Access" ,
2946+ server_id = "prod-server-123" ,
2947+ resource_scopes = ["tools.read" , "resources.read" ],
2948+ description = "Read-only access to production tools" ,
2949+ expires_at = datetime (2026 , 3 , 8 , 12 , 0 , 0 ),
2950+ )
2951+ assert token .is_expired () is True
2952+
2953+
2954+ def test_email_api_token_is_expired_naive_not_expired ():
2955+ """Naive datetime in the future is detected as not expired."""
2956+ with patch ("mcpgateway.db.utc_now" , return_value = datetime (2026 , 3 , 9 , 12 , 0 , 0 , tzinfo = timezone .utc )):
2957+ token = db .EmailApiToken (
2958+ user_email = "alice@example.com" ,
2959+ name = "Production API Access" ,
2960+ server_id = "prod-server-123" ,
2961+ resource_scopes = ["tools.read" , "resources.read" ],
2962+ description = "Read-only access to production tools" ,
2963+ expires_at = datetime (2026 , 3 , 13 , 12 , 0 , 0 ),
2964+ )
2965+ assert token .is_expired () is False
2966+
2967+
2968+ def test_email_api_token_is_expired_aware_not_expired ():
2969+ """Aware datetime in the future is detected as not expired."""
2970+ with patch ("mcpgateway.db.utc_now" , return_value = datetime (2026 , 3 , 9 , 12 , 0 , 0 , tzinfo = timezone .utc )):
2971+ token = db .EmailApiToken (
2972+ user_email = "alice@example.com" ,
2973+ name = "Production API Access" ,
2974+ server_id = "prod-server-123" ,
2975+ resource_scopes = ["tools.read" , "resources.read" ],
2976+ description = "Read-only access to production tools" ,
2977+ expires_at = datetime (2026 , 3 , 10 , 12 , 0 , 0 , tzinfo = timezone .utc ),
2978+ )
2979+ assert token .is_expired () is False
2980+
2981+
2982+ def test_email_api_token_is_expired_none ():
2983+ """None expires_at means token never expires."""
2984+ token = db .EmailApiToken (
2985+ user_email = "alice@example.com" ,
2986+ name = "Production API Access" ,
2987+ server_id = "prod-server-123" ,
2988+ resource_scopes = ["tools.read" , "resources.read" ],
2989+ description = "Read-only access to production tools" ,
2990+ expires_at = None ,
2991+ )
2992+ assert token .is_expired () is False
0 commit comments