from django.core.exceptions import ValidationError from django.test import TestCase from apps.decision.services import decide_for_flag from apps.flags.models import ( FeatureFlagType, validate_value_for_type, ) from apps.flags.selectors import feature_flag_get_by_key, feature_flag_list from apps.flags.services import ( feature_flag_create, feature_flag_update_default, ) from config.errors import ConflictError class ValidateValueForTypeTest(TestCase): def test_boolean_true_variants(self) -> None: for val in ("true", "True", "1", "yes", "YES"): self.assertEqual( validate_value_for_type(val, FeatureFlagType.BOOLEAN), "true" ) def test_boolean_false_variants(self) -> None: for val in ("false", "False", "0", "no", "NO"): self.assertEqual( validate_value_for_type(val, FeatureFlagType.BOOLEAN), "false" ) def test_boolean_invalid(self) -> None: with self.assertRaises(ValidationError): validate_value_for_type("maybe", FeatureFlagType.BOOLEAN) def test_integer_valid(self) -> None: self.assertEqual( validate_value_for_type("42", FeatureFlagType.INTEGER), "42" ) def test_integer_negative(self) -> None: self.assertEqual( validate_value_for_type("-7", FeatureFlagType.INTEGER), "-7" ) def test_integer_invalid(self) -> None: with self.assertRaises(ValidationError): validate_value_for_type("abc", FeatureFlagType.INTEGER) def test_integer_float(self) -> None: with self.assertRaises(ValidationError): validate_value_for_type("3.14", FeatureFlagType.INTEGER) def test_string_passthrough(self) -> None: self.assertEqual( validate_value_for_type("anything", FeatureFlagType.STRING), "anything", ) class FeatureFlagServiceTest(TestCase): def test_create_string_flag(self) -> None: flag = feature_flag_create( key="svc_string", name="Service String", value_type=FeatureFlagType.STRING, default_value="hello", ) self.assertEqual(flag.key, "svc_string") self.assertEqual(flag.value_type, FeatureFlagType.STRING) self.assertEqual(flag.default_value, "hello") def test_create_boolean_normalises(self) -> None: flag = feature_flag_create( key="svc_bool", name="Service Bool", value_type=FeatureFlagType.BOOLEAN, default_value="Yes", ) self.assertEqual(flag.default_value, "true") def test_create_integer_normalises(self) -> None: flag = feature_flag_create( key="svc_int", name="Service Int", value_type=FeatureFlagType.INTEGER, default_value="007", ) self.assertEqual(flag.default_value, "7") def test_create_invalid_type_raises(self) -> None: with self.assertRaises(ValidationError): feature_flag_create( key="svc_bad", name="Bad", value_type="float", default_value="1.0", ) def test_create_bool_invalid_value_raises(self) -> None: with self.assertRaises(ValidationError): feature_flag_create( key="svc_bad_bool", name="Bad Bool", value_type=FeatureFlagType.BOOLEAN, default_value="maybe", ) def test_create_int_invalid_value_raises(self) -> None: with self.assertRaises(ValidationError): feature_flag_create( key="svc_bad_int", name="Bad Int", value_type=FeatureFlagType.INTEGER, default_value="abc", ) def test_create_duplicate_key_raises(self) -> None: feature_flag_create( key="svc_dup", name="First", value_type=FeatureFlagType.STRING, default_value="a", ) with self.assertRaises(ConflictError): feature_flag_create( key="svc_dup", name="Second", value_type=FeatureFlagType.STRING, default_value="b", ) def test_update_default_value(self) -> None: flag = feature_flag_create( key="svc_upd", name="Update", value_type=FeatureFlagType.STRING, default_value="old", ) updated = feature_flag_update_default(flag=flag, default_value="new") self.assertEqual(updated.default_value, "new") flag.refresh_from_db() self.assertEqual(flag.default_value, "new") def test_update_bool_validates(self) -> None: flag = feature_flag_create( key="svc_upd_bool", name="Update Bool", value_type=FeatureFlagType.BOOLEAN, default_value="true", ) with self.assertRaises(ValidationError): feature_flag_update_default(flag=flag, default_value="maybe") def test_update_int_validates(self) -> None: flag = feature_flag_create( key="svc_upd_int", name="Update Int", value_type=FeatureFlagType.INTEGER, default_value="5", ) with self.assertRaises(ValidationError): feature_flag_update_default(flag=flag, default_value="bad") def test_update_default_invalidates_decide_flag_cache(self) -> None: flag = feature_flag_create( key="svc_upd_cache", name="Update Cache", value_type=FeatureFlagType.STRING, default_value="old", ) first = decide_for_flag(flag.key, "svc_subj_1", {}) self.assertEqual(first["value"], "old") feature_flag_update_default(flag=flag, default_value="new") second = decide_for_flag(flag.key, "svc_subj_2", {}) self.assertEqual(second["reason"], "no_active_experiment") self.assertEqual(second["value"], "new") class FeatureFlagSelectorTest(TestCase): def test_list_all(self) -> None: feature_flag_create( key="sel_a", name="A", value_type="string", default_value="a" ) feature_flag_create( key="sel_b", name="B", value_type="boolean", default_value="true" ) self.assertEqual(feature_flag_list().count(), 2) def test_list_filter_type(self) -> None: feature_flag_create( key="sel_ft_s", name="S", value_type="string", default_value="s", ) feature_flag_create( key="sel_ft_b", name="B", value_type="boolean", default_value="true", ) qs = feature_flag_list(value_type="boolean") self.assertEqual(qs.count(), 1) self.assertEqual(qs.first().key, "sel_ft_b") def test_list_search(self) -> None: feature_flag_create( key="sel_search_match", name="M", value_type="string", default_value="m", ) feature_flag_create( key="sel_other", name="O", value_type="string", default_value="o" ) qs = feature_flag_list(search="match") self.assertEqual(qs.count(), 1) def test_get_by_key(self) -> None: feature_flag_create( key="sel_get", name="G", value_type="string", default_value="g" ) flag = feature_flag_get_by_key("sel_get") self.assertIsNotNone(flag) self.assertEqual(flag.key, "sel_get") def test_get_by_key_not_found(self) -> None: flag = feature_flag_get_by_key("nonexistent") self.assertIsNone(flag)