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)
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)
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
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
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"
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
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}'")
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
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://"), }, }
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