Skip to content

Commit 6ba3a04

Browse files
committed
feat: Add tests for email and sms blocking triggers, format code, solve cyclic import issue with event types
1 parent f9b8ae5 commit 6ba3a04

File tree

7 files changed

+141
-35
lines changed

7 files changed

+141
-35
lines changed

samples/identity/functions/main.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def beforeusercreated(
1111
event: identity_fn.AuthBlockingEvent
1212
) -> identity_fn.BeforeCreateResponse | None:
1313
print(event)
14-
if not event.data.email:
14+
if not event.data or not event.data.email:
1515
return None
1616
if "@cats.com" in event.data.email:
1717
return identity_fn.BeforeCreateResponse(display_name="Meow!",)
@@ -29,7 +29,7 @@ def beforeusersignedin(
2929
event: identity_fn.AuthBlockingEvent
3030
) -> identity_fn.BeforeSignInResponse | None:
3131
print(event)
32-
if not event.data.email:
32+
if not event.data or not event.data.email:
3333
return None
3434

3535
if "@cats.com" in event.data.email:

src/firebase_functions/firestore_fn.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,11 @@ def _firestore_endpoint_handler(
218218
auth_id=event_auth_id)
219219
func(database_event_with_auth_context)
220220
else:
221-
# mypy cannot infer that the event type is correct, hence the cast
222-
_typing.cast(_C1 | _C2, func)(database_event)
221+
# Split the casting into two separate branches based on event type
222+
if event_type in (_event_type_written, _event_type_updated):
223+
_typing.cast(_C1, func)(_typing.cast(_E1, database_event))
224+
else:
225+
_typing.cast(_C2, func)(_typing.cast(_E2, database_event))
223226

224227

225228
@_util.copy_func_kwargs(FirestoreOptions)

src/firebase_functions/identity_fn.py

+16-9
Original file line numberDiff line numberDiff line change
@@ -243,16 +243,19 @@ class Credential:
243243
sign_in_method: str
244244
"""The user's sign-in method."""
245245

246+
246247
EmailType = _typing.Literal["EMAIL_SIGN_IN", "PASSWORD_RESET"]
247-
SmsType = _typing.Literal["SIGN_IN_OR_SIGN_UP", "MULTI_FACTOR_SIGN_IN", "MULTI_FACTOR_ENROLLMENT"]
248+
SmsType = _typing.Literal["SIGN_IN_OR_SIGN_UP", "MULTI_FACTOR_SIGN_IN",
249+
"MULTI_FACTOR_ENROLLMENT"]
250+
248251

249252
@_dataclasses.dataclass(frozen=True)
250253
class AuthBlockingEvent:
251254
"""
252255
Defines an auth event for identitytoolkit v2 auth blocking events.
253256
"""
254257

255-
data: AuthUserRecord | None # This is None for beforeEmailSent and beforeSmsSent events
258+
data: AuthUserRecord | None # This is None for beforeEmailSent and beforeSmsSent events
256259
"""
257260
The UserRecord passed to auth blocking functions from the identity platform.
258261
"""
@@ -345,7 +348,7 @@ class BeforeEmailSentResponse(_typing.TypedDict, total=False):
345348
recaptcha_action_override: RecaptchaActionOptions | None
346349

347350

348-
class BeforeSmsSentResponse(BeforeEmailSentResponse, total=False):
351+
class BeforeSmsSentResponse(_typing.TypedDict, total=False):
349352
"""
350353
The handler response type for 'before_sms_sent' blocking events.
351354
"""
@@ -372,7 +375,7 @@ class BeforeSmsSentResponse(BeforeEmailSentResponse, total=False):
372375
"""
373376

374377
BeforeSmsSentCallable = _typing.Callable[[AuthBlockingEvent],
375-
BeforeSmsSentResponse | None]
378+
BeforeSmsSentResponse | None]
376379
"""
377380
The type of the callable for 'before_sms_sent' blocking events.
378381
"""
@@ -485,6 +488,7 @@ def before_user_created_wrapped(request: _Request) -> _Response:
485488

486489
return before_user_created_decorator
487490

491+
488492
@_util.copy_func_kwargs(_options.BlockingOptions)
489493
def before_email_sent(
490494
**kwargs,
@@ -499,20 +503,23 @@ def before_email_sent(
499503
from firebase_functions import identity_fn
500504
501505
@identity_fn.before_email_sent()
502-
def example(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeEmailSentResponse | None:
506+
def example(
507+
event: identity_fn.AuthBlockingEvent
508+
) -> identity_fn.BeforeEmailSentResponse | None:
503509
pass
504510
505511
:param \\*\\*kwargs: Options.
506512
:type \\*\\*kwargs: as :exc:`firebase_functions.options.BlockingOptions`
507513
:rtype: :exc:`typing.Callable`
508514
\\[ \\[ :exc:`firebase_functions.identity_fn.AuthBlockingEvent` \\],
509515
:exc:`firebase_functions.identity_fn.BeforeEmailSentResponse` \\| `None` \\]
510-
A function that takes a AuthBlockingEvent and optionally returns BeforeEmailSentResponse.
516+
A function that takes a AuthBlockingEvent and optionally returns
517+
BeforeEmailSentResponse.
511518
"""
512519
options = _options.BlockingOptions(**kwargs)
513520

514521
def before_email_sent_decorator(func: BeforeEmailSentCallable):
515-
from firebase_functions.private._identity_fn import event_type_before_email_sent
522+
from firebase_functions.private._identity_fn_event_types import event_type_before_email_sent
516523

517524
@_functools.wraps(func)
518525
def before_email_sent_wrapped(request: _Request) -> _Response:
@@ -566,7 +573,7 @@ def example(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeSmsSentRe
566573
options = _options.BlockingOptions(**kwargs)
567574

568575
def before_sms_sent_decorator(func: BeforeSmsSentCallable):
569-
from firebase_functions.private._identity_fn import event_type_before_sms_sent
576+
from firebase_functions.private._identity_fn_event_types import event_type_before_sms_sent
570577

571578
@_functools.wraps(func)
572579
def before_sms_sent_wrapped(request: _Request) -> _Response:
@@ -590,4 +597,4 @@ def before_sms_sent_wrapped(request: _Request) -> _Response:
590597
)
591598
return before_sms_sent_wrapped
592599

593-
return before_sms_sent_decorator
600+
return before_sms_sent_decorator

src/firebase_functions/options.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -994,17 +994,20 @@ def _endpoint(
994994
self,
995995
**kwargs,
996996
) -> _manifest.ManifestEndpoint:
997-
from firebase_functions.private._identity_fn import event_type_before_create, event_type_before_sign_in
997+
from firebase_functions.private._identity_fn_event_types import event_type_before_create, event_type_before_sign_in
998998

999999
assert kwargs["event_type"] is not None
10001000

10011001
blocking_trigger_options: _manifest.BlockingTriggerOptions
1002-
1003-
if kwargs["event_type"] == event_type_before_create or kwargs["event_type"] == event_type_before_sign_in:
1002+
1003+
if kwargs["event_type"] == event_type_before_create or kwargs[
1004+
"event_type"] == event_type_before_sign_in:
10041005
blocking_trigger_options = _manifest.BlockingTriggerOptions(
10051006
idToken=self.id_token if self.id_token is not None else False,
1006-
accessToken=self.access_token if self.access_token is not None else False,
1007-
refreshToken=self.refresh_token if self.refresh_token is not None else False,
1007+
accessToken=self.access_token
1008+
if self.access_token is not None else False,
1009+
refreshToken=self.refresh_token
1010+
if self.refresh_token is not None else False,
10081011
)
10091012
else:
10101013
blocking_trigger_options = _manifest.BlockingTriggerOptions()

src/firebase_functions/private/_identity_fn.py

+6-10
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
from firebase_functions.core import _with_init
2222
from firebase_functions.https_fn import HttpsError, FunctionsErrorCode
23+
from firebase_functions.private._identity_fn_event_types import event_type_before_create, event_type_before_sign_in
2324

2425
import firebase_functions.private.util as _util
2526
import firebase_functions.private.token_verifier as _token_verifier
@@ -202,11 +203,12 @@ def _credential_from_token_data(token_data: dict[str, _typing.Any],
202203
)
203204

204205

205-
def _auth_blocking_event_from_token_data(token_data: dict[str, _typing.Any], event_type: str):
206+
def _auth_blocking_event_from_token_data(token_data: dict[str, _typing.Any],
207+
event_type: str):
206208
from firebase_functions.identity_fn import AuthBlockingEvent, AuthUserRecord
207209

208210
data: AuthUserRecord | None = None
209-
if event_type == event_type_before_create or event_type == event_type_before_sign_in:
211+
if event_type in (event_type_before_create, event_type_before_sign_in):
210212
data = _auth_user_record_from_token_data(token_data["user_record"])
211213

212214
return AuthBlockingEvent(
@@ -223,12 +225,6 @@ def _auth_blocking_event_from_token_data(token_data: dict[str, _typing.Any], eve
223225
)
224226

225227

226-
event_type_before_create = "providers/cloud.auth/eventTypes/user.beforeCreate"
227-
event_type_before_sign_in = "providers/cloud.auth/eventTypes/user.beforeSignIn"
228-
event_type_before_email_sent = "providers/cloud.auth/eventTypes/user.beforeSendEmail"
229-
event_type_before_sms_sent = "providers/cloud.auth/eventTypes/user.beforeSendSms"
230-
231-
232228
def _validate_auth_response(
233229
event_type: str,
234230
auth_response,
@@ -363,8 +359,8 @@ def before_operation_handler(
363359
jwt_token = request.json["data"]["jwt"]
364360
decoded_token = _token_verifier.verify_auth_blocking_token(jwt_token)
365361
event = _auth_blocking_event_from_token_data(decoded_token, event_type)
366-
auth_response: BeforeCreateResponse | BeforeSignInResponse | BeforeEmailSentResponse | BeforeSmsSentResponse | None = _with_init(
367-
func)(event)
362+
auth_response: BeforeCreateResponse | BeforeSignInResponse | BeforeEmailSentResponse | \
363+
BeforeSmsSentResponse | None = _with_init(func)(event)
368364
if not auth_response:
369365
return _jsonify({})
370366
auth_response_dict = _validate_auth_response(event_type, auth_response)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""
2+
Identity function event types.
3+
"""
4+
5+
# We need to import these from the identity_fn module, but due to circular import
6+
# issues, we need to define them here.
7+
event_type_before_create = "providers/cloud.auth/eventTypes/user.beforeCreate"
8+
event_type_before_sign_in = "providers/cloud.auth/eventTypes/user.beforeSignIn"
9+
event_type_before_email_sent = "providers/cloud.auth/eventTypes/user.beforeSendEmail"
10+
event_type_before_sms_sent = "providers/cloud.auth/eventTypes/user.beforeSendSms"

tests/test_identity_fn.py

+94-7
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
from flask import Flask, Request
88
from werkzeug.test import EnvironBuilder
99

10-
from firebase_functions import core, identity_fn
11-
1210
token_verifier_mock = MagicMock()
1311
token_verifier_mock.verify_auth_blocking_token = Mock(
1412
return_value={
@@ -24,8 +22,14 @@
2422
"user_agent": "user_agent",
2523
"iat": 0
2624
})
25+
26+
firebase_admin_mock = MagicMock()
27+
firebase_admin_mock.initialize_app = Mock()
28+
firebase_admin_mock.get_app = Mock()
29+
2730
mocked_modules = {
2831
"firebase_functions.private.token_verifier": token_verifier_mock,
32+
"firebase_admin": firebase_admin_mock
2933
}
3034

3135

@@ -37,12 +41,14 @@ class TestIdentity(unittest.TestCase):
3741
def test_calls_init_function(self):
3842
hello = None
3943

40-
@core.init
41-
def init():
42-
nonlocal hello
43-
hello = "world"
44-
4544
with patch.dict("sys.modules", mocked_modules):
45+
from firebase_functions import core, identity_fn
46+
47+
@core.init
48+
def init():
49+
nonlocal hello
50+
hello = "world"
51+
4652
app = Flask(__name__)
4753

4854
func = Mock(__name__="example_func",
@@ -62,3 +68,84 @@ def init():
6268
decorated_func(request)
6369

6470
self.assertEqual("world", hello)
71+
72+
def test_auth_blocking_event_from_token_data_email(self):
73+
"""Test parsing a beforeSendEmail event."""
74+
# Mock token data for email event
75+
token_data = {
76+
"iss": "https://securetoken.google.com/project_id",
77+
"aud": "https://us-east1-project_id.cloudfunctions.net/function-1",
78+
"iat": 1, # Unix timestamp
79+
"exp": 60 * 60 + 1,
80+
"event_id": "EVENT_ID",
81+
"event_type": "beforeSendEmail",
82+
"user_agent": "USER_AGENT",
83+
"ip_address": "1.2.3.4",
84+
"locale": "en",
85+
"recaptcha_score": 0.9,
86+
"email_type": "PASSWORD_RESET",
87+
"email": "[email protected]"
88+
}
89+
90+
with patch.dict("sys.modules", mocked_modules):
91+
from firebase_functions.private._identity_fn import _auth_blocking_event_from_token_data
92+
from firebase_functions.private._identity_fn_event_types import event_type_before_email_sent
93+
import datetime
94+
95+
event = _auth_blocking_event_from_token_data(
96+
token_data, event_type_before_email_sent)
97+
98+
self.assertEqual(event.event_id, "EVENT_ID")
99+
self.assertEqual(event.ip_address, "1.2.3.4")
100+
self.assertEqual(event.user_agent, "USER_AGENT")
101+
self.assertEqual(event.locale, "en")
102+
self.assertEqual(event.email_type, "PASSWORD_RESET")
103+
self.assertEqual(event.sms_type, None)
104+
self.assertEqual(event.data, None) # No user record for email events
105+
self.assertEqual(event.timestamp, datetime.datetime.fromtimestamp(1))
106+
107+
self.assertEqual(event.additional_user_info.email, "[email protected]")
108+
self.assertEqual(event.additional_user_info.recaptcha_score, 0.9)
109+
self.assertEqual(event.additional_user_info.is_new_user, False)
110+
self.assertEqual(event.additional_user_info.phone_number, None)
111+
112+
def test_auth_blocking_event_from_token_data_sms(self):
113+
"""Test parsing a beforeSendSms event."""
114+
import datetime
115+
116+
token_data = {
117+
"iss": "https://securetoken.google.com/project_id",
118+
"aud": "https://us-east1-project_id.cloudfunctions.net/function-1",
119+
"iat": 1, # Unix timestamp
120+
"exp": 60 * 60 + 1,
121+
"event_id": "EVENT_ID",
122+
"event_type": "beforeSendSms",
123+
"user_agent": "USER_AGENT",
124+
"ip_address": "1.2.3.4",
125+
"locale": "en",
126+
"recaptcha_score": 0.9,
127+
"sms_type": "SIGN_IN_OR_SIGN_UP",
128+
"phone_number": "+11234567890"
129+
}
130+
131+
with patch.dict("sys.modules", mocked_modules):
132+
from firebase_functions.private._identity_fn import _auth_blocking_event_from_token_data
133+
from firebase_functions.private._identity_fn_event_types import event_type_before_sms_sent
134+
135+
event = _auth_blocking_event_from_token_data(
136+
token_data, event_type_before_sms_sent)
137+
138+
self.assertEqual(event.event_id, "EVENT_ID")
139+
self.assertEqual(event.ip_address, "1.2.3.4")
140+
self.assertEqual(event.user_agent, "USER_AGENT")
141+
self.assertEqual(event.locale, "en")
142+
self.assertEqual(event.email_type, None)
143+
self.assertEqual(event.sms_type, "SIGN_IN_OR_SIGN_UP")
144+
self.assertEqual(event.data, None) # No user record for SMS events
145+
self.assertEqual(event.timestamp, datetime.datetime.fromtimestamp(1))
146+
147+
self.assertEqual(event.additional_user_info.phone_number,
148+
"+11234567890")
149+
self.assertEqual(event.additional_user_info.recaptcha_score, 0.9)
150+
self.assertEqual(event.additional_user_info.is_new_user, False)
151+
self.assertEqual(event.additional_user_info.email, None)

0 commit comments

Comments
 (0)