def test_unique_name_update_collision(self):
            action = ActionFactory(name="opt-out-study")
            arguments_a = OptOutStudyArgumentsFactory()
            arguments_b = OptOutStudyArgumentsFactory()
            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}}
Esempio n. 2
0
        def test_list_filter_action(self, api_client):
            a1 = ActionFactory()
            a2 = ActionFactory()
            r1 = RecipeFactory(action=a1)
            r2 = RecipeFactory(action=a2)

            assert a1.name != a2.name

            res = api_client.get(f"/api/v1/recipe/?action={a1.name}")
            assert res.status_code == 200
            assert [r["id"] for r in res.data] == [r1.id]

            res = api_client.get(f"/api/v1/recipe/?action={a2.name}")
            assert res.status_code == 200
            assert [r["id"] for r in res.data] == [r2.id]

            assert a1.name != "nonexistant" and a2.name != "nonexistant"
            res = api_client.get("/api/v1/recipe/?action=nonexistant")
            assert res.status_code == 200
            assert res.data == []
Esempio n. 3
0
 def test_cannot_delete_in_use_extension(self, api_client, storage):
     xpi = WebExtensionFileFactory()
     e = ExtensionFactory(xpi__from_func=xpi.open)
     a = ActionFactory(name="opt-out-study")
     RecipeFactory(action=a, arguments={"extensionId": e.id})
     res = api_client.delete(f"/api/v3/extension/{e.id}/")
     assert res.status_code == 400
     assert res.data == [
         "Extension cannot be updated while in use by a recipe."
     ]
     assert Extension.objects.count() == 1
Esempio n. 4
0
    def test_it_doesnt_disable_recipes(self, mock_action):
        action = ActionFactory(name='test-action', implementation='old')
        recipe = RecipeFactory(action=action,
                               approver=UserFactory(),
                               enabled=True)
        action = recipe.action
        mock_action(action.name, 'impl', action.arguments_schema)

        call_command('update_actions')
        recipe.refresh_from_db()
        assert recipe.enabled
Esempio n. 5
0
    def test_it_works(self, storage):
        extension = ExtensionFactory()
        fake_old_url = extension.xpi.url.replace("/media/", "/media-old/")
        action = ActionFactory(name="opt-out-study")
        recipe = RecipeFactory(action=action,
                               arguments={addonUrl: fake_old_url})
        call_command("update_addon_urls")

        # For reasons that I don't understand, recipe.update_from_db() doesn't work here.
        recipe = Recipe.objects.get(id=recipe.id)
        assert recipe.arguments[addonUrl] == extension.xpi.url
Esempio n. 6
0
    def test_it_doesnt_disable_recipes(self, mock_action):
        action = ActionFactory(name="test-action", implementation="old")
        recipe = RecipeFactory(action=action,
                               approver=UserFactory(),
                               enabler=UserFactory())
        action = recipe.action
        mock_action(action.name, "impl", action.arguments_schema)

        call_command("update_actions")
        recipe.refresh_from_db()
        assert recipe.enabled
Esempio n. 7
0
        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}}
Esempio n. 8
0
    def test_recipes_used_by(self):
        approver = UserFactory()
        enabler = UserFactory()
        recipe = RecipeFactory(approver=approver, enabler=enabler)
        assert [recipe] == list(recipe.action.recipes_used_by)

        action = ActionFactory()
        recipes = RecipeFactory.create_batch(2,
                                             action=action,
                                             approver=approver,
                                             enabler=enabler)
        assert set(action.recipes_used_by) == set(recipes)
Esempio n. 9
0
    def test_it_updates_existing_actions(self, mock_action):
        action = ActionFactory(name="test-action",
                               implementation="old_impl",
                               arguments_schema={})
        mock_action(action.name, {"type": "int"}, "new_impl")

        call_command("update_actions")
        assert Action.objects.count() == 1

        action.refresh_from_db()
        assert action.implementation == "new_impl"
        assert action.arguments_schema == {"type": "int"}
Esempio n. 10
0
    def test_it_serves_actions_without_implementation(self, api_client):
        ActionFactory(name="foo-remote", implementation=None, arguments_schema={"type": "object"})

        res = api_client.get("/api/v1/action/")
        assert res.status_code == 200
        assert res.data == [
            {
                "name": "foo-remote",
                "implementation_url": None,
                "arguments_schema": {"type": "object"},
            }
        ]
Esempio n. 11
0
 def test_it_404s_if_hash_doesnt_match(self, api_client):
     action = ActionFactory(implementation='asdf')
     bad_hash = hashlib.sha1('nomatch'.encode()).hexdigest()
     res = api_client.get(
         '/api/v1/action/{name}/implementation/{hash}/'.format(
             name=action.name,
             hash=bad_hash,
         ))
     assert res.status_code == 404
     assert res.content.decode(
     ) == '/* Hash does not match current stored action. */'
     assert res['Content-Type'] == 'application/javascript; charset=utf-8'
Esempio n. 12
0
    def test_cancel_approval(self, api_client, mocked_autograph):
        action = ActionFactory()
        user1 = UserFactory(is_superuser=True)
        user2 = UserFactory(is_superuser=True)
        api_client.force_authenticate(user1)

        # Create a recipe
        res = api_client.post(
            '/api/v1/recipe/', {
                'action': action.name,
                'arguments': {},
                'name': 'test recipe',
                'extra_filter_expression': 'counter == 0',
                'enabled': 'false',
            })
        assert res.status_code == 201
        recipe_id = res.json()['id']
        revision_id = res.json()['latest_revision_id']

        # Request approval
        res = api_client.post(
            f'/api/v1/recipe_revision/{revision_id}/request_approval/')
        assert res.status_code == 201
        approval_request_id = res.json()['id']

        # Approve the recipe
        api_client.force_authenticate(user2)
        res = api_client.post(
            f'/api/v1/approval_request/{approval_request_id}/approve/',
            {'comment': 'r+'})
        assert res.status_code == 200

        # Make another change
        api_client.force_authenticate(user1)
        res = api_client.patch(f'/api/v1/recipe/{recipe_id}/',
                               {'extra_filter_expression': 'counter == 1'})
        assert res.status_code == 200
        revision_id = res.json()['latest_revision_id']

        # Request approval for the second change
        res = api_client.post(
            f'/api/v1/recipe_revision/{revision_id}/request_approval/')
        approval_request_id = res.json()['id']
        assert res.status_code == 201

        # Cancel the approval request
        res = api_client.post(
            f'/api/v1/approval_request/{approval_request_id}/close/')
        assert res.status_code == 204

        # The API should still have correct signatures
        self.verify_signatures(api_client, expected_count=1)
Esempio n. 13
0
    def test_validation_with_valid_data(self):
        mockAction = ActionFactory(name='show-heartbeat',
                                   arguments_schema=ARGUMENTS_SCHEMA)

        channel = ChannelFactory(slug='release')
        country = CountryFactory(code='CA')
        locale = LocaleFactory(code='en-US')

        serializer = RecipeSerializer(
            data={
                'name': 'bar',
                'enabled': True,
                'extra_filter_expression': '[]',
                'action': 'show-heartbeat',
                'channels': ['release'],
                'countries': ['CA'],
                'locales': ['en-US'],
                'arguments': {
                    'surveyId':
                    'lorem-ipsum-dolor',
                    'surveys': [{
                        'title': 'adipscing',
                        'weight': 1
                    }, {
                        'title': 'consequetar',
                        'weight': 1
                    }]
                }
            })

        assert serializer.is_valid()
        assert serializer.validated_data == {
            'name': 'bar',
            'enabled': True,
            'extra_filter_expression': '[]',
            'action': mockAction,
            'arguments': {
                'surveyId':
                'lorem-ipsum-dolor',
                'surveys': [{
                    'title': 'adipscing',
                    'weight': 1
                }, {
                    'title': 'consequetar',
                    'weight': 1
                }]
            },
            'channels': [channel],
            'countries': [country],
            'locales': [locale],
        }
        assert serializer.errors == {}
Esempio n. 14
0
 def test_cant_change_signature_and_other_fields(self, mocker):
     # Mock the Autographer
     mock_autograph = mocker.patch("normandy.recipes.models.Autographer")
     mock_autograph.return_value.sign_data.return_value = [{
         "signature":
         "fake signature"
     }]
     action = ActionFactory(name="unchanged", signed=False)
     action.update_signature()
     action.name = "changed"
     with pytest.raises(ValidationError) as exc_info:
         action.save()
     assert exc_info.value.message == "Signatures must change alone"
Esempio n. 15
0
        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}}
Esempio n. 16
0
 def test_cannot_update_in_use_extension(self, api_client, storage):
     xpi = WebExtensionFileFactory()
     e = ExtensionFactory(xpi__from_func=xpi.open)
     a = ActionFactory(name="opt-out-study")
     RecipeFactory(action=a, arguments={"extensionId": e.id})
     res = api_client.patch(f"/api/v3/extension/{e.id}/",
                            {"name": "new name"})
     assert res.status_code == 400
     assert res.data == [
         "Extension cannot be updated while in use by a recipe."
     ]
     e.refresh_from_db()
     assert e.name != "new name"
Esempio n. 17
0
        def test_no_errors(self):
            action = ActionFactory(name="preference-experiment")
            arguments = PreferenceExperimentArgumentsFactory(
                slug="a", branches=[{"slug": "a", "value": "a"}, {"slug": "b", "value": "b"}],
            )
            # does not throw when saving the revision
            recipe = RecipeFactory(action=action, arguments=arguments)

            # Approve and enable the revision
            rev = recipe.latest_revision
            approval_request = rev.request_approval(UserFactory())
            approval_request.approve(UserFactory(), "r+")
            rev.enable(UserFactory())
Esempio n. 18
0
    def test_creation_when_identicon_seed_is_invalid(self, api_client):
        action = ActionFactory()

        res = api_client.post(
            '/api/v2/recipe/', {
                'name': 'Test Recipe',
                'action_id': action.id,
                'arguments': {},
                'extra_filter_expression': 'whatever',
                'enabled': True,
                'identicon_seed': 'invalid_identicon_seed'
            })
        assert res.status_code == 400
Esempio n. 19
0
    def test_it_includes_cache_headers(self, api_client, settings):
        # Note: Can't override the cache time setting, since it is read
        # when invoking the decorator at import time. Changing it would
        # require mocking, and that isn't worth it.
        action = ActionFactory()
        res = api_client.get(
            "/api/v1/action/{name}/implementation/{hash}/".format(
                name=action.name, hash=action.implementation_hash))
        assert res.status_code == 200

        max_age = "max-age={}".format(settings.IMMUTABLE_CACHE_TIME)
        assert max_age in res["Cache-Control"]
        assert "public" in res["Cache-Control"]
Esempio n. 20
0
 def test_it_doesnt_update_other_actions(self):
     action = ActionFactory(name="some-other-action")
     recipe = RecipeFactory(
         action=action,
         arguments={
             addonUrl: "https://before.example.com/extensions/addon.xpi"
         })
     call_command("update_addon_urls")
     # For reasons that I don't understand, recipe.update_from_db() doesn't work here.
     recipe = Recipe.objects.get(id=recipe.id)
     # Url should not be not updated
     assert recipe.arguments[
         addonUrl] == "https://before.example.com/extensions/addon.xpi"
Esempio n. 21
0
    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}}
Esempio n. 22
0
        def test_v1_marker_included_only_if_non_baseline_capabilities_are_present(
                self, settings):
            action = ActionFactory()
            settings.BASELINE_CAPABILITIES |= action.capabilities

            recipe = RecipeFactory(extra_capabilities=[], action=action)
            assert recipe.latest_revision.capabilities <= settings.BASELINE_CAPABILITIES
            assert "capabilities-v1" not in recipe.latest_revision.capabilities

            recipe = RecipeFactory(extra_capabilities=["non-baseline"],
                                   action=action)
            assert "non-baseline" not in settings.BASELINE_CAPABILITIES
            assert "capabilities-v1" in recipe.latest_revision.capabilities
Esempio n. 23
0
        def test_list_filter_action_legacy(self, api_client):
            a1 = ActionFactory()
            a2 = ActionFactory()
            r1 = RecipeFactory(action=a1)
            r2 = RecipeFactory(action=a2)

            assert a1.id != a2.id

            res = api_client.get(
                f'/api/v1/recipe/?latest_revision__action={a1.id}')
            assert res.status_code == 200
            assert [r['id'] for r in res.data] == [r1.id]

            res = api_client.get(
                f'/api/v1/recipe/?latest_revision__action={a2.id}')
            assert res.status_code == 200
            assert [r['id'] for r in res.data] == [r2.id]

            assert a1.id != -1 and a2.id != -1
            res = api_client.get('/api/v1/recipe/?latest_revision__action=-1')
            assert res.status_code == 200
            assert res.data == []
Esempio n. 24
0
        def test_no_errors(self):
            action = ActionFactory(name="preference-rollout")
            arguments = {
                "slug": "test-rollout",
                "preferences": [{"preferenceName": "foo", "value": 5}],
            }
            # does not throw when saving the revision
            recipe = RecipeFactory(action=action, arguments=arguments)

            # Approve and enable the revision
            rev = recipe.latest_revision
            approval_request = rev.request_approval(UserFactory())
            approval_request.approve(UserFactory(), "r+")
            rev.enable(UserFactory())
Esempio n. 25
0
    def test_it_updates_existing_actions(self, mock_action):
        action = ActionFactory(
            name='test-action',
            implementation='old_impl',
            arguments_schema={},
        )
        mock_action(action.name, 'new_impl', {'type': 'int'})

        call_command('update_actions')
        assert Action.objects.count() == 1

        action.refresh_from_db()
        assert action.implementation == 'new_impl'
        assert action.arguments_schema == {'type': 'int'}
Esempio n. 26
0
        def test_list_filter_action_legacy(self, api_client):
            a1 = ActionFactory()
            a2 = ActionFactory()
            r1 = RecipeFactory(action=a1)
            r2 = RecipeFactory(action=a2)

            assert a1.id != a2.id

            res = api_client.get(
                f"/api/v1/recipe/?latest_revision__action={a1.id}")
            assert res.status_code == 200
            assert [r["id"] for r in res.data] == [r1.id]

            res = api_client.get(
                f"/api/v1/recipe/?latest_revision__action={a2.id}")
            assert res.status_code == 200
            assert [r["id"] for r in res.data] == [r2.id]

            assert a1.id != -1 and a2.id != -1
            res = api_client.get("/api/v1/recipe/?latest_revision__action=-1")
            assert res.status_code == 400
            assert res.data["latest_revision__action"][
                0].code == "invalid_choice"
Esempio n. 27
0
 def test_preference_experiments_unique_branch_values(self):
     action = ActionFactory(name="preference-experiment")
     arguments = PreferenceExperimentArgumentsFactory(
         slug="test",
         branches=[
             {"slug": "a", "value": "unique"},
             {"slug": "b", "value": "duplicate"},
             {"slug": "c", "value": "duplicate"},
         ],
     )
     with pytest.raises(serializers.ValidationError) as exc_info:
         action.validate_arguments(arguments, RecipeRevisionFactory())
     error = action.errors["duplicate_branch_value"]
     assert exc_info.value.detail == {"arguments": {"branches": {2: {"value": error}}}}
Esempio n. 28
0
 def test_canonical_json(self):
     action = ActionFactory(name="test-action",
                            implementation="console.log(true)")
     # Yes, this is ugly, but it needs to compare an exact byte
     # sequence, since this is used for hashing and signing
     expected = (
         "{"
         '"arguments_schema":{},'
         '"implementation_url":"/api/v1/action/test-action/implementation'
         '/sha384-ZRkmoh4lizeQ_jdtJBOQZmPzc3x09DKCA4gkdJmwEnO31F7Ttl8RyXkj3wG93lAP/",'
         '"name":"test-action"'
         "}")
     expected = expected.encode()
     assert action.canonical_json() == expected
Esempio n. 29
0
    def test_it_works(self):
        action = ActionFactory(name="opt-out-study")
        recipe = RecipeFactory(
            action=action,
            arguments={
                "addonUrl": "https://before.example.com/extensions/addon.xpi"
            },
        )
        call_command("update_addon_urls", "after.example.com")

        # For reasons that I don't understand, recipe.update_from_db() doesn't work here.
        recipe = Recipe.objects.get(id=recipe.id)
        assert recipe.arguments[
            "addonUrl"] == "https://after.example.com/extensions/addon.xpi"
Esempio n. 30
0
    def test_validation_with_wrong_arguments(self):
        action = ActionFactory(name="show-heartbeat",
                               arguments_schema=ARGUMENTS_SCHEMA)

        serializer = RecipeSerializer(
            data={
                "action_id": action.id,
                "name": "Any name",
                "extra_filter_expression": "true",
                "arguments": {
                    "surveyId":
                    "",
                    "surveys": [
                        {
                            "title": "",
                            "weight": 1
                        },
                        {
                            "title": "bar",
                            "weight": 1
                        },
                        {
                            "title": "foo",
                            "weight": 0
                        },
                        {
                            "title": "baz",
                            "weight": "lorem ipsum"
                        },
                    ],
                },
            })

        with pytest.raises(serializers.ValidationError):
            serializer.is_valid(raise_exception=True)

        assert serializer.errors["arguments"] == {
            "surveyId": "This field may not be blank.",
            "surveys": {
                0: {
                    "title": "This field may not be blank."
                },
                2: {
                    "weight": "0 is less than the minimum of 1"
                },
                3: {
                    "weight": "'lorem ipsum' is not of type 'integer'"
                },
            },
        }