def test_it_works(self, rf): recipe = RecipeFactory(arguments={"foo": "bar"}) approval = ApprovalRequestFactory(revision=recipe.latest_revision) action = recipe.action serializer = RecipeSerializer(recipe, context={"request": rf.get("/")}) assert serializer.data == { "name": recipe.name, "id": recipe.id, "last_updated": Whatever(), "enabled": recipe.enabled, "extra_filter_expression": recipe.extra_filter_expression, "filter_expression": recipe.filter_expression, "revision_id": recipe.revision_id, "action": action.name, "arguments": { "foo": "bar" }, "is_approved": False, "latest_revision_id": recipe.latest_revision.id, "approved_revision_id": None, "approval_request": { "id": approval.id, "created": Whatever(), "creator": Whatever(), "approved": None, "approver": None, "comment": None, }, "identicon_seed": Whatever.startswith("v1:"), "capabilities": sorted(recipe.capabilities), }
def test_update_signatures(self, mocker, mock_logger): # Make sure the test environment is clean. This test is invalid otherwise. assert Recipe.objects.all().count() == 0 # Mock the Autographer mock_autograph = mocker.patch('normandy.recipes.models.Autographer') mock_autograph.return_value.sign_data.return_value = [ { 'signature': 'fake signature 1' }, { 'signature': 'fake signature 2' }, ] # Make and sign two recipes (recipe1, recipe2) = RecipeFactory.create_batch(2) Recipe.objects.all().update_signatures() # Assert that the signature update is logged. mock_logger.info.assert_called_with( Whatever.contains(str(recipe1.id), str(recipe2.id)), extra={ 'code': INFO_REQUESTING_RECIPE_SIGNATURES, 'recipe_ids': Whatever.contains(recipe1.id, recipe2.id) }) # Assert the autographer was used as expected assert mock_autograph.called assert mock_autograph.return_value.sign_data.called_with( [Whatever(), Whatever()]) signatures = list(Recipe.objects.all().values_list( 'signature__signature', flat=True)) assert signatures == ['fake signature 1', 'fake signature 2']
def test_update_signatures(self, mocker, mock_logger): # Make sure the test environment is clean. This test is invalid otherwise. assert Action.objects.all().count() == 0 # Mock the Autographer mock_autograph = mocker.patch('normandy.recipes.models.Autographer') mock_autograph.return_value.sign_data.return_value = [ { 'signature': 'fake signature 1' }, { 'signature': 'fake signature 2' }, ] # Make and sign two actions (action1, action2) = ActionFactory.create_batch(2) Action.objects.all().update_signatures() # Assert that the signature update is logged. mock_logger.info.assert_called_with( Whatever.contains(action1.name, action2.name), extra={ 'code': INFO_REQUESTING_ACTION_SIGNATURES, 'action_names': Whatever.contains(action1.name, action2.name), }) # Assert the autographer was used as expected assert mock_autograph.called assert mock_autograph.return_value.sign_data.called_with( [Whatever(), Whatever()]) signatures = list(Action.objects.all().values_list( 'signature__signature', flat=True)) assert signatures == ['fake signature 1', 'fake signature 2']
def test_forwards(self, migrations): # Get the pre-migration models old_apps = migrations.migrate("recipes", "0006_reciperevision_filter_object_json") Recipe = old_apps.get_model("recipes", "Recipe") Action = old_apps.get_model("recipes", "Action") RecipeRevision = old_apps.get_model("recipes", "RecipeRevision") Channel = old_apps.get_model("recipes", "Channel") Country = old_apps.get_model("recipes", "Country") Locale = old_apps.get_model("recipes", "Locale") # Create test data recipe = Recipe.objects.create() action = Action.objects.create() channel1 = Channel.objects.create(slug="beta") channel2 = Channel.objects.create(slug="release") country1 = Country.objects.create(code="US") country2 = Country.objects.create(code="CA") locale1 = Locale.objects.create(code="en-US") locale2 = Locale.objects.create(code="fr-CA") revision = RecipeRevision.objects.create( recipe=recipe, action=action, name="Test Revision", identicon_seed="v1:test" ) revision.channels.set([channel1, channel2]) revision.countries.set([country1, country2]) revision.locales.set([locale1, locale2]) revision.save() # Apply the migration new_apps = migrations.migrate("recipes", "0007_convert_simple_filters_to_filter_objects") # Get the post-migration models RecipeRevision = new_apps.get_model("recipes", "RecipeRevision") # Fetch the revision revision = RecipeRevision.objects.get() revision.filter_object = json.loads(revision.filter_object_json) # All simple filters should be removed assert revision.channels.count() == 0 assert revision.countries.count() == 0 assert revision.locales.count() == 0 # Order and duplication don't matter, so index the filter by type, and # compare the inner values using sets assert len(revision.filter_object) == 3 filters_by_type = {f["type"]: f for f in revision.filter_object} assert filters_by_type["channel"] == { "type": "channel", "channels": Whatever(lambda v: set(v) == {channel1.slug, channel2.slug}), } assert filters_by_type["country"] == { "type": "country", "countries": Whatever(lambda v: set(v) == {country1.code, country2.code}), } assert filters_by_type["locale"] == { "type": "locale", "locales": Whatever(lambda v: set(v) == {locale1.code, locale2.code}), }
def test_bundle_serializer(rf): recipe = RecipeFactory() bundle = BundleFactory(recipes=[recipe]) serializer = BundleSerializer(bundle, context={'request': rf.get('/')}) assert serializer.data['recipes'] == [{ 'name': recipe.name, 'id': recipe.id, 'revision_id': recipe.revision_id, 'action': Whatever(), 'arguments': Whatever(), }]
def test_it_works_with_signature(self, rf): recipe = RecipeFactory(approver=UserFactory(), signed=True) context = {"request": rf.get("/")} combined_serializer = SignedRecipeSerializer(instance=recipe, context=context) recipe_serializer = MinimalRecipeSerializer(instance=recipe, context=context) # Testing for shape of data, not contents assert combined_serializer.data == { "signature": { "signature": Whatever(), "timestamp": Whatever(), "x5u": Whatever(), "public_key": Whatever(), }, "recipe": recipe_serializer.data, }
def test_it_logs_when_geoip_fails(self, geolocation, mocker, mock_logger): mock_reader = mocker.patch('normandy.recipes.geolocation.geoip_reader') mock_reader.country.side_effect = GeoIP2Error() assert geolocation.get_country_code('207.126.102.129') is None mock_logger.warning.assert_called_with( Whatever(), extra={'code': WARNING_UNKNOWN_GEOIP_ERROR})
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), "capabilities": Whatever(lambda caps: set(caps) == recipe.capabilities), }, "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_it_works(self, rf): channel = ChannelFactory() country = CountryFactory() locale = LocaleFactory() recipe = RecipeFactory(arguments={'foo': 'bar'}, channels=[channel], countries=[country], locales=[locale]) action = recipe.action serializer = RecipeSerializer(recipe, context={'request': rf.get('/')}) assert serializer.data == { 'name': recipe.name, 'id': recipe.id, 'last_updated': Whatever(), 'enabled': recipe.enabled, 'extra_filter_expression': recipe.extra_filter_expression, 'filter_expression': recipe.filter_expression, 'revision_id': recipe.revision_id, 'action': action.name, 'arguments': { 'foo': 'bar', }, 'channels': [channel.slug], 'countries': [country.code], 'locales': [locale.code] }
def test_it_works_with_no_signature(self, rf): recipe = RecipeFactory(signed=False) action = recipe.action serializer = SignedRecipeSerializer(instance=recipe, context={'request': rf.get('/')}) assert serializer.data == { 'signature': None, 'recipe': { 'name': recipe.name, 'id': recipe.id, 'enabled': recipe.enabled, 'extra_filter_expression': recipe.extra_filter_expression, 'filter_expression': recipe.filter_expression, 'revision_id': recipe.revision_id, 'action': action.name, 'arguments': recipe.arguments, 'last_updated': Whatever(), 'channels': [], 'countries': [], 'locales': [], 'is_approved': False, 'latest_revision_id': recipe.latest_revision.id, 'approved_revision_id': recipe.approved_revision_id, 'approval_request': None, } }
def test_it_warns_when_cant_load_database(self, mocker, mock_logger): MockReader = mocker.patch('normandy.recipes.geolocation.Reader') MockReader.side_effect = IOError() load_geoip_database() mock_logger.warning.assert_called_with( Whatever(), extra={'code': WARNING_CANNOT_LOAD_DATABASE})
def test_it_doesnt_pass_the_api_root_url_to_the_api_root_view( self, mocker): mock_api_view = mocker.Mock() router = MixedViewRouter(view=mock_api_view) router.register_view('view', View, name='standalone-view') router.get_urls() assert mock_api_view.called_once_with( [Whatever(lambda v: v.name == 'standalone-view')])
def test_it_works_with_signature(self, rf): recipe = RecipeFactory(signed=True) context = {'request': rf.get('/')} combined_serializer = SignedRecipeSerializer(instance=recipe, context=context) recipe_serializer = RecipeSerializer(instance=recipe, context=context) # Testing for shape of data, not contents assert combined_serializer.data == { 'signature': { 'signature': Whatever(), 'timestamp': Whatever(), 'x5u': Whatever(), 'public_key': Whatever(), }, 'recipe': recipe_serializer.data, }
def test_it_works(self, rf): channel = ChannelFactory() country = CountryFactory() locale = LocaleFactory() recipe = RecipeFactory(arguments={'foo': 'bar'}, channels=[channel], countries=[country], locales=[locale]) approval = ApprovalRequestFactory(revision=recipe.latest_revision) action = recipe.action serializer = RecipeSerializer(recipe, context={'request': rf.get('/')}) assert serializer.data == { 'name': recipe.name, 'id': recipe.id, 'last_updated': Whatever(), 'enabled': recipe.enabled, 'extra_filter_expression': recipe.extra_filter_expression, 'filter_expression': recipe.filter_expression, 'action': { 'arguments_schema': {}, 'id': action.id, 'implementation_url': Whatever(), 'name': action.name, }, 'arguments': { 'foo': 'bar', }, 'channels': [channel.slug], 'countries': [country.code], 'locales': [locale.code], 'is_approved': False, 'latest_revision': RecipeRevisionSerializer(recipe.latest_revision).data, 'approved_revision': None, 'approval_request': { 'id': approval.id, 'created': Whatever(), 'creator': Whatever(), 'approved': None, 'approver': None, 'comment': None, }, }
def test_it_serves_extensions(self, api_client, storage): extension = ExtensionFactory(name="foo") res = api_client.get("/api/v3/extension/") assert res.status_code == 200 assert res.data["results"] == [{ "id": extension.id, "name": "foo", "xpi": Whatever() }]
def test_it_serves_extensions(self, api_client): extension = ExtensionFactory(name='foo', ) res = api_client.get('/api/v2/extension/') assert res.status_code == 200 assert res.data['results'] == [{ 'id': extension.id, 'name': 'foo', 'xpi': Whatever(), }]
def test_password_not_queryable(self, gql_client): u = UserFactory() res = gql_client.execute(GQ().query.user(id=u.id).fields("password")) assert res == { "errors": [{ "locations": Whatever(), "message": 'Cannot query field "password" on type "UserType".', }] }
def test_update_locales(self, tmpdir, mock_logger): assert Locale.objects.count() == 0 storage = ProductDetailsRelationalStorage(json_dir=tmpdir.strpath) storage.update('languages.json', LANGUAGES_JSON, '1999-01-01') mock_logger.info.assert_called_with( Whatever(), extra={'code': INFO_UPDATE_PRODUCT_DETAILS}) assert Locale.objects.count() == 12 assert Locale.objects.filter(code='en-US', name='English (US)').exists()
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
def test_it_does_not_send_excessive_remote_settings_traffic( self, mocker, settings, mocked_autograph ): # 10 to update recipes = RecipeFactory.create_batch( 10, approver=UserFactory(), enabler=UserFactory(), signed=False ) assert all(not r.approved_revision.uses_only_baseline_capabilities() for r in recipes) # Set up a version of the Remote Settings helper with a mocked out client client_mock = None 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 mocker.patch( "normandy.recipes.management.commands.update_recipe_signatures.RemoteSettings", side_effect=rs_with_mocked_client, ) call_command("update_recipe_signatures") # Make sure that our mock was actually used assert client_mock # One signing request to the capabilities collection assert client_mock.patch_collection.mock_calls == [ mocker.call( id=settings.REMOTE_SETTINGS_CAPABILITIES_COLLECTION_ID, data={"status": "to-sign"}, bucket=settings.REMOTE_SETTINGS_WORKSPACE_BUCKET_ID, ) ] # one publish to the capabilities collection per recipe expected_calls = [] for recipe in recipes: expected_calls.append( mocker.call( data=Whatever(lambda r: r["id"] == recipe.id, name=f"Recipe {recipe.id}"), bucket=settings.REMOTE_SETTINGS_WORKSPACE_BUCKET_ID, collection=settings.REMOTE_SETTINGS_CAPABILITIES_COLLECTION_ID, ) ) client_mock.update_record.has_calls(expected_calls, any_order=True) # all expected calls assert client_mock.update_record.call_count == len(expected_calls) # no extra calls
def test_it_works(self, rf): recipe = RecipeFactory(arguments={"foo": "bar"}, filter_object_json=None) approval = ApprovalRequestFactory(revision=recipe.latest_revision) action = recipe.action serializer = RecipeSerializer(recipe, context={"request": rf.get("/")}) assert serializer.data == { "name": recipe.name, "id": recipe.id, "last_updated": Whatever(), "enabled": recipe.enabled, "extra_filter_expression": recipe.extra_filter_expression, "filter_expression": recipe.filter_expression, "filter_object": [], "action": { "arguments_schema": {}, "id": action.id, "implementation_url": Whatever(), "name": action.name, }, "arguments": { "foo": "bar" }, "is_approved": False, "latest_revision": RecipeRevisionSerializer(recipe.latest_revision).data, "approved_revision": None, "approval_request": { "id": approval.id, "created": Whatever(), "creator": Whatever(), "approved": None, "approver": None, "comment": None, }, "identicon_seed": Whatever.startswith("v1:"), }
def test_validation_with_wrong_arguments(self): action = ActionFactory(name="show-heartbeat", arguments_schema=ARGUMENTS_SCHEMA) serializer = RecipeSerializer( data={ "action_id": action.id, "name": "Any name", "extra_filter_expression": "true", "arguments": { "surveys": [ { "weight": 1 }, { "title": "bar", "weight": 1 }, { "title": "foo", "weight": 0 }, { "title": "baz", "weight": "lorem ipsum" }, ], }, }) with pytest.raises(serializers.ValidationError): serializer.is_valid(raise_exception=True) assert serializer.errors["arguments"] == { "surveyId": Whatever(lambda err: str(err) == "This field is required."), "surveys": { 0: { "title": "This field is required." }, 2: { "weight": "0 is less than the minimum of 1" }, 3: { "weight": "'lorem ipsum' is not of type 'integer'" }, }, }
def test_it_serves_extensions(self, api_client, storage): extension = ExtensionFactory(name="foo") res = api_client.get("/api/v3/extension/") assert res.status_code == 200 assert res.data["results"] == [ { "id": extension.id, "name": "foo", "xpi": Whatever(), "extension_id": extension.extension_id, "version": extension.version, "hash": extension.hash, "hash_algorithm": extension.hash_algorithm, } ]
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, })
def test_it_interacts_with_autograph_correctly(self, settings, mock_logger): settings.AUTOGRAPH_URL = "https://autograph.example.com" settings.AUTOGRAPH_HAWK_ID = "hawk id" settings.AUTOGRAPH_HAWK_SECRET_KEY = "hawk secret key" autographer = signing.Autographer() autographer.session = MagicMock() autographer.session.post.return_value.json.return_value = [ { "content-signature": ('x5u="https://example.com/fake_x5u_1";p384ecdsa=fake_signature_1' ), "x5u": "https://example.com/fake_x5u_1", "hash_algorithm": "sha384", "ref": "fake_ref_1", "signature": "fake_signature_1", "public_key": "fake_pubkey_1", }, { "content-signature": ('x5u="https://example.com/fake_x5u_2";p384ecdsa=fake_signature_2' ), "x5u": "https://example.com/fake_x5u_2", "hash_algorithm": "sha384", "ref": "fake_ref_2", "signature": "fake_signature_2", "public_key": "fake_pubkey_2", }, ] url = self.test_settings["URL"] + "sign/data" foo_base64 = base64.b64encode(b"foo").decode("utf8") bar_base64 = base64.b64encode(b"bar").decode("utf8") # Assert the correct data is returned assert autographer.sign_data([b"foo", b"bar"]) == [ { "timestamp": Whatever(), "signature": "fake_signature_1", "x5u": "https://example.com/fake_x5u_1", "public_key": "fake_pubkey_1", }, { "timestamp": Whatever(), "signature": "fake_signature_2", "x5u": "https://example.com/fake_x5u_2", "public_key": "fake_pubkey_2", }, ] # Assert that logging happened mock_logger.info.assert_called_with( Whatever.contains("2"), extra={"code": signing.INFO_RECEIVED_SIGNATURES}) # Assert the correct request was made assert autographer.session.post.called_once_with([ url, [ { "template": "content-signature", "input": foo_base64 }, { "template": "content-signature", "input": bar_base64 }, ], ])
def test_it_interacts_with_autograph_correctly(self, settings, mock_logger): settings.AUTOGRAPH_URL = 'https://autograph.example.com' settings.AUTOGRAPH_HAWK_ID = 'hawk id' settings.AUTOGRAPH_HAWK_SECRET_KEY = 'hawk secret key' autographer = signing.Autographer() autographer.session = MagicMock() autographer.session.post.return_value.json.return_value = [{ 'content-signature': ('x5u="https://example.com/fake_x5u_1";p384ecdsa=fake_signature_1' ), 'x5u': 'https://example.com/fake_x5u_1', 'hash_algorithm': 'sha384', 'ref': 'fake_ref_1', 'signature': 'fake_signature_1', 'public_key': 'fake_pubkey_1', }, { 'content-signature': ('x5u="https://example.com/fake_x5u_2";p384ecdsa=fake_signature_2' ), 'x5u': 'https://example.com/fake_x5u_2', 'hash_algorithm': 'sha384', 'ref': 'fake_ref_2', 'signature': 'fake_signature_2', 'public_key': 'fake_pubkey_2', }] url = self.test_settings['URL'] + 'sign/data' foo_base64 = base64.b64encode(b'foo').decode('utf8') bar_base64 = base64.b64encode(b'bar').decode('utf8') # Assert the correct data is returned assert autographer.sign_data([b'foo', b'bar']) == [{ 'timestamp': Whatever(), 'signature': 'fake_signature_1', 'x5u': 'https://example.com/fake_x5u_1', 'public_key': 'fake_pubkey_1', }, { 'timestamp': Whatever(), 'signature': 'fake_signature_2', 'x5u': 'https://example.com/fake_x5u_2', 'public_key': 'fake_pubkey_2', }] # Assert that logging happened mock_logger.info.assert_called_with( Whatever.contains('2'), extra={'code': signing.INFO_RECEIVED_SIGNATURES}) # Assert the correct request was made assert autographer.session.post.called_once_with([ url, [ { 'template': 'content-signature', 'input': foo_base64 }, { 'template': 'content-signature', 'input': bar_base64 }, ] ])