Add support for soft/hard block in upload_to_mllf_to_remote_settings cron:

- adds support to process fort blocks in the cron
- does not actually support soft blocks, we skip them always
- adds test proving we always skip them

This commit makes actually adding them more clear and easier to grok
This commit is contained in:
Kevin Meinhardt 2024-11-22 18:43:31 +01:00
Родитель 0684796e89
Коммит 7ad8894e89
4 изменённых файлов: 499 добавлений и 175 удалений

Просмотреть файл

@ -1,11 +1,11 @@
from datetime import datetime from datetime import datetime
from typing import List
import waffle import waffle
from django_statsd.clients import statsd from django_statsd.clients import statsd
import olympia.core.logger import olympia.core.logger
from olympia.constants.blocklist import ( from olympia.constants.blocklist import (
BASE_REPLACE_THRESHOLD,
MLBF_BASE_ID_CONFIG_KEY, MLBF_BASE_ID_CONFIG_KEY,
MLBF_TIME_CONFIG_KEY, MLBF_TIME_CONFIG_KEY,
) )
@ -13,7 +13,7 @@ from olympia.zadmin.models import get_config
from .mlbf import MLBF from .mlbf import MLBF
from .models import Block, BlocklistSubmission, BlockType from .models import Block, BlocklistSubmission, BlockType
from .tasks import cleanup_old_files, process_blocklistsubmission, upload_filter from .tasks import process_blocklistsubmission, upload_filter
from .utils import datetime_to_ts from .utils import datetime_to_ts
@ -28,9 +28,9 @@ def get_last_generation_time():
return get_config(MLBF_TIME_CONFIG_KEY, None, json_value=True) return get_config(MLBF_TIME_CONFIG_KEY, None, json_value=True)
def get_base_generation_time(): def get_base_generation_time(block_type: BlockType):
return get_config( return get_config(
MLBF_BASE_ID_CONFIG_KEY(BlockType.BLOCKED, compat=True), None, json_value=True MLBF_BASE_ID_CONFIG_KEY(block_type, compat=True), None, json_value=True
) )
@ -66,48 +66,45 @@ def _upload_mlbf_to_remote_settings(*, force_base=False):
# An add-on version/file from after this time can't be reliably asserted - # An add-on version/file from after this time can't be reliably asserted -
# there may be false positives or false negatives. # there may be false positives or false negatives.
# https://github.com/mozilla/addons-server/issues/13695 # https://github.com/mozilla/addons-server/issues/13695
generation_time = get_generation_time() mlbf = MLBF.generate_from_db(get_generation_time())
# This timestamp represents the last time the MLBF was generated and uploaded. previous_filter = MLBF.load_from_storage(
# It could have been a base filter or a stash. # This timestamp represents the last time the MLBF was generated and uploaded.
last_generation_time = get_last_generation_time() # It could have been a base filter or a stash.
# This timestamp represents the point in time when get_last_generation_time()
# the base filter was generated and uploaded.
base_generation_time = get_base_generation_time()
mlbf = MLBF.generate_from_db(generation_time)
base_filter = (
MLBF.load_from_storage(base_generation_time)
if base_generation_time is not None
else None
) )
previous_filter = ( base_filters_to_update: List[BlockType] = []
# Only load previoous filter if there is a timestamp to use create_stash = False
# and that timestamp is not the same as the base_filter
MLBF.load_from_storage(last_generation_time)
if last_generation_time is not None
and (base_filter is None or base_filter.created_at != last_generation_time)
else base_filter
)
changes_count = mlbf.blocks_changed_since_previous( # Determine which base filters need to be re uploaded
BlockType.BLOCKED, previous_filter # and whether a new stash needs to be created
) for block_type in BlockType:
statsd.incr( # This prevents us from updating a stash or filter based on new soft blocks
'blocklist.cron.upload_mlbf_to_remote_settings.blocked_changed', changes_count if block_type == BlockType.SOFT_BLOCKED:
) log.info(
need_update = ( 'Skipping soft-blocks because enable-soft-blocking switch is inactive'
force_base )
or base_filter is None continue
or (
previous_filter is not None base_filter = MLBF.load_from_storage(get_base_generation_time(block_type))
and previous_filter.created_at < get_blocklist_last_modified_time()
) # add this block type to the list of filters to be re-uploaded
or changes_count > 0 if (
) force_base
if not need_update: or base_filter is None
or mlbf.should_upload_filter(block_type, base_filter)
):
base_filters_to_update.append(block_type)
# only update the stash if we should AND if
# we aren't already reuploading the filter for this block type
elif mlbf.should_upload_stash(block_type, previous_filter or base_filter):
create_stash = True
skip_update = len(base_filters_to_update) == 0 and not create_stash
if skip_update:
log.info('No new/modified/deleted Blocks in database; skipping MLBF generation') log.info('No new/modified/deleted Blocks in database; skipping MLBF generation')
# Delete the locally generated MLBF directory and files as they are not needed
mlbf.delete()
return return
statsd.incr( statsd.incr(
@ -119,27 +116,24 @@ def _upload_mlbf_to_remote_settings(*, force_base=False):
len(mlbf.data.not_blocked_items), len(mlbf.data.not_blocked_items),
) )
make_base_filter = ( # Until we are ready to enable soft blocking, it should not be possible
force_base # to create a stash and a filter at the same iteration
or base_filter is None if create_stash and len(base_filters_to_update) > 0:
or previous_filter is None raise Exception(
or mlbf.blocks_changed_since_previous(BlockType.BLOCKED, base_filter) 'Cannot upload stash and filter without implementing soft blocking'
> BASE_REPLACE_THRESHOLD )
)
if make_base_filter: if create_stash:
mlbf.generate_and_write_filter()
else:
mlbf.generate_and_write_stash(previous_filter) mlbf.generate_and_write_stash(previous_filter)
upload_filter.delay( for block_type in base_filters_to_update:
generation_time, mlbf.generate_and_write_filter(block_type)
filter_list=[BlockType.BLOCKED.name] if make_base_filter else [],
create_stash=not make_base_filter,
)
if base_filter: upload_filter.delay(
cleanup_old_files.delay(base_filter_id=base_filter.created_at) mlbf.created_at,
filter_list=[key.name for key in base_filters_to_update],
create_stash=create_stash,
)
def process_blocklistsubmissions(): def process_blocklistsubmissions():

Просмотреть файл

@ -14,6 +14,7 @@ import olympia.core.logger
from olympia.amo.utils import SafeStorage from olympia.amo.utils import SafeStorage
from olympia.blocklist.models import BlockType, BlockVersion from olympia.blocklist.models import BlockType, BlockVersion
from olympia.blocklist.utils import datetime_to_ts from olympia.blocklist.utils import datetime_to_ts
from olympia.constants.blocklist import BASE_REPLACE_THRESHOLD
from olympia.versions.models import Version from olympia.versions.models import Version
@ -79,7 +80,8 @@ class BaseMLBFLoader:
def __init__(self, storage: SafeStorage): def __init__(self, storage: SafeStorage):
self.storage = storage self.storage = storage
def data_type_key(self, key: MLBFDataType) -> str: @classmethod
def data_type_key(cls, key: MLBFDataType) -> str:
return key.name.lower() return key.name.lower()
@cached_property @cached_property
@ -207,13 +209,21 @@ class MLBF:
for (guid, version) in input_list for (guid, version) in input_list
] ]
def filter_path(self, _block_type: BlockType = BlockType.BLOCKED): def filter_path(self, block_type: BlockType):
return self.storage.path('filter') # TODO: explain / test
if block_type == BlockType.BLOCKED:
return self.storage.path('filter')
return self.storage.path(f'filter-{BaseMLBFLoader.data_type_key(block_type)}')
@property @property
def stash_path(self): def stash_path(self):
return self.storage.path('stash.json') return self.storage.path('stash.json')
def delete(self):
if self.storage.exists(self.storage.base_location):
self.storage.rm_stored_dir(self.storage.base_location)
log.info(f'Deleted {self.storage.base_location}')
def generate_and_write_filter(self, block_type: BlockType = BlockType.BLOCKED): def generate_and_write_filter(self, block_type: BlockType = BlockType.BLOCKED):
stats = {} stats = {}
@ -297,6 +307,26 @@ class MLBF:
_, _, changed_count = self.generate_diffs(previous_mlbf)[block_type] _, _, changed_count = self.generate_diffs(previous_mlbf)[block_type]
return changed_count return changed_count
def should_upload_filter(
self, block_type: BlockType = BlockType.BLOCKED, previous_mlbf: 'MLBF' = None
):
return (
self.blocks_changed_since_previous(
block_type=block_type, previous_mlbf=previous_mlbf
)
> BASE_REPLACE_THRESHOLD
)
def should_upload_stash(
self, block_type: BlockType = BlockType.BLOCKED, previous_mlbf: 'MLBF' = None
):
return (
self.blocks_changed_since_previous(
block_type=block_type, previous_mlbf=previous_mlbf
)
> 0
)
@classmethod @classmethod
def load_from_storage( def load_from_storage(
cls, created_at: str = datetime_to_ts(), error_on_missing: bool = False cls, created_at: str = datetime_to_ts(), error_on_missing: bool = False

Просмотреть файл

@ -9,6 +9,7 @@ from olympia.amo.tests import (
version_factory, version_factory,
) )
from olympia.blocklist.mlbf import MLBF from olympia.blocklist.mlbf import MLBF
from olympia.blocklist.models import BlockType
class TestExportBlocklist(TestCase): class TestExportBlocklist(TestCase):
@ -38,4 +39,4 @@ class TestExportBlocklist(TestCase):
call_command('export_blocklist', '1') call_command('export_blocklist', '1')
mlbf = MLBF.load_from_storage(1) mlbf = MLBF.load_from_storage(1)
assert mlbf.storage.exists(mlbf.filter_path()) assert mlbf.storage.exists(mlbf.filter_path(BlockType.BLOCKED))

Просмотреть файл

@ -1,6 +1,7 @@
import json import json
import uuid import uuid
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import List, Union
from unittest import mock from unittest import mock
from django.conf import settings from django.conf import settings
@ -26,6 +27,7 @@ from olympia.blocklist.cron import (
) )
from olympia.blocklist.mlbf import MLBF from olympia.blocklist.mlbf import MLBF
from olympia.blocklist.models import Block, BlocklistSubmission, BlockType, BlockVersion from olympia.blocklist.models import Block, BlocklistSubmission, BlockType, BlockVersion
from olympia.blocklist.tasks import upload_filter
from olympia.blocklist.utils import datetime_to_ts from olympia.blocklist.utils import datetime_to_ts
from olympia.constants.blocklist import MLBF_BASE_ID_CONFIG_KEY, MLBF_TIME_CONFIG_KEY from olympia.constants.blocklist import MLBF_BASE_ID_CONFIG_KEY, MLBF_TIME_CONFIG_KEY
from olympia.zadmin.models import set_config from olympia.zadmin.models import set_config
@ -45,7 +47,6 @@ class TestUploadToRemoteSettings(TestCase):
self.mocks: dict[str, mock.Mock] = {} self.mocks: dict[str, mock.Mock] = {}
for mock_name in ( for mock_name in (
'olympia.blocklist.cron.statsd.incr', 'olympia.blocklist.cron.statsd.incr',
'olympia.blocklist.cron.cleanup_old_files.delay',
'olympia.blocklist.cron.upload_filter.delay', 'olympia.blocklist.cron.upload_filter.delay',
'olympia.blocklist.cron.get_generation_time', 'olympia.blocklist.cron.get_generation_time',
'olympia.blocklist.cron.get_last_generation_time', 'olympia.blocklist.cron.get_last_generation_time',
@ -58,9 +59,9 @@ class TestUploadToRemoteSettings(TestCase):
self.base_time = datetime_to_ts(self.block.modified) self.base_time = datetime_to_ts(self.block.modified)
self.last_time = datetime_to_ts(self.block.modified + timedelta(seconds=1)) self.last_time = datetime_to_ts(self.block.modified + timedelta(seconds=1))
self.current_time = datetime_to_ts(self.block.modified + timedelta(seconds=2)) self.current_time = datetime_to_ts(self.block.modified + timedelta(seconds=2))
self.mocks[ self.mocks['olympia.blocklist.cron.get_base_generation_time'].side_effect = (
'olympia.blocklist.cron.get_base_generation_time' lambda _block_type: self.base_time
].return_value = self.base_time )
self.mocks[ self.mocks[
'olympia.blocklist.cron.get_last_generation_time' 'olympia.blocklist.cron.get_last_generation_time'
].return_value = self.last_time ].return_value = self.last_time
@ -84,49 +85,104 @@ class TestUploadToRemoteSettings(TestCase):
block=block, version=version, block_type=block_type block=block, version=version, block_type=block_type
) )
def test_skip_update_unless_force_base(self): def _test_skip_update_unless_force_base(self, enable_soft_blocking=False):
""" """
skip update unless force_base is true skip update unless force_base is true
""" """
upload_mlbf_to_remote_settings(force_base=False)
# We skip update at this point because there is no reason to update. # We skip update at this point because there is no reason to update.
upload_mlbf_to_remote_settings(force_base=False)
assert not self.mocks['olympia.blocklist.cron.upload_filter.delay'].called assert not self.mocks['olympia.blocklist.cron.upload_filter.delay'].called
# But if we force the base filter, we update. filter_list = [BlockType.BLOCKED.name]
upload_mlbf_to_remote_settings(force_base=True)
assert self.mocks['olympia.blocklist.cron.upload_filter.delay'].called with override_switch('enable-soft-blocking', active=enable_soft_blocking):
upload_mlbf_to_remote_settings(force_base=True)
# Check that a filter was created on the second attempt assert (
mlbf = MLBF.load_from_storage(self.current_time) mock.call(
assert mlbf.storage.exists(mlbf.filter_path()) self.current_time,
assert not mlbf.storage.exists(mlbf.stash_path) filter_list=filter_list,
create_stash=False,
)
) in self.mocks['olympia.blocklist.cron.upload_filter.delay'].call_args_list
def test_skip_update_unless_no_base_mlbf(self): # Check that both filters were created on the second attempt
mlbf = MLBF.load_from_storage(self.current_time)
self.assertTrue(
mlbf.storage.exists(mlbf.filter_path(BlockType.BLOCKED)),
)
self.assertEqual(
mlbf.storage.exists(mlbf.filter_path(BlockType.SOFT_BLOCKED)),
# Until we are ready to enable soft blocking
# there should never be a soft block filter.
False,
)
assert not mlbf.storage.exists(mlbf.stash_path)
def test_skip_update_unless_forced_soft_blocking_disabled(self):
self._test_skip_update_unless_force_base(enable_soft_blocking=False)
def test_skip_update_unless_forced_soft_blocking_enabled(self):
self._test_skip_update_unless_force_base(enable_soft_blocking=True)
def _test_skip_update_unless_no_base_mlbf(
self, block_type: BlockType, filter_list: Union[List[BlockType], None] = None
):
""" """
skip update unless there is no base mlbf skip update unless there is no base mlbf for the given block type
""" """
# We skip update at this point because there is a base filter. # We skip update at this point because there is a base filter.
upload_mlbf_to_remote_settings(force_base=False) upload_mlbf_to_remote_settings(force_base=False)
assert not self.mocks['olympia.blocklist.cron.upload_filter.delay'].called assert not self.mocks['olympia.blocklist.cron.upload_filter.delay'].called
self.mocks[ self.mocks['olympia.blocklist.cron.get_base_generation_time'].side_effect = (
'olympia.blocklist.cron.get_base_generation_time' lambda _block_type: None if _block_type == block_type else self.base_time
].return_value = None )
upload_mlbf_to_remote_settings(force_base=False) upload_mlbf_to_remote_settings(force_base=False)
assert self.mocks['olympia.blocklist.cron.upload_filter.delay'].called if filter_list is None:
assert not self.mocks['olympia.blocklist.cron.upload_filter.delay'].called
else:
assert (
mock.call(
self.current_time,
filter_list=filter_list,
create_stash=False,
)
) in self.mocks['olympia.blocklist.cron.upload_filter.delay'].call_args_list
def test_skip_update_unless_no_base_mlbf_for_blocked(self):
self._test_skip_update_unless_no_base_mlbf(
BlockType.BLOCKED, filter_list=[BlockType.BLOCKED.name]
)
@override_switch('enable-soft-blocking', active=True)
def test_skip_update_unless_no_base_mlbf_for_soft_blocked_with_switch_enabled(self):
self._test_skip_update_unless_no_base_mlbf(
# Until we enable soft blocking even if there is no soft block base filter
# and the switch is active, no update expected
BlockType.SOFT_BLOCKED,
filter_list=None,
)
def test_skip_update_unless_no_base_mlbf_for_soft_blocked_with_switch_disabled(
self,
):
self._test_skip_update_unless_no_base_mlbf(
BlockType.SOFT_BLOCKED, filter_list=None
)
def test_missing_last_filter_uses_base_filter(self): def test_missing_last_filter_uses_base_filter(self):
""" """
When there is a base filter and no last filter, When there is a base filter and no last filter,
fallback to using the base filter fallback to using the base filter
""" """
self._block_version(is_signed=True) block_version = self._block_version(is_signed=True)
# Re-created the last filter created after the new block # Re-create the last filter so we ensure
# the block is already processed comparing to previous
MLBF.generate_from_db(self.last_time) MLBF.generate_from_db(self.last_time)
assert datetime_to_ts(block_version.modified) < self.last_time
# We skip the update at this point because the new last filter already # We skip the update at this point because the new last filter already
# accounted for the new block. # accounted for the new block.
upload_mlbf_to_remote_settings(force_base=False) upload_mlbf_to_remote_settings(force_base=False)
@ -138,48 +194,74 @@ class TestUploadToRemoteSettings(TestCase):
'olympia.blocklist.cron.get_last_generation_time' 'olympia.blocklist.cron.get_last_generation_time'
].return_value = None ].return_value = None
upload_mlbf_to_remote_settings(force_base=False) upload_mlbf_to_remote_settings(force_base=False)
assert self.mocks['olympia.blocklist.cron.upload_filter.delay'].called
assert ( assert (
mock.call( mock.call(
'blocklist.cron.upload_mlbf_to_remote_settings.blocked_changed', 1 self.current_time,
filter_list=[],
create_stash=True,
) )
in self.mocks['olympia.blocklist.cron.statsd.incr'].call_args_list ) in self.mocks['olympia.blocklist.cron.upload_filter.delay'].call_args_list
)
def test_skip_update_unless_recent_modified_blocks(self): @override_switch('enable-soft-blocking', active=True)
def test_skip_update_if_unsigned_blocks_added(self):
""" """
skip update unless there are recent modified blocks skip update if there are only unsigned new blocks
""" """
self._block_version(block_type=BlockType.BLOCKED, is_signed=False)
self._block_version(block_type=BlockType.SOFT_BLOCKED, is_signed=False)
upload_mlbf_to_remote_settings(force_base=False) upload_mlbf_to_remote_settings(force_base=False)
assert not self.mocks['olympia.blocklist.cron.upload_filter.delay'].called assert not self.mocks['olympia.blocklist.cron.upload_filter.delay'].called
# Now the last filter is older than the most recently modified block. def _test_skip_update_unless_new_blocks(
older_last_time = datetime_to_ts(self.block.modified - timedelta(seconds=1)) self, block_type: BlockType, enable_soft_blocking=False, expect_update=False
self.mocks[ ):
'olympia.blocklist.cron.get_last_generation_time'
].return_value = older_last_time
MLBF.generate_from_db(older_last_time)
upload_mlbf_to_remote_settings(force_base=False)
assert self.mocks['olympia.blocklist.cron.upload_filter.delay'].called
def test_skip_update_unless_new_blocks(self):
""" """
skip update unless there are new blocks skip update unless there are new blocks
""" """
upload_mlbf_to_remote_settings(force_base=False) with override_switch('enable-soft-blocking', active=enable_soft_blocking):
assert not self.mocks['olympia.blocklist.cron.upload_filter.delay'].called upload_mlbf_to_remote_settings(force_base=False)
assert not self.mocks['olympia.blocklist.cron.upload_filter.delay'].called
# Now there is a new blocked version # Now there is a new blocked version
self._block_version(is_signed=True) self._block_version(block_type=block_type, is_signed=True)
upload_mlbf_to_remote_settings(force_base=False) upload_mlbf_to_remote_settings(force_base=False)
assert self.mocks['olympia.blocklist.cron.upload_filter.delay'].called
self.assertEqual(
expect_update,
self.mocks['olympia.blocklist.cron.upload_filter.delay'].called,
)
def test_skip_update_unless_new_blocks_for_blocked(self):
self._test_skip_update_unless_new_blocks(
block_type=BlockType.BLOCKED,
expect_update=True,
)
def test_skip_update_unless_new_blocks_for_soft_blocked_with_switch_disabled(self):
self._test_skip_update_unless_new_blocks(
block_type=BlockType.SOFT_BLOCKED,
enable_soft_blocking=False,
expect_update=False,
)
def test_skip_update_unless_new_blocks_for_soft_blocked_with_switch_enabled(self):
self._test_skip_update_unless_new_blocks(
block_type=BlockType.SOFT_BLOCKED,
enable_soft_blocking=True,
# Until we enable soft blocking
# even if there is a new soft block
# and switch is active, expect no update
expect_update=False,
)
def test_send_statsd_counts(self): def test_send_statsd_counts(self):
""" """
Send statsd counts for the number of blocked and not blocked items. Send statsd counts for the number of blocked,
soft blocked, and not blocked items.
""" """
self._block_version(is_signed=True) self._block_version(block_type=BlockType.BLOCKED)
self._block_version(block_type=BlockType.SOFT_BLOCKED)
upload_mlbf_to_remote_settings() upload_mlbf_to_remote_settings()
statsd_calls = self.mocks['olympia.blocklist.cron.statsd.incr'].call_args_list statsd_calls = self.mocks['olympia.blocklist.cron.statsd.incr'].call_args_list
@ -206,27 +288,172 @@ class TestUploadToRemoteSettings(TestCase):
upload_mlbf_to_remote_settings(bypass_switch=True) upload_mlbf_to_remote_settings(bypass_switch=True)
assert self.mocks['olympia.blocklist.cron.statsd.incr'].called assert self.mocks['olympia.blocklist.cron.statsd.incr'].called
def test_upload_stash_unless_force_base(self): def _test_upload_stash_unless_force_base(
self,
block_types: List[BlockType],
expect_stash: bool,
filter_list: Union[List[BlockType], None],
enable_soft_blocking: bool,
):
""" """
Upload a stash unless force_base is true. When there is a new block, Upload a stash unless force_base is true. When there is a new block,
We expect to upload a stash, unless the force_base is true, in which case We expect to upload a stash, unless the force_base is true, in which case
we upload a new filter. we upload a new filter.
""" """
force_base = False for block_type in block_types:
self._block_version(is_signed=True) self._block_version(block_type=block_type)
upload_mlbf_to_remote_settings(force_base=force_base)
assert self.mocks[ with override_switch('enable-soft-blocking', active=enable_soft_blocking):
'olympia.blocklist.cron.upload_filter.delay' upload_mlbf_to_remote_settings(force_base=False)
].call_args_list == [
mock.call( self.assertEqual(
self.current_time, expect_stash,
filter_list=[BlockType.BLOCKED.name] if force_base else [], mock.call(
create_stash=not force_base, self.current_time,
filter_list=[],
create_stash=True,
)
in self.mocks[
'olympia.blocklist.cron.upload_filter.delay'
].call_args_list,
) )
]
mlbf = MLBF.load_from_storage(self.current_time) mlbf = MLBF.load_from_storage(self.current_time)
assert mlbf.storage.exists(mlbf.filter_path()) == force_base
assert mlbf.storage.exists(mlbf.stash_path) != force_base if expect_stash:
assert mlbf.storage.exists(mlbf.stash_path)
for block_type in BlockType:
assert not mlbf.storage.exists(mlbf.filter_path(block_type))
else:
assert mlbf is None
upload_mlbf_to_remote_settings(force_base=True)
next_mlbf = MLBF.load_from_storage(self.current_time)
expected_block_types = []
for block_type in filter_list:
assert next_mlbf.storage.exists(next_mlbf.filter_path(block_type))
expected_block_types.append(block_type.name)
assert (
mock.call(
self.current_time,
filter_list=expected_block_types,
create_stash=False,
)
in self.mocks[
'olympia.blocklist.cron.upload_filter.delay'
].call_args_list
)
def test_upload_stash_unless_force_base_for_blocked_with_switch_disabled(self):
"""
When force base is false, it uploads a stash because there is a new hard blocked
version. When force base is true, it uploads the blocked filter for the same
reason.
"""
self._test_upload_stash_unless_force_base(
block_types=[BlockType.BLOCKED],
expect_stash=True,
filter_list=[BlockType.BLOCKED],
enable_soft_blocking=False,
)
def test_upload_stash_unless_force_base_for_blocked_with_switch_enabled(self):
"""
When force base is false, it uploads a stash because soft block is enabled
and there is a new hard blocked version. When force base is true, it uploads
both blocked and soft blocked filters for the previous reason and because
soft blocking is enabled.
"""
self._test_upload_stash_unless_force_base(
block_types=[BlockType.BLOCKED],
expect_stash=True,
# Even if updating a soft block and the switch is active
# don't expect a soft block filter update until we
# implement support for that
filter_list=[BlockType.BLOCKED],
enable_soft_blocking=True,
)
def test_upload_stash_unless_force_base_for_soft_blocked_with_switch_disabled(self):
"""
When force base is false, it does not upload a stash even when there is a new
soft blocked version, because soft blocking is disabled.
When force base is true, it uploads only the blocked filter
for the same reason.
"""
self._test_upload_stash_unless_force_base(
block_types=[BlockType.SOFT_BLOCKED],
expect_stash=False,
filter_list=[BlockType.BLOCKED],
enable_soft_blocking=False,
)
def test_upload_stash_unless_force_base_for_soft_blocked_with_switch_enabled(self):
"""
When force base is false, it uploads a stash because soft block is enabled
and there is a new soft blocked version. When force base is true, it uploads
both blocked and soft blocked filters.
"""
self._test_upload_stash_unless_force_base(
block_types=[BlockType.SOFT_BLOCKED],
# Even if updating a soft block and the switch is active
# don't expect a soft block filter update until we
# implement support for that
expect_stash=False,
filter_list=[BlockType.BLOCKED],
enable_soft_blocking=True,
)
def test_upload_stash_unless_force_base_for_both_blocked_with_switch_disabled(self):
"""
When force base is false, it uploads a stash even though soft blocking disabled
because there is a hard blocked version. When force base is true,
it uploads only the blocked filter for the same reason.
"""
self._test_upload_stash_unless_force_base(
block_types=[BlockType.BLOCKED, BlockType.SOFT_BLOCKED],
expect_stash=True,
filter_list=[BlockType.BLOCKED],
enable_soft_blocking=False,
)
def test_upload_stash_unless_force_base_for_both_blocked_with_switch_enabled(self):
"""
When force base is false, it uploads a stash because there are new hard and soft
blocked versions. When force base is true,
it uploads both blocked + soft blocked filters for the same reason.
"""
self._test_upload_stash_unless_force_base(
block_types=[BlockType.BLOCKED, BlockType.SOFT_BLOCKED],
expect_stash=True,
# Even if updating a soft block and the switch is active
# don't expect a soft block filter update until we
# implement support for that
filter_list=[BlockType.BLOCKED],
enable_soft_blocking=True,
)
def test_dont_upload_stash_unless_force_base_for_both_blocked_with_switch_enabled(
self,
):
"""
When force base is false, it does not upload a stash because
there are no new versions.When force base is true,
it uploads both blocked and soft blocked filters because
soft blocking is enabled.
"""
self._test_upload_stash_unless_force_base(
block_types=[],
expect_stash=False,
# Even if updating a soft block and the switch is active
# don't expect a soft block filter update until we
# implement support for that
filter_list=[BlockType.BLOCKED],
enable_soft_blocking=True,
)
def test_upload_stash_unless_missing_base_filter(self): def test_upload_stash_unless_missing_base_filter(self):
""" """
@ -244,12 +471,13 @@ class TestUploadToRemoteSettings(TestCase):
) )
] ]
mlbf = MLBF.load_from_storage(self.current_time) mlbf = MLBF.load_from_storage(self.current_time)
assert not mlbf.storage.exists(mlbf.filter_path()) assert not mlbf.storage.exists(mlbf.filter_path(BlockType.BLOCKED))
assert not mlbf.storage.exists(mlbf.filter_path(BlockType.SOFT_BLOCKED))
assert mlbf.storage.exists(mlbf.stash_path) assert mlbf.storage.exists(mlbf.stash_path)
self.mocks[ self.mocks['olympia.blocklist.cron.get_base_generation_time'].side_effect = (
'olympia.blocklist.cron.get_base_generation_time' lambda _block_type: None
].return_value = None )
upload_mlbf_to_remote_settings() upload_mlbf_to_remote_settings()
assert ( assert (
mock.call( mock.call(
@ -259,15 +487,34 @@ class TestUploadToRemoteSettings(TestCase):
) )
in self.mocks['olympia.blocklist.cron.upload_filter.delay'].call_args_list in self.mocks['olympia.blocklist.cron.upload_filter.delay'].call_args_list
) )
assert mlbf.storage.exists(mlbf.filter_path()) assert mlbf.storage.exists(mlbf.filter_path(BlockType.BLOCKED))
@mock.patch('olympia.blocklist.cron.BASE_REPLACE_THRESHOLD', 1) with override_switch('enable-soft-blocking', active=True):
upload_mlbf_to_remote_settings()
assert not mlbf.storage.exists(mlbf.filter_path(BlockType.SOFT_BLOCKED))
assert (
mock.call(
self.current_time,
# Even if updating a soft block and the switch is active
# don't expect a soft block filter update until we
# implement support for that
filter_list=[BlockType.BLOCKED.name],
create_stash=False,
)
) in self.mocks['olympia.blocklist.cron.upload_filter.delay'].call_args_list
# TODO: add test for soft blocks and ensure stash/filter compatibility
@mock.patch('olympia.blocklist.mlbf.BASE_REPLACE_THRESHOLD', 1)
@override_switch('enable-soft-blocking', active=True)
def test_upload_stash_unless_enough_changes(self): def test_upload_stash_unless_enough_changes(self):
block_type = BlockType.BLOCKED
""" """
When there are new blocks, upload either a stash or a filter depending on When there are new blocks, upload either a stash or a filter depending on
whether we have surpased the BASE_REPLACE_THRESHOLD amount. whether we have surpased the BASE_REPLACE_THRESHOLD amount.
""" """
self._block_version(is_signed=True) for _block_type in BlockType:
self._block_version(is_signed=True, block_type=_block_type)
upload_mlbf_to_remote_settings() upload_mlbf_to_remote_settings()
assert self.mocks[ assert self.mocks[
'olympia.blocklist.cron.upload_filter.delay' 'olympia.blocklist.cron.upload_filter.delay'
@ -279,44 +526,71 @@ class TestUploadToRemoteSettings(TestCase):
) )
] ]
mlbf = MLBF.load_from_storage(self.current_time) mlbf = MLBF.load_from_storage(self.current_time)
assert not mlbf.storage.exists(mlbf.filter_path()) assert not mlbf.storage.exists(mlbf.filter_path(block_type))
assert mlbf.storage.exists(mlbf.stash_path) assert mlbf.storage.exists(mlbf.stash_path)
self._block_version(is_signed=True) # delete the mlbf so we can test again with different conditions
# Create a new current time so we can test that the stash is not created mlbf.delete()
self.current_time = datetime_to_ts(self.block.modified + timedelta(seconds=4))
self.mocks[ self._block_version(is_signed=True, block_type=block_type)
'olympia.blocklist.cron.get_generation_time'
].return_value = self.current_time
upload_mlbf_to_remote_settings() upload_mlbf_to_remote_settings()
assert ( assert (
mock.call( mock.call(
self.current_time, self.current_time,
filter_list=[BlockType.BLOCKED.name], filter_list=[block_type.name],
create_stash=False, create_stash=False,
) )
in self.mocks['olympia.blocklist.cron.upload_filter.delay'].call_args_list in self.mocks['olympia.blocklist.cron.upload_filter.delay'].call_args_list
) )
new_mlbf = MLBF.load_from_storage(self.current_time) new_mlbf = MLBF.load_from_storage(self.current_time)
assert new_mlbf.storage.exists(new_mlbf.filter_path()) assert new_mlbf.storage.exists(new_mlbf.filter_path(block_type))
assert not new_mlbf.storage.exists(new_mlbf.stash_path) assert not new_mlbf.storage.exists(new_mlbf.stash_path)
def test_cleanup_old_files(self): @mock.patch('olympia.blocklist.mlbf.BASE_REPLACE_THRESHOLD', 1)
def test_upload_stash_even_if_filter_is_updated(self):
""" """
Cleanup old files only if a base filter already exists. If enough changes of one type are made, update the filter, but still upload
a stash if there are changes of other types.
""" """
upload_mlbf_to_remote_settings(force_base=True) self._block_version(is_signed=True, block_type=BlockType.BLOCKED)
self._block_version(is_signed=True, block_type=BlockType.BLOCKED)
self._block_version(is_signed=True, block_type=BlockType.SOFT_BLOCKED)
upload_mlbf_to_remote_settings()
assert self.mocks[ assert self.mocks[
'olympia.blocklist.cron.cleanup_old_files.delay' 'olympia.blocklist.cron.upload_filter.delay'
].call_args_list == [mock.call(base_filter_id=self.base_time)] ].call_args_list == [
mock.call(
self.current_time,
filter_list=[BlockType.BLOCKED.name],
create_stash=False,
)
]
mlbf = MLBF.load_from_storage(self.current_time)
assert mlbf.storage.exists(mlbf.filter_path(BlockType.BLOCKED))
assert not mlbf.storage.exists(mlbf.stash_path)
self.mocks[ with override_switch('enable-soft-blocking', active=True):
'olympia.blocklist.cron.get_base_generation_time' self._block_version(is_signed=True, block_type=BlockType.BLOCKED)
].return_value = None self._block_version(is_signed=True, block_type=BlockType.BLOCKED)
upload_mlbf_to_remote_settings(force_base=True) upload_mlbf_to_remote_settings()
assert ( self.mocks['olympia.blocklist.cron.upload_filter.delay'].assert_called_with(
self.mocks['olympia.blocklist.cron.cleanup_old_files.delay'].call_count == 1 self.current_time,
) filter_list=[BlockType.BLOCKED.name],
create_stash=False,
)
mlbf = MLBF.load_from_storage(self.current_time)
mlbf = MLBF.load_from_storage(self.current_time)
assert mlbf.storage.exists(mlbf.filter_path(BlockType.BLOCKED))
assert not mlbf.storage.exists(mlbf.stash_path)
def test_remove_storage_if_no_update(self):
"""
If there is no update, remove the storage used by the current mlbf.
"""
upload_mlbf_to_remote_settings(force_base=False)
assert not self.mocks['olympia.blocklist.cron.upload_filter.delay'].called
assert MLBF.load_from_storage(self.current_time) is None
def test_creates_base_filter_if_base_generation_time_invalid(self): def test_creates_base_filter_if_base_generation_time_invalid(self):
""" """
@ -327,36 +601,47 @@ class TestUploadToRemoteSettings(TestCase):
upload_mlbf_to_remote_settings(force_base=True) upload_mlbf_to_remote_settings(force_base=True)
assert self.mocks['olympia.blocklist.cron.upload_filter.delay'].called assert self.mocks['olympia.blocklist.cron.upload_filter.delay'].called
def test_creates_base_filter_if_last_generation_time_invalid(self): def test_compares_against_base_filter_if_missing_previous_filter(self):
""" """
When a last_generation_time is provided, but no filter exists for it, When no previous filter is found, compare blocks against the base filter
raise no filter found. of that block type.
""" """
self.mocks['olympia.blocklist.cron.get_last_generation_time'].return_value = 1 # Hard block version is accounted for in the base filter
upload_mlbf_to_remote_settings(force_base=True) self._block_version(block_type=BlockType.BLOCKED)
assert self.mocks['olympia.blocklist.cron.upload_filter.delay'].called MLBF.generate_from_db(self.base_time)
# Soft block version is not accounted for in the base filter
# but accounted for in the last filter
self._block_version(block_type=BlockType.SOFT_BLOCKED)
MLBF.generate_from_db(self.last_time)
upload_mlbf_to_remote_settings(force_base=False)
assert not self.mocks['olympia.blocklist.cron.upload_filter.delay'].called
# delete the last filter, now the base filter will be used to compare
MLBF.load_from_storage(self.last_time).delete()
upload_mlbf_to_remote_settings(force_base=False)
# We expect to not upload anything as soft blocking is disabled
# and only the soft blocked version is missing from the base filter
assert not self.mocks['olympia.blocklist.cron.upload_filter.delay'].called
@override_switch('enable-soft-blocking', active=True)
def test_dont_skip_update_if_all_blocked_or_not_blocked(self): def test_dont_skip_update_if_all_blocked_or_not_blocked(self):
""" """
If all versions are either blocked or not blocked, skip the update. If all versions are either blocked or not blocked, skip the update.
""" """
version = self._block_version(is_signed=True) for _ in range(0, 10):
upload_mlbf_to_remote_settings(force_base=True) self._block_version(block_type=BlockType.BLOCKED)
assert self.mocks['olympia.blocklist.cron.upload_filter.delay'].called
version.update(block_type=BlockType.SOFT_BLOCKED) upload_mlbf_to_remote_settings()
upload_mlbf_to_remote_settings(force_base=True)
assert self.mocks['olympia.blocklist.cron.upload_filter.delay'].called assert self.mocks['olympia.blocklist.cron.upload_filter.delay'].called
def test_invalid_cache_results_in_diff(self): def test_invalid_cache_results_in_diff(self):
self._block_version(block_type=BlockType.BLOCKED) self._block_version(block_type=BlockType.BLOCKED)
# First we re-create the last filter including the blocked version # First we create the current filter including the blocked version
self.mocks[
'olympia.blocklist.cron.get_generation_time'
].return_value = self.last_time
upload_mlbf_to_remote_settings() upload_mlbf_to_remote_settings()
base_mlbf = MLBF.load_from_storage(self.last_time) base_mlbf = MLBF.load_from_storage(self.current_time)
# Remove the blocked version from the cache.json file so we can test that # Remove the blocked version from the cache.json file so we can test that
# the next generation includes the blocked version. # the next generation includes the blocked version.
@ -367,24 +652,37 @@ class TestUploadToRemoteSettings(TestCase):
json.dump(data, f) json.dump(data, f)
f.truncate() f.truncate()
# Reset the generation time to the current time so we can test that the # Set the generation time to after the current time so we can test that the
# diff includes the blocked version after it is removed from the cache.json # diff includes the blocked version after it is removed from the cache.json
next_time = self.current_time + 1
self.mocks[ self.mocks[
'olympia.blocklist.cron.get_generation_time' 'olympia.blocklist.cron.get_generation_time'
].return_value = self.current_time ].return_value = next_time
upload_mlbf_to_remote_settings() upload_mlbf_to_remote_settings()
# We expect to upload a stash because the cache.json we are comparing against # We expect to upload a stash because the cache.json we are comparing against
# is missing the blocked version. # is missing the blocked version.
assert ( assert (
mock.call( mock.call(
self.current_time, next_time,
filter_list=[], filter_list=[],
create_stash=True, create_stash=True,
) )
in self.mocks['olympia.blocklist.cron.upload_filter.delay'].call_args_list in self.mocks['olympia.blocklist.cron.upload_filter.delay'].call_args_list
) )
def test_pass_correct_arguments_to_upload_filter(self):
self.mocks['olympia.blocklist.cron.upload_filter.delay'].stop()
with mock.patch(
'olympia.blocklist.cron.upload_filter.delay', wraps=upload_filter.delay
) as spy_delay:
upload_mlbf_to_remote_settings(force_base=True)
spy_delay.assert_called_with(
self.current_time,
filter_list=[BlockType.BLOCKED.name],
create_stash=False,
)
class TestTimeMethods(TestCase): class TestTimeMethods(TestCase):
@freeze_time('2024-10-10 12:34:56') @freeze_time('2024-10-10 12:34:56')
@ -398,9 +696,10 @@ class TestTimeMethods(TestCase):
assert get_last_generation_time() == 1 assert get_last_generation_time() == 1
def test_get_base_generation_time(self): def test_get_base_generation_time(self):
assert get_base_generation_time() is None for block_type in BlockType:
set_config(MLBF_BASE_ID_CONFIG_KEY(BlockType.BLOCKED, compat=True), 1) assert get_base_generation_time(block_type) is None
assert get_base_generation_time() == 1 set_config(MLBF_BASE_ID_CONFIG_KEY(block_type, compat=True), 1)
assert get_base_generation_time(block_type) == 1
@pytest.mark.django_db @pytest.mark.django_db