Skip to content

Commit 7ebc652

Browse files
committed
chore: add tests
1 parent c22cb8c commit 7ebc652

1 file changed

Lines changed: 196 additions & 1 deletion

File tree

tests/test_fpnv.py

Lines changed: 196 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,200 @@
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+
17116
class 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

Comments
 (0)