chromium-dashboard/api/origin_trials_api_test.py

736 строки
27 KiB
Python

# Copyright 2024 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.
from unittest import mock
import flask
import werkzeug.exceptions # Flask HTTP stuff.
import testing_config # Must be imported before the module under test.
from api import origin_trials_api
from internals.core_models import FeatureEntry, MilestoneSet, Stage
from internals.review_models import Gate, Vote
from internals.user_models import AppUser
test_app = flask.Flask(__name__)
class OriginTrialsAPITest(testing_config.CustomTestCase):
def setUp(self):
self.feature_1 = FeatureEntry(
feature_type=1, name='feature one', summary='sum', category=1,
owner_emails=['owner@example.com'])
self.feature_1.put()
self.feature_1_id = self.feature_1.key.integer_id()
self.ot_stage_1 = Stage(
feature_id=self.feature_1_id, stage_type=150,
origin_trial_id='-1234567890')
self.ot_stage_1.put()
self.extension_stage_1 = Stage(
feature_id=self.feature_1_id, stage_type=151,
ot_stage_id=self.ot_stage_1.key.integer_id(),
milestones=MilestoneSet(desktop_last=153),
intent_thread_url='https://example.com/intent')
self.extension_stage_1.put()
self.extension_gate_1 = Gate(feature_id=self.feature_1_id,
stage_id=self.extension_stage_1.key.integer_id(),
gate_type=3, state=Vote.APPROVED)
self.extension_gate_1.put()
self.feature_2 = FeatureEntry(
feature_type=1, name='feature two', summary='sum', category=1)
self.feature_2.put()
self.ot_stage_2 = Stage(
feature_id=self.feature_2.key.integer_id(), stage_type=150,
origin_trial_id='9876543210')
self.ot_stage_2.put()
self.handler = origin_trials_api.OriginTrialsAPI()
self.request_path = (
'/api/v0/origintrials/'
f'{self.feature_1_id}/{self.extension_stage_1.key.integer_id()}/extend')
self.google_user = AppUser(email='feature_owner@google.com')
self.google_user.put()
self.mock_web_usecounters_file = """
enum WebFeature {
kSomeFeature = 1,
kValidFeature = 2,
kNoThirdParty = 3
kSample = 4,
kNoCriticalTrial = 5,
};
"""
self.mock_webdx_usecounters_file = """
enum WebFeature {
kSomeFeature = 1,
kValidFeature = 2,
kNoThirdParty = 3
kSample = 4,
kNoCriticalTrial = 5,
};
"""
self.mock_features_file = """
{
"data": [
{
"name": "ValidFeaturePart1",
"origin_trial_feature_name": "ValidFeature",
"origin_trial_allows_third_party": true
},
{
"name": "ValidFeaturePart2",
"origin_trial_feature_name": "ValidFeature",
"origin_trial_allows_third_party": true
},
{
"name": "InvalidFeaturePart1",
"origin_trial_feature_name": "InvalidFeature"
},
{
"name": "NoThirdParty",
"origin_trial_feature_name": "NoThirdParty"
},
{
"name": "NoCriticalTrialEntry",
"origin_trial_feature_name": "NoCriticalTrial"
}
]
}
"""
self.mock_grace_period_file = """
bool FeatureHasExpiryGracePeriod(blink::mojom::OriginTrialFeature feature) {
static blink::mojom::OriginTrialFeature const kHasExpiryGracePeriod[] = {
// Production grace period trials start here:
blink::mojom::OriginTrialFeature::kSomeFeature,
blink::mojom::OriginTrialFeature::kValidFeature,
blink::mojom::OriginTrialFeature::kInvalidFeature,
};
return base::Contains(kHasExpiryGracePeriod, feature);
}
"""
self.existing_origin_trials = [
{
'origin_trial_feature_name': 'ExistingFeature',
}
]
def tearDown(self):
for kind in [AppUser, FeatureEntry, Gate, Stage]:
for entity in kind.query():
entity.key.delete()
def mock_chromium_file_return_value_generator(self, *args, **kwargs):
"""Returns mock milestone info based on input."""
if args == ('https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom?format=TEXT',):
return self.mock_web_usecounters_file
if args == ('https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/public/mojom/use_counter/metrics/webdx_feature.mojom?format=TEXT',):
return self.mock_webdx_usecounters_file
if args == ('https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/platform/runtime_enabled_features.json5?format=TEXT',):
return self.mock_features_file
if args == ('https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/common/origin_trials/manual_completion_origin_trial_features.cc?format=TEXT',):
return self.mock_grace_period_file
return ''
def test_check_post_permissions__anon(self):
"""Anon users cannot request origin trials."""
testing_config.sign_out()
with test_app.test_request_context(
self.request_path, method='POST', json={}):
with self.assertRaises(werkzeug.exceptions.Forbidden):
self.handler.check_post_permissions(self.feature_1_id)
def test_check_post_permissions__rando(self):
"""Random users cannot request origin trials."""
testing_config.sign_in('user@example.com', 1234567890)
with test_app.test_request_context(
self.request_path, method='POST', json={}):
with self.assertRaises(werkzeug.exceptions.Forbidden):
self.handler.check_post_permissions(self.feature_1_id)
def test_check_post_permissions__googler_chromie(self):
"""Googlers and chromies can request origin trials."""
testing_config.sign_in('user@google.com', 1234567890)
with test_app.test_request_context(
self.request_path, method='POST', json={}):
result = self.handler.check_post_permissions(self.feature_1_id)
self.assertEqual({}, result)
testing_config.sign_in('user@chromium.org', 1234567890)
with test_app.test_request_context(
self.request_path, method='POST', json={}):
result = self.handler.check_post_permissions(self.feature_1_id)
self.assertEqual({}, result)
def test_check_post_permissions__feature_owner(self):
"""Feature owners may request an OT."""
testing_config.sign_in('owner@example.com', 1234567890)
with test_app.test_request_context(
self.request_path, method='POST', json={}):
result = self.handler.check_post_permissions(self.feature_1_id)
self.assertEqual({}, result)
def test_do_post__feature_not_found(self):
"""Give a 404 if the no such feature exists."""
kwargs = {'feature_id': '999'}
with test_app.test_request_context(self.request_path):
with self.assertRaises(werkzeug.exceptions.NotFound):
self.handler.do_post(**kwargs)
def test_do_post__stage_not_found(self):
"""Give a 404 if the no such stage exists."""
kwargs = {
'feature_id': str(self.feature_1_id),
'stage_id': '999'}
with test_app.test_request_context(self.request_path):
with self.assertRaises(werkzeug.exceptions.NotFound):
self.handler.do_post(**kwargs)
def test_do_post__anon(self):
"""Anon users cannot request origin trials."""
testing_config.sign_out()
kwargs = {
'feature_id': str(self.feature_1_id),
'stage_id': str(self.ot_stage_1.key.integer_id())}
with test_app.test_request_context(
self.request_path, method='POST', json={}):
with self.assertRaises(werkzeug.exceptions.Forbidden):
self.handler.do_post(**kwargs)
@mock.patch('api.origin_trials_api.get_chromium_file')
def test_validate_creation_args__valid_webfeature(self, mock_get_chromium_file):
"""No error messages should be returned if all args are valid using a
WebFeature use counter."""
mock_get_chromium_file.side_effect = self.mock_chromium_file_return_value_generator
body = {
'ot_chromium_trial_name': {
'form_field_name': 'ot_chromium_trial_name',
'value': 'ValidFeature',
},
'ot_webfeature_use_counter': {
'form_field_name': 'ot_webfeature_use_counter',
'value': 'kValidFeature',
},
'ot_is_critical_trial': {
'form_field_name': 'ot_is_critical_trial',
'value': True,
},
'ot_is_deprecation_trial': {
'form_field_name': 'ot_is_deprecation_trial',
'value': False,
},
'ot_has_third_party_support': {
'form_field_name': 'ot_has_third_party_support',
'value': True,
},
}
# No exception should be raised.
with test_app.test_request_context(self.request_path):
result = self.handler._validate_creation_args(body)
expected = {}
self.assertEqual(expected, result)
@mock.patch('api.origin_trials_api.get_chromium_file')
def test_validate_creation_args__valid_webdxfeature(self, mock_get_chromium_file):
"""No error messages should be returned if all args are valid using a
WebDXFeature use counter."""
mock_get_chromium_file.side_effect = self.mock_chromium_file_return_value_generator
body = {
'ot_chromium_trial_name': {
'form_field_name': 'ot_chromium_trial_name',
'value': 'ValidFeature',
},
'ot_webfeature_use_counter': {
'form_field_name': 'ot_webfeature_use_counter',
'value': 'WebDXFeature::kValidFeature',
},
'ot_is_critical_trial': {
'form_field_name': 'ot_is_critical_trial',
'value': True,
},
'ot_is_deprecation_trial': {
'form_field_name': 'ot_is_deprecation_trial',
'value': False,
},
'ot_has_third_party_support': {
'form_field_name': 'ot_has_third_party_support',
'value': True,
},
}
# No exception should be raised.
with test_app.test_request_context(self.request_path):
result = self.handler._validate_creation_args(body)
expected = {}
self.assertEqual(expected, result)
@mock.patch('api.origin_trials_api.get_chromium_file')
def test_validate_creation_args__invalid_webfeature_use_counter(
self, mock_get_chromium_file):
"""Error message returned if UseCounter not found in file."""
mock_get_chromium_file.side_effect = self.mock_chromium_file_return_value_generator
body = {
'ot_chromium_trial_name': {
'form_field_name': 'ot_chromium_trial_name',
'value': 'ValidFeature',
},
'ot_webfeature_use_counter': {
'form_field_name': 'ot_webfeature_use_counter',
'value': 'kBadUseCounter',
},
'ot_is_critical_trial': {
'form_field_name': 'ot_is_critical_trial',
'value': False,
},
'ot_is_deprecation_trial': {
'form_field_name': 'ot_is_deprecation_trial',
'value': False,
},
'ot_has_third_party_support': {
'form_field_name': 'ot_has_third_party_support',
'value': False,
},
}
with test_app.test_request_context(self.request_path):
result = self.handler._validate_creation_args(body)
expected = {
'ot_webfeature_use_counter': 'UseCounter not landed in web_feature.mojom'}
self.assertEqual(expected, result)
@mock.patch('api.origin_trials_api.get_chromium_file')
def test_validate_creation_args__invalid_webdxfeature_use_counter(
self, mock_get_chromium_file):
"""Error message returned if WebDXFeature UseCounter not found in file."""
mock_get_chromium_file.side_effect = self.mock_chromium_file_return_value_generator
body = {
'ot_chromium_trial_name': {
'form_field_name': 'ot_chromium_trial_name',
'value': 'ValidFeature',
},
'ot_webfeature_use_counter': {
'form_field_name': 'ot_webfeature_use_counter',
'value': 'WebDXFeature::kBadUseCounter'
},
'ot_is_critical_trial': {
'form_field_name': 'ot_is_critical_trial',
'value': False,
},
'ot_is_deprecation_trial': {
'form_field_name': 'ot_is_deprecation_trial',
'value': False,
},
'ot_has_third_party_support': {
'form_field_name': 'ot_has_third_party_support',
'value': False,
},
}
with test_app.test_request_context(self.request_path):
result = self.handler._validate_creation_args(body)
expected = {
'ot_webfeature_use_counter': 'UseCounter not landed in webdx_feature.mojom'}
self.assertEqual(expected, result)
@mock.patch('api.origin_trials_api.get_chromium_file')
def test_validate_creation_args__missing_webdxfeature_use_counter(
self, mock_get_chromium_file):
"""Error message returned if WebDXFeature UseCounter not found in file."""
mock_get_chromium_file.side_effect = self.mock_chromium_file_return_value_generator
body = {
'ot_chromium_trial_name': {
'form_field_name': 'ot_chromium_trial_name',
'value': 'ValidFeature',
},
'ot_webfeature_use_counter': {
'form_field_name': 'ot_webfeature_use_counter',
'value': 'WebDXFeature::'
},
'ot_is_critical_trial': {
'form_field_name': 'ot_is_critical_trial',
'value': False,
},
'ot_is_deprecation_trial': {
'form_field_name': 'ot_is_deprecation_trial',
'value': False,
},
'ot_has_third_party_support': {
'form_field_name': 'ot_has_third_party_support',
'value': False,
},
}
with test_app.test_request_context(self.request_path):
result = self.handler._validate_creation_args(body)
expected = {
'ot_webfeature_use_counter': 'No WebDXFeature use counter provided.'}
self.assertEqual(expected, result)
@mock.patch('api.origin_trials_api.get_chromium_file')
def test_validate_creation_args__missing_webfeature_use_counter(
self, mock_get_chromium_file):
"""Error message returned if both UseCounter types are missing for a
non-deprecation trial."""
mock_get_chromium_file.side_effect = self.mock_chromium_file_return_value_generator
body = {
'ot_chromium_trial_name': {
'form_field_name': 'ot_chromium_trial_name',
'value': 'ValidFeature',
},
'ot_is_critical_trial': {
'form_field_name': 'ot_is_critical_trial',
'value': False,
},
'ot_is_deprecation_trial': {
'form_field_name': 'ot_is_deprecation_trial',
'value': False,
},
'ot_has_third_party_support': {
'form_field_name': 'ot_has_third_party_support',
'value': False,
},
}
with test_app.test_request_context(self.request_path):
result = self.handler._validate_creation_args(body)
# A validation error should exist for both use counter fields.
expected = {
'ot_webfeature_use_counter': (
'No UseCounter specified for non-deprecation trial.')
}
self.assertEqual(expected, result)
@mock.patch('api.origin_trials_api.get_chromium_file')
def test_validate_creation_args__missing_webfeature_use_counter_deprecation(
self, mock_get_chromium_file):
"""No error message returned for missing UseCounter if deprecation trial."""
"""Deprecation trial does not need a webfeature use counter value."""
mock_get_chromium_file.side_effect = self.mock_chromium_file_return_value_generator
body = {
'ot_chromium_trial_name': {
'form_field_name': 'ot_chromium_trial_name',
'value': 'ValidFeature',
},
'ot_is_critical_trial': {
'form_field_name': 'ot_is_critical_trial',
'value': False,
},
'ot_is_deprecation_trial': {
'form_field_name': 'ot_is_deprecation_trial',
'value': True,
},
'ot_has_third_party_support': {
'form_field_name': 'ot_has_third_party_support',
'value': False,
},
}
with test_app.test_request_context(self.request_path):
result = self.handler._validate_creation_args(body)
expected = {}
self.assertEqual(expected, result)
@mock.patch('api.origin_trials_api.get_chromium_file')
def test_validate_creation_args__invalid_chromium_trial_name(
self, mock_get_chromium_file):
"""Error message returned if Chromium trial name not found in file."""
mock_get_chromium_file.side_effect = self.mock_chromium_file_return_value_generator
body = {
'ot_chromium_trial_name': {
'form_field_name': 'ot_chromium_trial_name',
'value': 'NonexistantFeature',
},
'ot_webfeature_use_counter': {
'form_field_name': 'ot_webfeature_use_counter',
'value': 'kValidFeature',
},
'ot_is_critical_trial': {
'form_field_name': 'ot_is_critical_trial',
'value': False,
},
'ot_is_deprecation_trial': {
'form_field_name': 'ot_is_deprecation_trial',
'value': False,
},
'ot_has_third_party_support': {
'form_field_name': 'ot_has_third_party_support',
'value': False,
},
}
with test_app.test_request_context(self.request_path):
result = self.handler._validate_creation_args(body)
expected = {'ot_chromium_trial_name': (
'Origin trial feature name not found in file')}
self.assertEqual(expected, result)
@mock.patch('api.origin_trials_api.get_chromium_file')
def test_validate_creation_args__missing_chromium_trial_name(
self, mock_get_chromium_file):
"""Error message returned if Chromium trial is missing from request."""
mock_get_chromium_file.side_effect = self.mock_chromium_file_return_value_generator
body = {
'ot_webfeature_use_counter': {
'form_field_name': 'ot_webfeature_use_counter',
'value': 'kValidFeature',
},
'ot_is_critical_trial': {
'form_field_name': 'ot_is_critical_trial',
'value': False,
},
'ot_is_deprecation_trial': {
'form_field_name': 'ot_is_deprecation_trial',
'value': False,
},
'ot_has_third_party_support': {
'form_field_name': 'ot_has_third_party_support',
'value': False,
},
}
with test_app.test_request_context(self.request_path):
with self.assertRaises(werkzeug.exceptions.BadRequest):
self.handler._validate_creation_args(body)
@mock.patch('api.origin_trials_api.get_chromium_file')
def test_validate_creation_args__invalid_third_party_trial(
self, mock_get_chromium_file):
"""Error message returned if third party support not found in file."""
mock_get_chromium_file.side_effect = self.mock_chromium_file_return_value_generator
body = {
'ot_chromium_trial_name': {
'form_field_name': 'ot_chromium_trial_name',
'value': 'NoThirdParty',
},
'ot_webfeature_use_counter': {
'form_field_name': 'ot_webfeature_use_counter',
'value': 'kNoThirdParty',
},
'ot_is_critical_trial': {
'form_field_name': 'ot_is_critical_trial',
'value': False,
},
'ot_is_deprecation_trial': {
'form_field_name': 'ot_is_deprecation_trial',
'value': False,
},
'ot_has_third_party_support': {
'form_field_name': 'ot_has_third_party_support',
'value': True,
},
}
with test_app.test_request_context(self.request_path):
result = self.handler._validate_creation_args(body)
expected = {'ot_has_third_party_support': (
'One or more features do not have third party '
'support set in runtime_enabled_features.json5. '
'Feature name: NoThirdParty')}
self.assertEqual(expected, result)
@mock.patch('api.origin_trials_api.get_chromium_file')
def test_validate_creation_args__invalid_critical_trial(
self, mock_get_chromium_file):
"""Error message returned if critical trial name not found in file."""
mock_get_chromium_file.side_effect = self.mock_chromium_file_return_value_generator
body = {
'ot_chromium_trial_name': {
'form_field_name': 'ot_chromium_trial_name',
'value': 'NoCriticalTrial',
},
'ot_webfeature_use_counter': {
'form_field_name': 'ot_webfeature_use_counter',
'value': 'kNoCriticalTrial',
},
'ot_is_critical_trial': {
'form_field_name': 'ot_is_critical_trial',
'value': True,
},
'ot_is_deprecation_trial': {
'form_field_name': 'ot_is_deprecation_trial',
'value': False,
},
'ot_has_third_party_support': {
'form_field_name': 'ot_has_third_party_support',
'value': False,
},
}
with test_app.test_request_context(self.request_path):
result = self.handler._validate_creation_args(body)
expected = {'ot_is_critical_trial': (
'Use counter has not landed in grace period array for critical trial')}
self.assertEqual(expected, result)
def test_validate_extension_args__valid(self):
# No exception should be raised.
with test_app.test_request_context(self.request_path):
self.handler._validate_extension_args(
self.feature_1_id, self.ot_stage_1, self.extension_stage_1)
def test_validate_extension_args__missing_ot_id(self):
self.ot_stage_1.origin_trial_id = None
self.ot_stage_1.put()
with test_app.test_request_context(self.request_path):
with self.assertRaises(werkzeug.exceptions.BadRequest):
self.handler._validate_extension_args(
self.feature_1_id, self.ot_stage_1, self.extension_stage_1)
def test_validate_extension_args__missing_end_milestone(self):
self.extension_stage_1.milestones.desktop_last = None
self.extension_stage_1.put()
with test_app.test_request_context(self.request_path):
with self.assertRaises(werkzeug.exceptions.NotFound):
self.handler._validate_extension_args(
self.feature_1_id, self.ot_stage_1, self.extension_stage_1)
def test_validate_extension_args__invalid_intent_url(self):
self.extension_stage_1.intent_thread_url = 'This can\'t be right.'
self.extension_stage_1.put()
with test_app.test_request_context(self.request_path):
with self.assertRaises(werkzeug.exceptions.BadRequest):
self.handler._validate_extension_args(
self.feature_1_id, self.ot_stage_1, self.extension_stage_1)
def test_validate_extension_args__missing_intent_url(self):
self.extension_stage_1.intent_thread_url = None
self.extension_stage_1.put()
with test_app.test_request_context(self.request_path):
with self.assertRaises(werkzeug.exceptions.BadRequest):
self.handler._validate_extension_args(
self.feature_1_id, self.ot_stage_1, self.extension_stage_1)
def test_validate_extension_args__na(self):
"""N/A state should be counted as approved."""
self.extension_gate_1.state = Vote.NA
self.extension_gate_1.put()
with test_app.test_request_context(self.request_path):
self.handler._validate_extension_args(
self.feature_1_id, self.ot_stage_1, self.extension_stage_1)
def test_validate_extension_args__not_approved(self):
"""Non-approved extensions should not be processed."""
self.extension_gate_1.state = Vote.DENIED
self.extension_gate_1.put()
with test_app.test_request_context(self.request_path):
with self.assertRaises(werkzeug.exceptions.BadRequest):
self.handler._validate_extension_args(
self.feature_1_id, self.ot_stage_1, self.extension_stage_1)
@mock.patch('api.origin_trials_api.OriginTrialsAPI._validate_creation_args')
def test_post__valid(self, mock_validate_func):
"""A valid OT creation request is processed and marked for creation."""
mock_validate_func.return_value = {}
testing_config.sign_in('feature_owner@google.com', 1234567890)
body = {
'ot_display_name': {
'form_field_name': 'ot_display_name',
'value': 'example trial',
},
'ot_description': {
'form_field_name': 'ot_description',
'value': 'a short description',
},
'ot_owner_email': {
'form_field_name': 'ot_owner_email',
'value': 'user@google.com',
},
'ot_emails': {
'form_field_name': 'ot_emails',
'value': 'contact1@example.com,contact2@example.com',
},
'desktop_first': {
'form_field_name': 'ot_milestone_desktop_start',
'value': 100,
},
'desktop_last': {
'form_field_name': 'ot_milestone_desktop_end',
'value': 106,
},
'intent_thread_url': {
'form_field_name': 'ot_creation__intent_to_experiment_url',
'value': 'https://example.com/intent',
},
'ot_documentation_url': {
'form_field_name': 'ot_documentation_url',
'value': 'https://example.com/docs',
},
'ot_feedback_submission_url': {
'form_field_name': 'ot_feedback_submission_url',
'value': 'https://example.com/feedback',
},
'ot_require_approvals': {
'form_field_name': 'ot_require_approvals',
'value': True,
},
'ot_approval_buganizer_component': {
'form_field_name': 'ot_approval_buganizer_component',
'value': '123456',
},
'ot_approval_buganizer_custom_field_id': {
'form_field_name': 'ot_approval_buganizer_custom_field_id',
'value': '111111',
},
'ot_approval_group_email': {
'form_field_name': 'ot_approval_group_email',
'value': 'users@google.com',
},
'ot_approval_criteria_url': {
'form_field_name': 'ot_approval_criteria_url',
'value': 'https://example.com/criteria',
},
'ot_chromium_trial_name': {
'form_field_name': 'ot_chromium_trial_name',
'value': 'ValidTrial',
},
'ot_webfeature_use_counter': {
'form_field_name': 'ot_webfeature_use_counter',
'value': 'kValidTrial',
},
'ot_is_critical_trial': {
'form_field_name': 'ot_is_critical_trial',
'value': True,
},
'ot_is_deprecation_trial': {
'form_field_name': 'ot_is_deprecation_trial',
'value': False,
},
'ot_has_third_party_support': {
'form_field_name': 'ot_has_third_party_support',
'value': False,
},
}
request_path = (
f'{self.feature_1_id}/{self.ot_stage_1.key.integer_id()}/create')
with test_app.test_request_context(request_path, json=body):
response = self.handler.do_post(feature_id=self.feature_1_id,
stage_id=self.ot_stage_1.key.integer_id())
self.assertEqual(response,
{'message': 'Origin trial creation request submitted.'})
# Verify fields have been updated.
for field, field_info in body.items():
value = field_info['value']
# Handle string of emails as a list.
if field == 'ot_emails':
value = field_info['value'].split(',')
elif (field == 'ot_approval_buganizer_component' or
field == 'ot_approval_buganizer_custom_field_id'):
value = int(value)
# Check milestone fields
elif field == 'desktop_first' or field == 'desktop_last':
self.assertEqual(getattr(self.ot_stage_1.milestones, field), value)
continue
self.assertEqual(getattr(self.ot_stage_1, field), value)
# Stage should be marked as "Ready for creation"
self.assertEqual(self.ot_stage_1.ot_setup_status, 2)