1414
1515"""Test cases for the firebase_admin.fpnv module."""
1616
17+ import json
18+ from datetime import time
19+
20+ import jwt
21+ import pytest
22+ from unittest import mock
23+
24+ import firebase_admin
25+ from firebase_admin import fpnv
26+ from firebase_admin import _utils
27+ from tests import testutils
28+
29+ # Mock Data
30+ _PROJECT_ID = 'mock-project-id'
31+ _FPNV_TOKEN = 'fpnv_token_string'
32+ _EXP_TIMESTAMP = 2000000000
33+ _ISSUER = f'https://fpnv.googleapis.com/projects/{ _PROJECT_ID } '
34+ _JWKS_URL = 'https://fpnv.googleapis.com/v1beta/jwks'
35+ _PHONE_NUMBER = '+1234567890'
36+ _ISSUER_PREFIX = 'https://fpnv.googleapis.com/projects/'
37+ _PRIVATE_KEY = 'test-private-key' # In real tests, use a real RSA/EC private key
38+ _PUBLIC_KEY = 'test-public-key' # In real tests, use the corresponding public key
39+
40+ _MOCK_PAYLOAD = {
41+ 'iss' : _ISSUER ,
42+ 'sub' : '+1234567890' ,
43+ 'aud' : [_ISSUER ],
44+ 'exp' : _EXP_TIMESTAMP ,
45+ 'iat' : _EXP_TIMESTAMP - 3600 ,
46+ "other" : 'other'
47+ }
48+
49+
50+ @pytest .fixture
51+ def app ():
52+ cred = testutils .MockCredential ()
53+ return firebase_admin .initialize_app (cred , {'projectId' : _PROJECT_ID })
54+
55+
56+ @pytest .fixture
57+ def client (app ):
58+ return fpnv .client (app )
59+
60+
61+ class TestCommon :
62+ @classmethod
63+ def teardown_class (cls ):
64+ testutils .cleanup_apps ()
65+
66+
67+ class TestFpnvToken (TestCommon ):
68+ def test_properties (self ):
69+ token = fpnv .FpnvToken (_MOCK_PAYLOAD )
70+
71+ assert token .phone_number == _PHONE_NUMBER
72+ assert token .sub == _PHONE_NUMBER
73+ assert token .issuer == _ISSUER
74+ assert token .audience == [_ISSUER ]
75+ assert token .exp == _MOCK_PAYLOAD ['exp' ]
76+ assert token .iat == _MOCK_PAYLOAD ['iat' ]
77+ assert token .claims == _MOCK_PAYLOAD
78+ assert token ['other' ] == _MOCK_PAYLOAD ['other' ]
79+
80+
81+ class TestFpnvClient (TestCommon ):
82+
83+ def test_client_no_app (self ):
84+ with mock .patch ('firebase_admin._utils.get_app_service' ) as mock_get_service :
85+ fpnv .client ()
86+ mock_get_service .assert_called_once ()
87+ with pytest .raises (ValueError ):
88+ fpnv .client ()
89+
90+ def test_client (self , app ):
91+ client = fpnv .client (app )
92+ assert isinstance (client , fpnv .FpnvClient )
93+ assert client ._project_id == _PROJECT_ID
94+
95+ def test_requires_project_id (self ):
96+ cred = testutils .MockCredential ()
97+ # Create app without project ID
98+ app = firebase_admin .initialize_app (cred , name = 'no_project_id' )
99+ # Mock credential to not have project_id
100+ app .credential .get_credential ().project_id = None
101+
102+ with pytest .raises (ValueError , match = 'Project ID is required' ):
103+ fpnv .client (app )
104+
105+ def test_client_default_app (self ):
106+ client = fpnv .client ()
107+ assert isinstance (client , fpnv .FpnvClient )
108+
109+ def test_client_explicit_app (self ):
110+ cred = testutils .MockCredential ()
111+ app = firebase_admin .initialize_app (cred , {'projectId' : _PROJECT_ID }, name = 'custom' )
112+ client = fpnv .client (app )
113+ assert isinstance (client , fpnv .FpnvClient )
114+
115+
17116class TestVerifyToken :
18- pass
117+ @mock .patch ('jwt.PyJWKClient' )
118+ @mock .patch ('jwt.decode' )
119+ @mock .patch ('jwt.get_unverified_header' )
120+ def test_verify_token_success (self , mock_header , mock_decode , mock_jwks_cls , client ):
121+ token_str = 'valid.token.string'
122+ # Mock Header
123+ mock_header .return_value = {'kid' : 'key1' , 'typ' : 'JWT' , 'alg' : 'ES256' }
124+
125+ # Mock Signing Key
126+ mock_jwks_instance = mock_jwks_cls .return_value
127+ mock_signing_key = mock .Mock ()
128+ mock_signing_key .key = _PUBLIC_KEY
129+ mock_jwks_instance .get_signing_key_from_jwt .return_value = mock_signing_key
130+
131+ mock_decode .return_value = _MOCK_PAYLOAD
132+
133+ # Execute
134+ token = client .verify_token (token_str )
135+
136+ # Verify
137+ assert isinstance (token , fpnv .FpnvToken )
138+ assert token .phone_number == _PHONE_NUMBER
139+
140+ mock_header .assert_called_with (token_str )
141+ mock_jwks_instance .get_signing_key_from_jwt .assert_called_with (token_str )
142+ mock_decode .assert_called_with (
143+ token_str ,
144+ _PUBLIC_KEY ,
145+ algorithms = ['ES256' ],
146+ audience = _ISSUER ,
147+ issuer = _ISSUER
148+ )
149+
150+ @mock .patch ('jwt.get_unverified_header' )
151+ def test_verify_token_no_kid (self , mock_header , client ):
152+ mock_header .return_value = {'typ' : 'JWT' , 'alg' : 'ES256' } # Missing kid
153+ with pytest .raises (ValueError , match = "no 'kid' claim" ):
154+ client .verify_token ('token' )
155+
156+ @mock .patch ('jwt.get_unverified_header' )
157+ def test_verify_token_wrong_alg (self , mock_header , client ):
158+ mock_header .return_value = {'kid' : 'k' , 'typ' : 'JWT' , 'alg' : 'RS256' } # Wrong alg
159+ with pytest .raises (ValueError , match = "incorrect alg" ):
160+ client .verify_token ('token' )
161+
162+ @mock .patch ('jwt.PyJWKClient' )
163+ @mock .patch ('jwt.get_unverified_header' )
164+ def test_verify_token_jwk_error (self , mock_header , mock_jwks_cls , client ):
165+ mock_header .return_value = {'kid' : 'k' , 'typ' : 'JWT' , 'alg' : 'ES256' }
166+ mock_jwks_instance = mock_jwks_cls .return_value
167+ # Simulate Key not found or other PyJWKClient error
168+ mock_jwks_instance .get_signing_key_from_jwt .side_effect = jwt .PyJWKClientError ("Key not found" )
169+
170+ with pytest .raises (ValueError , match = "Verifying FPNV token failed" ):
171+ client .verify_token ('token' )
172+
173+ @mock .patch ('jwt.PyJWKClient' )
174+ @mock .patch ('jwt.decode' )
175+ @mock .patch ('jwt.get_unverified_header' )
176+ def test_verify_token_expired (self , mock_header , mock_decode , mock_jwks_cls , client ):
177+ mock_header .return_value = {'kid' : 'k' , 'typ' : 'JWT' , 'alg' : 'ES256' }
178+ mock_jwks_instance = mock_jwks_cls .return_value
179+ mock_jwks_instance .get_signing_key_from_jwt .return_value .key = _PUBLIC_KEY
180+
181+ # Simulate ExpiredSignatureError
182+ mock_decode .side_effect = jwt .ExpiredSignatureError ("Expired" )
183+
184+ with pytest .raises (ValueError , match = "token has expired" ):
185+ client .verify_token ('token' )
186+
187+ @mock .patch ('jwt.PyJWKClient' )
188+ @mock .patch ('jwt.decode' )
189+ @mock .patch ('jwt.get_unverified_header' )
190+ def test_verify_token_invalid_audience (self , mock_header , mock_decode , mock_jwks_cls , client ):
191+ mock_header .return_value = {'kid' : 'k' , 'typ' : 'JWT' , 'alg' : 'ES256' }
192+ mock_jwks_instance = mock_jwks_cls .return_value
193+ mock_jwks_instance .get_signing_key_from_jwt .return_value .key = _PUBLIC_KEY
194+
195+ # Simulate InvalidAudienceError
196+ mock_decode .side_effect = jwt .InvalidAudienceError ("Wrong Aud" )
197+
198+ with pytest .raises (ValueError , match = "incorrect \" aud\" " ):
199+ client .verify_token ('token' )
200+
201+ @mock .patch ('jwt.PyJWKClient' )
202+ @mock .patch ('jwt.decode' )
203+ @mock .patch ('jwt.get_unverified_header' )
204+ def test_verify_token_invalid_issuer (self , mock_header , mock_decode , mock_jwks_cls , client ):
205+ mock_header .return_value = {'kid' : 'k' , 'typ' : 'JWT' , 'alg' : 'ES256' }
206+ mock_jwks_instance = mock_jwks_cls .return_value
207+ mock_jwks_instance .get_signing_key_from_jwt .return_value .key = _PUBLIC_KEY
208+
209+ # Simulate InvalidIssuerError
210+ mock_decode .side_effect = jwt .InvalidIssuerError ("Wrong Iss" )
211+
212+ with pytest .raises (ValueError , match = "incorrect \" iss\" " ):
213+ client .verify_token ('token' )
0 commit comments