def process(self, version): """Process a single version, figuring out if it should be auto-approved and calling the approval code if necessary.""" already_locked = AutoApprovalSummary.check_is_locked(version) if not already_locked: # Lock the addon for ourselves if possible. Even though # AutoApprovalSummary.create_summary_for_version() will do # call check_is_locked() again later when calculating the verdict, # we have to do it now to prevent overwriting an existing lock with # our own. set_reviewing_cache(version.addon.pk, settings.TASK_USER_ID) try: log.info('Processing %s version %s...', unicode(version.addon.name), unicode(version.version)) summary, info = AutoApprovalSummary.create_summary_for_version( version, dry_run=self.dry_run) log.info('Auto Approval for %s version %s: %s', unicode(version.addon.name), unicode(version.version), summary.get_verdict_display()) self.stats.update({k: int(v) for k, v in info.items()}) if summary.verdict == self.successful_verdict: self.stats['auto_approved'] += 1 if summary.verdict == amo.AUTO_APPROVED: self.approve(version) except (AutoApprovalNotEnoughFilesError, AutoApprovalNoValidationResultError): log.info( 'Version %s was skipped either because it had no ' 'file or because it had no validation attached.', version) self.stats['error'] += 1 finally: # Always clear our own lock no matter what happens (but only ours). if not already_locked: clear_reviewing_cache(version.addon.pk)
def test_verdict_info_prettifier(self): verdict_info = { 'too_few_approved_updates': True, 'too_many_average_daily_users': True, 'uses_content_script_for_all_urls': True, 'uses_custom_csp': True, 'uses_native_messaging': True, 'has_info_request': True, 'is_locked': True, 'is_under_admin_review': True, } result = list( AutoApprovalSummary.verdict_info_prettifier(verdict_info)) assert result == [ u'Has a pending info request.', u'Is locked by a reviewer.', u'Is flagged for admin review.', u'Has too few consecutive human-approved updates.', u'Has too many daily users.', u'Uses a content script for all URLs.', u'Uses a custom CSP.', u'Uses nativeMessaging permission.' ] verdict_info = { 'too_few_approved_updates': True, 'uses_content_script_for_all_urls': True, 'uses_native_messaging': True } result = list( AutoApprovalSummary.verdict_info_prettifier(verdict_info)) assert result == [ u'Has too few consecutive human-approved updates.', u'Uses a content script for all URLs.', u'Uses nativeMessaging permission.' ]
def test_check_is_under_admin_review(self): assert AutoApprovalSummary.check_is_under_admin_review( self.version) is False self.version.addon.update(admin_review=True) assert AutoApprovalSummary.check_is_under_admin_review( self.version) is True
def test_check_is_locked(self): assert AutoApprovalSummary.check_is_locked(self.version) is False set_reviewing_cache(self.version.addon.pk, settings.TASK_USER_ID) assert AutoApprovalSummary.check_is_locked(self.version) is False set_reviewing_cache(self.version.addon.pk, settings.TASK_USER_ID + 42) assert AutoApprovalSummary.check_is_locked(self.version) is True
def test_check_uses_custom_csp(self): assert AutoApprovalSummary.check_uses_custom_csp(self.version) is False validation_data = { 'messages': [{ 'id': ['MANIFEST_CSP'], }] } self.file_validation.update(validation=json.dumps(validation_data)) assert AutoApprovalSummary.check_uses_custom_csp(self.version) is True
def test_check_uses_custom_csp_file_validation_missing(self): self.file_validation.delete() del self.version.all_files with self.assertRaises(AutoApprovalNoValidationResultError): AutoApprovalSummary.check_uses_custom_csp(self.version) # Also happens if only one file is missing validation info. self.file_validation = FileValidation.objects.create( file=self.version.all_files[0], validation=u'{}') del self.version.all_files file_factory(version=self.version, status=amo.STATUS_AWAITING_REVIEW) with self.assertRaises(AutoApprovalNoValidationResultError): AutoApprovalSummary.check_uses_custom_csp(self.version)
def test_check_uses_native_messaging(self): assert (AutoApprovalSummary.check_uses_native_messaging(self.version) is False) webext_permissions = WebextPermission.objects.create( file=self.file, permissions=['foobar']) del self.file.webext_permissions_list assert (AutoApprovalSummary.check_uses_native_messaging(self.version) is False) webext_permissions.update(permissions=['nativeMessaging', 'foobar']) del self.file.webext_permissions_list assert (AutoApprovalSummary.check_uses_native_messaging(self.version) is True)
def test_check_uses_content_script_for_all_urls(self): assert AutoApprovalSummary.check_uses_content_script_for_all_urls( self.version) is False webext_permissions = WebextPermission.objects.create( file=self.file, permissions=['https://example.com/*']) del self.file.webext_permissions_list assert AutoApprovalSummary.check_uses_content_script_for_all_urls( self.version) is False webext_permissions.update(permissions=['<all_urls>']) del self.file.webext_permissions_list assert AutoApprovalSummary.check_uses_content_script_for_all_urls( self.version) is True
def test_failed_verdict(self, create_summary_for_version_mock, approve_mock): fake_verdict_info = { 'uses_custom_csp': True, 'uses_native_messaging': True, 'uses_content_script_for_all_urls': True, 'too_many_average_daily_users': True, 'too_few_approved_updates': True, } create_summary_for_version_mock.return_value = (AutoApprovalSummary( verdict=amo.NOT_AUTO_APPROVED), fake_verdict_info) call_command('auto_approve') assert approve_mock.call_count == 0 assert create_summary_for_version_mock.call_args == (( self.version, ), { 'max_average_daily_users': 10000, 'min_approved_updates': 1, 'dry_run': False, 'post_review': False }) assert get_reviewing_cache(self.addon.pk) is None self._check_stats({ 'total': 1, 'uses_custom_csp': 1, 'uses_native_messaging': 1, 'uses_content_script_for_all_urls': 1, 'too_many_average_daily_users': 1, 'too_few_approved_updates': 1, })
def test_verdict_info_pretty(self): summary = AutoApprovalSummary.objects.create( version=self.version, uses_custom_csp=True, uses_native_messaging=True, uses_content_script_for_all_urls=True, average_daily_users=self.addon.average_daily_users, approved_updates=333) expected_result = list( AutoApprovalSummary.verdict_info_prettifier({ 'too_few_approved_updates': True, 'too_many_average_daily_users': True, 'uses_content_script_for_all_urls': True, 'uses_custom_csp': True, 'uses_native_messaging': True })) result = list( summary.calculate_verdict( max_average_daily_users=summary.average_daily_users - 1, min_approved_updates=summary.approved_updates + 1, pretty=True)) assert result == expected_result assert summary.verdict == amo.NOT_AUTO_APPROVED
def queue_content_review(request): qs = ( AutoApprovalSummary.get_content_review_queue() .select_related('addonapprovalscounter') .order_by('addonapprovalscounter__last_content_review', 'created') ) return _queue(request, ContentReviewTable, 'content_review', qs=qs, SearchForm=None)
def test_successful_verdict(self, create_summary_for_version_mock): create_summary_for_version_mock.return_value = ( AutoApprovalSummary(verdict=amo.AUTO_APPROVED), {}) call_command('auto_approve') assert create_summary_for_version_mock.call_args == ( (self.version, ), {'max_average_daily_users': 10000, 'min_approved_updates': 1, 'dry_run': False}) assert get_reviewing_cache(self.addon.pk) is None self._check_stats({'total': 1, 'auto_approved': 1})
def test_no_locking_if_already_locked( self, create_summary_for_version_mock, check_is_locked_mock, clear_reviewing_cache_mock, set_reviewing_cache_mock): check_is_locked_mock.return_value = True create_summary_for_version_mock.return_value = ( AutoApprovalSummary(), {}) call_command('auto_approve') assert create_summary_for_version_mock.call_count == 1 assert set_reviewing_cache_mock.call_count == 0 assert clear_reviewing_cache_mock.call_count == 0
def test_create_summary_no_previously_approved_versions( self, calculate_verdict_mock): AddonApprovalsCounter.objects.all().delete() self.version.reload() calculate_verdict_mock.return_value = {'dummy_verdict': True} summary, info = AutoApprovalSummary.create_summary_for_version( self.version, max_average_daily_users=111, min_approved_updates=222) assert summary.pk assert summary.approved_updates == 0 assert info == {'dummy_verdict': True}
def queue_auto_approved(request): qs = (AutoApprovalSummary.get_auto_approved_queue().select_related( 'addonapprovalscounter', '_current_version__autoapprovalsummary').order_by( '-_current_version__autoapprovalsummary__weight', 'addonapprovalscounter__last_human_review', 'created')) return _queue(request, AutoApprovedTable, 'auto_approved', qs=qs, SearchForm=None)
def queue_counts(type=None, unlisted=False, admin_reviewer=False, limited_reviewer=False, **kw): def construct_query(query_type, days_min=None, days_max=None): query = query_type.objects if not admin_reviewer: query = exclude_admin_only_addons(query) if days_min: query = query.having('waiting_time_days >=', days_min) if days_max: query = query.having('waiting_time_days <=', days_max) if limited_reviewer: query = query.having('waiting_time_hours >=', amo.REVIEW_LIMITED_DELAY_HOURS) return query.count counts = { 'pending': construct_query(ViewPendingQueue, **kw), 'nominated': construct_query(ViewFullReviewQueue, **kw), 'moderated': Review.objects.all().to_moderate().count, 'auto_approved': (AutoApprovalSummary.get_auto_approved_queue().count), 'content_review': (AutoApprovalSummary.get_content_review_queue().count), } if unlisted: counts = { 'all': (ViewUnlistedAllList.objects if admin_reviewer else exclude_admin_only_addons(ViewUnlistedAllList.objects)).count } rv = {} if isinstance(type, basestring): return counts[type]() for k, v in counts.items(): if not isinstance(type, list) or k in type: rv[k] = v() return rv
def test_locking(self, create_summary_for_version_mock, clear_reviewing_cache_mock, set_reviewing_cache_mock): create_summary_for_version_mock.return_value = (AutoApprovalSummary(), {}) call_command('auto_approve') assert create_summary_for_version_mock.call_count == 1 assert set_reviewing_cache_mock.call_count == 1 assert set_reviewing_cache_mock.call_args == ((self.addon.pk, settings.TASK_USER_ID), {}) assert clear_reviewing_cache_mock.call_count == 1 assert clear_reviewing_cache_mock.call_args == ((self.addon.pk, ), {})
def test_successful_verdict_dry_run(self, create_summary_for_version_mock, approve_mock): create_summary_for_version_mock.return_value = (AutoApprovalSummary( verdict=amo.WOULD_HAVE_BEEN_AUTO_APPROVED), {}) call_command('auto_approve', '--dry-run') assert approve_mock.call_count == 0 assert create_summary_for_version_mock.call_args == ((self.version, ), { 'dry_run': True }) assert get_reviewing_cache(self.addon.pk) is None self._check_stats({'total': 1, 'auto_approved': 1})
def test_failed_verdict(self, create_summary_for_version_mock, approve_mock): fake_verdict_info = {'is_locked': True} create_summary_for_version_mock.return_value = (AutoApprovalSummary( verdict=amo.NOT_AUTO_APPROVED), fake_verdict_info) call_command('auto_approve') assert approve_mock.call_count == 0 assert create_summary_for_version_mock.call_args == ((self.version, ), { 'dry_run': False }) assert get_reviewing_cache(self.addon.pk) is None self._check_stats({ 'total': 1, 'is_locked': 1, })
def test_create_summary_already_existing(self): # Create a dummy summary manually, then call the method to create a # real one. It should have just updated the previous instance. summary = AutoApprovalSummary.objects.create( version=self.version, uses_custom_csp=True, uses_native_messaging=True, uses_content_script_for_all_urls=True) assert summary.pk assert summary.version == self.version assert summary.uses_custom_csp is True assert summary.uses_native_messaging is True assert summary.uses_content_script_for_all_urls is True assert summary.average_daily_users == 0 assert summary.approved_updates == 0 assert summary.verdict == amo.NOT_AUTO_APPROVED previous_summary_pk = summary.pk summary, info = AutoApprovalSummary.create_summary_for_version( self.version, max_average_daily_users=self.addon.average_daily_users + 1, min_approved_updates=1) assert summary.pk == previous_summary_pk assert summary.version == self.version assert summary.uses_custom_csp is False assert summary.uses_native_messaging is False assert summary.uses_content_script_for_all_urls is False assert summary.average_daily_users == self.addon.average_daily_users assert summary.approved_updates == 1 assert summary.verdict == amo.AUTO_APPROVED assert info == { 'too_few_approved_updates': False, 'too_many_average_daily_users': False, 'uses_content_script_for_all_urls': False, 'uses_custom_csp': False, 'uses_native_messaging': False, 'has_info_request': False, 'is_locked': False, 'is_under_admin_review': False, }
def test_create_summary_for_version(self, calculate_verdict_mock): calculate_verdict_mock.return_value = {'dummy_verdict': True} summary, info = AutoApprovalSummary.create_summary_for_version( self.version, max_average_daily_users=111, min_approved_updates=222) assert calculate_verdict_mock.call_count == 1 assert calculate_verdict_mock.call_args == ({ 'max_average_daily_users': 111, 'min_approved_updates': 222, 'dry_run': False },) assert summary.pk assert summary.version == self.version assert summary.uses_custom_csp is False assert summary.uses_native_messaging is False assert summary.uses_content_script_for_all_urls is False assert summary.average_daily_users == self.addon.average_daily_users assert (summary.approved_updates == self.addon.addonapprovalscounter.counter) assert info == {'dummy_verdict': True}
def test_create_summary_no_files(self): self.file.delete() del self.version.all_files with self.assertRaises(AutoApprovalNotEnoughFilesError): AutoApprovalSummary.create_summary_for_version(self.version)
def handle(self, *args, **options): dry_run = options.get('dry_run', False) max_average_daily_users = int( get_config('AUTO_APPROVAL_MAX_AVERAGE_DAILY_USERS') or 0) min_approved_updates = int( get_config('AUTO_APPROVAL_MIN_APPROVED_UPDATES') or 0) if min_approved_updates <= 0 or max_average_daily_users <= 0: # Auto approval are shut down if one of those values is not present # or <= 0. url = '%s%s' % (settings.SITE_URL, reverse('admin:zadmin_config_changelist')) raise CommandError( 'Auto-approvals are deactivated because either ' 'AUTO_APPROVAL_MAX_AVERAGE_DAILY_USERS or ' 'AUTO_APPROVAL_MIN_APPROVED_UPDATES have not been ' 'set or were set to 0. Use the admin tools Config model to ' 'set them by going to %s.' % url) stats = Counter() qs = self.fetch_candidates() stats['total'] = len(qs) successful_verdict = (amo.WOULD_HAVE_BEEN_AUTO_APPROVED if dry_run else amo.AUTO_APPROVED) for version in qs: # Is the addon already locked by a reviewer ? if get_reviewing_cache(version.addon.pk): stats['locked'] += 1 continue # If admin review or more information was requested, skip this # version, let a human handle it. if version.addon.admin_review or version.has_info_request: stats['flagged'] += 1 continue # Lock the addon for ourselves, no reviewer should touch it. set_reviewing_cache(version.addon.pk, settings.TASK_USER_ID) try: log.info('Processing %s version %s...', unicode(version.addon.name), unicode(version.version)) summary, info = AutoApprovalSummary.create_summary_for_version( version, max_average_daily_users=max_average_daily_users, min_approved_updates=min_approved_updates, dry_run=dry_run) log.info('Auto Approval for %s version %s: %s', unicode(version.addon.name), unicode(version.version), summary.get_verdict_display()) stats.update({k: int(v) for k, v in info.items()}) if summary.verdict == successful_verdict: stats['auto_approved'] += 1 # FIXME: implement auto-approve if verdict is amo.AUTO_APPROVED except (AutoApprovalNotEnoughFilesError, AutoApprovalNoValidationResultError): log.info( 'Version %s was skipped either because it had no ' 'file or because it had no validation attached.', version) stats['error'] += 1 finally: clear_reviewing_cache(version.addon.pk) self.log_final_summary(stats, dry_run=dry_run)
def test_check_has_info_request(self): assert AutoApprovalSummary.check_has_info_request( self.version) is False self.version.update(has_info_request=True) assert AutoApprovalSummary.check_has_info_request(self.version) is True