def test_delete_pending_approval_request_on_revise(self): recipe = RecipeFactory(name="old") approval = ApprovalRequestFactory(revision=recipe.latest_revision) recipe.revise(name="new") with pytest.raises(ApprovalRequest.DoesNotExist): ApprovalRequest.objects.get(pk=approval.pk)
def test_only_signed_when_approved_and_enabled(self, mocked_autograph): sign_data_mock = mocked_autograph.return_value.sign_data # This uses the signer, so do it first action = ActionFactory() sign_data_mock.reset_mock() sign_data_mock.side_effect = Exception("Can't sign yet") recipe = RecipeFactory(name="unchanged", action=action) assert not recipe.enabled assert not recipe.is_approved assert recipe.signature is None # Updating does not generate a signature recipe.revise(name="changed") assert recipe.signature is None # Approving does not sign the recipe rev = recipe.latest_revision approval_request = rev.request_approval(UserFactory()) approval_request.approve(UserFactory(), "r+") recipe.refresh_from_db() assert recipe.signature is None mocked_autograph.return_value.sign_data.assert_not_called() # Enabling signs the recipe mocked_autograph.return_value.sign_data.side_effect = fake_sign rev.enable(UserFactory()) recipe.refresh_from_db() expected_sig = fake_sign([recipe.canonical_json()])[0]["signature"] assert recipe.signature.signature == expected_sig assert mocked_autograph.return_value.sign_data.called_once()
def test_delete_pending_approval_request_on_revise(self): recipe = RecipeFactory(name="old") approval = ApprovalRequestFactory(revision=recipe.latest_revision) recipe.revise(name="new") with pytest.raises(ApprovalRequest.DoesNotExist): ApprovalRequest.objects.get(pk=approval.pk)
def test_only_signed_when_approved_and_enabled(self, mocked_autograph): sign_data_mock = mocked_autograph.return_value.sign_data # This uses the signer, so do it first action = ActionFactory() sign_data_mock.reset_mock() sign_data_mock.side_effect = Exception("Can't sign yet") recipe = RecipeFactory(name="unchanged", action=action) assert not recipe.enabled assert not recipe.is_approved assert recipe.signature is None # Updating does not generate a signature recipe.revise(name="changed") assert recipe.signature is None # Approving does not sign the recipe rev = recipe.latest_revision approval_request = rev.request_approval(UserFactory()) approval_request.approve(UserFactory(), "r+") recipe.refresh_from_db() assert recipe.signature is None mocked_autograph.return_value.sign_data.assert_not_called() # Enabling signs the recipe mocked_autograph.return_value.sign_data.side_effect = fake_sign rev.enable(UserFactory()) recipe.refresh_from_db() expected_sig = fake_sign([recipe.canonical_json()])[0]["signature"] assert recipe.signature.signature == expected_sig assert mocked_autograph.return_value.sign_data.called_once()
def test_enabled_state_carried_over_on_approval(self): recipe = RecipeFactory(approver=UserFactory(), enabler=UserFactory()) carryover_from = recipe.approved_revision.enabled_state recipe.revise(name="New name") approval_request = recipe.latest_revision.request_approval(UserFactory()) approval_request.approve(UserFactory(), "r+") assert recipe.enabled assert recipe.approved_revision.enabled_state.carryover_from == carryover_from
def test_enabled_state_carried_over_on_approval(self): recipe = RecipeFactory(approver=UserFactory(), enabler=UserFactory()) carryover_from = recipe.approved_revision.enabled_state recipe.revise(name="New name") approval_request = recipe.latest_revision.request_approval(UserFactory()) approval_request.approve(UserFactory(), "r+") assert recipe.approved_revision.enabled assert recipe.approved_revision.enabled_state.carryover_from == carryover_from
def test_recipe_doesnt_revise_when_clean(self): recipe = RecipeFactory(name="my name") revision_id = recipe.revision_id last_updated = recipe.last_updated recipe.revise(name="my name") assert revision_id == recipe.revision_id assert last_updated == recipe.last_updated
def test_can_delete_extensions_no_longer_in_use(self, api_client, storage): xpi = WebExtensionFileFactory() e = ExtensionFactory(xpi__from_func=xpi.open) a = ActionFactory(name="opt-out-study") r = RecipeFactory(action=a, arguments={"extensionId": e.id}) r.revise(arguments=OptOutStudyArgumentsFactory(extensionId=e.id + 1)) res = api_client.delete(f"/api/v3/extension/{e.id}/") assert res.status_code == 204 assert Extension.objects.count() == 0
def test_recipe_doesnt_revise_when_clean(self): recipe = RecipeFactory(name="my name") revision_id = recipe.revision_id last_updated = recipe.last_updated recipe.revise(name="my name") assert revision_id == recipe.revision_id assert last_updated == recipe.last_updated
def test_can_update_extensions_no_longer_in_use(self, api_client, storage): xpi = WebExtensionFileFactory() e = ExtensionFactory(xpi__from_func=xpi.open) a = ActionFactory(name="opt-out-study") r = RecipeFactory(action=a, arguments={"extensionId": e.id}) r.revise(arguments={"extensionId": 0}) res = api_client.patch(f"/api/v3/extension/{e.id}/", {"name": "new name"}) assert res.status_code == 200 assert res.data["name"] == "new name"
def test_can_delete_extensions_no_longer_in_use(self, api_client, storage): xpi = WebExtensionFileFactory() e = ExtensionFactory(xpi__from_func=xpi.open) a = ActionFactory(name="opt-out-study") r = RecipeFactory(action=a, arguments={"extensionId": e.id}) r.revise(arguments={"extensionId": 0}) res = api_client.delete(f"/api/v3/extension/{e.id}/") assert res.status_code == 204 assert Extension.objects.count() == 0
def test_can_update_extensions_no_longer_in_use(self, api_client, storage): xpi = WebExtensionFileFactory() e = ExtensionFactory(xpi__from_func=xpi.open) a = ActionFactory(name="opt-out-study") r = RecipeFactory(action=a, arguments={"extensionId": e.id}) r.revise(arguments={"extensionId": 0}) res = api_client.patch(f"/api/v3/extension/{e.id}/", {"name": "new name"}) assert res.status_code == 200 assert res.data["name"] == "new name"
def test_recipe_doesnt_revise_when_clean(self): channel = ChannelFactory() recipe = RecipeFactory(name='my name', channels=[channel]) revision_id = recipe.revision_id last_updated = recipe.last_updated recipe.revise(name='my name', channels=[channel]) assert revision_id == recipe.revision_id assert last_updated == recipe.last_updated
def test_history(self, api_client): recipe = RecipeFactory(name='version 1') recipe.revise(name='version 2') recipe.revise(name='version 3') res = api_client.get('/api/v1/recipe/%s/history/' % recipe.id) assert res.data[0]['recipe']['name'] == 'version 3' assert res.data[1]['recipe']['name'] == 'version 2' assert res.data[2]['recipe']['name'] == 'version 1'
def test_history(self, api_client): recipe = RecipeFactory(name="version 1") recipe.revise(name="version 2") recipe.revise(name="version 3") res = api_client.get("/api/v1/recipe/%s/history/" % recipe.id) assert res.data[0]["recipe"]["name"] == "version 3" assert res.data[1]["recipe"]["name"] == "version 2" assert res.data[2]["recipe"]["name"] == "version 1"
def test_signature_is_cleared_if_autograph_unavailable(self, mocker): # Mock the Autographer to return an error mock_autograph = mocker.patch("normandy.recipes.models.Autographer") mock_autograph.side_effect = ImproperlyConfigured recipe = RecipeFactory(name="unchanged", signed=True) original_signature = recipe.signature recipe.revise(name="changed") assert recipe.name == "changed" assert recipe.signature is not original_signature assert recipe.signature is None
def test_unique_name_update_collision(self): action = ActionFactory(name="opt-out-study") arguments_a = {"name": "foo"} arguments_b = {"name": "bar"} RecipeFactory(action=action, arguments=arguments_a) recipe = RecipeFactory(action=action, arguments=arguments_b) with pytest.raises(serializers.ValidationError) as exc_info1: recipe.revise(arguments=arguments_a) error = action.errors["duplicate_study_name"] assert exc_info1.value.detail == {"arguments": {"name": error}}
def test_unique_name_update_collision(self): action = ActionFactory(name="opt-out-study") arguments_a = {"name": "foo"} arguments_b = {"name": "bar"} RecipeFactory(action=action, arguments=arguments_a) recipe = RecipeFactory(action=action, arguments=arguments_b) with pytest.raises(serializers.ValidationError) as exc_info1: recipe.revise(arguments=arguments_a) error = action.errors["duplicate_study_name"] assert exc_info1.value.detail == {"arguments": {"name": error}}
def test_signature_is_updated_if_autograph_available(self, mocked_autograph): recipe = RecipeFactory(name="unchanged", approver=UserFactory(), enabler=UserFactory()) original_signature = recipe.signature assert original_signature is not None recipe.revise(name="changed") assert recipe.latest_revision.name == "changed" assert recipe.signature is not original_signature expected_sig = fake_sign([recipe.canonical_json()])[0]["signature"] assert recipe.signature.signature == expected_sig
def test_signature_is_updated_if_autograph_available(self, mocked_autograph): recipe = RecipeFactory(name="unchanged", approver=UserFactory(), enabler=UserFactory()) original_signature = recipe.signature assert original_signature is not None recipe.revise(name="changed") assert recipe.latest_revision.name == "changed" assert recipe.signature is not original_signature expected_sig = fake_sign([recipe.canonical_json()])[0]["signature"] assert recipe.signature.signature == expected_sig
def test_signature_is_cleared_if_autograph_unavailable(self, mocker): # Mock the Autographer to return an error mock_autograph = mocker.patch("normandy.recipes.models.Autographer") mock_autograph.side_effect = ImproperlyConfigured recipe = RecipeFactory(name="unchanged", signed=True) original_signature = recipe.signature recipe.revise(name="changed") assert recipe.name == "changed" assert recipe.signature is not original_signature assert recipe.signature is None
def test_recipe_is_approved(self): recipe = RecipeFactory(name="old") assert not recipe.is_approved approval = ApprovalRequestFactory(revision=recipe.latest_revision) approval.approve(UserFactory(), "r+") assert recipe.is_approved assert recipe.approved_revision == recipe.latest_revision recipe.revise(name="new") assert recipe.is_approved assert recipe.approved_revision != recipe.latest_revision
def test_signature_is_updated_if_autograph_available( self, mocked_autograph): recipe = RecipeFactory(name='unchanged') original_signature = recipe.signature assert original_signature is not None recipe.revise(name='changed') assert recipe.name == 'changed' assert recipe.signature is not original_signature expected_sig = hashlib.sha256(recipe.canonical_json()).hexdigest() assert recipe.signature.signature == expected_sig
def test_recipe_is_approved(self): recipe = RecipeFactory(name="old") assert not recipe.is_approved approval = ApprovalRequestFactory(revision=recipe.latest_revision) approval.approve(UserFactory(), "r+") assert recipe.is_approved assert recipe.approved_revision == recipe.latest_revision recipe.revise(name="new") assert recipe.is_approved assert recipe.approved_revision != recipe.latest_revision
def test_it_publishes_new_revisions_if_enabled(self, mocked_remotesettings): recipe = RecipeFactory(name="Test", approver=UserFactory(), enabler=UserFactory()) assert mocked_remotesettings.return_value.publish.call_count == 1 recipe.revise(name="Modified") approval_request = recipe.latest_revision.request_approval(creator=UserFactory()) approval_request.approve(approver=UserFactory(), comment="r+") assert mocked_remotesettings.return_value.publish.call_count == 2 second_call_args, _ = mocked_remotesettings.return_value.publish.call_args_list[1] (modified_recipe,) = second_call_args assert modified_recipe.latest_revision.name == "Modified"
def test_unique_experiment_slug_update_collision(self): action = ActionFactory(name="preference-experiment") arguments_a = {"slug": "a", "branches": []} arguments_b = {"slug": "b", "branches": []} # Does not throw when saving revisions RecipeFactory(action=action, arguments=arguments_a) recipe = RecipeFactory(action=action, arguments=arguments_b) with pytest.raises(serializers.ValidationError) as exc_info1: recipe.revise(arguments=arguments_a) error = action.errors["duplicate_experiment_slug"] assert exc_info1.value.detail == {"arguments": {"slug": error}}
def test_it_publishes_new_revisions_if_enabled(self, mocked_remotesettings): recipe = RecipeFactory(name="Test", approver=UserFactory(), enabler=UserFactory()) assert mocked_remotesettings.return_value.publish.call_count == 1 recipe.revise(name="Modified") approval_request = recipe.latest_revision.request_approval(creator=UserFactory()) approval_request.approve(approver=UserFactory(), comment="r+") assert mocked_remotesettings.return_value.publish.call_count == 2 second_call_args, _ = mocked_remotesettings.return_value.publish.call_args_list[1] modified_recipe, = second_call_args assert modified_recipe.name == "Modified"
def test_unique_experiment_slug_update_collision(self): action = ActionFactory(name="preference-experiment") arguments_a = {"slug": "a", "branches": []} arguments_b = {"slug": "b", "branches": []} # Does not throw when saving revisions RecipeFactory(action=action, arguments=arguments_a) recipe = RecipeFactory(action=action, arguments=arguments_b) with pytest.raises(serializers.ValidationError) as exc_info1: recipe.revise(arguments=arguments_a) error = action.errors["duplicate_experiment_slug"] assert exc_info1.value.detail == {"arguments": {"slug": error}}
def test_unique_experiment_slug_update_collision(self): """A recipe can't be updated to have the same slug as another existing recipe""" action = ActionFactory(name="multi-preference-experiment") arguments_a = MultiPreferenceExperimentArgumentsFactory() arguments_b = MultiPreferenceExperimentArgumentsFactory() # Does not throw when saving revisions RecipeFactory(action=action, arguments=arguments_a) recipe = RecipeFactory(action=action, arguments=arguments_b) with pytest.raises(serializers.ValidationError) as exc_info1: recipe.revise(arguments=arguments_a) error = action.errors["duplicate_experiment_slug"] assert exc_info1.value.detail == {"arguments": {"slug": error}}
def test_preference_experiments_unique_experiment_slug_update_collision( self): action = ActionFactory(name='preference-experiment') arguments_a = {'slug': 'a', 'branches': []} arguments_b = {'slug': 'b', 'branches': []} # Does not throw when saving revisions RecipeFactory(action=action, arguments=arguments_a) recipe = RecipeFactory(action=action, arguments=arguments_b) with pytest.raises(serializers.ValidationError) as exc_info1: recipe.revise(arguments=arguments_a) error = action.errors['duplicate_experiment_slug'] assert exc_info1.value.detail == {'arguments': {'slug': error}}
def test_disable(self): recipe = RecipeFactory(name="Test", approver=UserFactory(), enabler=UserFactory()) assert recipe.enabled recipe.approved_revision.disable(user=UserFactory()) assert not recipe.enabled with pytest.raises(EnabledState.NotActionable): recipe.approved_revision.disable(user=UserFactory()) recipe.revise(name="New name") with pytest.raises(EnabledState.NotActionable): recipe.latest_revision.disable(user=UserFactory())
def test_disable(self): recipe = RecipeFactory(name="Test", approver=UserFactory(), enabler=UserFactory()) assert recipe.approved_revision.enabled recipe.approved_revision.disable(user=UserFactory()) assert not recipe.approved_revision.enabled with pytest.raises(EnabledState.NotActionable): recipe.approved_revision.disable(user=UserFactory()) recipe.revise(name="New name") with pytest.raises(EnabledState.NotActionable): recipe.latest_revision.disable(user=UserFactory())
def test_recipe_revise_partial(self): a1 = ActionFactory() recipe = RecipeFactory( name="unchanged", action=a1, arguments={"message": "something"}, extra_filter_expression="something !== undefined", filter_object_json=None, ) a2 = ActionFactory() recipe.revise(name="changed", action=a2) assert recipe.action == a2 assert recipe.name == "changed" assert recipe.arguments == {"message": "something"} assert recipe.filter_expression == "something !== undefined"
def test_recipe_revise_partial(self): a1 = ActionFactory() recipe = RecipeFactory( name="unchanged", action=a1, arguments={"message": "something"}, extra_filter_expression="something !== undefined", filter_object_json=None, ) a2 = ActionFactory() recipe.revise(name="changed", action=a2) assert recipe.action == a2 assert recipe.name == "changed" assert recipe.arguments == {"message": "something"} assert recipe.filter_expression == "something !== undefined"
def test_it_works(self): author = UserFactory() reviewer = UserFactory() recipe = RecipeFactory(name="first", user=author) revision1 = recipe.latest_revision approval_request = revision1.request_approval(author) approval_request.approve(reviewer, "r+") revision1.enable(author) state1 = revision1.enabled_state assert state1 is not None recipe.revise("second", user=author) revision2 = recipe.latest_revision approval_request = revision2.request_approval(author) approval_request.approve(reviewer, "r+") state2 = revision2.enabled_state assert state2 is not None serializer = EnabledStateSerializer(state1) assert serializer.data == { "id": state1.id, "carryover_from": None, "created": Whatever.iso8601(), "creator": { "id": author.id, "first_name": author.first_name, "last_name": author.last_name, "email": author.email, }, "enabled": True, "revision_id": revision1.id, } serializer = EnabledStateSerializer(state2) assert serializer.data == { "id": state2.id, "carryover_from": state1.id, "created": Whatever.iso8601(), "creator": { "id": reviewer.id, "first_name": reviewer.first_name, "last_name": reviewer.last_name, "email": reviewer.email, }, "enabled": True, "revision_id": revision2.id, }
def test_it_works(self): author = UserFactory() reviewer = UserFactory() recipe = RecipeFactory(name="first", user=author) revision1 = recipe.latest_revision approval_request = revision1.request_approval(author) approval_request.approve(reviewer, "r+") revision1.enable(author) state1 = revision1.enabled_state assert state1 is not None recipe.revise("second", user=author) revision2 = recipe.latest_revision approval_request = revision2.request_approval(author) approval_request.approve(reviewer, "r+") state2 = revision2.enabled_state assert state2 is not None serializer = EnabledStateSerializer(state1) assert serializer.data == { "id": state1.id, "carryover_from": None, "created": Whatever.iso8601(), "creator": { "id": author.id, "first_name": author.first_name, "last_name": author.last_name, "email": author.email, }, "enabled": True, "revision_id": revision1.id, } serializer = EnabledStateSerializer(state2) assert serializer.data == { "id": state2.id, "carryover_from": state1.id, "created": Whatever.iso8601(), "creator": { "id": reviewer.id, "first_name": reviewer.first_name, "last_name": reviewer.last_name, "email": reviewer.email, }, "enabled": True, "revision_id": revision2.id, }
def test_recipe_revise_partial(self): a1 = ActionFactory() recipe = RecipeFactory( name='unchanged', action=a1, arguments={'message': 'something'}, extra_filter_expression='something !== undefined') a2 = ActionFactory() c = ChannelFactory(slug='beta') recipe.revise(name='changed', action=a2, channels=[c]) assert recipe.action == a2 assert recipe.name == 'changed' assert recipe.arguments == {'message': 'something'} assert recipe.filter_expression == ( "(normandy.channel in ['beta']) && " "(something !== undefined)")
def test_no_duplicates(self): action = ActionFactory(name="preference-rollout") arguments_a = {"slug": "a", "preferences": [{"preferenceName": "a", "value": "a"}]} arguments_b = {"slug": "b", "preferences": [{"preferenceName": "b", "value": "b"}]} RecipeFactory(action=action, arguments=arguments_a) recipe_b = RecipeFactory(action=action, arguments=arguments_b) expected_error = action.errors["duplicate_rollout_slug"] # Creating a new recipe fails with pytest.raises(serializers.ValidationError) as exc_info1: RecipeFactory(action=action, arguments=arguments_a) assert exc_info1.value.detail == {"arguments": {"slug": expected_error}} # Revising an existing recipe fails with pytest.raises(serializers.ValidationError) as exc_info2: recipe_b.revise(arguments=arguments_a) assert exc_info2.value.detail == {"arguments": {"slug": expected_error}}
def test_no_duplicates(self): action = ActionFactory(name="preference-rollout") arguments_a = {"slug": "a", "preferences": [{"preferenceName": "a", "value": "a"}]} arguments_b = {"slug": "b", "preferences": [{"preferenceName": "b", "value": "b"}]} RecipeFactory(action=action, arguments=arguments_a) recipe_b = RecipeFactory(action=action, arguments=arguments_b) expected_error = action.errors["duplicate_rollout_slug"] # Creating a new recipe fails with pytest.raises(serializers.ValidationError) as exc_info1: RecipeFactory(action=action, arguments=arguments_a) assert exc_info1.value.detail == {"arguments": {"slug": expected_error}} # Revising an existing recipe fails with pytest.raises(serializers.ValidationError) as exc_info2: recipe_b.revise(arguments=arguments_a) assert exc_info2.value.detail == {"arguments": {"slug": expected_error}}
def test_current_revision_property(self): """Ensure current revision properties work as expected.""" recipe = RecipeFactory(name="first") assert recipe.name == "first" recipe.revise(name="second") assert recipe.name == "second" approval = ApprovalRequestFactory(revision=recipe.latest_revision) approval.approve(UserFactory(), "r+") assert recipe.name == "second" # When `revise` is called on a recipe with at least one approved revision, the new revision # is treated as a draft and as such the `name` property of the recipe should return the # `name` from the `approved_revision` not the `latest_revision`. recipe.revise(name="third") assert recipe.latest_revision.name == "third" # The latest revision ("draft") is updated assert recipe.name == "second" # The current revision is unchanged
def test_current_revision_property(self): """Ensure current revision properties work as expected.""" recipe = RecipeFactory(name="first") assert recipe.name == "first" recipe.revise(name="second") assert recipe.name == "second" approval = ApprovalRequestFactory(revision=recipe.latest_revision) approval.approve(UserFactory(), "r+") assert recipe.name == "second" # When `revise` is called on a recipe with at least one approved revision, the new revision # is treated as a draft and as such the `name` property of the recipe should return the # `name` from the `approved_revision` not the `latest_revision`. recipe.revise(name="third") assert recipe.latest_revision.name == "third" # The latest revision ("draft") is updated assert recipe.name == "second" # The current revision is unchanged
def test_enable(self): recipe = RecipeFactory(name="Test") with pytest.raises(EnabledState.NotActionable): recipe.latest_revision.enable(user=UserFactory()) approval_request = recipe.latest_revision.request_approval(creator=UserFactory()) approval_request.approve(approver=UserFactory(), comment="r+") recipe.revise(name="New name") with pytest.raises(EnabledState.NotActionable): recipe.latest_revision.enable(user=UserFactory()) recipe.approved_revision.enable(user=UserFactory()) assert recipe.enabled with pytest.raises(EnabledState.NotActionable): recipe.approved_revision.enable(user=UserFactory()) approval_request = recipe.latest_revision.request_approval(creator=UserFactory()) approval_request.approve(approver=UserFactory(), comment="r+") assert recipe.enabled
def test_enable(self): recipe = RecipeFactory(name="Test") with pytest.raises(EnabledState.NotActionable): recipe.latest_revision.enable(user=UserFactory()) approval_request = recipe.latest_revision.request_approval(creator=UserFactory()) approval_request.approve(approver=UserFactory(), comment="r+") recipe.revise(name="New name") with pytest.raises(EnabledState.NotActionable): recipe.latest_revision.enable(user=UserFactory()) recipe.approved_revision.enable(user=UserFactory()) assert recipe.approved_revision.enabled with pytest.raises(EnabledState.NotActionable): recipe.approved_revision.enable(user=UserFactory()) approval_request = recipe.latest_revision.request_approval(creator=UserFactory()) approval_request.approve(approver=UserFactory(), comment="r+") assert recipe.approved_revision.enabled
def test_error_during_approval_rolls_back_changes(self, mocker): recipe = RecipeFactory(approver=UserFactory(), enabler=UserFactory()) old_approved_revision = recipe.approved_revision recipe.revise(name="New name") latest_revision = recipe.latest_revision approval_request = recipe.latest_revision.request_approval(UserFactory()) # Simulate an error during signing mocked_update_signature = mocker.patch.object(recipe, "update_signature") mocked_update_signature.side_effect = Exception with pytest.raises(Exception): approval_request.approve(UserFactory(), "r+") # Ensure the changes to the approval request and the recipe are rolled back and the recipe # is still enabled recipe.refresh_from_db() approval_request.refresh_from_db() assert approval_request.approved is None assert recipe.approved_revision == old_approved_revision assert recipe.latest_revision == latest_revision assert recipe.approved_revision.enabled
def test_filter_expression(self): channel1 = ChannelFactory(slug='beta', name='Beta') channel2 = ChannelFactory(slug='release', name='Release') country1 = CountryFactory(code='US', name='USA') country2 = CountryFactory(code='CA', name='Canada') locale1 = LocaleFactory(code='en-US', name='English (US)') locale2 = LocaleFactory(code='fr-CA', name='French (CA)') r = RecipeFactory() assert r.filter_expression == '' r = RecipeFactory(channels=[channel1]) assert r.filter_expression == "normandy.channel in ['beta']" r.revise(channels=[channel1, channel2]) assert r.filter_expression == "normandy.channel in ['beta', 'release']" r = RecipeFactory(countries=[country1]) assert r.filter_expression == "normandy.country in ['US']" r.revise(countries=[country1, country2]) assert r.filter_expression == "normandy.country in ['CA', 'US']" r = RecipeFactory(locales=[locale1]) assert r.filter_expression == "normandy.locale in ['en-US']" r.revise(locales=[locale1, locale2]) assert r.filter_expression == "normandy.locale in ['en-US', 'fr-CA']" r = RecipeFactory(extra_filter_expression='2 + 2 == 4') assert r.filter_expression == '2 + 2 == 4' r.revise(channels=[channel1], countries=[country1], locales=[locale1]) assert r.filter_expression == ("(normandy.locale in ['en-US']) && " "(normandy.country in ['US']) && " "(normandy.channel in ['beta']) && " "(2 + 2 == 4)")
def test_recipe_revise_locales(self): l1 = LocaleFactory(code='en-US') recipe = RecipeFactory(locales=[l1]) l2 = LocaleFactory(code='fr-CA') recipe.revise(locales=[l2]) assert recipe.locales.count() == 1 assert list(recipe.locales.all()) == [l2] recipe.revise(locales=[l1, l2]) locales = list(recipe.locales.all()) assert recipe.locales.count() == 2 assert l1 in locales assert l2 in locales recipe.revise(locales=[]) assert recipe.locales.count() == 0
def test_recipe_revise_channels(self): c1 = ChannelFactory(slug='beta') recipe = RecipeFactory(channels=[c1]) c2 = ChannelFactory(slug='release') recipe.revise(channels=[c2]) assert recipe.channels.count() == 1 assert list(recipe.channels.all()) == [c2] recipe.revise(channels=[c1, c2]) channels = list(recipe.channels.all()) assert recipe.channels.count() == 2 assert c1 in channels assert c2 in channels recipe.revise(channels=[]) assert recipe.channels.count() == 0
def test_recipe_revise_countries(self): c1 = CountryFactory(code='CA') recipe = RecipeFactory(countries=[c1]) c2 = CountryFactory(code='US') recipe.revise(countries=[c2]) assert recipe.countries.count() == 1 assert list(recipe.countries.all()) == [c2] recipe.revise(countries=[c1, c2]) countries = list(recipe.countries.all()) assert recipe.countries.count() == 2 assert c1 in countries assert c2 in countries recipe.revise(countries=[]) assert recipe.countries.count() == 0
def test_recipe_revise_arguments(self): recipe = RecipeFactory(arguments_json="{}") recipe.revise(arguments={"something": "value"}) assert recipe.arguments_json == '{"something": "value"}'
def test_recipe_force_revise(self): recipe = RecipeFactory(name="my name") revision_id = recipe.revision_id recipe.revise(name="my name", force=True) assert revision_id != recipe.revision_id
def test_update_logging(self, mock_logger): recipe = RecipeFactory(name="my name") recipe.revise(name="my name", force=True) mock_logger.info.assert_called_with( Whatever.contains(str(recipe.id)), extra={"code": INFO_CREATE_REVISION} )
def test_revision_id_changes(self): """Ensure that the revision id is incremented on each save""" recipe = RecipeFactory() revision_id = recipe.revision_id recipe.revise(action=ActionFactory()) assert recipe.revision_id != revision_id
def test_revise_arguments(self): recipe = RecipeFactory(arguments_json="[]") recipe.revise(arguments=[{"id": 1}]) assert recipe.arguments_json == '[{"id": 1}]'
def test_cant_change_signature_and_other_fields(self): recipe = RecipeFactory(name="unchanged", signed=False) recipe.signature = SignatureFactory() with pytest.raises(ValidationError) as exc_info: recipe.revise(name="changed") assert exc_info.value.message == "Signatures must change alone"