Beispiel #1
0
    def test_republishes_outdated_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.
        to_update = {**exports.recipe_as_record(r2), "name": "Outdated name"}
        requestsmock.get(
            self.published_records_url,
            json={"data": [exports.recipe_as_record(r1), to_update]})
        # It will be updated.
        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)
Beispiel #2
0
    def test_unpublishes_extra_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", approver=UserFactory())
        # Mock the server responses.
        # `r2` should not be on the server (not enabled)
        requestsmock.get(
            self.published_records_url,
            json={
                "data":
                [exports.recipe_as_record(r1),
                 exports.recipe_as_record(r2)]
            },
        )
        # It will be deleted.
        r2_url = self.workspace_collection_url + f"/records/{r2.id}"
        requestsmock.delete(r2_url, json={"data": {}})

        call_command("sync_remote_settings")

        assert requestsmock.request_history[-2].method == "DELETE"
        assert requestsmock.request_history[-2].url.endswith(r2_url)
Beispiel #3
0
    def test_unpublishes_extra_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.capabilities_workspace_collection_url, json={})
        # Instantiate local recipes.
        r1 = RecipeFactory(name="Test 1", enabler=UserFactory(), approver=UserFactory())
        r2 = RecipeFactory(name="Test 2", approver=UserFactory())
        # Mock the server responses.
        # `r2` should not be on the server (not enabled)
        requestsmock.get(
            self.capabilities_published_records_url,
            json={"data": [exports.recipe_as_record(r1), exports.recipe_as_record(r2)]},
        )
        # It will be deleted.
        r2_capabilities_url = self.capabilities_workspace_collection_url + f"/records/{r2.id}"
        requestsmock.delete(r2_capabilities_url, json={"data": ""})

        # Ignore any requests before this point
        requestsmock._adapter.request_history = []
        call_command("sync_remote_settings")

        requests = requestsmock.request_history
        # The first request should be to get the existing records
        assert requests[0].method == "GET"
        assert requests[0].url.endswith(self.capabilities_published_records_url)
        # The next one should be to PUT the outdated recipe2
        assert requests[1].method == "DELETE"
        assert requests[1].url.endswith(r2_capabilities_url)
        # The final one should be to approve the changes
        assert requests[2].method == "PATCH"
        assert requests[2].url.endswith(self.capabilities_workspace_collection_url)
        # And there are no extra requests
        assert len(requests) == 3
Beispiel #4
0
    def test_republishes_outdated_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.baseline_workspace_collection_url, json={})
        requestsmock.patch(self.capabilities_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())

        rs_settings.BASELINE_CAPABILITIES |= r1.capabilities
        rs_settings.BASELINE_CAPABILITIES |= r2.capabilities

        # Mock the server responses.
        to_update = {**exports.recipe_as_record(r2), "name": "Outdated name"}
        requestsmock.get(
            self.baseline_published_records_url,
            json={"data": [exports.recipe_as_record(r1), to_update]},
        )
        requestsmock.get(
            self.capabilities_published_records_url,
            json={"data": [exports.recipe_as_record(r1), to_update]},
        )
        # It will be updated.
        r2_baseline_url = self.baseline_workspace_collection_url + f"/records/{r2.id}"
        r2_capabilities_url = self.capabilities_workspace_collection_url + f"/records/{r2.id}"
        requestsmock.put(r2_baseline_url, json={})
        requestsmock.put(r2_capabilities_url, json={})

        # Ignore any requests before this point
        requestsmock._adapter.request_history = []
        call_command("sync_remote_settings")

        requests = requestsmock.request_history
        # The first two requests should be to get the existing records
        assert requests[0].method == "GET"
        assert requests[0].url.endswith(
            self.capabilities_published_records_url)
        assert requests[1].method == "GET"
        assert requests[1].url.endswith(self.baseline_published_records_url)
        # The next two should be to PUT the outdated recipe2
        assert requests[2].method == "PUT"
        assert requests[2].url.endswith(r2_capabilities_url)
        assert requests[3].method == "PUT"
        assert requests[3].url.endswith(r2_baseline_url)
        # The final two should be to approve the changes to the two collections
        assert requests[4].method == "PATCH"
        assert requests[4].url.endswith(
            self.capabilities_workspace_collection_url)
        assert requests[5].method == "PATCH"
        assert requests[5].url.endswith(self.baseline_workspace_collection_url)
        # And there are no extra requests
        assert len(requests) == 6
Beispiel #5
0
    def test_unpublish_reverts_changes_if_approval_fails(
            self, rs_settings, requestsmock):
        recipe = RecipeFactory(name="Test", approver=UserFactory())
        record = exports.recipe_as_record(recipe)
        unchanged = exports.recipe_as_record(
            RecipeFactory(name="Unchanged", 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}")
        records_url = f"{collection_url}/records"
        record_url = f"{collection_url}/records/{recipe.id}"
        records_prod_url = records_url.replace(
            f"/buckets/{rs_settings.REMOTE_SETTINGS_BUCKET_ID}/",
            f"/buckets/{exports.RemoteSettings.MAIN_BUCKET_ID}/",
        )
        # Deleting the record works.
        requestsmock.request("delete",
                             record_url,
                             content=b'{"data": {"deleted":true}}')
        # Approving fails.
        requestsmock.request("patch", collection_url, status_code=403)
        # Simulate that the record exists in prod but not in workspace
        requestsmock.request("get",
                             records_url,
                             content=json.dumps({
                                 "data": [unchanged]
                             }).encode())
        requestsmock.request("get",
                             records_prod_url,
                             content=json.dumps({
                                 "data": [unchanged, record]
                             }).encode())
        # Reverting changes means recreating this record.
        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) == 5
        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[1].method == "PATCH"
        assert requestsmock.request_history[2].url == records_url
        assert requestsmock.request_history[3].url == records_prod_url
        assert requestsmock.request_history[4].url == record_url
        assert requestsmock.request_history[4].method == "PUT"
        submitted = requestsmock.request_history[4].json()
        assert submitted["data"]["id"] == str(recipe.id)
        assert submitted["data"]["recipe"]["name"] == "Test"
Beispiel #6
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
Beispiel #7
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}'")
Beispiel #8
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"
def compare_remote(recipe, record):
    as_record = recipe_as_record(recipe)
    cleaned_record = {
        k: v
        for k, v in record.items() if k not in KINTO_INTERNAL_FIELDS
    }
    return as_record == cleaned_record
Beispiel #10
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.action.name,
                "arguments": recipe.arguments,
                "filter_expression": recipe.filter_expression,
                "id": recipe.id,
                "name": recipe.name,
                "revision_id": str(recipe.revision_id),
            },
            "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://"),
            },
        }
Beispiel #11
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}'")
def compare_remote(recipe, record):
    as_record = recipe_as_record(recipe)
    cleaned_record = {k: v for k, v in record.items() if k not in KINTO_INTERNAL_FIELDS}
    return as_record == cleaned_record