示例#1
0
    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),
        }
示例#2
0
    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']
示例#3
0
    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']
示例#4
0
    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}),
        }
示例#5
0
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(),
    }]
示例#6
0
    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,
        }
示例#7
0
    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})
示例#8
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),
                "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]
        }
示例#10
0
    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,
            }
        }
示例#11
0
    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})
示例#12
0
 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')])
示例#13
0
    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,
        }
示例#14
0
    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,
            },
        }
示例#15
0
    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()
        }]
示例#16
0
    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(),
        }]
示例#17
0
 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".',
         }]
     }
示例#18
0
    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()
示例#19
0
 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
示例#20
0
    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
示例#21
0
    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:"),
        }
示例#22
0
    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'"
                },
            },
        }
示例#23
0
    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,
            }
        ]
示例#24
0
    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,
                                          })
示例#25
0
    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
                },
            ],
        ])
示例#26
0
    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
                },
            ]
        ])