chromium-dashboard/internals/search_queries_test.py

318 строки
12 KiB
Python

# Copyright 2020 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License")
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import testing_config # Must be imported before the module under test.
import datetime
from unittest import mock
from internals import core_enums
from internals.core_models import FeatureEntry, MilestoneSet, Stage
from internals.review_models import Gate, Vote
from internals import search
from internals import search_queries
class SearchFeaturesTest(testing_config.CustomTestCase):
def setUp(self):
self.feature_1 = FeatureEntry(
name='feature a', summary='sum',
category=1, impl_status_chrome=3)
self.feature_1.owner_emails = ['owner@example.com']
self.feature_1.editor_emails = ['editor@example.com']
self.feature_1.cc_emails = ['cc@example.com']
self.feature_1.put()
self.feature_1_id = self.feature_1.key.integer_id()
self.stage_1_ship = Stage(
feature_id=self.feature_1_id,
stage_type=core_enums.STAGE_BLINK_SHIPPING,
milestones=MilestoneSet(desktop_first=99))
self.stage_1_ship.put()
self.feature_2 = FeatureEntry(
name='feature b', summary='sum', owner_emails=['owner@example.com'],
category=1, impl_status_chrome=3)
self.feature_2.put()
self.feature_2_id = self.feature_2.key.integer_id()
self.feature_3 = FeatureEntry(
name='feature c', summary='sum', owner_emails=['random@example.com'],
category=1, impl_status_chrome=4)
self.feature_3.put()
self.feature_3_id = self.feature_3.key.integer_id()
self.gate_1 = Gate(
feature_id=self.feature_1_id, stage_id=1,
gate_type=core_enums.GATE_API_PROTOTYPE,
state=Vote.APPROVED,
requested_on=datetime.datetime(2022, 7, 1))
self.gate_1.put()
self.gate_1_id = self.gate_1.key.integer_id()
self.vote_1_1 = Vote(
feature_id=self.feature_1_id, gate_type=core_enums.GATE_API_PROTOTYPE,
gate_id=self.gate_1_id,
state=Vote.REVIEW_REQUESTED,
set_on=datetime.datetime(2022, 7, 1),
set_by='feature_owner@example.com')
self.vote_1_1.put()
self.vote_1_2 = Vote(
feature_id=self.feature_1_id, gate_type=core_enums.GATE_API_PROTOTYPE,
gate_id=self.gate_1_id,
state=Vote.APPROVED,
set_on=datetime.datetime(2022, 7, 2),
set_by='reviewer@example.com')
self.vote_1_2.put()
self.gate_2 = Gate(
feature_id=self.feature_2_id, stage_id=1,
gate_type=core_enums.GATE_API_SHIP,
state=Vote.REVIEW_REQUESTED,
requested_on=datetime.datetime(2022, 8, 1))
self.gate_2.put()
self.gate_2_id = self.gate_2.key.integer_id()
self.vote_2_1 = Vote(
feature_id=self.feature_2_id, gate_type=core_enums.GATE_API_SHIP,
gate_id=self.gate_2_id,
state=Vote.REVIEW_REQUESTED,
set_on=datetime.datetime(2022, 8, 1),
set_by='feature_owner@example.com')
self.vote_2_1.put()
self.vote_2_2 = Vote(
feature_id=self.feature_2_id, gate_type=core_enums.GATE_API_SHIP,
gate_id=self.gate_2_id,
state=Vote.APPROVED,
set_on=datetime.datetime(2022, 8, 2),
set_by='reviewer@example.com')
self.vote_2_2.put()
def tearDown(self):
self.feature_1.key.delete()
self.feature_2.key.delete()
self.feature_3.key.delete()
self.stage_1_ship.key.delete()
for gate in Gate.query():
gate.key.delete()
for vote in Vote.query():
vote.key.delete()
def test_single_field_query_async__normal(self):
"""We get a promise to run the DB query, which produces results."""
actual_promise = search_queries.single_field_query_async(
'owner', '=', 'owner@example.com')
actual = actual_promise.get_result()
self.assertCountEqual(
[self.feature_1_id, self.feature_2_id],
[key.integer_id() for key in actual])
actual_promise = search_queries.single_field_query_async(
'unlisted', '=', True)
actual = actual_promise.get_result()
self.assertCountEqual([], actual)
actual_promise = search_queries.single_field_query_async(
'deleted', '=', True)
actual = actual_promise.get_result()
self.assertCountEqual([], actual)
def test_single_field_query_async__multiple_vals(self):
"""We get a promise to run the DB query with multiple values."""
actual_promise = search_queries.single_field_query_async(
'owner', '=', 'owner@example.com,random@example.com')
actual = actual_promise.get_result()
self.assertCountEqual(
[self.feature_1_id, self.feature_2_id, self.feature_3_id],
[key.integer_id() for key in actual])
def check_wrong_type(self, field_name, bad_value):
with self.assertRaises(ValueError) as cm:
search_queries.single_field_query_async(
field_name, '=', bad_value)
self.assertEqual(
cm.exception.args[0], 'Query value does not match field type')
def test_single_field_query_async__wrong_types(self):
"""We reject requests with values that parse to the wrong type."""
# Feature entry fields
self.check_wrong_type('owner', True)
self.check_wrong_type('owner', 123)
self.check_wrong_type('deleted', 'not a boolean')
self.check_wrong_type('star_count', 'not an integer')
self.check_wrong_type('created.when', 'not a date')
# Stage fields
self.check_wrong_type('browsers.chrome.android', 'not an integer')
self.check_wrong_type('finch_url', 123)
self.check_wrong_type('finch_url', True)
def test_single_field_query_async__normal_stage_field(self):
"""We can find a FeatureEntry based on values in an associated Stage."""
actual_promise = search_queries.single_field_query_async(
'browsers.chrome.desktop', '=', 99)
actual = actual_promise.get_result()
self.assertCountEqual(
[self.feature_1_id],
[projection.feature_id for projection in actual])
def test_single_field_query_async__other_stage_field(self):
"""We only consider the appropriate Stage."""
actual_promise = search_queries.single_field_query_async(
'browsers.chrome.ot.desktop.start', '=', 99)
actual = actual_promise.get_result()
self.assertCountEqual([], actual)
def test_single_field_query_async__zero_results(self):
"""When there are no matching results, we get back a promise for []."""
actual_promise = search_queries.single_field_query_async(
'owner', '=', 'nope')
actual = actual_promise.get_result()
self.assertCountEqual([], actual)
@mock.patch('logging.warning')
def test_single_field_query_async__bad_field(self, mock_warn):
"""An unknown field imediately gives zero results."""
actual = search_queries.single_field_query_async('zodiac', '=', 'leo')
self.assertCountEqual([], actual)
def test_handle_me_query_async__owner_anon(self):
"""We can return a list of features owned by the user."""
testing_config.sign_in('visitor@example.com', 111)
future = search_queries.handle_me_query_async('owner')
actual = search._resolve_promise_to_id_list(future)
self.assertEqual(actual, [])
def test_handle_me_query__owner_some(self):
"""We can return a list of features owned by the user."""
testing_config.sign_in('owner@example.com', 111)
future = search_queries.handle_me_query_async('owner')
actual = search._resolve_promise_to_id_list(future)
self.assertEqual(
[self.feature_1_id, self.feature_2_id], actual)
def test_handle_me_query__editor_none(self):
"""We can return a list of features the user can edit."""
testing_config.sign_in('visitor@example.com', 111)
future = search_queries.handle_me_query_async('editor')
actual = search._resolve_promise_to_id_list(future)
self.assertEqual([], actual)
def test_handle_me_query__editor_some(self):
"""We can return a list of features the user can edit."""
testing_config.sign_in('editor@example.com', 111)
future = search_queries.handle_me_query_async('editor')
actual = search._resolve_promise_to_id_list(future)
self.assertEqual([self.feature_1_id], actual)
def test_handle_me_query__cc_none(self):
"""We can return a list of features the user is CC'd on."""
testing_config.sign_in('visitor@example.com', 111)
future = search_queries.handle_me_query_async('cc')
actual = search._resolve_promise_to_id_list(future)
self.assertEqual(actual, [])
def test_handle_me_query__cc_some(self):
"""We can return a list of features the user is CC'd on."""
testing_config.sign_in('cc@example.com', 111)
future = search_queries.handle_me_query_async('cc')
actual = search._resolve_promise_to_id_list(future)
self.assertEqual([self.feature_1_id], actual)
def test_handle_can_edit_me_query_async__anon(self):
"""Anon cannot edit any features."""
testing_config.sign_out()
future = search_queries.handle_can_edit_me_query_async()
actual = search._resolve_promise_to_id_list(future)
self.assertEqual([], actual)
def test_handle_can_edit_me_query_async__visitor(self):
"""Visitor cannot edit any features."""
testing_config.sign_in('visitor@example.com', 111)
future = search_queries.handle_can_edit_me_query_async()
actual = search._resolve_promise_to_id_list(future)
self.assertEqual([], actual)
def test_handle_can_edit_me_query_async__owner(self):
"""A feature owner can edit those features."""
testing_config.sign_in('owner@example.com', 111)
future = search_queries.handle_can_edit_me_query_async()
actual = search._resolve_promise_to_id_list(future)
self.assertEqual(
[self.feature_1_id, self.feature_2_id], actual)
def test_handle_can_edit_me_query_async__editor(self):
"""A feature editor can edit those features."""
testing_config.sign_in('editor@example.com', 111)
future = search_queries.handle_can_edit_me_query_async()
actual = search._resolve_promise_to_id_list(future)
self.assertEqual([self.feature_1_id], actual)
def test_total_order_query_async__field_asc(self):
"""We can get keys used to sort features in ascending order."""
future = search_queries.total_order_query_async('name')
actual = search._resolve_promise_to_id_list(future)
self.assertEqual(
[self.feature_1_id, self.feature_2_id, self.feature_3_id], actual)
def test_total_order_query_async__field_desc(self):
"""We can get keys used to sort features in descending order."""
future = search_queries.total_order_query_async('-name')
actual = search._resolve_promise_to_id_list(future)
self.assertEqual(
[self.feature_3_id, self.feature_2_id, self.feature_1_id], actual)
def test_total_order_query_async__requested_on(self):
"""We can get feature IDs sorted by gate review requests."""
actual = search_queries.total_order_query_async('gate.requested_on')
self.assertEqual(
[self.feature_2_id],
actual)
def test_total_order_query_async__reviewed_on(self):
"""We can get feature IDs sorted by gate resolution times."""
actual = search_queries.total_order_query_async('gate.reviewed_on')
self.assertEqual(
[self.feature_1_id],
actual)
def test_stage_fields_have_join_conditions(self):
"""Every STAGE_QUERIABLE_FIELDS has a STAGE_TYPES_BY_QUERY_FIELD entry."""
self.assertCountEqual(
search_queries.STAGE_QUERIABLE_FIELDS.keys(),
search_queries.STAGE_TYPES_BY_QUERY_FIELD.keys())
def test_negate_operator(self):
"""We can get correct negated operators"""
actual = search_queries.negate_operator('=')
self.assertEqual('!=', actual)
actual = search_queries.negate_operator('!=')
self.assertEqual('=', actual)
actual = search_queries.negate_operator('<')
self.assertEqual('>=', actual)
actual = search_queries.negate_operator('<=')
self.assertEqual('>', actual)
actual = search_queries.negate_operator('>')
self.assertEqual('<=', actual)
actual = search_queries.negate_operator('>=')
self.assertEqual('<', actual)