2024-02-14 20:01:17 +03:00
|
|
|
# Copyright 2024 Google LLC
|
|
|
|
#
|
|
|
|
# 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
|
|
|
|
#
|
|
|
|
# https://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 # isort: split
|
|
|
|
|
|
|
|
import json
|
|
|
|
import os.path
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
import flask
|
|
|
|
from werkzeug.exceptions import HTTPException
|
|
|
|
|
|
|
|
from api import features_api, spec_mentors_api
|
|
|
|
from framework import rediscache
|
|
|
|
from internals import user_models
|
|
|
|
from internals.core_models import FeatureEntry
|
|
|
|
|
|
|
|
test_app = flask.Flask(__name__)
|
|
|
|
|
|
|
|
BASE_FEATURE_CREATE_BODY = {
|
|
|
|
'name': 'A name',
|
|
|
|
'summary': 'A summary',
|
|
|
|
'owner_emails': 'user@example.com',
|
|
|
|
'category': 2,
|
|
|
|
'feature_type': 1,
|
|
|
|
'impl_status_chrome': 3,
|
|
|
|
'standard_maturity': 2,
|
|
|
|
'ff_views': 1,
|
|
|
|
'safari_views': 1,
|
|
|
|
'web_dev_views': 1,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class SpecMentorsAPITest(testing_config.CustomTestCase):
|
|
|
|
def setUp(self):
|
|
|
|
self.api_base = '/api/v0'
|
|
|
|
self.request_path = f'{self.api_base}/spec_mentors'
|
|
|
|
self.spec_mentors_handler = spec_mentors_api.SpecMentorsAPI()
|
|
|
|
|
|
|
|
self.feature_handler = features_api.FeaturesAPI()
|
|
|
|
|
|
|
|
self.app_admin = user_models.AppUser(email='admin@example.com')
|
|
|
|
self.app_admin.is_admin = True
|
|
|
|
self.app_admin.put()
|
|
|
|
|
|
|
|
self.created_features = []
|
|
|
|
|
|
|
|
def tearDown(self):
|
|
|
|
for feature in self.created_features:
|
|
|
|
feature.key.delete()
|
|
|
|
self.app_admin.key.delete()
|
|
|
|
testing_config.sign_out()
|
|
|
|
|
2024-09-24 21:02:02 +03:00
|
|
|
rediscache.delete_keys_with_prefix('features')
|
|
|
|
rediscache.delete_keys_with_prefix('FeatureEntries')
|
2024-02-14 20:01:17 +03:00
|
|
|
|
|
|
|
def createFeature(self, params) -> FeatureEntry:
|
|
|
|
with test_app.test_request_context(
|
|
|
|
f'{self.api_base}/features/create', json=BASE_FEATURE_CREATE_BODY | params
|
|
|
|
):
|
|
|
|
response = self.feature_handler.do_post()
|
|
|
|
feature = FeatureEntry.get_by_id(response['feature_id'])
|
|
|
|
self.created_features.append(feature)
|
|
|
|
return feature
|
|
|
|
|
|
|
|
def test_finds_one_mentored_feature(self):
|
|
|
|
"""Returns the single feature that exists, which has a mentor."""
|
|
|
|
# Create a feature using the admin user.
|
|
|
|
testing_config.sign_in(self.app_admin.email, 123567890)
|
|
|
|
|
|
|
|
feature = self.createFeature({'name': 'The feature'})
|
|
|
|
feature.spec_mentor_emails = ['mentor@example.org']
|
|
|
|
feature.put()
|
|
|
|
testing_config.sign_out()
|
|
|
|
|
|
|
|
with test_app.test_request_context(self.request_path):
|
|
|
|
response = self.spec_mentors_handler.do_get()
|
|
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
response,
|
|
|
|
[
|
|
|
|
{
|
|
|
|
'email': 'mentor@example.org',
|
|
|
|
'mentored_features': [{'id': feature.key.id(), 'name': 'The feature'}],
|
|
|
|
}
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_omits_unlisted_feature(self):
|
|
|
|
"""Does not return an unlisted feature that has a mentor."""
|
|
|
|
# Create a feature using the admin user.
|
|
|
|
testing_config.sign_in(self.app_admin.email, 123567890)
|
|
|
|
|
|
|
|
feature = self.createFeature({'name': 'The feature'})
|
|
|
|
feature.spec_mentor_emails = ['mentor@example.org']
|
|
|
|
feature.unlisted = True
|
|
|
|
feature.put()
|
|
|
|
testing_config.sign_out()
|
|
|
|
|
|
|
|
with test_app.test_request_context(self.request_path):
|
|
|
|
response = self.spec_mentors_handler.do_get()
|
|
|
|
|
|
|
|
self.assertEqual(response, [])
|
|
|
|
|
|
|
|
def test_obeys_after_param(self):
|
|
|
|
"""The ?after URL parameter filters features."""
|
|
|
|
# Create a feature using the admin user.
|
|
|
|
testing_config.sign_in(self.app_admin.email, 123567890)
|
|
|
|
|
|
|
|
feature = self.createFeature({'name': 'The feature'})
|
|
|
|
feature.spec_mentor_emails = ['mentor@example.org']
|
|
|
|
feature.updated = datetime.fromisoformat('2024-01-14')
|
|
|
|
feature.put()
|
|
|
|
testing_config.sign_out()
|
|
|
|
|
|
|
|
with test_app.test_request_context(f'{self.request_path}?after=2024-01-15'):
|
|
|
|
response = self.spec_mentors_handler.do_get()
|
|
|
|
|
|
|
|
self.assertEqual(response, [])
|
|
|
|
|
|
|
|
# Now the feature was last updated after the 'after' param.
|
|
|
|
feature.updated = datetime.fromisoformat('2024-01-16')
|
|
|
|
feature.put()
|
|
|
|
|
|
|
|
with test_app.test_request_context(f'{self.request_path}?after=2024-01-15'):
|
|
|
|
response = self.spec_mentors_handler.do_get()
|
|
|
|
self.assertEqual(
|
|
|
|
response,
|
|
|
|
[
|
|
|
|
{
|
|
|
|
'email': 'mentor@example.org',
|
|
|
|
'mentored_features': [{'id': feature.key.id(), 'name': 'The feature'}],
|
|
|
|
}
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_fails_on_malformed_after_param(self):
|
|
|
|
"""An ?after value that isn't a date returns a 400 error."""
|
|
|
|
with test_app.test_request_context(f'{self.request_path}?after=arglebargle'):
|
|
|
|
with self.assertRaises(HTTPException) as cm:
|
|
|
|
self.spec_mentors_handler.do_get()
|
|
|
|
self.assertEqual(cm.exception.code, 400)
|
|
|
|
|
|
|
|
def test_sorts_mentors_alphabetically(self):
|
|
|
|
# Create a feature using the admin user.
|
|
|
|
testing_config.sign_in(self.app_admin.email, 123567890)
|
|
|
|
|
|
|
|
feature = self.createFeature({'name': 'The feature'})
|
|
|
|
feature.spec_mentor_emails = ['bmentor@example.org', 'amentor@example.org']
|
|
|
|
feature.put()
|
|
|
|
testing_config.sign_out()
|
|
|
|
|
|
|
|
with test_app.test_request_context(self.request_path):
|
|
|
|
response = self.spec_mentors_handler.do_get()
|
|
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
response,
|
|
|
|
[
|
|
|
|
{
|
|
|
|
'email': 'amentor@example.org',
|
|
|
|
'mentored_features': [{'id': feature.key.id(), 'name': 'The feature'}],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'email': 'bmentor@example.org',
|
|
|
|
'mentored_features': [{'id': feature.key.id(), 'name': 'The feature'}],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_sorts_features_by_updated_date_recent_first(self):
|
|
|
|
# Create a feature using the admin user.
|
|
|
|
testing_config.sign_in(self.app_admin.email, 123567890)
|
|
|
|
|
|
|
|
feature1 = self.createFeature({'name': 'First feature'})
|
|
|
|
feature1.spec_mentor_emails = ['mentor@example.org']
|
|
|
|
feature1.put()
|
|
|
|
|
|
|
|
feature2 = self.createFeature({'name': 'Second feature'})
|
|
|
|
feature2.spec_mentor_emails = ['mentor@example.org']
|
|
|
|
feature2.put()
|
|
|
|
testing_config.sign_out()
|
|
|
|
|
|
|
|
with test_app.test_request_context(self.request_path):
|
|
|
|
response = self.spec_mentors_handler.do_get()
|
|
|
|
self.assertEqual(
|
|
|
|
response,
|
|
|
|
[
|
|
|
|
{
|
|
|
|
'email': 'mentor@example.org',
|
|
|
|
'mentored_features': [
|
|
|
|
# The later-created feature is listed first.
|
|
|
|
{'id': feature2.key.id(), 'name': 'Second feature'},
|
|
|
|
{'id': feature1.key.id(), 'name': 'First feature'},
|
|
|
|
],
|
|
|
|
}
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
# Make the first feature more-recently updated.
|
|
|
|
feature1.updated = datetime.now()
|
|
|
|
feature1.put()
|
|
|
|
|
|
|
|
with test_app.test_request_context(self.request_path):
|
|
|
|
response = self.spec_mentors_handler.do_get()
|
|
|
|
self.assertEqual(
|
|
|
|
response,
|
|
|
|
[
|
|
|
|
{
|
|
|
|
'email': 'mentor@example.org',
|
|
|
|
'mentored_features': [
|
|
|
|
# And the order switches.
|
|
|
|
{'id': feature1.key.id(), 'name': 'First feature'},
|
|
|
|
{'id': feature2.key.id(), 'name': 'Second feature'},
|
|
|
|
],
|
|
|
|
}
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_organizes_features_by_mentor(self):
|
|
|
|
# Create a feature using the admin user.
|
|
|
|
testing_config.sign_in(self.app_admin.email, 123567890)
|
|
|
|
|
|
|
|
feature1 = self.createFeature({'name': 'First feature'})
|
|
|
|
feature1.spec_mentor_emails = ['mentor@example.org', 'expert@example.com']
|
|
|
|
feature1.put()
|
|
|
|
|
|
|
|
feature2 = self.createFeature({'name': 'Second feature'})
|
|
|
|
feature2.spec_mentor_emails = ['mentor@example.org']
|
|
|
|
feature2.put()
|
|
|
|
testing_config.sign_out()
|
|
|
|
|
|
|
|
with test_app.test_request_context(self.request_path):
|
|
|
|
response = self.spec_mentors_handler.do_get()
|
|
|
|
|
|
|
|
# Unlike the other test expectations in this file, this one is saved to a JSON file so the
|
|
|
|
# Playwright tests can use it as a mock API response. Because the real feature IDs are
|
|
|
|
# dynamically generated, we have to slot them into the right places here.
|
|
|
|
with open(
|
|
|
|
os.path.join(
|
|
|
|
os.path.dirname(__file__),
|
|
|
|
'../packages/playwright/tests/spec_mentor_api_result.json',
|
|
|
|
)
|
|
|
|
) as f:
|
|
|
|
expected_response = json.load(f)
|
|
|
|
expected_response[0]['mentored_features'][0]['id'] = feature1.key.id()
|
|
|
|
expected_response[1]['mentored_features'][0]['id'] = feature2.key.id()
|
|
|
|
expected_response[1]['mentored_features'][1]['id'] = feature1.key.id()
|
|
|
|
|
|
|
|
self.assertEqual(response, expected_response)
|