def rs_with_mocked_client(): nonlocal client_mock assert client_mock is None rs = exports.RemoteSettings() client_mock = mocker.MagicMock() rs.client = client_mock return rs
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 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}'")
def test_publish_and_unpublish_are_noop_if_not_enabled(self, requestsmock): recipe = RecipeFactory(name="Test", approver=UserFactory(), enabler=UserFactory()) remotesettings = exports.RemoteSettings() remotesettings.publish(recipe) remotesettings.unpublish(recipe) assert len(requestsmock.request_history) == 0
def test_publish_and_unpublish_non_baseline_recipe_only_to_capabilities_collection( self, rs_settings, rs_urls, requestsmock): ws_urls = rs_urls["workspace"] recipe = RecipeFactory() assert not recipe.uses_only_baseline_capabilities() # Expect calls only to the capabilities collection requestsmock.put(ws_urls["capabilities"]["record"].format(recipe.id), json={"data": {}}, status_code=201) requestsmock.patch(ws_urls["capabilities"]["collection"], json={"data": {}}) remotesettings = exports.RemoteSettings() remotesettings.publish(recipe) requests = requestsmock.request_history assert len(requests) == 2 # First it publishes the recipe assert requests[0].method == "PUT" assert requests[0].url == ws_urls["capabilities"]["record"].format( recipe.id) # and then approves the change assert requests[1].method == "PATCH" assert requests[1].url == rs_urls["workspace"]["capabilities"][ "collection"] # reset request history requestsmock._adapter.request_history = [] # Expect delete calls requestsmock.delete(ws_urls["capabilities"]["record"].format( recipe.id), json={"data": {}}) # Baseline is expected, just in case the recipe changed from baseline to not baseline requestsmock.delete(ws_urls["baseline"]["record"].format(recipe.id), status_code=404) remotesettings.unpublish(recipe) requests = requestsmock.request_history assert len(requests) == 3 # First it removes the recipe assert requests[0].method == "DELETE" assert requests[0].url == ws_urls["capabilities"]["record"].format( recipe.id) # Tries to delete from baseline, just in case assert requests[1].method == "DELETE" assert requests[1].url == ws_urls["baseline"]["record"].format( recipe.id) # and then approves the change only from capabilities assert requests[2].method == "PATCH" assert requests[2].url == rs_urls["workspace"]["capabilities"][ "collection"]
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"]["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
def test_it_checks_config(self, settings): """Test that each required key is required individually""" # Leave out URL with Remote Settings (default) settings.REMOTE_SETTINGS_URL = None # assert doesn't raise exports.RemoteSettings().check_config() # Set empty URL settings.REMOTE_SETTINGS_URL = "" # assert doesn't raise exports.RemoteSettings().check_config() # Leave out USERNAME settings.REMOTE_SETTINGS_URL = "http://some-server/v1" settings.REMOTE_SETTINGS_USERNAME = None settings.REMOTE_SETTINGS_PASSWORD = "******" with pytest.raises(ImproperlyConfigured) as exc: exports.RemoteSettings().check_config() assert "REMOTE_SETTINGS_USERNAME" in str(exc.value) # Set empty USERNAME settings.REMOTE_SETTINGS_USERNAME = "" with pytest.raises(ImproperlyConfigured) as exc: exports.RemoteSettings().check_config() assert "REMOTE_SETTINGS_USERNAME" in str(exc.value) # Leave out PASSWORD settings.REMOTE_SETTINGS_URL = "http://some-server/v1" settings.REMOTE_SETTINGS_USERNAME = "******" settings.REMOTE_SETTINGS_PASSWORD = None with pytest.raises(ImproperlyConfigured) as exc: exports.RemoteSettings().check_config() assert "REMOTE_SETTINGS_PASSWORD" in str(exc.value) # Leave out BASELINE_COLLECTION_ID settings.REMOTE_SETTINGS_URL = "http://some-server/v1" settings.REMOTE_SETTINGS_USERNAME = "******" settings.REMOTE_SETTINGS_PASSWORD = "******" settings.REMOTE_SETTINGS_BASELINE_COLLECTION_ID = None settings.REMOTE_SETTINGS_CAPABILITIES_COLLECTION_ID = "normandy-recipes-capabilities" with pytest.raises(ImproperlyConfigured) as exc: exports.RemoteSettings().check_config() assert "REMOTE_SETTINGS_BASELINE_COLLECTION_ID" in str(exc.value) # Leave out CAPABILITIES_COLLECTION_ID settings.REMOTE_SETTINGS_URL = "http://some-server/v1" settings.REMOTE_SETTINGS_USERNAME = "******" settings.REMOTE_SETTINGS_PASSWORD = "******" settings.REMOTE_SETTINGS_BASELINE_COLLECTION_ID = "normandy-recipes" settings.REMOTE_SETTINGS_CAPABILITIES_COLLECTION_ID = None with pytest.raises(ImproperlyConfigured) as exc: exports.RemoteSettings().check_config() assert "REMOTE_SETTINGS_CAPABILITIES_COLLECTION_ID" in str(exc.value)
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_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
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"]
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_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.")
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"]
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")) ]
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}'" )
def test_check_connection(self, rs_settings, requestsmock): # Root URL should return currently authenticated user. requestsmock.get(f"{rs_settings.REMOTE_SETTINGS_URL}/", json={"capabilities": {}}) with pytest.raises(ImproperlyConfigured) as exc: exports.RemoteSettings().check_config() assert "Invalid Remote Settings credentials" in str(exc) requestsmock.get( f"{rs_settings.REMOTE_SETTINGS_URL}/", json={ "user": { "id": f"account:{rs_settings.REMOTE_SETTINGS_USERNAME}" }, "capabilities": { "signer": { "to_review_enabled": True, "group_check_enabled": True, "resources": [{ "source": { "bucket": "main-workspace", "collection": None }, "destination": { "bucket": "main", "collection": None }, }], } }, }, ) # Collection should be writable. collection_url = ( f"{rs_settings.REMOTE_SETTINGS_URL}/buckets/{rs_settings.REMOTE_SETTINGS_BUCKET_ID}" f"/collections/{rs_settings.REMOTE_SETTINGS_COLLECTION_ID}") requestsmock.get(collection_url, json={"data": {}, "permissions": {}}) with pytest.raises(ImproperlyConfigured) as exc: exports.RemoteSettings().check_config() assert ( f"Remote Settings collection {rs_settings.REMOTE_SETTINGS_COLLECTION_ID} " "is not writable") in str(exc) requestsmock.get( collection_url, json={ "data": {}, "permissions": { "write": [f"account:{rs_settings.REMOTE_SETTINGS_USERNAME}"] }, }, ) # Collection review should be explicitly disabled. with pytest.raises(ImproperlyConfigured) as exc: exports.RemoteSettings().check_config() assert ("Review was not disabled on Remote Settings collection " f"{rs_settings.REMOTE_SETTINGS_COLLECTION_ID}.") in str(exc) requestsmock.get( f"{rs_settings.REMOTE_SETTINGS_URL}/", json={ "user": { "id": f"account:{rs_settings.REMOTE_SETTINGS_USERNAME}" }, "capabilities": { "signer": { "to_review_enabled": True, "group_check_enabled": True, "resources": [ { "source": { "bucket": "main-workspace", "collection": None }, "destination": { "bucket": "main", "collection": None }, }, { "source": { "bucket": "main-workspace", "collection": "normandy-recipes", }, "destination": { "bucket": "main", "collection": "normandy-recipes", }, "to_review_enabled": False, "group_check_enabled": False, }, ], } }, }, ) # Assert does not raise. exports.RemoteSettings().check_config()
def test_publish_and_unpublish_baseline_recipe_to_both_collections( self, rs_settings, rs_urls, requestsmock): ws_urls = rs_urls["workspace"] recipe = RecipeFactory() rs_settings.BASELINE_CAPABILITIES |= recipe.capabilities assert recipe.uses_only_baseline_capabilities() # Expect publish calls to both collections requestsmock.put(ws_urls["baseline"]["record"].format(recipe.id), json={"data": {}}, status_code=201) requestsmock.put(ws_urls["capabilities"]["record"].format(recipe.id), json={"data": {}}, status_code=201) # Expect both workspaces to be approved requestsmock.patch(ws_urls["baseline"]["collection"], json={"data": {}}) requestsmock.patch(ws_urls["capabilities"]["collection"], json={"data": {}}) remotesettings = exports.RemoteSettings() remotesettings.publish(recipe) requests = requestsmock.request_history assert len(requests) == 4 # First it publishes a recipe to both collections assert requests[0].method == "PUT" assert requests[0].url == ws_urls["capabilities"]["record"].format( recipe.id) assert requests[1].method == "PUT" assert requests[1].url == ws_urls["baseline"]["record"].format( recipe.id) # and then approves both changes 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"] # reset request history requestsmock._adapter.request_history = [] # Expect delete calls requestsmock.delete(ws_urls["baseline"]["record"].format(recipe.id), json={"data": {}}) requestsmock.delete(ws_urls["capabilities"]["record"].format( recipe.id), json={"data": {}}) remotesettings.unpublish(recipe) requests = requestsmock.request_history assert len(requests) == 4 # First it removes the recipe from both collections assert requests[0].method == "DELETE" assert requests[0].url == ws_urls["capabilities"]["record"].format( recipe.id) assert requests[1].method == "DELETE" assert requests[1].url == ws_urls["baseline"]["record"].format( recipe.id) # and then approves both changes 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"]
def test_check_connection(self, rs_settings, requestsmock): # Root URL should return currently authenticated user. requestsmock.get(f"{rs_settings.REMOTE_SETTINGS_URL}/", json={"capabilities": {}}) with pytest.raises(ImproperlyConfigured) as exc: exports.RemoteSettings().check_config() assert "Invalid Remote Settings credentials" in str(exc.value) # Collections writable. requestsmock.get( f"{rs_settings.REMOTE_SETTINGS_URL}/", json={ "user": { "id": f"account:{rs_settings.REMOTE_SETTINGS_USERNAME}" }, "capabilities": { "signer": { "version": "5.2.0", "to_review_enabled": True, "group_check_enabled": True, "resources": [{ "source": { "bucket": "main-workspace", "collection": None }, "destination": { "bucket": "main", "collection": None }, }], } }, }, ) # Workspace collections should be writable bucket = rs_settings.REMOTE_SETTINGS_WORKSPACE_BUCKET_ID collection = rs_settings.REMOTE_SETTINGS_CAPABILITIES_COLLECTION_ID # Allow writes for all collections allow_write_payload = { "data": {}, "permissions": { "write": [f"account:{rs_settings.REMOTE_SETTINGS_USERNAME}"] }, } readonly_payload = {"data": {}, "permissions": {}} collection_url = ( f"{rs_settings.REMOTE_SETTINGS_URL}/buckets/{bucket}/collections/{collection}" ) requestsmock.get(collection_url, json=allow_write_payload) collection_url = ( f"{rs_settings.REMOTE_SETTINGS_URL}/buckets/{bucket}/collections/{collection}" ) # make the collection readonly requestsmock.get(collection_url, json=readonly_payload) # test it with pytest.raises(ImproperlyConfigured) as exc: exports.RemoteSettings().check_config() assert f"Remote Settings collection {collection} is not writable" in str( exc.value) # restore write permissions requestsmock.get(collection_url, json=allow_write_payload) # Signer version should be >= 5.1.0. requestsmock.get( f"{rs_settings.REMOTE_SETTINGS_URL}/", json={ "user": { "id": f"account:{rs_settings.REMOTE_SETTINGS_USERNAME}" }, "capabilities": { "signer": { "version": "5.0.0" } }, }, ) with pytest.raises(ImproperlyConfigured) as exc: exports.RemoteSettings().check_config() assert "kinto-signer 5.1.0+ is required" in str(exc.value) # Capabilities collection review should be explicitly disabled. requestsmock.get( f"{rs_settings.REMOTE_SETTINGS_URL}/", json={ "user": { "id": f"account:{rs_settings.REMOTE_SETTINGS_USERNAME}" }, "capabilities": { "signer": { "version": "5.2.0", "resources": [] } }, }, ) with pytest.raises(ImproperlyConfigured) as exc: exports.RemoteSettings().check_config() assert ("Review was not disabled on Remote Settings collection " f"{rs_settings.REMOTE_SETTINGS_CAPABILITIES_COLLECTION_ID}." ) in str(exc.value) requestsmock.get( f"{rs_settings.REMOTE_SETTINGS_URL}/", json={ "user": { "id": f"account:{rs_settings.REMOTE_SETTINGS_USERNAME}" }, "capabilities": { "signer": { "version": "5.2.0", "to_review_enabled": True, "group_check_enabled": True, "resources": [ { "source": { "bucket": "main-workspace", "collection": None }, "destination": { "bucket": "main", "collection": None }, }, { "source": { "bucket": "main-workspace", "collection": "normandy-recipes-capabilities", }, "destination": { "bucket": "main", "collection": "normandy-recipes-capabilities", }, "to_review_enabled": False, "group_check_enabled": False, }, ], } }, }, ) # Assert does not raise. exports.RemoteSettings().check_config()