Пример #1
0
        def test_enable_rollback_enable_rollout_invariance(self):
            rollout_recipe = RecipeFactory(
                name="Rollout",
                approver=UserFactory(),
                enabler=UserFactory(),
                action=ActionFactory(name="preference-rollout"),
                arguments={"slug": "myslug"},
            )
            assert rollout_recipe.approved_revision.enabled

            rollback_recipe = RecipeFactory(
                name="Rollback",
                action=ActionFactory(name="preference-rollback"),
                arguments={"rolloutSlug": "myslug"},
            )
            approval_request = rollback_recipe.latest_revision.request_approval(
                creator=UserFactory()
            )
            approval_request.approve(approver=UserFactory(), comment="r+")

            with pytest.raises(ValidationError) as exc_info:
                rollback_recipe.approved_revision.enable(user=UserFactory())
            assert exc_info.value.message == "Rollout recipe 'Rollout' is currently enabled"

            rollout_recipe.approved_revision.disable(user=UserFactory())
            assert not rollout_recipe.approved_revision.enabled
            # Now it should be possible to enable the rollback recipe.
            rollback_recipe.approved_revision.enable(user=UserFactory())
            assert rollback_recipe.approved_revision.enabled

            # Can't make up your mind. Now try to enable the rollout recipe again even though
            # the rollback recipe is enabled.
            with pytest.raises(ValidationError) as exc_info:
                rollout_recipe.approved_revision.enable(user=UserFactory())
            assert exc_info.value.message == "Rollback recipe 'Rollback' is currently enabled"
Пример #2
0
    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())
Пример #3
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())
Пример #4
0
    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
Пример #5
0
        def test_no_errors(self):
            action = ActionFactory(name="preference-experiment")
            arguments = {
                "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())
Пример #6
0
 def test_update_signature(self, mock_logger, mocked_autograph):
     recipe = RecipeFactory(enabler=UserFactory(), approver=UserFactory())
     recipe.signature = None
     recipe.update_signature()
     mock_logger.info.assert_called_with(
         Whatever.contains(str(recipe.id)),
         extra={
             "code": INFO_REQUESTING_RECIPE_SIGNATURES,
             "recipe_ids": [recipe.id]
         },
     )
     mocked_autograph.return_value.sign_data.assert_called_with(
         [Whatever(lambda s: json.loads(s)["id"] == recipe.id)])
     assert recipe.signature is not None
Пример #7
0
    def test_update_user(self, api_client):
        u = UserFactory(first_name="John", last_name="Doe")
        res = api_client.patch(f"/api/v3/user/{u.id}/", {"first_name": "Jane"})
        assert res.status_code == 200
        u.refresh_from_db()
        assert u.first_name == "Jane"

        res = api_client.patch(
            f"/api/v3/user/{u.id}/", {"first_name": "Lejames", "last_name": "Bron"}
        )
        assert res.status_code == 200
        u.refresh_from_db()
        assert u.first_name == "Lejames"
        assert u.last_name == "Bron"
Пример #8
0
    def test_it_unpublishes_when_disabled(self, mocked_remotesettings):
        recipe = RecipeFactory(name="Test",
                               approver=UserFactory(),
                               enabler=UserFactory())

        recipe.approved_revision.disable(user=UserFactory())

        mocked_remotesettings.return_value.unpublish.assert_called_with(recipe)

        # Unpublishes once when disabled twice.
        with pytest.raises(EnabledState.NotActionable):
            recipe.approved_revision.disable(user=UserFactory())

        assert mocked_remotesettings.return_value.publish.call_count == 1
Пример #9
0
    def test_it_updates_remote_settings_if_enabled(self, mocker, mocked_autograph):
        mocked_remotesettings = mocker.patch(
            "normandy.recipes.management.commands.update_recipe_signatures.RemoteSettings"
        )
        for i in range(3):
            r = RecipeFactory(approver=UserFactory(), enabler=UserFactory(), signed=True)
            r.signature.signature = "old signature"
            r.signature.save()
        mocked_remotesettings.reset_mock()

        call_command("update_recipe_signatures", "--force")

        assert mocked_remotesettings.return_value.publish.call_count == 3
        assert mocked_remotesettings.return_value.approve_changes.call_count == 1
Пример #10
0
        def test_it_publishes_when_enabled(self, mocked_remotesettings):
            recipe = RecipeFactory(name="Test")

            approval_request = recipe.latest_revision.request_approval(creator=UserFactory())
            approval_request.approve(approver=UserFactory(), comment="r+")
            recipe.approved_revision.enable(user=UserFactory())

            mocked_remotesettings.return_value.publish.assert_called_with(recipe)

            # Publishes once when enabled twice.
            with pytest.raises(EnabledState.NotActionable):
                recipe.approved_revision.enable(user=UserFactory())

            assert mocked_remotesettings.return_value.publish.call_count == 1
Пример #11
0
    def test_it_does_nothing_on_dry_run(self, rs_settings, requestsmock,
                                        mocked_remotesettings):
        r1 = RecipeFactory(name="Test 1",
                           enabler=UserFactory(),
                           approver=UserFactory())
        requestsmock.get(self.baseline_published_records_url,
                         json={"data": [exports.recipe_as_record(r1)]})
        requestsmock.get(self.capabilities_published_records_url,
                         json={"data": [exports.recipe_as_record(r1)]})

        call_command("sync_remote_settings", "--dry-run")

        assert not mocked_remotesettings.publish.called
        assert not mocked_remotesettings.unpublish.called
Пример #12
0
    def test_cannot_update_email(self, api_client):
        u = UserFactory(email="*****@*****.**")

        res = api_client.patch(f"/api/v3/user/{u.id}/", {"email": "*****@*****.**"})
        assert res.status_code == 200
        u.refresh_from_db()
        assert u.email == "*****@*****.**"

        res = api_client.put(
            f"/api/v3/user/{u.id}/",
            {"first_name": "Lejames", "last_name": "Bron", "email": "*****@*****.**"},
        )
        assert res.status_code == 200
        u.refresh_from_db()
        assert u.email == "*****@*****.**"
Пример #13
0
    def test_existing_user_with_no_email(self, csrf_api_client, mock_oidc):
        user = UserFactory(username="******", email="")
        mock_oidc(
            content=json.dumps(
                {"email": "*****@*****.**", "given_name": "John", "family_name": "Doe"}
            ).encode("utf-8")
        )
        response = csrf_api_client.post(
            "/bearer/", {"example": "example"}, HTTP_AUTHORIZATION="Bearer valid-token"
        )
        assert response.status_code == 200
        assert response.data.get("user") == "*****@*****.**"

        user.refresh_from_db()
        assert user.email == "*****@*****.**"
Пример #14
0
    def test_existing_user_with_no_email(self, csrf_api_client, mock_oidc):
        user = UserFactory(username="******", email="")
        mock_oidc(content=json.dumps({
            "email": "*****@*****.**",
            "given_name": "John",
            "family_name": "Doe"
        }).encode("utf-8"))
        response = csrf_api_client.post(
            "/bearer/", {"example": "example"},
            HTTP_AUTHORIZATION="Bearer valid-token")
        assert response.status_code == 200
        assert response.data.get("user") == "*****@*****.**"

        user.refresh_from_db()
        assert user.email == "*****@*****.**"
Пример #15
0
        def test_signed_listing_filters_by_enabled(Self, api_client):
            enabled_recipe = RecipeFactory(
                signed=True, approver=UserFactory(), enabler=UserFactory()
            )
            disabled_recipe = RecipeFactory(signed=True)

            res = api_client.get("/api/v1/recipe/signed/?enabled=1")
            assert res.status_code == 200
            assert len(res.data) == 1
            assert res.data[0]["recipe"]["id"] == enabled_recipe.id

            res = api_client.get("/api/v1/recipe/signed/?enabled=0")
            assert res.status_code == 200
            assert len(res.data) == 1
            assert res.data[0]["recipe"]["id"] == disabled_recipe.id
Пример #16
0
    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,
        }
Пример #17
0
 def test_canonical_json(self):
     recipe = RecipeFactory(
         action=ActionFactory(name="action"),
         arguments_json='{"foo": 1, "bar": 2}',
         extra_filter_expression="2 + 2 == 4",
         name="canonical",
         approver=UserFactory(),
         filter_object_json=None,
     )
     # Yes, this is really ugly, but we really do need to compare an exact
     # byte sequence, since this is used for hashing and signing
     filter_expression = "2 + 2 == 4"
     expected = (
         "{"
         '"action":"action",'
         '"arguments":{"bar":2,"foo":1},'
         '"capabilities":["action.action","capabilities-v1"],'
         '"filter_expression":"%(filter_expression)s",'
         '"id":%(id)s,'
         '"name":"canonical",'
         '"revision_id":"%(revision_id)s",'
         '"uses_only_baseline_capabilities":false'
         "}"
     ) % {
         "id": recipe.id,
         "revision_id": recipe.latest_revision.id,
         "filter_expression": filter_expression,
     }
     expected = expected.encode()
     assert recipe.canonical_json() == expected
Пример #18
0
 def test_it_unsigns_out_of_date_disabled_recipes(self, settings, mocked_autograph):
     r = RecipeFactory(approver=UserFactory(), signed=True, enabled=False)
     r.signature.timestamp -= timedelta(seconds=settings.AUTOGRAPH_SIGNATURE_MAX_AGE * 2)
     r.signature.save()
     call_command("update_recipe_signatures")
     r.refresh_from_db()
     assert r.signature is None
Пример #19
0
    def test_cannot_reject_already_rejected(self):
        u = UserFactory()
        req = ApprovalRequestFactory()
        req.reject(u, "r-")

        with pytest.raises(req.NotActionable):
            req.reject(u, "r-")
Пример #20
0
    def test_filtering_by_enabled_lowercase(self, api_client):
        r1 = RecipeFactory(approver=UserFactory(), enabled=True)
        RecipeFactory(enabled=False)

        res = api_client.get('/api/v1/recipe/?enabled=true')
        assert res.status_code == 200
        assert [r['id'] for r in res.data] == [r1.id]
Пример #21
0
 def test_it_resigns_signed_recipes_with_force(self, mocked_autograph):
     r = RecipeFactory(approver=UserFactory(), enabled=True, signed=True)
     r.signature.signature = 'old signature'
     r.signature.save()
     call_command('update_recipe_signatures', '--force')
     r.refresh_from_db()
     assert r.signature.signature is not 'old signature'
Пример #22
0
 def test_setting_signature_doesnt_change_canonical_json(self):
     recipe = RecipeFactory(approver=UserFactory(), name="unchanged", signed=False)
     serialized = recipe.canonical_json()
     recipe.signature = SignatureFactory()
     recipe.save()
     assert recipe.signature is not None
     assert recipe.canonical_json() == serialized
Пример #23
0
    def test_unpublish_reverts_changes_if_approval_fails(
            self, rs_settings, requestsmock):
        recipe = RecipeFactory(name="Test", approver=UserFactory())
        collection_url = (
            f"{rs_settings.REMOTE_SETTINGS_URL}/buckets/{rs_settings.REMOTE_SETTINGS_BUCKET_ID}"
            f"/collections/{rs_settings.REMOTE_SETTINGS_COLLECTION_ID}")
        record_url = f"{collection_url}/records/{recipe.id}"
        requestsmock.request("delete",
                             record_url,
                             content=b'{"data": {"deleted":true}}')
        requestsmock.request("patch", collection_url, status_code=403)
        record_prod_url = record_url.replace(
            f"/buckets/{rs_settings.REMOTE_SETTINGS_BUCKET_ID}/",
            f"/buckets/{exports.RemoteSettings.MAIN_BUCKET_ID}/",
        )
        record_in_prod = json.dumps({
            "data": exports.recipe_as_record(recipe)
        }).encode()
        requestsmock.request("get", record_prod_url, content=record_in_prod)
        requestsmock.request("put", record_url, content=b'{"data": {}}')

        remotesettings = exports.RemoteSettings()
        with pytest.raises(kinto_http.KintoException):
            remotesettings.unpublish(recipe)

        assert len(requestsmock.request_history) == 4
        assert requestsmock.request_history[0].url == record_url
        assert requestsmock.request_history[0].method == "DELETE"
        assert requestsmock.request_history[1].url == collection_url
        assert requestsmock.request_history[2].url == record_prod_url
        assert requestsmock.request_history[3].url == record_url
        assert requestsmock.request_history[3].method == "PUT"
        assert requestsmock.request_history[3].json()["data"]["name"] == "Test"
Пример #24
0
 def test_add_user(self, api_client):
     user = UserFactory()
     group = GroupFactory()
     res = api_client.post(f"/api/v3/group/{group.id}/add_user/",
                           {"user_id": user.id})
     assert res.status_code == 204
     assert user.groups.filter(pk=group.pk).count() == 1
Пример #25
0
    def test_unpublish_deletes_record_and_approves(self, rs_settings,
                                                   requestsmock, mock_logger):
        """Test that requests are sent to Remote Settings on unpublish."""

        recipe = RecipeFactory(name="Test", approver=UserFactory())
        collection_url = (
            f"{rs_settings.REMOTE_SETTINGS_URL}/buckets/{rs_settings.REMOTE_SETTINGS_BUCKET_ID}"
            f"/collections/{rs_settings.REMOTE_SETTINGS_COLLECTION_ID}")
        record_url = f"{collection_url}/records/{recipe.id}"
        auth = (rs_settings.REMOTE_SETTINGS_USERNAME + ":" +
                rs_settings.REMOTE_SETTINGS_PASSWORD).encode()
        request_headers = {
            "User-Agent": KINTO_USER_AGENT,
            "Content-Type": "application/json",
            "Authorization": f"Basic {base64.b64encode(auth).decode()}",
        }
        requestsmock.request("delete",
                             record_url,
                             content=b'{"data": {}}',
                             request_headers=request_headers)
        requestsmock.request("patch",
                             collection_url,
                             content=b'{"data": {}}',
                             request_headers=request_headers)

        remotesettings = exports.RemoteSettings()
        remotesettings.unpublish(recipe)

        assert len(requestsmock.request_history) == 2
        assert requestsmock.request_history[0].url == record_url
        assert requestsmock.request_history[1].url == collection_url
        mock_logger.info.assert_called_with(
            f"Deleted record '{recipe.id}' of recipe '{recipe.name}'")
Пример #26
0
 def test_user_exists(self, csrf_api_client, mock_oidc):
     user = UserFactory()
     mock_oidc(user=user)
     response = csrf_api_client.get("/bearer/",
                                    HTTP_AUTHORIZATION="Bearer valid-token")
     assert response.status_code == 200
     assert response.data.get("user") == user.email
Пример #27
0
    def test_cannot_approve_already_approved(self):
        u = UserFactory()
        req = ApprovalRequestFactory()
        req.approve(u, "r+")

        with pytest.raises(req.NotActionable):
            req.approve(u, "r+")
Пример #28
0
    def test_caching(self, csrf_api_client, requestsmock, settings):
        user = UserFactory()
        user_data = json.dumps({
            "email": user.email,
            "given_name": user.first_name,
            "family_name": user.last_name
        }).encode("utf-8")

        # The reason for the `+ 2` here is that if we only add +1 second, there's a small
        # chance that the `time.mktime(utcnow)` done here is *different* from the
        # `time.mktime(utcnow)` that happens within the `fetch_oidc_user_profile()` method
        # inside BearerTokenAuthentication.
        # I.e.
        #
        #    print(time.mktime(datetime.utcnow().timetuple()))  # outputs 1534380068.0
        #    time.sleep(0.1)
        #    print(time.mktime(datetime.utcnow().timetuple()))  # outputs 1534380069.0
        #
        # Note! This doesn't always happen. Run those three lines 100 times and it's
        # guaranteed to be different ~10% of the time. That's when the milliseconds is 900 and
        # with the sleep(0.1) it will round up to the next second.
        # By adding +2 to the mock here we allow a whole 1 second between now and when
        # the `fetch_oidc_user_profile()` uses `time.mktime(datetime.utcnow().timetuple())`.
        ratelimit_reset = int(time.mktime(datetime.utcnow().timetuple())) + 2
        requestsmock.get(
            settings.OIDC_USER_ENDPOINT,
            [
                {
                    "content": user_data,
                    "status_code": 200,
                    "headers": {
                        "X-RateLimit-Reset": f"{ratelimit_reset}"
                    },
                },
                {
                    "status_code": 401
                },
            ],
        )

        response = csrf_api_client.post(
            "/bearer/", {"example": "example"},
            HTTP_AUTHORIZATION="Bearer valid-token")
        assert response.status_code == 200
        assert response.data.get("user") == user.email

        # Response should be cached and you shouldn't hit the 401
        response = csrf_api_client.post(
            "/bearer/", {"example": "example"},
            HTTP_AUTHORIZATION="Bearer valid-token")
        assert response.status_code == 200
        assert response.data.get("user") == user.email

        # Sleep till cache expires
        time.sleep(2)
        response = csrf_api_client.post(
            "/bearer/", {"example": "example"},
            HTTP_AUTHORIZATION="Bearer valid-token")
        assert response.status_code == 401
Пример #29
0
    def test_disabling_recipe_removes_approval(self):
        recipe = RecipeFactory(approver=UserFactory(), enabled=True)
        assert recipe.is_approved

        recipe.enabled = False
        recipe.save()
        recipe.refresh_from_db()
        assert not recipe.is_approved
Пример #30
0
 def test_it_does_not_resign_up_to_date_recipes(self, settings,
                                                mocked_autograph):
     r = RecipeFactory(approver=UserFactory(), enabled=True, signed=True)
     r.signature.signature = 'original signature'
     r.signature.save()
     call_command('update_recipe_signatures')
     r.refresh_from_db()
     assert r.signature.signature == 'original signature'
Пример #31
0
 def test_user_is_not_active(self, bare_api_client):
     user = UserFactory(username="******",
                        email="*****@*****.**",
                        is_active=False)
     response = bare_api_client.post(
         "/insecure/", {"example": "example"},
         HTTP_AUTHORIZATION=f"Insecure {user.email}")
     assert response.status_code == 401, (response.data, response.user)
Пример #32
0
    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"
Пример #33
0
    def test_it_can_enable_recipes(self, api_client):
        recipe = RecipeFactory(enabled=False, approver=UserFactory())

        res = api_client.post('/api/v1/recipe/%s/enable/' % recipe.id)
        assert res.status_code == 200
        assert res.data['enabled'] is True

        recipe = Recipe.objects.all()[0]
        assert recipe.enabled