コード例 #1
0
ファイル: test_models.py プロジェクト: leplatrem/normandy
    def test_reject(self, mocker):
        u = UserFactory()
        req = ApprovalRequestFactory()
        mocker.patch.object(req, 'verify_approver')

        req.reject(u, 'r-')
        assert not req.approved
        assert req.approver == u
        assert req.comment == 'r-'
        req.verify_approver.assert_called_with(u)

        recipe = req.revision.recipe
        assert not recipe.is_approved
コード例 #2
0
ファイル: test_models.py プロジェクト: punkstar25/normandy
    def test_no_errors(self):
        action = ActionFactory(name="show-heartbeat")
        arguments = {
            "repeatOption": "nag",
            "surveyId": "001",
            "message": "Message!",
            "learnMoreMessage": "More!?!",
            "learnMoreUrl": "https://example.com/learnmore",
            "engagementButtonLabel": "Label!",
            "thanksMessage": "Thanks!",
            "postAnswerUrl": "https://example.com/answer",
            "includeTelemetryUUID": True,
        }
        # 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())
        assert rev.arguments["surveyId"] == "001"
コード例 #3
0
    def test_publish_raises_an_error_if_request_fails(self, rs_urls,
                                                      rs_settings,
                                                      requestsmock):
        recipe = RecipeFactory(name="Test", approver=UserFactory())
        record_url = rs_urls["workspace"]["capabilities"]["record"].format(
            recipe.id)
        requestsmock.request("put", record_url, status_code=503)

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

        assert requestsmock.call_count == rs_settings.REMOTE_SETTINGS_RETRY_REQUESTS + 1
コード例 #4
0
    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.enabled
コード例 #5
0
    def test_list_filter_status(self, api_client):
        r1 = RecipeFactory(enabled=False)
        r2 = RecipeFactory(approver=UserFactory(), enabled=True)

        res = api_client.get('/api/v1/recipe/?status=enabled')
        assert res.status_code == 200
        assert len(res.data) == 1
        assert res.data[0]['id'] == r2.id

        res = api_client.get('/api/v1/recipe/?status=disabled')
        assert res.status_code == 200
        assert len(res.data) == 1
        assert res.data[0]['id'] == r1.id
コード例 #6
0
    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(approver=UserFactory(),
                               name="unchanged",
                               signed=True)
        original_signature = recipe.signature
        recipe.revise(name="changed")
        assert recipe.latest_revision.name == "changed"
        assert recipe.signature is not original_signature
        assert recipe.signature is None
コード例 #7
0
ファイル: test_commands.py プロジェクト: mythmon/normandy
    def test_signatures_are_updated(self, mocked_autograph, 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},
            approver=UserFactory(),
            enabler=UserFactory(),
            signed=True,
        )
        # preconditions
        assert recipe.signature is not None
        assert recipe.signature.signature == hashlib.sha256(recipe.canonical_json()).hexdigest()
        signature_before = recipe.signature.signature

        call_command("update_addon_urls")
        recipe.refresh_from_db()

        assert recipe.signature is not None
        assert recipe.signature != signature_before
        assert recipe.signature.signature == hashlib.sha256(recipe.canonical_json()).hexdigest()
コード例 #8
0
    def test_approve(self, mocker):
        u = UserFactory()
        req = ApprovalRequestFactory()
        mocker.patch.object(req, "verify_approver")

        req.approve(u, "r+")
        assert req.approved
        assert req.approver == u
        assert req.comment == "r+"
        req.verify_approver.assert_called_with(u)

        recipe = req.revision.recipe
        assert recipe.is_approved
コード例 #9
0
ファイル: test_models.py プロジェクト: punkstar25/normandy
    def test_verify_approver_unenforced(self, settings, mocker):
        logger = mocker.patch("normandy.recipes.models.logger")
        settings.PEER_APPROVAL_ENFORCED = False

        creator = UserFactory()
        user = UserFactory()
        req = ApprovalRequestFactory(creator=creator)

        # Do not raise when creator and approver are different
        req.verify_approver(user)

        # Do not raise when creator and approver are the same since enforcement
        # is disabled.
        req.verify_approver(creator)
        logger.warning.assert_called_with(
            Whatever(),
            extra={
                "code": WARNING_BYPASSING_PEER_APPROVAL,
                "approval_id": req.id,
                "approver": creator,
            },
        )
コード例 #10
0
ファイル: test_api.py プロジェクト: mythmon/normandy
    def test_must_have_permission(self, api_client):
        user = UserFactory(is_superuser=False)
        api_client.force_authenticate(user=user)
        res = api_client.get("/api/v3/group/")
        assert res.status_code == 403

        ct = ContentType.objects.get_for_model(User)
        permission = Permission.objects.get(codename="change_user",
                                            content_type=ct)
        user.user_permissions.add(permission)
        user = User.objects.get(pk=user.pk)
        api_client.force_authenticate(user=user)
        res = api_client.get("/api/v3/group/")
        assert res.status_code == 200
コード例 #11
0
    def test_publish_reverts_changes_if_approval_fails(self, rs_urls,
                                                       rs_settings,
                                                       requestsmock):
        # This test forces the recipe to not use baseline capabilities to
        # simplify the test. This simplifies the test.
        recipe = RecipeFactory(name="Test", approver=UserFactory())
        assert not recipe.uses_only_baseline_capabilities()

        capabilities_record_url = rs_urls["workspace"]["capabilities"][
            "record"].format(recipe.id)
        # Creating the record works.
        requestsmock.request("put",
                             capabilities_record_url,
                             json={"data": {}},
                             status_code=201)
        requestsmock.register_uri(
            "patch",
            rs_urls["workspace"]["capabilities"]["collection"],
            [
                # Approving fails.
                {
                    "status_code": 403
                },
                # Rollback succeeds.
                {
                    "status_code": 200,
                    "json": {
                        "data": {}
                    }
                },
            ],
        )

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

        requests = requestsmock.request_history
        assert len(requests) == 3
        # First it publishes a recipe
        assert requests[0].method == "PUT"
        assert requests[0].url == capabilities_record_url
        # and then tries to approve it, which fails.
        assert requests[1].method == "PATCH"
        assert requests[1].url == rs_urls["workspace"]["capabilities"][
            "collection"]
        # so it rollsback
        assert requests[2].method == "PATCH"
        assert requests[2].url == rs_urls["workspace"]["capabilities"][
            "collection"]
コード例 #12
0
ファイル: test_api.py プロジェクト: rbillings/normandy
    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"
コード例 #13
0
    def test_publish_raises_an_error_if_request_fails(self, rs_settings,
                                                      requestsmock):
        recipe = RecipeFactory(name="Test", approver=UserFactory())
        record_url = (
            f"{rs_settings.REMOTE_SETTINGS_URL}/buckets/{rs_settings.REMOTE_SETTINGS_BUCKET_ID}"
            f"/collections/{rs_settings.REMOTE_SETTINGS_COLLECTION_ID}"
            f"/records/{recipe.id}")
        requestsmock.request("put", record_url, status_code=503)

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

        assert requestsmock.call_count == rs_settings.REMOTE_SETTINGS_RETRY_REQUESTS + 1
コード例 #14
0
    def test_publish_puts_record_and_approves(self, rs_urls, rs_settings,
                                              requestsmock, mock_logger):
        """Test that requests are sent to Remote Settings on publish."""

        recipe = RecipeFactory(name="Test", approver=UserFactory())
        rs_settings.BASELINE_CAPABILITIES |= recipe.capabilities

        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()}",
        }

        for collection in ["baseline", "capabilities"]:
            requestsmock.request(
                "PUT",
                rs_urls["workspace"][collection]["record"].format(recipe.id),
                json={"data": {}},
                request_headers=request_headers,
            )
            requestsmock.request(
                "PATCH",
                rs_urls["workspace"][collection]["collection"],
                json={"data": {}},
                request_headers=request_headers,
            )

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

        requests = requestsmock.request_history
        assert len(requests) == 4
        assert requests[0].url == rs_urls["workspace"]["capabilities"][
            "record"].format(recipe.id)
        assert requests[0].method == "PUT"
        assert requests[0].json() == {"data": exports.recipe_as_record(recipe)}
        assert requests[1].url == rs_urls["workspace"]["baseline"][
            "record"].format(recipe.id)
        assert requests[1].method == "PUT"
        assert requests[1].json() == {"data": exports.recipe_as_record(recipe)}
        assert requests[2].method == "PATCH"
        assert requests[2].url == rs_urls["workspace"]["capabilities"][
            "collection"]
        assert requests[3].method == "PATCH"
        assert requests[3].url == rs_urls["workspace"]["baseline"][
            "collection"]
        mock_logger.info.assert_called_with(
            f"Published record '{recipe.id}' for recipe '{recipe.name}'")
コード例 #15
0
    def test_recipe_as_remotesettings_record(self, mocked_autograph):
        """Test that recipes are serialized as expected by our clients."""

        recipe = RecipeFactory(name="Test",
                               approver=UserFactory(),
                               enabler=UserFactory(),
                               signed=True)

        record = exports.recipe_as_record(recipe)
        assert record == {
            "id": str(recipe.id),
            "recipe": {
                "action":
                recipe.approved_revision.action.name,
                "arguments":
                recipe.approved_revision.arguments,
                "filter_expression":
                recipe.approved_revision.filter_expression,
                "id":
                recipe.id,
                "name":
                recipe.approved_revision.name,
                "revision_id":
                str(recipe.approved_revision.id),
                "capabilities":
                Whatever(lambda caps: set(caps) == recipe.approved_revision.
                         capabilities),
                "uses_only_baseline_capabilities":
                False,
            },
            "signature": {
                "public_key": Whatever.regex(r"[a-zA-Z0-9/+]{160}"),
                "signature": Whatever.regex(r"[a-f0-9]{40}"),
                "timestamp": Whatever.iso8601(),
                "x5u": Whatever.startswith("https://"),
            },
        }
コード例 #16
0
    def test_signatures_are_updated(self, mocked_autograph):
        action = ActionFactory(name="opt-out-study")
        recipe = RecipeFactory(
            action=action,
            arguments={
                "addonUrl": "https://before.example.com/extensions/addon.xpi"
            },
            approver=UserFactory(),
            enabler=UserFactory(),
            signed=True,
        )
        # preconditions
        assert recipe.signature is not None
        assert recipe.signature.signature == hashlib.sha256(
            recipe.canonical_json()).hexdigest()
        signature_before = recipe.signature.signature

        call_command("update_addon_urls", "after.example.com")
        recipe.refresh_from_db()

        assert recipe.signature is not None
        assert recipe.signature != signature_before
        assert recipe.signature.signature == hashlib.sha256(
            recipe.canonical_json()).hexdigest()
コード例 #17
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 == "*****@*****.**"
コード例 #18
0
ファイル: test_commands.py プロジェクト: amreshk005/normandy
    def test_publishes_missing_recipes(self, rs_settings, requestsmock):
        # Some records will be created with PUT.
        requestsmock.put(requests_mock.ANY, json={})
        # A signature request will be sent.
        requestsmock.patch(self.workspace_collection_url, json={})
        # Instantiate local recipes.
        r1 = RecipeFactory(name="Test 1",
                           enabler=UserFactory(),
                           approver=UserFactory())
        r2 = RecipeFactory(name="Test 2",
                           enabler=UserFactory(),
                           approver=UserFactory())
        # Mock the server responses.
        # `r2` should be on the server
        requestsmock.get(self.published_records_url,
                         json={"data": [exports.recipe_as_record(r1)]})
        # It will be created.
        r2_url = self.workspace_collection_url + f"/records/{r2.id}"
        requestsmock.put(r2_url, json={})

        call_command("sync_remote_settings")

        assert requestsmock.request_history[-2].method == "PUT"
        assert requestsmock.request_history[-2].url.endswith(r2_url)
コード例 #19
0
    def test_signed_listing_filters_by_enabled(Self, api_client):
        enabled_recipe = RecipeFactory(signed=True,
                                       approver=UserFactory(),
                                       enabled=True)
        disabled_recipe = RecipeFactory(signed=True, enabled=False)

        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
コード例 #20
0
ファイル: test_api.py プロジェクト: rbillings/normandy
    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 == "*****@*****.**"
コード例 #21
0
    def test_unpublish_ignores_error_about_missing_record(
            self, rs_settings, requestsmock, mock_logger):
        recipe = RecipeFactory(name="Test", approver=UserFactory())
        record_url = (
            f"{rs_settings.REMOTE_SETTINGS_URL}/buckets/{rs_settings.REMOTE_SETTINGS_BUCKET_ID}"
            f"/collections/{rs_settings.REMOTE_SETTINGS_COLLECTION_ID}"
            f"/records/{recipe.id}")
        requestsmock.request("delete", record_url, status_code=404)

        remotesettings = exports.RemoteSettings()
        # Assert doesn't raise.
        remotesettings.unpublish(recipe)

        assert requestsmock.call_count == 1
        mock_logger.warning.assert_called_with(
            f"The recipe '{recipe.id}' was never published. Skip.")
コード例 #22
0
    def test_unpublish_reverts_changes_if_approval_fails(
            self, rs_urls, rs_settings, requestsmock):
        recipe = RecipeFactory(name="Test", approver=UserFactory())
        capabilities_record_url = rs_urls["workspace"]["record"].format(
            recipe.id)
        # Deleting the record works.
        requestsmock.request("delete",
                             capabilities_record_url,
                             json={"data": {
                                 "deleted": True
                             }})

        requestsmock.register_uri(
            "patch",
            rs_urls["workspace"]["collection"],
            [
                # Approving fails.
                {
                    "status_code": 403
                },
                # Rollback succeeds.
                {
                    "status_code": 200,
                    "json": {
                        "data": {}
                    }
                },
            ],
        )

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

        requests = requestsmock.request_history
        assert len(requests) == 3
        # Unpublish the recipe in collection
        assert requests[0].url == capabilities_record_url
        assert requests[0].method == "DELETE"
        # Try (and fail) to approve the capabilities change
        assert requests[1].url == rs_urls["workspace"]["collection"]
        assert requests[1].method == "PATCH"
        # so it rollsback collection
        assert requests[2].method == "PATCH"
        assert requests[2].url == rs_urls["workspace"]["collection"]
コード例 #23
0
    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
コード例 #24
0
    def test_unpublish_ignores_error_about_missing_records(
            self, rs_urls, rs_settings, requestsmock, mock_logger):
        recipe = RecipeFactory(name="Test", approver=UserFactory())
        capabilities_record_url = rs_urls["workspace"]["record"].format(
            recipe.id)
        warning_message = (
            f"The recipe '{recipe.id}' was not published in the {{}} collection. Skip."
        )

        requestsmock.request("delete",
                             capabilities_record_url,
                             status_code=404)
        remotesettings = exports.RemoteSettings()
        # Assert doesn't raise.
        remotesettings.unpublish(recipe)
        assert mock_logger.warning.call_args_list == [
            call(warning_message.format("capabilities"))
        ]
コード例 #25
0
ファイル: test_models.py プロジェクト: micdurodola/normandy
    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
コード例 #26
0
ファイル: test_models.py プロジェクト: leplatrem/normandy
    def test_signatures_update_correctly_on_enable(self, mocker):
        mock_autograph = mocker.patch('normandy.recipes.models.Autographer')

        def fake_sign(datas):
            sigs = []
            for d in datas:
                sigs.append({'signature': hashlib.sha256(d).hexdigest()})
            return sigs

        mock_autograph.return_value.sign_data.side_effect = fake_sign

        recipe = RecipeFactory(enabled=False,
                               signed=False,
                               approver=UserFactory())
        recipe.enabled = True
        recipe.save()
        recipe.refresh_from_db()

        assert recipe.signature is not None
        assert recipe.signature.signature == hashlib.sha256(
            recipe.canonical_json()).hexdigest()
コード例 #27
0
    def test_retry_on_5xx_error(self, csrf_api_client, settings, requestsmock,
                                caplog):
        user = UserFactory()
        user_data = json.dumps({
            "email": user.email,
            "given_name": user.first_name,
            "family_name": user.last_name
        }).encode("utf-8")

        requestsmock.get(
            settings.OIDC_USER_ENDPOINT,
            [
                {
                    "status_code": 504
                },
                {
                    "status_code": 504
                },
                {
                    "content": user_data,
                    "status_code": 200
                },
            ],
        )

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

        # Count the number of retries
        retry_count = 0
        expected_message = "requests.exceptions.RequestException: 504 on {}".format(
            settings.OIDC_USER_ENDPOINT)
        for record in caplog.records:
            if expected_message in record.message:
                retry_count += 1

        assert retry_count == 2
コード例 #28
0
    def test_publish_puts_record_and_approves(self, rs_settings, requestsmock,
                                              mock_logger):
        """Test that requests are sent to Remote Settings on publish."""

        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("put",
                             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.publish(recipe)

        assert len(requestsmock.request_history) == 2
        assert requestsmock.request_history[0].url == record_url
        assert requestsmock.request_history[0].method == "PUT"
        assert requestsmock.request_history[0].json() == {
            "data": exports.recipe_as_record(recipe)
        }
        assert requestsmock.request_history[1].method == "PATCH"
        assert requestsmock.request_history[1].url == collection_url
        mock_logger.info.assert_called_with(
            f"Published record '{recipe.id}' for recipe '{recipe.name}'")
コード例 #29
0
    def test_unpublish_deletes_record_and_approves(self, rs_urls, rs_settings,
                                                   requestsmock, mock_logger):
        """Test that requests are sent to Remote Settings on unpublish."""

        recipe = RecipeFactory(name="Test", approver=UserFactory())
        urls = rs_urls["workspace"]

        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",
            urls["record"].format(recipe.id),
            json={"data": {}},
            request_headers=request_headers,
        )
        requestsmock.request("patch",
                             urls["collection"],
                             json={"data": {}},
                             request_headers=request_headers)

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

        assert len(requestsmock.request_history) == 2
        requests = requestsmock.request_history
        assert requests[0].url == urls["record"].format(recipe.id)
        assert requests[0].method == "DELETE"
        assert requests[1].url == urls["collection"]
        assert requests[1].method == "PATCH"
        mock_logger.info.assert_called_with(
            f"Deleted record '{recipe.id}' of recipe '{recipe.approved_revision.name}'"
        )
コード例 #30
0
    def test_full_approval_flow(self, settings, api_client, mocked_autograph):
        settings.PEER_APPROVAL_ENFORCED = True

        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_data_0 = res.json()

        # Request approval for it
        res = api_client.post(
            '/api/v1/recipe_revision/{}/request_approval/'.format(
                recipe_data_0['revision_id']))
        approval_data = res.json()
        assert res.status_code == 201

        # The requester isn't allowed to approve a recipe
        res = api_client.post(
            '/api/v1/approval_request/{}/approve/'.format(approval_data['id']),
            {'comment': 'r+'})
        assert res.status_code == 403  # Forbidden

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

        # It is now visible in the API
        res = api_client.get('/api/v1/recipe/{}/'.format(recipe_data_0['id']))
        assert res.status_code == 200
        recipe_data_1 = res.json()
        self.verify_signatures(api_client, expected_count=1)

        # Make another change
        api_client.force_authenticate(user1)
        res = api_client.patch(
            '/api/v1/recipe/{}/'.format(recipe_data_1['id']), {
                'extra_filter_expression': 'counter == 1',
            })
        assert res.status_code == 200

        # The change should not be visible yet, since it isn't approved
        res = api_client.get('/api/v1/recipe/{}/'.format(recipe_data_1['id']))
        assert res.status_code == 200
        recipe_data_2 = res.json()
        assert recipe_data_2['extra_filter_expression'] == 'counter == 0'
        self.verify_signatures(api_client, expected_count=1)

        # Request approval for the change
        res = api_client.post(
            '/api/v1/recipe_revision/{}/request_approval/'.format(
                recipe_data_2['latest_revision_id']))
        approval_data = res.json()
        recipe_data_2['approval_request'] = approval_data
        assert res.status_code == 201

        # The change should not be visible yet, since it isn't approved
        res = api_client.get('/api/v1/recipe/{}/'.format(recipe_data_1['id']))
        assert res.status_code == 200
        assert res.json() == recipe_data_2
        self.verify_signatures(api_client, expected_count=1)

        # Reject the change
        api_client.force_authenticate(user2)
        res = api_client.post(
            '/api/v1/approval_request/{}/reject/'.format(approval_data['id']),
            {'comment': 'r-'})
        approval_data = res.json()
        recipe_data_2['approval_request'] = approval_data
        assert res.status_code == 200

        # The change should not be visible yet, since it isn't approved
        res = api_client.get('/api/v1/recipe/{}/'.format(recipe_data_1['id']))
        assert res.status_code == 200
        assert res.json() == recipe_data_2
        self.verify_signatures(api_client, expected_count=1)

        # Make a third version of the recipe
        api_client.force_authenticate(user1)
        res = api_client.patch(
            '/api/v1/recipe/{}/'.format(recipe_data_1['id']), {
                'extra_filter_expression': 'counter == 2',
            })
        recipe_data_3 = res.json()
        assert res.status_code == 200

        # Request approval
        res = api_client.post(
            '/api/v1/recipe_revision/{}/request_approval/'.format(
                recipe_data_3['latest_revision_id']))
        approval_data = res.json()
        assert res.status_code == 201

        # Approve the change
        api_client.force_authenticate(user2)
        res = api_client.post(
            '/api/v1/approval_request/{}/approve/'.format(approval_data['id']),
            {'comment': 'r+'})
        assert res.status_code == 200

        # The change should be visible now, since it is approved
        res = api_client.get('/api/v1/recipe/{}/'.format(recipe_data_1['id']))
        assert res.status_code == 200
        recipe_data_4 = res.json()
        assert recipe_data_4['extra_filter_expression'] == 'counter == 2'
        assert recipe_data_4['latest_revision_id'] == recipe_data_4[
            'revision_id']
        self.verify_signatures(api_client, expected_count=1)