Merge pull request #947 from nofusscomputing/initial-integration-tests

This commit is contained in:
Jon
2025-08-12 17:05:47 +09:30
committed by GitHub
36 changed files with 724 additions and 152 deletions

View File

@ -46,6 +46,19 @@ jobs:
WORKFLOW_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
integration-test:
name: 'Integration Test'
uses: nofusscomputing/action_python/.github/workflows/python-integration.yaml@development
needs:
- docker
with:
POSTGRES_VERSIONS: '[ "13", "14", "15", "16", "17" ]'
PYTHON_VERSION: '3.11'
RABBITMQ_VERSIONS: '[ "3.12", "3.13", "4.0", "4.1" ]'
secrets:
WORKFLOW_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
gitlab-mirror:
if: ${{ github.repository == 'nofusscomputing/centurion_erp' }}
runs-on: ubuntu-latest

5
.gitignore vendored
View File

@ -21,3 +21,8 @@ feature_flags.json
coverage_*.json
*-coverage.xml
log/
# Integration testing
app/artifacts/
app/pyproject.toml
app/histogram_**
app/counter_**

File diff suppressed because one or more lines are too long

View File

@ -1,9 +1,10 @@
from django.apps import apps
from django.conf import settings
from django.contrib.auth.models import (
ContentType,
Permission
)
from django.conf import settings
from django.db.models import QuerySet
def permission_queryset():
@ -61,47 +62,66 @@ def permission_queryset():
if not settings.RUNNING_TESTS:
models = apps.get_models()
try:
# This blocks purpose is to cater for fresh install
# so that the app does not crash before the DB is setup.
for model in models:
models = apps.get_models()
if(
not str(model._meta.object_name).endswith('AuditHistory')
and not str(model._meta.model_name).lower().endswith('history')
):
# check `endswith('history')` can be removed when the old history models are removed
continue
content_type = ContentType.objects.get(
app_label = model._meta.app_label,
model = model._meta.model_name
)
permissions = Permission.objects.filter(
content_type = content_type,
)
for permission in permissions:
for model in models:
if(
not permission.codename == 'view_' + str(model._meta.model_name)
and str(model._meta.object_name).endswith('AuditHistory')
):
exclude_permissions += [ permission.codename ]
elif(
not str(model._meta.object_name).endswith('AuditHistory')
and str(model._meta.model_name).lower().endswith('history')
and not str(model._meta.model_name).lower().endswith('history')
):
# This `elif` can be removed when the old history models are removed
# check `endswith('history')` can be removed when the old history models are removed
continue
exclude_permissions += [ permission.codename ]
content_type = ContentType.objects.get(
app_label = model._meta.app_label,
model = model._meta.model_name
)
permissions = Permission.objects.filter(
content_type = content_type,
)
for permission in permissions:
if(
not permission.codename == 'view_' + str(model._meta.model_name)
and str(model._meta.object_name).endswith('AuditHistory')
):
exclude_permissions += [ permission.codename ]
elif(
not str(model._meta.object_name).endswith('AuditHistory')
and str(model._meta.model_name).lower().endswith('history')
):
# This `elif` can be removed when the old history models are removed
exclude_permissions += [ permission.codename ]
return Permission.objects.select_related('content_type').filter(
content_type__app_label__in = centurion_apps,
).exclude(
content_type__model__in = exclude_models
).exclude(
codename__in = exclude_permissions
)
return Permission.objects.select_related('content_type').filter(
content_type__app_label__in = centurion_apps,
).exclude(
content_type__model__in = exclude_models
).exclude(
codename__in = exclude_permissions
)
except:
pass
return QuerySet()
else:
return Permission.objects.select_related('content_type').filter(
content_type__app_label__in = centurion_apps,
).exclude(
content_type__model__in = exclude_models
).exclude(
codename__in = exclude_permissions
)

View File

@ -34,17 +34,17 @@ router = DefaultRouter(trailing_slash=False)
router.register('', access_v2.Index, basename = '_api_v2_access_home')
router.register(
prefix = '(?P<model_name>[company]+)', viewset = entity.ViewSet,
prefix = '/(?P<model_name>[company]+)', viewset = entity.ViewSet,
feature_flag = '2025-00008',basename = '_api_v2_company'
)
router.register(
prefix=f'entity/(?P<model_name>[{entity_type_names}]+)?', viewset = entity.ViewSet,
prefix=f'/entity/(?P<model_name>[{entity_type_names}]+)?', viewset = entity.ViewSet,
feature_flag = '2025-00002', basename = '_api_entity_sub'
)
router.register(
prefix = 'entity', viewset = entity.NoDocsViewSet,
prefix = '/entity', viewset = entity.NoDocsViewSet,
feature_flag = '2025-00002', basename = '_api_entity'
)
@ -54,7 +54,7 @@ router.register(
# )
router.register(
prefix = 'tenant', viewset = organization.ViewSet,
prefix = '/tenant', viewset = organization.ViewSet,
basename = '_api_tenant'
)
@ -64,7 +64,7 @@ router.register(
# )
router.register(
prefix = 'tenant/(?P<organization_id>[0-9]+)/team', viewset = team_v2.ViewSet,
prefix = '/tenant/(?P<organization_id>[0-9]+)/team', viewset = team_v2.ViewSet,
basename = '_api_v2_organization_team'
)
@ -75,13 +75,13 @@ router.register(
# )
router.register(
prefix = 'access/tenant/(?P<organization_id>[0-9]+)/team/(?P<team_id>[0-9]+)/user',
prefix = '/access/tenant/(?P<organization_id>[0-9]+)/team/(?P<team_id>[0-9]+)/user',
viewset = team_user_v2.ViewSet,
basename = '_api_v2_organization_team_user'
)
router.register(
prefix = 'role', viewset = role.ViewSet,
prefix = '/role', viewset = role.ViewSet,
feature_flag = '2025-00003', basename = '_api_role'
)

View File

@ -42,7 +42,7 @@ asset_type_names = str(asset_type_names)[:-1]
if not asset_type_names:
asset_type_names = 'none'
router.register(f'asset/(?P<model_name>[{asset_type_names}]+)?', asset.ViewSet, feature_flag = '2025-00004', basename='_api_asset_sub')
router.register('asset', asset.NoDocsViewSet, feature_flag = '2025-00004', basename='_api_asset')
router.register(f'/asset/(?P<model_name>[{asset_type_names}]+)?', asset.ViewSet, feature_flag = '2025-00004', basename='_api_asset_sub')
router.register('/asset', asset.NoDocsViewSet, feature_flag = '2025-00004', basename='_api_asset')
urlpatterns = router.urls

View File

@ -61,22 +61,22 @@ router = DefaultRouter(trailing_slash=False)
router.register('', v2.Index, basename='_api_v2_home')
router.register('base', base_index_v2.Index, basename='_api_v2_base_home')
router.register('base/content_type', content_type_v2.ViewSet, basename='_api_v2_content_type')
router.register('base/permission', permission_v2.ViewSet, basename='_api_v2_permission')
router.register('base/user', user_v2.ViewSet, basename='_api_v2_user')
router.register('/base', base_index_v2.Index, basename='_api_v2_base_home')
router.register('/base/content_type', content_type_v2.ViewSet, basename='_api_v2_content_type')
router.register('/base/permission', permission_v2.ViewSet, basename='_api_v2_permission')
router.register('/base/user', user_v2.ViewSet, basename='_api_v2_user')
router.register(
prefix = f'(?P<app_label>[{history_app_labels}]+)/(?P<model_name>[{history_type_names} \
prefix = f'/(?P<app_label>[{history_app_labels}]+)/(?P<model_name>[{history_type_names} \
]+)/(?P<model_id>[0-9]+)/history',
viewset = audit_history.ViewSet,
basename = '_api_centurionaudit_sub'
)
router.register(
prefix = f'(?P<app_label>[{notes_app_labels}]+)/(?P<model_name>[{notes_type_names} \
prefix = f'/(?P<app_label>[{notes_app_labels}]+)/(?P<model_name>[{notes_type_names} \
]+)/(?P<model_id>[0-9]+)/notes',
viewset = centurion_model_notes.ViewSet,
basename = '_api_centurionmodelnote_sub'
@ -85,24 +85,24 @@ router.register(
urlpatterns = [
path('schema', SpectacularAPIView.as_view(api_version='v2'), name='schema-v2',),
path('docs', SpectacularSwaggerView.as_view(url_name='schema-v2'), name='_api_v2_docs'),
path('/schema', SpectacularAPIView.as_view(api_version='v2'), name='schema-v2',),
path('/docs', SpectacularSwaggerView.as_view(url_name='schema-v2'), name='_api_v2_docs'),
]
urlpatterns += router.urls
urlpatterns += [
path(route = "access/", view = include("access.urls_api")),
path(route = "accounting/", view = include("accounting.urls")),
path(route = "assistance/", view = include("assistance.urls_api")),
path(route = "config_management/", view = include("config_management.urls_api")),
path(route = "core/", view = include("core.urls_api")),
path(route = "devops/", view = include("devops.urls")),
path(route = "hr/", view = include('human_resources.urls')),
path(route = "itam/", view = include("itam.urls_api")),
path(route = "itim/", view = include("itim.urls_api")),
path(route = "project_management/", view = include("project_management.urls_api")),
path(route = "settings/", view = include("settings.urls_api")),
path(route = 'public/', view = include('api.urls_public')),
path(route = "/access", view = include("access.urls_api")),
path(route = "/accounting", view = include("accounting.urls")),
path(route = "/assistance", view = include("assistance.urls_api")),
path(route = "/config_management", view = include("config_management.urls_api")),
path(route = "/core", view = include("core.urls_api")),
path(route = "/devops", view = include("devops.urls")),
path(route = "/hr", view = include('human_resources.urls')),
path(route = "/itam", view = include("itam.urls_api")),
path(route = "/itim", view = include("itim.urls_api")),
path(route = "/project_management", view = include("project_management.urls_api")),
path(route = "/settings", view = include("settings.urls_api")),
path(route = '/public', view = include('api.urls_public')),
]

View File

@ -19,16 +19,16 @@ router.register(
basename = '_api_v2_assistance_home'
)
router.register(
prefix = 'knowledge_base', viewset = knowledge_base_v2.ViewSet,
prefix = '/knowledge_base', viewset = knowledge_base_v2.ViewSet,
basename = '_api_knowledgebase'
)
router.register(
prefix = '(?P<model>.+)/(?P<model_pk>[0-9]+)/knowledge_base',
prefix = '/(?P<model>.+)/(?P<model_pk>[0-9]+)/knowledge_base',
viewset = model_knowledge_base_article.ViewSet,
basename = '_api_v2_model_kb'
)
router.register(
prefix = 'ticket/request', viewset = request_ticket_v2.ViewSet,
prefix = '/ticket/request', viewset = request_ticket_v2.ViewSet,
basename = '_api_v2_ticket_request'
)

View File

@ -20,6 +20,7 @@ import django.db.models.options as options
options.DEFAULT_NAMES = (*options.DEFAULT_NAMES, 'sub_model_type', 'itam_sub_model_type')
APPEND_SLASH = False
AUTH_USER_MODEL = 'auth.User'
# Build paths inside the project like this: BASE_DIR / 'subdir'.

View File

@ -0,0 +1,182 @@
import pytest
import re
import requests
from django.urls import get_resolver, URLPattern, URLResolver
def list_urls(urlpatterns, parent_pattern=''):
urls = []
for entry in urlpatterns:
if isinstance(entry, URLPattern):
urls.append(parent_pattern + str(entry.pattern))
elif isinstance(entry, URLResolver):
urls.extend(list_urls(entry.url_patterns, parent_pattern + str(entry.pattern)))
filtered = [
re.sub(r"\^([a-z\-]+)\$$", r"\1", u).rstrip('/') for u in urls if (
re.sub(r"\^([a-z\-]+)\$$", r"\1", u).startswith('api/')
and '(' not in re.sub(r"\^([a-z\-]+)\$$", r"\1", u).rstrip('/')
and '<' not in re.sub(r"\^([a-z\-]+)\$$", r"\1", u).rstrip('/')
and '$' not in re.sub(r"\^([a-z\-]+)\$$", r"\1", u).rstrip('/')
)
]
return filtered
no_auth_urls = [
'api/v2/auth/login',
'api/v2/docs',
'api/v2/schema',
]
urls_list_view_auth_required_excluded = [
'api/v2/auth/logout',
]
urls_list_view_auth_required_authenticated_excluded = [
'api/v2/itam/inventory',
'api/v2/auth/logout',
]
@pytest.mark.integration
@pytest.mark.regression
class URLChecksPyTest:
@pytest.fixture(scope="class")
def auto_login_client(self):
session = requests.Session()
login_page_url = "http://127.0.0.1:8003/api/v2/auth/login"
login_post_url = "http://127.0.0.1:8003/api/v2/auth/login"
resp = session.get(login_page_url)
resp.raise_for_status()
# Extract CSRF token from cookies (Django sets csrftoken cookie)
csrf_token = session.cookies.get("csrftoken")
if not csrf_token:
raise RuntimeError("CSRF token cookie not found")
login_data = {
"username": "admin",
"password": "admin",
"csrfmiddlewaretoken": csrf_token,
}
headers = {
"Referer": login_page_url,
"X-CSRFToken": csrf_token, # Include CSRF token header
}
resp = session.post(login_post_url, data=login_data, headers=headers, allow_redirects=True)
resp.raise_for_status()
class Client:
def __init__(self, session):
self._session = session
self._unauth_session = requests.Session()
resp = self._unauth_session.get(login_page_url)
resp.raise_for_status()
self._headers = csrf_token = {
"Referer": login_page_url,
"X-CSRFToken": self._unauth_session.cookies.get("csrftoken"),
}
def request(self, method, url, auth = False, **kwargs):
if auth:
session = self._session
else:
session = self._unauth_session
return session.request(method, url, headers=self._headers, **kwargs)
@property
def cookies(self):
return self._session.cookies
return Client(session)
list_view_urls = list_urls(urlpatterns = get_resolver().url_patterns)
@pytest.mark.parametrize(
argnames = "url_path",
argvalues = [
url for url in list_view_urls if( url in no_auth_urls )
],
ids = [
re.sub(r'[^\w_\-.:]', '_', url) for url in list_view_urls if( url in no_auth_urls )
],
)
def test_urls_no_auth_required(self, url_path, auto_login_client):
url = f"http://127.0.0.1:8003/{url_path}"
response = auto_login_client.request("GET", url)
assert response.status_code == 200
@pytest.mark.permissions
@pytest.mark.parametrize(
argnames = "url_path",
argvalues = [
url for url in list_view_urls if(
url not in no_auth_urls
and url not in urls_list_view_auth_required_excluded
)
],
ids = [
re.sub(r'[^\w_\-.:]', '_', url) for url in list_view_urls if(
url not in no_auth_urls
and url not in urls_list_view_auth_required_excluded
)
],
)
def test_urls_list_view_auth_required(self, url_path, auto_login_client):
url = f"http://127.0.0.1:8003/{url_path}"
response = auto_login_client.request("GET", url)
assert response.status_code == 401
@pytest.mark.permissions
@pytest.mark.parametrize(
argnames = "url_path",
argvalues = [
url for url in list_view_urls if(
url not in no_auth_urls
and url not in urls_list_view_auth_required_authenticated_excluded
)
],
ids = [
re.sub(r'[^\w_\-.:]', '_', url) for url in list_view_urls if(
url not in no_auth_urls
and url not in urls_list_view_auth_required_authenticated_excluded
)
],
)
def test_urls_list_view_auth_required_authenticated(self, url_path, auto_login_client):
url = f"http://127.0.0.1:8003/{url_path}"
response = auto_login_client.request(method = "GET", url = url, auth = True)
assert response.status_code == 200

View File

@ -4,14 +4,14 @@ from django.contrib.auth import views as auth_views
from django.views.static import serve
from django.urls import include, path, re_path
from rest_framework import urls
urlpatterns = [
path('admin/', admin.site.urls, name='_administration'),
path('admin', admin.site.urls, name='_administration'),
path('account/password_change/', auth_views.PasswordChangeView.as_view(template_name="password_change.html.j2"), name="change_password"),
path('account/password_change', auth_views.PasswordChangeView.as_view(template_name="password_change.html.j2"), name="change_password"),
path("account/", include("django.contrib.auth.urls")),
path("account", include("django.contrib.auth.urls")),
re_path(r'^static/(?P<path>.*)$', serve,{'document_root': settings.STATIC_ROOT}),
@ -30,15 +30,16 @@ if settings.API_ENABLED:
urlpatterns += [
path("api/", include("api.urls", namespace = 'v1')),
path("api", include("api.urls", namespace = 'v1')),
path("api/v2/", include("api.urls_v2", namespace = 'v2')),
path("api/v2", include("api.urls_v2", namespace = 'v2')),
]
urlpatterns += [
path('api/v2/auth/', include('rest_framework.urls')),
path('api/v2/auth/login', auth_views.LoginView.as_view(template_name='rest_framework/login.html'), name='login'),
path('api/v2/auth/logout', auth_views.LogoutView.as_view(), name='logout'),
]

View File

@ -18,15 +18,15 @@ router.register(
basename = '_api_v2_config_management_home'
)
router.register(
prefix = 'group', viewset = config_group_v2.ViewSet,
prefix = '/group', viewset = config_group_v2.ViewSet,
basename = '_api_configgroups'
)
router.register(
prefix = 'group/(?P<parent_group>[0-9]+)/child_group', viewset = config_group_v2.ViewSet,
prefix = '/group/(?P<parent_group>[0-9]+)/child_group', viewset = config_group_v2.ViewSet,
basename = '_api_configgroups_child'
)
router.register(
prefix = 'group/(?P<config_group_id>[0-9]+)/software',
prefix = '/group/(?P<config_group_id>[0-9]+)/software',
viewset = config_group_software_v2.ViewSet,
basename = '_api_configgroupsoftware'
)

View File

@ -41,59 +41,59 @@ router: DefaultRouter = DefaultRouter(trailing_slash=False)
router.register(
'history', audit_history.NoDocsViewSet,
'/history', audit_history.NoDocsViewSet,
basename = '_api_centurionaudit'
)
router.register(
prefix=f'ticket', viewset = ticket.NoDocsViewSet,
prefix=f'/ticket', viewset = ticket.NoDocsViewSet,
feature_flag = '2025-00006', basename = '_api_ticketbase'
)
router.register(
prefix=f'ticket/(?P<ticket_type>[{ticket_type_names}]+)', viewset = ticket.ViewSet,
prefix=f'/ticket/(?P<ticket_type>[{ticket_type_names}]+)', viewset = ticket.ViewSet,
feature_flag = '2025-00006', basename = '_api_ticketbase_sub'
)
router.register(
prefix = 'ticket/(?P<ticket_id>[0-9]+)/comment', viewset = ticket_comment.NoDocsViewSet,
prefix = '/ticket/(?P<ticket_id>[0-9]+)/comment', viewset = ticket_comment.NoDocsViewSet,
feature_flag = '2025-00006', basename = '_api_ticket_comment_base'
)
router.register(
prefix = 'ticket/(?P<ticket_id>[0-9]+)/comment/(?P<parent_id>[0-9]+)/threads',
prefix = '/ticket/(?P<ticket_id>[0-9]+)/comment/(?P<parent_id>[0-9]+)/threads',
viewset = ticket_comment.ViewSet,
feature_flag = '2025-00006', basename = '_api_ticket_comment_base_thread'
)
router.register(
prefix = 'ticket/(?P<ticket_id>[0-9]+)/comments', viewset = ticket_comment_depreciated.ViewSet,
prefix = '/ticket/(?P<ticket_id>[0-9]+)/comments', viewset = ticket_comment_depreciated.ViewSet,
basename = '_api_v2_ticket_comment'
)
router.register(
prefix = 'ticket/(?P<ticket_id>[0-9]+)/comments/(?P<parent_id>[0-9]+)/threads',
prefix = '/ticket/(?P<ticket_id>[0-9]+)/comments/(?P<parent_id>[0-9]+)/threads',
viewset = ticket_comment_depreciated.ViewSet,
basename = '_api_v2_ticket_comment_threads'
)
router.register(
prefix = 'ticket/(?P<ticket_id>[0-9]+)/linked_item', viewset = ticket_linked_item.ViewSet,
prefix = '/ticket/(?P<ticket_id>[0-9]+)/linked_item', viewset = ticket_linked_item.ViewSet,
basename = '_api_v2_ticket_linked_item'
)
router.register(
prefix = 'ticket/(?P<ticket_id>[0-9]+)/related_ticket', viewset = related_ticket.ViewSet,
prefix = '/ticket/(?P<ticket_id>[0-9]+)/related_ticket', viewset = related_ticket.ViewSet,
basename = '_api_v2_ticket_related'
)
router.register(
prefix=f'ticket/(?P<ticket_id>[0-9]+)/(?P<ticket_comment_model>[{ticket_comment_names}]+)',
prefix=f'/ticket/(?P<ticket_id>[0-9]+)/(?P<ticket_comment_model>[{ticket_comment_names}]+)',
viewset = ticket_comment.ViewSet,
feature_flag = '2025-00006', basename = '_api_ticket_comment_base_sub'
)
router.register(
prefix=f'ticket/(?P<ticket_id>[0-9]+)/(?P<ticket_comment_model>[{ticket_comment_names} \
prefix=f'/ticket/(?P<ticket_id>[0-9]+)/(?P<ticket_comment_model>[{ticket_comment_names} \
]+)/(?P<parent_id>[0-9]+)/threads',
viewset = ticket_comment.ViewSet,
feature_flag = '2025-00006', basename = '_api_ticket_comment_base_sub_thread'
)
router.register(
prefix = '(?P<item_class>[a-z_]+)/(?P<item_id>[0-9]+)/item_ticket',
prefix = '/(?P<item_class>[a-z_]+)/(?P<item_id>[0-9]+)/item_ticket',
viewset = ticket_linked_item.ViewSet,
basename = '_api_v2_item_tickets'
)

View File

@ -13,21 +13,21 @@ app_name = "devops"
router = DefaultRouter(trailing_slash=False)
router.register(
prefix = 'feature_flag', viewset = feature_flag.ViewSet,
prefix = '/feature_flag', viewset = feature_flag.ViewSet,
basename = '_api_featureflag'
)
router.register(
prefix = r'git_repository(?:/(?P<model_name>gitlab|github))?',
prefix = r'/git_repository(?:/(?P<model_name>gitlab|github))?',
viewset = git_repository.ViewSet,
feature_flag = '2025-00001', basename = '_api_gitrepository'
)
router.register(
prefix = r'(?P<model_name>githubrepository|gitlabrepository)',
prefix = r'/(?P<model_name>githubrepository|gitlabrepository)',
viewset = git_repository.ViewSet,
feature_flag = '2025-00001', basename = '_api_gitrepository_sub'
)
router.register(
prefix = 'git_group', viewset = git_group.ViewSet,
prefix = '/git_group', viewset = git_group.ViewSet,
feature_flag = '2025-00001', basename = '_api_gitgroup'
)

View File

@ -11,8 +11,8 @@ app_name = "devops"
router = SimpleRouter(trailing_slash=False)
router.register('flags', feature_flag_endpoints.Index, basename='_api_v2_flags')
router.register('/flags', feature_flag_endpoints.Index, basename='_api_v2_flags')
router.register('(?P<organization_id>[0-9]+)/flags/(?P<software_id>[0-9]+)', public_feature_flag.ViewSet, basename='_api_checkin')
router.register('/(?P<organization_id>[0-9]+)/flags/(?P<software_id>[0-9]+)', public_feature_flag.ViewSet, basename='_api_checkin')
urlpatterns = router.urls

View File

@ -34,57 +34,57 @@ router.register(
basename = '_api_v2_itam_home'
)
router.register(
prefix = '(?P<model_name>[itamassetbase]+)', viewset = asset.ViewSet,
prefix = '/(?P<model_name>[itamassetbase]+)', viewset = asset.ViewSet,
feature_flag = '2025-00007', basename = '_api_itamassetbase'
)
router.register(
prefix = 'device', viewset = device.ViewSet,
prefix = '/device', viewset = device.ViewSet,
basename = '_api_device'
)
router.register(
prefix = 'device/(?P<device_id>[0-9]+)/operating_system',
prefix = '/device/(?P<device_id>[0-9]+)/operating_system',
viewset = device_operating_system.ViewSet,
basename = '_api_deviceoperatingsystem')
router.register(
prefix = 'device/(?P<device_id>[0-9]+)/software', viewset = device_software_v2.ViewSet,
prefix = '/device/(?P<device_id>[0-9]+)/software', viewset = device_software_v2.ViewSet,
basename = '_api_devicesoftware'
)
router.register(
prefix = 'device/(?P<device_id>[0-9]+)/service', viewset = service_device_v2.ViewSet,
prefix = '/device/(?P<device_id>[0-9]+)/service', viewset = service_device_v2.ViewSet,
basename = '_api_v2_service_device'
)
router.register(
prefix = 'inventory', viewset = inventory.ViewSet,
prefix = '/inventory', viewset = inventory.ViewSet,
basename = '_api_v2_inventory'
)
router.register(
prefix = 'operating_system', viewset = operating_system_v2.ViewSet,
prefix = '/operating_system', viewset = operating_system_v2.ViewSet,
basename = '_api_operatingsystem'
)
router.register(
prefix = 'operating_system/(?P<operating_system_id>[0-9]+)/installs',
prefix = '/operating_system/(?P<operating_system_id>[0-9]+)/installs',
viewset = device_operating_system.ViewSet,
basename = '_api_v2_operating_system_installs'
)
router.register(
prefix = 'operating_system/(?P<operating_system_id>[0-9]+)/version',
prefix = '/operating_system/(?P<operating_system_id>[0-9]+)/version',
viewset = operating_system_version_v2.ViewSet,
basename = '_api_operatingsystemversion'
)
router.register(
prefix = 'software', viewset = software_v2.ViewSet,
prefix = '/software', viewset = software_v2.ViewSet,
basename = '_api_software'
)
router.register(
prefix = 'software/(?P<software_id>[0-9]+)/installs', viewset = device_software_v2.ViewSet,
prefix = '/software/(?P<software_id>[0-9]+)/installs', viewset = device_software_v2.ViewSet,
basename = '_api_v2_software_installs'
)
router.register(
prefix = 'software/(?P<software_id>[0-9]+)/version', viewset = software_version_v2.ViewSet,
prefix = '/software/(?P<software_id>[0-9]+)/version', viewset = software_version_v2.ViewSet,
basename = '_api_softwareversion'
)
router.register(
prefix = 'software/(?P<software_id>[0-9]+)/feature_flag',
prefix = '/software/(?P<software_id>[0-9]+)/feature_flag',
viewset = software_enable_feature_flag.ViewSet,
basename = '_api_softwareenablefeatureflag'
)

View File

@ -23,27 +23,27 @@ router.register(
basename = '_api_v2_itim_home'
)
router.register(
prefix = 'ticket/change', viewset = change.ViewSet,
prefix = '/ticket/change', viewset = change.ViewSet,
basename = '_api_v2_ticket_change'
)
router.register(
prefix = 'cluster', viewset = cluster_v2.ViewSet,
prefix = '/cluster', viewset = cluster_v2.ViewSet,
basename = '_api_cluster'
)
router.register(
prefix = 'cluster/(?P<cluster_id>[0-9]+)/service', viewset = service_cluster.ViewSet,
prefix = '/cluster/(?P<cluster_id>[0-9]+)/service', viewset = service_cluster.ViewSet,
basename = '_api_v2_service_cluster'
)
router.register(
prefix = 'ticket/incident', viewset = incident.ViewSet,
prefix = '/ticket/incident', viewset = incident.ViewSet,
basename = '_api_v2_ticket_incident'
)
router.register(
prefix = 'ticket/problem', viewset = problem.ViewSet,
prefix = '/ticket/problem', viewset = problem.ViewSet,
basename = '_api_v2_ticket_problem'
)
router.register(
prefix = 'service', viewset = service.ViewSet,
prefix = '/service', viewset = service.ViewSet,
basename = '_api_service'
)

View File

@ -64,7 +64,7 @@ class ProjectMilestoneSerializerTestCases(
def test_serializer_validation_no_name(self,
kwargs_api_create, model, model_serializer, request_user
kwargs_api_create, model, model_serializer, request_user, model_kwargs
):
"""Serializer Validation Check

View File

@ -20,16 +20,16 @@ router.register(
basename = '_api_v2_project_management_home'
)
router.register(
prefix = 'project', viewset = project.ViewSet,
prefix = '/project', viewset = project.ViewSet,
basename = '_api_project'
)
router.register(
prefix = 'project/(?P<project_id>[0-9]+)/milestone',
prefix = '/project/(?P<project_id>[0-9]+)/milestone',
viewset = project_milestone.ViewSet,
basename = '_api_projectmilestone'
)
router.register(
prefix = 'project/(?P<project_id>[0-9]+)/project_task',
prefix = '/project/(?P<project_id>[0-9]+)/project_task',
viewset = project_task.ViewSet,
basename = '_api_v2_ticket_project_task'
)

View File

@ -52,70 +52,70 @@ router.register(
basename = '_api_v2_settings_home'
)
router.register(
prefix = 'app_settings', viewset = app_settings.ViewSet,
prefix = '/app_settings', viewset = app_settings.ViewSet,
basename = '_api_appsettings'
)
router.register(
prefix = 'celery_log', viewset = celery_log_v2.ViewSet,
prefix = '/celery_log', viewset = celery_log_v2.ViewSet,
basename = '_api_v2_celery_log'
)
router.register(
prefix = 'cluster_type', viewset = cluster_type_v2.ViewSet,
prefix = '/cluster_type', viewset = cluster_type_v2.ViewSet,
basename = '_api_clustertype'
)
router.register(
prefix = 'device_model', viewset = device_model.ViewSet,
prefix = '/device_model', viewset = device_model.ViewSet,
basename = '_api_devicemodel'
)
router.register(
prefix = 'device_type', viewset = device_type.ViewSet,
prefix = '/device_type', viewset = device_type.ViewSet,
basename = '_api_devicetype'
)
router.register(
prefix = 'external_link', viewset = external_link.ViewSet,
prefix = '/external_link', viewset = external_link.ViewSet,
basename = '_api_externallink'
)
router.register(
prefix = 'knowledge_base_category',
prefix = '/knowledge_base_category',
viewset = knowledge_base_category_v2.ViewSet,
basename = '_api_knowledgebasecategory'
)
router.register(
prefix = 'manufacturer', viewset = manufacturer_v2.ViewSet,
prefix = '/manufacturer', viewset = manufacturer_v2.ViewSet,
basename = '_api_manufacturer'
)
router.register(
prefix = 'port', viewset = port_v2.ViewSet,
prefix = '/port', viewset = port_v2.ViewSet,
basename = '_api_port'
)
router.register(
prefix = 'project_state',
prefix = '/project_state',
viewset = project_state.ViewSet,
basename = '_api_projectstate'
)
router.register(
prefix = 'project_type', viewset = project_type.ViewSet,
prefix = '/project_type', viewset = project_type.ViewSet,
basename = '_api_projecttype'
)
router.register(
prefix = 'software_category', viewset = software_category_v2.ViewSet,
prefix = '/software_category', viewset = software_category_v2.ViewSet,
basename = '_api_softwarecategory'
)
router.register(
prefix = 'ticket_category',
prefix = '/ticket_category',
viewset = ticket_category.ViewSet, basename = '_api_ticketcategory'
)
router.register(
prefix = 'ticket_comment_category',
prefix = '/ticket_comment_category',
viewset = ticket_comment_category.ViewSet,
basename = '_api_ticketcommentcategory'
)
router.register(
prefix = 'user_settings', viewset = user_settings.ViewSet,
prefix = '/user_settings', viewset = user_settings.ViewSet,
basename = '_api_usersettings'
)
router.register(
prefix = 'user_(?P<model_id>[0-9]+)/token', viewset = auth_token.ViewSet,
prefix = '/user_(?P<model_id>[0-9]+)/token', viewset = auth_token.ViewSet,
basename = '_api_authtoken'
)

View File

@ -75,9 +75,13 @@ def kwargs_projectmilestone(django_db_blocker,
yield kwargs.copy()
# with django_db_blocker.unblock():
with django_db_blocker.unblock():
for proj in project.projectmilestone_set.all():
proj.delete()
project.delete()
# project.delete() # milestone is cascade delete
@pytest.fixture( scope = 'class')

View File

@ -48,10 +48,11 @@ def kwargs_projectstate(kwargs_centurionmodel, django_db_blocker,
with django_db_blocker.unblock():
try:
runbook.delete()
except models.deletion.ProtectedError:
pass
for proj in runbook.projectstate_set.all():
proj.delete()
runbook.delete()
@pytest.fixture( scope = 'class')

View File

@ -47,10 +47,11 @@ def kwargs_projecttype(kwargs_centurionmodel, django_db_blocker,
with django_db_blocker.unblock():
try:
runbook.delete()
except models.deletion.ProtectedError:
pass
for proj in runbook.projecttype_set.all():
proj.delete()
runbook.delete()
@pytest.fixture( scope = 'class')

View File

@ -159,7 +159,7 @@ EXPOSE 8000
VOLUME [ "/data", "/etc/itsm" ]
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD \
HEALTHCHECK --interval=10s --timeout=30s --start-period=30s --retries=3 CMD \
supervisorctl status || exit 1

File diff suppressed because one or more lines are too long

View File

@ -3,6 +3,7 @@
set -e
mkdir -p /etc/supervisor/conf.d;
mkdir -p /var/log/nginx;
if [ "$1" == "" ]; then

View File

@ -1,3 +1,4 @@
import coverage
import logging
import os
@ -9,6 +10,32 @@ from prometheus_client import multiprocess, start_http_server, REGISTRY
if bool(os.environ.get("IS_TESTING")):
def post_fork(server, worker):
worker_cov = coverage.Coverage(data_file=f"artifacts/.coverage.{os.getpid()}")
worker_cov.start()
worker.worker_cov = worker_cov
def worker_exit(server, worker):
if hasattr(worker, "worker_cov"):
worker.worker_cov.stop()
worker.worker_cov.save()
post_fork = post_fork
worker_exit = worker_exit
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'centurion.settings')
access_logfile = '-'
@ -21,9 +48,14 @@ forwarder_headers = "X-REAL-IP,X-FORWARDED-FOR,X-FORWARDED-PROTO"
logger = logging.getLogger(__name__)
preload_app = False
max_requests = 100
max_requests_jitter = 30
workers = 10
preload_app = True
timeout = 180
workers = 4
def when_ready(_):
@ -46,7 +78,8 @@ def when_ready(_):
proc_path = os.environ["PROMETHEUS_MULTIPROC_DIR"]
logger.info(f'Setting up prometheus metrics HTTP server on port {str(settings.METRICS_EXPORT_PORT)}.')
logger.info(f'Setting up prometheus metrics HTTP server on port \
{str(settings.METRICS_EXPORT_PORT)}.')
multiproc_folder_path = _setup_multiproc_folder()

View File

@ -7,4 +7,4 @@ autorestart=true
stdout_logfile=/var/log/%(program_name)s.log
stderr_logfile=/var/log/%(program_name)s.log
directory=/app
command=gunicorn --config=/etc/gunicorn.conf.py centurion.wsgi:application
command=gunicorn --config=/etc/gunicorn.conf.py --pid=/run/gunicorn.pid centurion.wsgi:application

View File

@ -69,6 +69,70 @@ lint: markdown-mkdocs-lint
test:
pytest --cov-report xml:artifacts/coverage_unit_functional.xml --cov-report html:artifacts/coverage/unit_functional/ --junit-xml=artifacts/unit_functional.JUnit.xml app/**/tests/unit app/**/tests/functional
test-integration:
export exit_code=0;
cp pyproject.toml app/;
sed -i 's|^source = \[ "./app" \]|source = [ "." ]|' app/pyproject.toml;
cd test;
if docker-compose up -d; then
docker ps -a;
chmod +x setup-integration.sh;
if ./setup-integration.sh; then
cd ..;
ls -laR test/;
docker exec -i centurion-erp supervisorctl stop gunicorn;
docker exec -i centurion-erp sh -c 'rm -rf /app/artifacts/* /app/artifacts/.[!.]*';
docker exec -i centurion-erp supervisorctl start gunicorn;
sleep 60;
docker ps -a;
curl --trace-ascii - http://localhost:8003/api;
echo '--------------------------------------------------------------------';
curl --trace-ascii - http://127.0.0.1:8003/api;
if [ "0${GITHUB_SHA}"!="0" ]; then
sudo chmod 777 -R ./test
fi;
docker logs centurion-erp;
pytest --override-ini addopts= --no-migrations --tb=long --verbosity=2 --full-trace --showlocals --junit-xml=integration.JUnit.xml app/*/tests/integration;
docker exec -i centurion-erp supervisorctl restart gunicorn;
docker exec -i centurion-erp sh -c 'coverage combine; coverage report --skip-covered; coverage html -d artifacts/html/;';
docker logs centurion-erp-init > ./test/volumes/log/docker-log-centurion-erp-init.log;
docker logs centurion-erp> ./test/volumes/log/docker-log-centurion-erp.log;
docker logs postgres > ./test/volumes/log/docker-log-postgres.log;
docker logs rabbitmq > ./test/volumes/log/docker-log-rabbitmq.log;
cd test;
else
echo 'Error: could not setup containers for testing';
export exit_code=10;
fi;
else
echo 'Error: Failed to launch containers';
export exit_code=20;
fi;
cd test;
docker-compose down -v;
cd ..;
exit ${exit_code};
test-functional:
pytest --cov-report xml:artifacts/coverage_functional.xml --cov-report html:artifacts/coverage/functional/ --junit-xml=artifacts/functional.JUnit.xml app/**/tests/functional

View File

@ -1100,6 +1100,7 @@ markers = [
"audit_models: Selects Audit models.",
"centurion_models: Selects Centurion models",
"functional: Selects all Functional tests.",
"integration: Selects all Integration tests.",
"meta_models: Selects Meta models",
"mixin: Selects all mixin test cases.",
"mixin_centurion: Selects all centurion mixin test cases.",

89
test/docker-compose.yaml Normal file
View File

@ -0,0 +1,89 @@
---
x-app: &centurion
image: ${CENTURION_IMAGE:-ghcr.io/nofusscomputing/centurion-erp}:${CENTURION_IMAGE_TAG:-dev}
volumes:
- ./docker/settings.py:/etc/itsm/settings.py:ro
services:
postgres:
image: ${CENTURION_POSTGRES_IMAGE:-postgres}:${CENTURION_POSTGRES_IMAGE_TAG:-13.21}-alpine # 14.18-alpine, 15.13-alpine, 16.9-alpine, 17.5-alpine
container_name: postgres
restart: always
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: admin
expose:
- 5432
volumes:
- ./docker/centurion.sql:/docker-entrypoint-initdb.d/centurion.sql:ro
rabbitmq:
image: ${CENTURION_RABBITMQ_IMAGE:-rabbitmq}:${CENTURION_RABBITMQ_IMAGE_TAG:-4.0.9}-management-alpine # 4.1.3-management-alpine
container_name: rabbitmq
environment:
- RABBITMQ_DEFAULT_USER=admin
- RABBITMQ_DEFAULT_PASS=admin
- RABBITMQ_DEFAULT_VHOST=itsm
expose:
- 5672
ports:
# - "5672:5672"
- "15672:15672"
centurion-init:
<<: *centurion
container_name: centurion-erp-init
restart: "no"
entrypoint: ""
command: sh -c 'sleep 15; python manage.py migrate'
depends_on:
- postgres
- rabbitmq
centurion:
<<: *centurion
container_name: centurion-erp
restart: always
hostname: centurion-erp
volumes:
- ./volumes/log:/var/log:rw
- ./docker/settings.py:/etc/itsm/settings.py:ro
- ./volumes/data:/data:rw
- ./volumes/artifacts:/app/artifacts:rw
ports:
- "8003:8000"
depends_on:
- postgres
- rabbitmq
worker:
<<: *centurion
container_name: centurion-worker
restart: always
environment:
- IS_WORKER=true
hostname: centurion-worker
depends_on:
- postgres
- rabbitmq
- centurion
centurion-ui:
image: ${CENTURION_UI_IMAGE:-ghcr.io/nofusscomputing/centurion-erp-ui}:${CENTURION_UI_IMAGE_TAG:-dev}
container_name: centurion-ui
restart: always
environment:
- API_URL=http://127.0.0.1:8003/api/v2
hostname: centurion-ui
ports:
- "3000:80"
depends_on:
- centurion

2
test/docker/centurion.sql Executable file
View File

@ -0,0 +1,2 @@
CREATE DATABASE itsm;
GRANT ALL PRIVILEGES ON DATABASE itsm TO admin;

79
test/docker/settings.py Executable file
View File

@ -0,0 +1,79 @@
# ITSM Docker Settings
# If metrics enabled, see https://nofusscomputing.com/projects/centurion_erp/administration/monitoring/#django-exporter-setup)
# to configure the database metrics.
API_TEST = True
AUTH_PASSWORD_VALIDATORS = []
CELERY_BROKER_URL = 'amqp://admin:admin@rabbitmq:5672/itsm' # 'amqp://' is the connection protocol
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_METHODS = (
"DELETE",
"GET",
"OPTIONS",
"PATCH",
"POST",
"PUT",
)
CORS_ALLOWED_ORIGINS = [
"http://127.0.0.1:3000",
"http://localhost:3000",
"http://127.0.0.1:8003",
"http://localhost:8003",
"http://127.0.0.1",
]
CORS_EXPOSE_HEADERS = ['Content-Type', 'X-CSRFToken']
CSRF_COOKIE_SECURE = False
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'itsm',
'USER': 'admin',
'PASSWORD': 'admin',
'HOST': 'postgres',
'PORT': '5432',
}
}
DEBUG = True
FEATURE_FLAGGING_ENABLED = True # Turn Feature Flagging on/off
FEATURE_FLAG_OVERRIDES = [] # Feature Flag Overrides. Takes preceedence over downloaded feature flags.
LOG_FILES = { # Location where log files will be created
"centurion": "/var/log/centurion.log",
"weblog": "/var/log/weblog.log",
"rest_api": "/var/log/rest_api.log",
"catch_all":"/var/log/catch-all.log"
}
METRICS_ENABLED = True
SECRET_KEY = 'django-insecure-b*41-$afq0yl)1e#qpz^-nbt-opvjwb#avv++b9rfdxa@b55sk'
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_SSL_REDIRECT = False
SESSION_COOKIE_SECURE = False
SITE_URL = 'http://127.0.0.1:8003'
TRUSTED_ORIGINS = [
"http://127.0.0.1:3000",
"http://localhost:3000",
"http://127.0.0.1:8003",
"http://localhost:8003",
"http://127.0.0.1",
]
USE_X_FORWARDED_HOST = True

0
test/page_speed.js Normal file → Executable file
View File

0
test/parameterizedData.json Normal file → Executable file
View File

70
test/setup-integration.sh Executable file
View File

@ -0,0 +1,70 @@
#!/bin/sh
set -e
docker exec -i centurion-erp pip install -r /requirements_test.txt
docker exec -i centurion-erp supervisorctl restart gunicorn
CONTAINER_NAME="centurion-erp-init"
TIMEOUT=400
INTERVAL=5
ELAPSED=0
STATUS=""
while [ "$STATUS" != "exited" ] && [ "$STATUS" != "dead" ]; do
STATUS=$(docker inspect --format '{{.State.Status}}' "$CONTAINER_NAME" 2>/dev/null || echo "not_found")
if [ "$STATUS" = "not_found" ]; then
docker ps -a
echo "Container $CONTAINER_NAME was not found."
exit 2
fi
if [ $ELAPSED -ge $TIMEOUT ]; then
echo "Timeout reached. Container $CONTAINER_NAME still running (status: $STATUS)."
exit 3
fi
echo "Waiting for container $CONTAINER_NAME to complete... Current status: $STATUS"
sleep $INTERVAL
ELAPSED=$((ELAPSED + INTERVAL))
done
echo "Container $CONTAINER_NAME has completed."
CONTAINER_NAME="centurion-erp"
TIMEOUT=90
INTERVAL=5
ELAPSED=0
STATUS=""
while [ "$STATUS" != "healthy" ]; do
STATUS=$(docker inspect --format '{{.State.Health.Status}}' "$CONTAINER_NAME" 2>/dev/null || echo "none")
if [ $ELAPSED -ge $TIMEOUT ]; then
echo "Timeout reached. Container $CONTAINER_NAME is not healthy."
exit 4
fi
echo "Waiting for container $CONTAINER_NAME to be healthy... Current status: $STATUS"
sleep $INTERVAL
ELAPSED=$((ELAPSED + INTERVAL))
done
docker exec -i centurion-erp python manage.py createsuperuser --username admin --email admin@localhost --noinput
docker exec -i centurion-erp apk add expect
docker exec -i centurion-erp expect -c "
spawn python manage.py changepassword admin
expect \"Password:\"
send \"admin\r\"
expect \"Password (again):\"
send \"admin\r\"
expect eof
"