# 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_fulltext 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() search_fulltext.index_feature(self.feature_1) self.stage_1_ship = Stage( feature_id=self.feature_1_id, stage_type=core_enums.STAGE_BLINK_SHIPPING, milestones=MilestoneSet(desktop_first=99, android_first=100), ) self.stage_1_ship.put() self.feature_2 = FeatureEntry( name='feature b', summary='summary of editor stuff', owner_emails=['owner@example.com'], category=1, impl_status_chrome=3, accurate_as_of=datetime.datetime(2023, 6, 1), ) self.feature_2.put() search_fulltext.index_feature(self.feature_1) self.feature_2_id = self.feature_2.key.integer_id() self.stage_2_ot = Stage( feature_id=self.feature_2_id, stage_type=core_enums.STAGE_BLINK_ORIGIN_TRIAL, milestones=MilestoneSet(desktop_first=89, desktop_last=95, webview_first=100), ) self.stage_2_ot.put() self.feature_3 = FeatureEntry( name='feature c', summary='sum', owner_emails=['random@example.com'], category=1, impl_status_chrome=4, accurate_as_of=datetime.datetime(2024, 6, 1), ) 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): for kind in [ FeatureEntry, search_fulltext.FeatureWords, Stage, Gate, Vote]: for entry in kind.query(): entry.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 test_single_field_query_async__inequality_nulls_first(self): """accurate_as_of treats None as before any comparison value.""" actual_promise = search_queries.single_field_query_async( 'accurate_as_of', '<', [datetime.datetime(2024, 1, 1)] ) 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( 'accurate_as_of', '>', [datetime.datetime(2024, 1, 1)] ) actual = actual_promise.get_result() self.assertCountEqual([self.feature_3_id], [key.integer_id() for key in actual]) def test_single_field_query_async__any_start_milestone(self): actual = search_queries.single_field_query_async( 'any_start_milestone', '=', [100] ).get_result() self.assertEqual( set([self.feature_1_id, self.feature_2_id]), set(proj.feature_id for proj in actual), 'Finds across multiple milestones.', ) actual = search_queries.single_field_query_async( 'any_start_milestone', '=', [95] ).get_result() self.assertEqual( set(), set(proj.feature_id for proj in actual), 'Does not find "last" milestones.' ) actual = search_queries.single_field_query_async( 'any_start_milestone', '=', [search_queries.Interval(97, 99)] ).get_result() self.assertCountEqual( set([self.feature_1_id]), set(proj.feature_id for proj in actual), 'Intervals are constrained to a single milestone.', ) def check_wrong_type(self, field_name, bad_values): with self.assertRaises(ValueError) as cm: search_queries.single_field_query_async( field_name, '=', bad_values) 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('shipping_year', ['not an integer']) self.check_wrong_type('star_count', ['not an integer']) self.check_wrong_type('created.when', ['not a date']) self.check_wrong_type('owner', ['ok@example.com', True]) # 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) def test_single_field_query_async__fulltext_in_field(self): """We can search for words within a field.""" actual = search_queries.single_field_query_async( 'editor', ':', ['editor']) self.assertCountEqual([self.feature_1_id], actual) actual = search_queries.single_field_query_async( 'editor', ':', ['wrongword']) self.assertCountEqual([], actual) actual = search_queries.single_field_query_async( 'owner', ':', ['editor']) 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.""" future = search_queries.total_order_query_async('gate.requested_on') actual = search._resolve_promise_to_id_list(future) 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.""" future = search_queries.total_order_query_async('gate.reviewed_on') actual = search._resolve_promise_to_id_list(future) self.assertEqual( [self.feature_1_id, self.feature_2_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)