mirror of
https://github.com/ansible/awx.git
synced 2026-05-01 22:55:28 -02:30
Enforce unified list field consistency
Discovered via bug: controller_node field present in project update list but not in detail view Added tests to assert that "unified" list serializer produces same fields as the ordinary serializer, for unified jobs & unified JTs Added test to check that list serializers do differ from detail serializer except for allowed differences Added test to check that list serializers are applied correctly - only on list views, and that detail views, likewise, do not use list serializers Fix the many many bugs discovered by these new testing mechanisms
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
# AWX
|
||||
from awx.api import serializers
|
||||
from awx.main.models import UnifiedJob, UnifiedJobTemplate
|
||||
|
||||
# DRF
|
||||
from rest_framework.generics import ListAPIView
|
||||
|
||||
|
||||
def test_unified_template_field_consistency():
|
||||
'''
|
||||
Example of what is being tested:
|
||||
The endpoints /projects/N/ and /projects/ should have the same fields as
|
||||
that same project when it is serialized by the unified job template serializer
|
||||
in /unified_job_templates/
|
||||
'''
|
||||
for cls in UnifiedJobTemplate.__subclasses__():
|
||||
detail_serializer = getattr(serializers, '{}Serializer'.format(cls.__name__))
|
||||
unified_serializer = serializers.UnifiedJobTemplateSerializer().get_sub_serializer(cls())
|
||||
assert set(detail_serializer().fields.keys()) == set(unified_serializer().fields.keys())
|
||||
|
||||
|
||||
def test_unified_job_list_field_consistency():
|
||||
'''
|
||||
Example of what is being tested:
|
||||
The endpoint /project_updates/ should have the same fields as that
|
||||
project update when it is serialized by the unified job template serializer
|
||||
in /unified_jobs/
|
||||
'''
|
||||
for cls in UnifiedJob.__subclasses__():
|
||||
list_serializer = getattr(serializers, '{}ListSerializer'.format(cls.__name__))
|
||||
unified_serializer = serializers.UnifiedJobListSerializer().get_sub_serializer(cls())
|
||||
assert set(list_serializer().fields.keys()) == set(unified_serializer().fields.keys()), (
|
||||
'Mismatch between {} list serializer & unified list serializer'.format(cls)
|
||||
)
|
||||
|
||||
|
||||
def test_unified_job_detail_exclusive_fields():
|
||||
'''
|
||||
For each type, assert that the only fields allowed to be exclusive to
|
||||
detail view are the allowed types
|
||||
'''
|
||||
allowed_detail_fields = frozenset(
|
||||
('result_traceback', 'job_args', 'job_cwd', 'job_env', 'event_processing_finished')
|
||||
)
|
||||
for cls in UnifiedJob.__subclasses__():
|
||||
list_serializer = getattr(serializers, '{}ListSerializer'.format(cls.__name__))
|
||||
detail_serializer = getattr(serializers, '{}Serializer'.format(cls.__name__))
|
||||
list_fields = set(list_serializer().fields.keys())
|
||||
detail_fields = set(detail_serializer().fields.keys()) - allowed_detail_fields
|
||||
assert list_fields == detail_fields, 'List / detail mismatch for serializers of {}'.format(cls)
|
||||
|
||||
|
||||
def test_list_views_use_list_serializers(all_views):
|
||||
'''
|
||||
Check that the list serializers are only used for list views,
|
||||
and vice versa
|
||||
'''
|
||||
list_serializers = tuple(
|
||||
getattr(serializers, '{}ListSerializer'.format(cls.__name__)) for
|
||||
cls in (UnifiedJob.__subclasses__() + [UnifiedJob])
|
||||
)
|
||||
for View in all_views:
|
||||
if hasattr(View, 'model') and issubclass(getattr(View, 'model'), UnifiedJob):
|
||||
if issubclass(View, ListAPIView):
|
||||
assert issubclass(View.serializer_class, list_serializers), (
|
||||
'View {} serializer {} is not a list serializer'.format(View, View.serializer_class)
|
||||
)
|
||||
else:
|
||||
assert not issubclass(View.model, list_serializers)
|
||||
@@ -3,6 +3,11 @@ import logging
|
||||
|
||||
from mock import PropertyMock
|
||||
|
||||
from awx.api.urls import urlpatterns as api_patterns
|
||||
|
||||
# Django
|
||||
from django.core.urlresolvers import RegexURLResolver, RegexURLPattern
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _disable_database_settings(mocker):
|
||||
@@ -10,6 +15,33 @@ def _disable_database_settings(mocker):
|
||||
m.return_value = []
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def all_views():
|
||||
'''
|
||||
returns a set of all views in the app
|
||||
'''
|
||||
patterns = set([])
|
||||
url_views = set([])
|
||||
# Add recursive URL patterns
|
||||
unprocessed = set(api_patterns)
|
||||
while unprocessed:
|
||||
to_process = unprocessed.copy()
|
||||
unprocessed = set([])
|
||||
for pattern in to_process:
|
||||
if hasattr(pattern, 'lookup_str') and not pattern.lookup_str.startswith('awx.api'):
|
||||
continue
|
||||
patterns.add(pattern)
|
||||
if isinstance(pattern, RegexURLResolver):
|
||||
for sub_pattern in pattern.url_patterns:
|
||||
if sub_pattern not in patterns:
|
||||
unprocessed.add(sub_pattern)
|
||||
# Get view classes
|
||||
for pattern in patterns:
|
||||
if isinstance(pattern, RegexURLPattern) and hasattr(pattern.callback, 'view_class'):
|
||||
url_views.add(pattern.callback.view_class)
|
||||
return url_views
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def dummy_log_record():
|
||||
return logging.LogRecord(
|
||||
|
||||
@@ -5,9 +5,6 @@ import mock
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.generics import ListAPIView
|
||||
|
||||
# Django
|
||||
from django.core.urlresolvers import RegexURLResolver, RegexURLPattern
|
||||
|
||||
# AWX
|
||||
from awx.main.views import ApiErrorView
|
||||
from awx.api.views import JobList, InventorySourceList
|
||||
@@ -58,33 +55,12 @@ def test_disable_post_on_v1_inventory_source_list(version, supports_post):
|
||||
assert ('POST' in inv_source_list.allowed_methods) == supports_post
|
||||
|
||||
|
||||
def test_views_have_search_fields():
|
||||
from awx.api.urls import urlpatterns as api_patterns
|
||||
patterns = set([])
|
||||
url_views = set([])
|
||||
# Add recursive URL patterns
|
||||
unprocessed = set(api_patterns)
|
||||
while unprocessed:
|
||||
to_process = unprocessed.copy()
|
||||
unprocessed = set([])
|
||||
for pattern in to_process:
|
||||
if hasattr(pattern, 'lookup_str') and not pattern.lookup_str.startswith('awx.api'):
|
||||
continue
|
||||
patterns.add(pattern)
|
||||
if isinstance(pattern, RegexURLResolver):
|
||||
for sub_pattern in pattern.url_patterns:
|
||||
if sub_pattern not in patterns:
|
||||
unprocessed.add(sub_pattern)
|
||||
# Get view classes
|
||||
for pattern in patterns:
|
||||
if isinstance(pattern, RegexURLPattern) and hasattr(pattern.callback, 'view_class'):
|
||||
cls = pattern.callback.view_class
|
||||
if issubclass(cls, ListAPIView):
|
||||
url_views.add(pattern.callback.view_class)
|
||||
|
||||
def test_views_have_search_fields(all_views):
|
||||
# Gather any views that don't have search fields defined
|
||||
views_missing_search = []
|
||||
for View in url_views:
|
||||
for View in all_views:
|
||||
if not issubclass(View, ListAPIView):
|
||||
continue
|
||||
view = View()
|
||||
if not hasattr(view, 'search_fields') or len(view.search_fields) == 0:
|
||||
views_missing_search.append(view)
|
||||
|
||||
Reference in New Issue
Block a user