Skip to content

Commit 8adb808

Browse files
committed
verify proper value type
1 parent 591b2db commit 8adb808

2 files changed

Lines changed: 93 additions & 15 deletions

File tree

functions-python/operations_api/src/feeds_operations/impl/user_feature_flags_impl.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,17 +217,21 @@ def put_user_feature_flags(
217217
# Validate all provided flag IDs exist before making any changes
218218
if flag_ids:
219219
existing_flags = (
220-
db_session.query(FeatureFlagORM.id)
220+
db_session.query(FeatureFlagORM.id, FeatureFlagORM.value_type)
221221
.filter(FeatureFlagORM.id.in_(flag_ids))
222222
.all()
223223
)
224-
found_ids = {row.id for row in existing_flags}
225-
missing = set(flag_ids) - found_ids
224+
value_type_by_id = {row.id: row.value_type for row in existing_flags}
225+
missing = set(flag_ids) - value_type_by_id.keys()
226226
if missing:
227227
raise HTTPException(
228228
status_code=404,
229229
detail=f"Feature flag(s) not found: {', '.join(sorted(missing))}",
230230
)
231+
# Verify each provided value matches the flag's declared value_type.
232+
for a in assignments:
233+
if a.value is not None:
234+
_validate_value_type(value_type_by_id[a.feature_flag_id], a.value)
231235

232236
# Replace: delete all existing assignments then insert the new set
233237
db_session.query(UserFeatureFlag).filter_by(user_id=user_id).delete()

functions-python/operations_api/tests/feeds_operations/impl/test_user_feature_flags.py

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -417,17 +417,6 @@ def setUp(self):
417417
self.api = UserFeatureFlagsApiImpl()
418418
self.session = MagicMock()
419419

420-
def _setup_flags_query(self, flag_ids):
421-
"""Make session.query(FeatureFlagORM.id).filter(...).all() return rows for given IDs."""
422-
mock_rows = [MagicMock(id=fid) for fid in flag_ids]
423-
mock_q = MagicMock()
424-
mock_q.filter.return_value = mock_q
425-
mock_q.all.return_value = mock_rows
426-
self.session.query.side_effect = lambda model: (
427-
mock_q if model is FeatureFlagORM.id else self.session.query(model)
428-
)
429-
return mock_q
430-
431420
def test_raises_404_when_user_not_found(self):
432421
_mock_query(self.session, None)
433422

@@ -440,7 +429,7 @@ def test_raises_404_when_user_not_found(self):
440429
def test_raises_404_when_flag_not_found(self):
441430
user = _make_user(user_feature_flags=[])
442431

443-
def query_side_effect(model):
432+
def query_side_effect(*args):
444433
mock_q = MagicMock()
445434
mock_q.options.return_value = mock_q
446435
mock_q.filter_by.return_value = mock_q
@@ -460,6 +449,91 @@ def query_side_effect(model):
460449
self.assertEqual(ctx.exception.status_code, 404)
461450
self.assertIn("nonexistent", ctx.exception.detail)
462451

452+
def _dispatch_put_queries(self, user, flag_meta_rows):
453+
"""Route session.query() calls for the put flow.
454+
455+
`flag_meta_rows` are the (id, value_type) rows returned for the
456+
flag-validation query.
457+
"""
458+
user_q = MagicMock()
459+
user_q.options.return_value = user_q
460+
user_q.filter_by.return_value = user_q
461+
user_q.first.return_value = user
462+
463+
meta_q = MagicMock()
464+
meta_q.filter.return_value = meta_q
465+
meta_q.all.return_value = flag_meta_rows
466+
467+
delete_q = MagicMock()
468+
delete_q.filter_by.return_value = delete_q
469+
470+
flags_q = MagicMock()
471+
flags_q.order_by.return_value = flags_q
472+
flags_q.all.return_value = []
473+
474+
def side_effect(*args):
475+
first = args[0]
476+
if first is AppUser:
477+
return user_q
478+
if first is FeatureFlagORM.id:
479+
return meta_q
480+
if first is UserFeatureFlag:
481+
return delete_q
482+
return flags_q
483+
484+
self.session.query.side_effect = side_effect
485+
return delete_q
486+
487+
def test_assigns_value_matching_flag_type(self):
488+
user = _make_user(user_feature_flags=[])
489+
rows = [MagicMock(id="beta_editor", value_type="boolean")]
490+
delete_q = self._dispatch_put_queries(user, rows)
491+
492+
req = PutUserFeatureFlagsRequest(
493+
assignments=[
494+
FeatureFlagAssignment(feature_flag_id="beta_editor", value=True)
495+
]
496+
)
497+
self.api.put_user_feature_flags("uid-1", req, db_session=self.session)
498+
499+
delete_q.delete.assert_called_once()
500+
added = self.session.add_all.call_args[0][0]
501+
self.assertEqual(len(added), 1)
502+
self.assertEqual(added[0].feature_flag_id, "beta_editor")
503+
self.assertEqual(added[0].value, True)
504+
505+
def test_raises_422_when_value_type_mismatch(self):
506+
user = _make_user(user_feature_flags=[])
507+
rows = [MagicMock(id="beta_editor", value_type="boolean")]
508+
self._dispatch_put_queries(user, rows)
509+
510+
req = PutUserFeatureFlagsRequest(
511+
assignments=[
512+
FeatureFlagAssignment(feature_flag_id="beta_editor", value="not-a-bool")
513+
]
514+
)
515+
with self.assertRaises(HTTPException) as ctx:
516+
self.api.put_user_feature_flags("uid-1", req, db_session=self.session)
517+
518+
self.assertEqual(ctx.exception.status_code, 422)
519+
self.session.add_all.assert_not_called()
520+
521+
def test_skips_type_validation_when_value_is_null(self):
522+
user = _make_user(user_feature_flags=[])
523+
rows = [MagicMock(id="beta_editor", value_type="boolean")]
524+
self._dispatch_put_queries(user, rows)
525+
526+
req = PutUserFeatureFlagsRequest(
527+
assignments=[
528+
FeatureFlagAssignment(feature_flag_id="beta_editor", value=None)
529+
]
530+
)
531+
self.api.put_user_feature_flags("uid-1", req, db_session=self.session)
532+
533+
added = self.session.add_all.call_args[0][0]
534+
self.assertEqual(len(added), 1)
535+
self.assertIsNone(added[0].value)
536+
463537
def test_clears_flags_when_empty_list_provided(self):
464538
flag = _make_flag()
465539
user = _make_user(user_feature_flags=[_make_uff(flag)])

0 commit comments

Comments
 (0)