@@ -1041,3 +1041,227 @@ def test_mixpanel_webhook__saas_free_plan__returns_403(
10411041
10421042 # Then
10431043 assert response .status_code == status .HTTP_403_FORBIDDEN
1044+
1045+
1046+ def test_webhook_add_members__csv_cohort__applies_memberships (
1047+ cohort : Cohort ,
1048+ ) -> None :
1049+ # Given
1050+ _ , plaintext = CohortSyncKey .objects .create_key (
1051+ name = "test key" , environment = cohort .environment
1052+ )
1053+ client = _authenticated_client (plaintext )
1054+ url = reverse ("api-v1:cohort-sync:webhook-add" , kwargs = {"pk" : str (cohort .uuid )})
1055+
1056+ # When
1057+ response = client .post (
1058+ url , data = {"identifiers" : ["user-1" , "user-2" ]}, format = "json"
1059+ )
1060+
1061+ # Then
1062+ assert response .status_code == status .HTTP_200_OK
1063+ assert sorted (
1064+ CohortMembership .objects .filter (cohort = cohort ).values_list (
1065+ "identifier" , "state"
1066+ )
1067+ ) == [
1068+ ("user-1" , CohortMembershipState .APPLIED ),
1069+ ("user-2" , CohortMembershipState .APPLIED ),
1070+ ]
1071+ identity = Identity .objects .get (environment = cohort .environment , identifier = "user-1" )
1072+ assert identity .system_traits == {cohort .system_trait_key : True }
1073+
1074+
1075+ def test_webhook_remove_members__applied_member__removes_membership_and_trait (
1076+ cohort : Cohort ,
1077+ ) -> None :
1078+ # Given
1079+ Identity .objects .create (
1080+ environment = cohort .environment ,
1081+ identifier = "member" ,
1082+ system_traits = {cohort .system_trait_key : True },
1083+ )
1084+ CohortMembership .objects .create (
1085+ cohort = cohort , identifier = "member" , state = CohortMembershipState .APPLIED
1086+ )
1087+ _ , plaintext = CohortSyncKey .objects .create_key (
1088+ name = "test key" , environment = cohort .environment
1089+ )
1090+ client = _authenticated_client (plaintext )
1091+ url = reverse ("api-v1:cohort-sync:webhook-remove" , kwargs = {"pk" : str (cohort .uuid )})
1092+
1093+ # When
1094+ response = client .post (url , data = {"identifiers" : ["member" ]}, format = "json" )
1095+
1096+ # Then
1097+ assert response .status_code == status .HTTP_200_OK
1098+ assert not CohortMembership .objects .filter (cohort = cohort ).exists ()
1099+ identity = Identity .objects .get (environment = cohort .environment , identifier = "member" )
1100+ assert identity .system_traits == {}
1101+
1102+
1103+ def test_webhook_add_members__non_csv_cohort__returns_404 (
1104+ cohort_sync_key : _KeyAndPlaintext ,
1105+ amplitude_cohort : Cohort ,
1106+ ) -> None :
1107+ # Given
1108+ _ , plaintext = cohort_sync_key
1109+ client = _authenticated_client (plaintext )
1110+ url = reverse (
1111+ "api-v1:cohort-sync:webhook-add" , kwargs = {"pk" : str (amplitude_cohort .uuid )}
1112+ )
1113+
1114+ # When
1115+ response = client .post (url , data = {"identifiers" : ["user-1" ]}, format = "json" )
1116+
1117+ # Then
1118+ assert response .status_code == status .HTTP_404_NOT_FOUND
1119+
1120+
1121+ def test_webhook_add_members__other_environment_key__returns_404 (
1122+ cohort : Cohort ,
1123+ ) -> None :
1124+ # Given - a key scoped to a different environment than the cohort's
1125+ other_environment = Environment .objects .create (
1126+ name = "Other environment" , project = cohort .environment .project
1127+ )
1128+ _ , plaintext = CohortSyncKey .objects .create_key (
1129+ name = "other key" , environment = other_environment
1130+ )
1131+ client = _authenticated_client (plaintext )
1132+ url = reverse ("api-v1:cohort-sync:webhook-add" , kwargs = {"pk" : str (cohort .uuid )})
1133+
1134+ # When
1135+ response = client .post (url , data = {"identifiers" : ["user-1" ]}, format = "json" )
1136+
1137+ # Then
1138+ assert response .status_code == status .HTTP_404_NOT_FOUND
1139+ assert not CohortMembership .objects .exists ()
1140+
1141+
1142+ def test_webhook_add_members__identifier_over_1024_bytes__returns_400 (
1143+ cohort : Cohort ,
1144+ ) -> None :
1145+ # Given - 512 three-byte characters: few characters, too many bytes
1146+ multibyte_identifier = "€" * 512
1147+ _ , plaintext = CohortSyncKey .objects .create_key (
1148+ name = "test key" , environment = cohort .environment
1149+ )
1150+ client = _authenticated_client (plaintext )
1151+ url = reverse ("api-v1:cohort-sync:webhook-add" , kwargs = {"pk" : str (cohort .uuid )})
1152+
1153+ # When
1154+ response = client .post (
1155+ url , data = {"identifiers" : [multibyte_identifier ]}, format = "json"
1156+ )
1157+
1158+ # Then
1159+ assert response .status_code == status .HTTP_400_BAD_REQUEST
1160+ assert "1024 bytes" in str (response .json ())
1161+ assert not CohortMembership .objects .exists ()
1162+
1163+
1164+ def test_webhook_add_members__over_10000_identifiers__returns_400 (
1165+ cohort : Cohort ,
1166+ ) -> None :
1167+ # Given
1168+ _ , plaintext = CohortSyncKey .objects .create_key (
1169+ name = "test key" , environment = cohort .environment
1170+ )
1171+ client = _authenticated_client (plaintext )
1172+ url = reverse ("api-v1:cohort-sync:webhook-add" , kwargs = {"pk" : str (cohort .uuid )})
1173+
1174+ # When
1175+ response = client .post (
1176+ url ,
1177+ data = {"identifiers" : [f"user-{ i } " for i in range (10001 )]},
1178+ format = "json" ,
1179+ )
1180+
1181+ # Then
1182+ assert response .status_code == status .HTTP_400_BAD_REQUEST
1183+ assert not CohortMembership .objects .exists ()
1184+
1185+
1186+ def test_webhook_add_members__empty_identifiers__returns_400 (
1187+ cohort : Cohort ,
1188+ ) -> None :
1189+ # Given
1190+ _ , plaintext = CohortSyncKey .objects .create_key (
1191+ name = "test key" , environment = cohort .environment
1192+ )
1193+ client = _authenticated_client (plaintext )
1194+ url = reverse ("api-v1:cohort-sync:webhook-add" , kwargs = {"pk" : str (cohort .uuid )})
1195+
1196+ # When
1197+ response = client .post (url , data = {"identifiers" : []}, format = "json" )
1198+
1199+ # Then
1200+ assert response .status_code == status .HTTP_400_BAD_REQUEST
1201+
1202+
1203+ def test_webhook_add_members__malformed_uuid__returns_404 (
1204+ cohort : Cohort ,
1205+ ) -> None :
1206+ # Given
1207+ _ , plaintext = CohortSyncKey .objects .create_key (
1208+ name = "test key" , environment = cohort .environment
1209+ )
1210+ client = _authenticated_client (plaintext )
1211+ url = reverse ("api-v1:cohort-sync:webhook-add" , kwargs = {"pk" : "not-a-uuid" })
1212+
1213+ # When
1214+ response = client .post (url , data = {"identifiers" : ["user-1" ]}, format = "json" )
1215+
1216+ # Then
1217+ assert response .status_code == status .HTTP_404_NOT_FOUND
1218+
1219+
1220+ def test_webhook_add_members__deletion_requested_cohort__returns_404 (
1221+ cohort : Cohort ,
1222+ ) -> None :
1223+ # Given
1224+ cohort .deletion_requested_at = timezone .now ()
1225+ cohort .save ()
1226+ _ , plaintext = CohortSyncKey .objects .create_key (
1227+ name = "test key" , environment = cohort .environment
1228+ )
1229+ client = _authenticated_client (plaintext )
1230+ url = reverse ("api-v1:cohort-sync:webhook-add" , kwargs = {"pk" : str (cohort .uuid )})
1231+
1232+ # When
1233+ response = client .post (url , data = {"identifiers" : ["user-1" ]}, format = "json" )
1234+
1235+ # Then
1236+ assert response .status_code == status .HTTP_404_NOT_FOUND
1237+
1238+
1239+ def test_webhook_add_members__missing_credentials__returns_401 (
1240+ cohort : Cohort ,
1241+ ) -> None :
1242+ # Given
1243+ url = reverse ("api-v1:cohort-sync:webhook-add" , kwargs = {"pk" : str (cohort .uuid )})
1244+
1245+ # When
1246+ response = APIClient ().post (url , data = {"identifiers" : ["user-1" ]}, format = "json" )
1247+
1248+ # Then
1249+ assert response .status_code == status .HTTP_401_UNAUTHORIZED
1250+
1251+
1252+ @pytest .mark .saas_mode
1253+ def test_webhook_add_members__saas_free_plan__returns_403 (
1254+ cohort : Cohort ,
1255+ ) -> None :
1256+ # Given
1257+ _ , plaintext = CohortSyncKey .objects .create_key (
1258+ name = "test key" , environment = cohort .environment
1259+ )
1260+ client = _authenticated_client (plaintext )
1261+ url = reverse ("api-v1:cohort-sync:webhook-add" , kwargs = {"pk" : str (cohort .uuid )})
1262+
1263+ # When
1264+ response = client .post (url , data = {"identifiers" : ["user-1" ]}, format = "json" )
1265+
1266+ # Then
1267+ assert response .status_code == status .HTTP_403_FORBIDDEN
0 commit comments