1515"""Test cases for the firebase_admin.app_check module."""
1616import base64
1717import pytest
18+ import requests
1819
1920from jwt import PyJWK , InvalidAudienceError , InvalidIssuerError
2021from jwt import ExpiredSignatureError , InvalidSignatureError
2122import firebase_admin
22- from firebase_admin import app_check
23+ from firebase_admin import app_check , exceptions
2324from tests import testutils
2425
2526NON_STRING_ARGS = [[], tuple (), {}, True , False , 1 , 0 ]
@@ -58,6 +59,15 @@ def setup_class(cls):
5859 def teardown_class (cls ):
5960 testutils .cleanup_apps ()
6061
62+
63+ @pytest .fixture
64+ def mock_jwt (mocker ):
65+ """Mocks JWT decoding and JWKS key retrieval for a valid App Check token."""
66+ mocker .patch ("jwt.decode" , return_value = JWT_PAYLOAD_SAMPLE )
67+ mocker .patch ("jwt.PyJWKClient.get_signing_key_from_jwt" , return_value = PyJWK (signing_key ))
68+ mocker .patch ("jwt.get_unverified_header" , return_value = JWT_PAYLOAD_SAMPLE .get ("headers" ))
69+
70+
6171class TestVerifyToken (TestBatch ):
6272
6373 def test_no_project_id (self ):
@@ -74,6 +84,12 @@ def test_verify_token_with_non_string_raises_error(self, token):
7484 expected = f'app check token "{ token } " must be a string.'
7585 assert str (excinfo .value ) == expected
7686
87+ @pytest .mark .parametrize ('consume' , [[], tuple (), {}, 1 , 0 , 'true' , 'false' , None ])
88+ def test_verify_token_with_non_boolean_consume_raises_error (self , consume ):
89+ with pytest .raises (ValueError ) as excinfo :
90+ app_check .verify_token ('app_check_token' , consume = consume )
91+ assert str (excinfo .value ) == 'consume must be a boolean.'
92+
7793 def test_has_valid_token_headers (self ):
7894 app = firebase_admin .get_app ()
7995 app_check_service = app_check ._get_app_check_service (app )
@@ -221,10 +237,8 @@ def test_decode_and_verify_with_non_string_sub_raises_error(self, mocker):
221237 f'"{ sub_number } " must be a string.' )
222238 assert str (excinfo .value ) == expected
223239
224- def test_verify_token (self , mocker ):
225- mocker .patch ("jwt.decode" , return_value = JWT_PAYLOAD_SAMPLE )
226- mocker .patch ("jwt.PyJWKClient.get_signing_key_from_jwt" , return_value = PyJWK (signing_key ))
227- mocker .patch ("jwt.get_unverified_header" , return_value = JWT_PAYLOAD_SAMPLE .get ("headers" ))
240+ @pytest .mark .usefixtures ("mock_jwt" )
241+ def test_verify_token (self ):
228242 app = firebase_admin .get_app ()
229243
230244 payload = app_check .verify_token ("encoded" , app )
@@ -273,3 +287,120 @@ def test_verify_token_with_incorrect_issuer_raises_error(self, mocker):
273287
274288 expected = 'Token does not contain the correct "iss" (issuer).'
275289 assert str (excinfo .value ) == expected
290+
291+ @pytest .mark .usefixtures ("mock_jwt" )
292+ @pytest .mark .parametrize ('consume_kwargs' , [{}, {'consume' : False }])
293+ def test_verify_token_with_consume_false_makes_no_http_call (
294+ self , mocker , consume_kwargs
295+ ):
296+ app = firebase_admin .get_app ()
297+ app_check_service = app_check ._get_app_check_service (app )
298+ mock_body = mocker .patch .object (app_check_service ._http_client , "body" )
299+
300+ payload = app_check .verify_token ("encoded" , app = app , ** consume_kwargs )
301+ expected = JWT_PAYLOAD_SAMPLE .copy ()
302+ expected ["app_id" ] = APP_ID
303+ assert payload == expected
304+ assert 'already_consumed' not in payload
305+ mock_body .assert_not_called ()
306+
307+ @pytest .mark .usefixtures ("mock_jwt" )
308+ def test_verify_token_with_consume_true_not_consumed (self , mocker ):
309+ app = firebase_admin .get_app ()
310+ app_check_service = app_check ._get_app_check_service (app )
311+ mock_body = mocker .patch .object (
312+ app_check_service ._http_client , "body" , return_value = {"alreadyConsumed" : False }
313+ )
314+
315+ payload = app_check .verify_token ("encoded" , app = app , consume = True )
316+ expected = JWT_PAYLOAD_SAMPLE .copy ()
317+ expected ["app_id" ] = APP_ID
318+ expected ["already_consumed" ] = False
319+ assert payload == expected
320+
321+ expected_url = (
322+ f"https://firebaseappcheck.googleapis.com/v1/projects/{ PROJECT_ID } :verifyAppCheckToken"
323+ )
324+ mock_body .assert_called_once_with (
325+ "post" , expected_url , json = {"app_check_token" : "encoded" }
326+ )
327+
328+ @pytest .mark .usefixtures ("mock_jwt" )
329+ def test_verify_token_with_consume_true_already_consumed (self , mocker ):
330+ app = firebase_admin .get_app ()
331+ app_check_service = app_check ._get_app_check_service (app )
332+ mock_body = mocker .patch .object (
333+ app_check_service ._http_client , "body" , return_value = {"alreadyConsumed" : True }
334+ )
335+
336+ payload = app_check .verify_token ("encoded" , app = app , consume = True )
337+ expected = JWT_PAYLOAD_SAMPLE .copy ()
338+ expected ["app_id" ] = APP_ID
339+ expected ["already_consumed" ] = True
340+ assert payload == expected
341+
342+ expected_url = (
343+ f"https://firebaseappcheck.googleapis.com/v1/projects/{ PROJECT_ID } :verifyAppCheckToken"
344+ )
345+ mock_body .assert_called_once_with (
346+ "post" , expected_url , json = {"app_check_token" : "encoded" }
347+ )
348+
349+ @pytest .mark .usefixtures ("mock_jwt" )
350+ def test_verify_token_with_consume_true_backend_error (self , mocker ):
351+ app = firebase_admin .get_app ()
352+ app_check_service = app_check ._get_app_check_service (app )
353+
354+ req_exc = requests .exceptions .RequestException ("Backend error" )
355+ mocker .patch .object (app_check_service ._http_client , "body" , side_effect = req_exc )
356+
357+ with pytest .raises (exceptions .UnknownError ) as excinfo :
358+ app_check .verify_token ("encoded" , app = app , consume = True )
359+ assert "Unknown error while making a remote service call" in str (excinfo .value )
360+
361+ @pytest .mark .usefixtures ("mock_jwt" )
362+ def test_verify_token_with_consume_true_http_error (self , mocker ):
363+ app = firebase_admin .get_app ()
364+ app_check_service = app_check ._get_app_check_service (app )
365+
366+ response = requests .Response ()
367+ response .status_code = 403
368+ response ._content = (
369+ b'{"error": {"status": "PERMISSION_DENIED", "message": "Permission denied."}}'
370+ )
371+ http_exc = requests .exceptions .HTTPError (response = response )
372+ mocker .patch .object (app_check_service ._http_client , "body" , side_effect = http_exc )
373+
374+ with pytest .raises (exceptions .PermissionDeniedError ) as excinfo :
375+ app_check .verify_token ("encoded" , app = app , consume = True )
376+ assert "Permission denied." in str (excinfo .value )
377+
378+ @pytest .mark .usefixtures ("mock_jwt" )
379+ @pytest .mark .parametrize ('malformed_body' , ['string_response' , [1 , 2 ], 123 , None ])
380+ def test_verify_token_with_consume_true_malformed_response_raises_error (
381+ self , mocker , malformed_body
382+ ):
383+ app = firebase_admin .get_app ()
384+ app_check_service = app_check ._get_app_check_service (app )
385+ mocker .patch .object (app_check_service ._http_client , "body" , return_value = malformed_body )
386+
387+ with pytest .raises (exceptions .UnknownError ) as excinfo :
388+ app_check .verify_token ("encoded" , app = app , consume = True )
389+ assert 'Unexpected response from App Check service' in str (excinfo .value )
390+
391+ @pytest .mark .usefixtures ("mock_jwt" )
392+ def test_verify_token_with_consume_true_json_decode_error (self , mocker ):
393+ app = firebase_admin .get_app ()
394+ app_check_service = app_check ._get_app_check_service (app )
395+ mocker .patch .object (
396+ app_check_service ._http_client ,
397+ "body" ,
398+ side_effect = ValueError ("Expecting value: line 1 column 1 (char 0)" ),
399+ )
400+
401+ with pytest .raises (exceptions .UnknownError ) as excinfo :
402+ app_check .verify_token ("encoded" , app = app , consume = True )
403+ expected_msg = (
404+ "Unexpected response from App Check service: Expecting value: line 1 column 1 (char 0)"
405+ )
406+ assert expected_msg in str (excinfo .value )
0 commit comments