diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d8ac6fad..6e5e55d8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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 diff --git a/.gitignore b/.gitignore index a7265761..5b75fc07 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,8 @@ feature_flags.json coverage_*.json *-coverage.xml log/ +# Integration testing +app/artifacts/ +app/pyproject.toml +app/histogram_** +app/counter_** diff --git a/README.md b/README.md index 532c29f0..0fb6432e 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ [![Docker Pulls](https://img.shields.io/docker/pulls/nofusscomputing/centurion-erp?style=plastic&logo=docker&color=0db7ed)](https://hub.docker.com/r/nofusscomputing/centurion-erp) [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/centurion-erp)](https://artifacthub.io/packages/container/centurion-erp/centurion-erp) +![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Frefs%2Fheads%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_integration_postgres_versions.json&style=plastic&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iNDMyLjA3MXB0IiBoZWlnaHQ9IjQ0NS4zODNwdCIgdmlld0JveD0iMCAwIDQzMi4wNzEgNDQ1LjM4MyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgaWQ9Im9yZ2luYWwiIHN0eWxlPSJmaWxsLXJ1bGU6bm9uemVybztjbGlwLXJ1bGU6bm9uemVybztzdHJva2U6IzAwMDAwMDtzdHJva2UtbWl0ZXJsaW1pdDo0OyI%2BCgk8L2c%2BCjxnIGlkPSJMYXllcl94MDAyMF8zIiBzdHlsZT0iZmlsbC1ydWxlOm5vbnplcm87Y2xpcC1ydWxlOm5vbnplcm87ZmlsbDpub25lO3N0cm9rZTojRkZGRkZGO3N0cm9rZS13aWR0aDoxMi40NjUxO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDo0OyI%2BCjxwYXRoIHN0eWxlPSJmaWxsOiMwMDAwMDA7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjM3LjM5NTM7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7IiBkPSJNMzIzLjIwNSwzMjQuMjI3YzIuODMzLTIzLjYwMSwxLjk4NC0yNy4wNjIsMTkuNTYzLTIzLjIzOWw0LjQ2MywwLjM5MmMxMy41MTcsMC42MTUsMzEuMTk5LTIuMTc0LDQxLjU4Ny03YzIyLjM2Mi0xMC4zNzYsMzUuNjIyLTI3LjcsMTMuNTcyLTIzLjE0OGMtNTAuMjk3LDEwLjM3Ni01My43NTUtNi42NTUtNTMuNzU1LTYuNjU1YzUzLjExMS03OC44MDMsNzUuMzEzLTE3OC44MzYsNTYuMTQ5LTIwMy4zMjIgICAgQzM1Mi41MTQtNS41MzQsMjYyLjAzNiwyNi4wNDksMjYwLjUyMiwyNi44NjlsLTAuNDgyLDAuMDg5Yy05LjkzOC0yLjA2Mi0yMS4wNi0zLjI5NC0zMy41NTQtMy40OTZjLTIyLjc2MS0wLjM3NC00MC4wMzIsNS45NjctNTMuMTMzLDE1LjkwNGMwLDAtMTYxLjQwOC02Ni40OTgtMTUzLjg5OSw4My42MjhjMS41OTcsMzEuOTM2LDQ1Ljc3NywyNDEuNjU1LDk4LjQ3LDE3OC4zMSAgICBjMTkuMjU5LTIzLjE2MywzNy44NzEtNDIuNzQ4LDM3Ljg3MS00Mi43NDhjOS4yNDIsNi4xNCwyMC4zMDcsOS4yNzIsMzEuOTEyLDguMTQ3bDAuODk3LTAuNzY1Yy0wLjI4MSwyLjg3Ni0wLjE1Nyw1LjY4OSwwLjM1OSw5LjAxOWMtMTMuNTcyLDE1LjE2Ny05LjU4NCwxNy44My0zNi43MjMsMjMuNDE2Yy0yNy40NTcsNS42NTktMTEuMzI2LDE1LjczNC0wLjc5NywxOC4zNjdjMTIuNzY4LDMuMTkzLDQyLjMwNSw3LjcxNiw2Mi4yNjgtMjAuMjI0ICAgIGwtMC43OTUsMy4xODhjNS4zMjUsNC4yNiw0Ljk2NSwzMC42MTksNS43Miw0OS40NTJjMC43NTYsMTguODM0LDIuMDE3LDM2LjQwOSw1Ljg1Niw0Ni43NzFjMy44MzksMTAuMzYsOC4zNjksMzcuMDUsNDQuMDM2LDI5LjQwNmMyOS44MDktNi4zODgsNTIuNi0xNS41ODIsNTQuNjc3LTEwMS4xMDciLz4KPHBhdGggc3R5bGU9ImZpbGw6IzMzNjc5MTtzdHJva2U6bm9uZTsiIGQ9Ik00MDIuMzk1LDI3MS4yM2MtNTAuMzAyLDEwLjM3Ni01My43Ni02LjY1NS01My43Ni02LjY1NWM1My4xMTEtNzguODA4LDc1LjMxMy0xNzguODQzLDU2LjE1My0yMDMuMzI2Yy01Mi4yNy02Ni43ODUtMTQyLjc1Mi0zNS4yLTE0NC4yNjItMzQuMzhsLTAuNDg2LDAuMDg3Yy05LjkzOC0yLjA2My0yMS4wNi0zLjI5Mi0zMy41Ni0zLjQ5NmMtMjIuNzYxLTAuMzczLTQwLjAyNiw1Ljk2Ny01My4xMjcsMTUuOTAyICAgIGMwLDAtMTYxLjQxMS02Ni40OTUtMTUzLjkwNCw4My42M2MxLjU5NywzMS45MzgsNDUuNzc2LDI0MS42NTcsOTguNDcxLDE3OC4zMTJjMTkuMjYtMjMuMTYzLDM3Ljg2OS00Mi43NDgsMzcuODY5LTQyLjc0OGM5LjI0Myw2LjE0LDIwLjMwOCw5LjI3MiwzMS45MDgsOC4xNDdsMC45MDEtMC43NjVjLTAuMjgsMi44NzYtMC4xNTIsNS42ODksMC4zNjEsOS4wMTljLTEzLjU3NSwxNS4xNjctOS41ODYsMTcuODMtMzYuNzIzLDIzLjQxNiAgICBjLTI3LjQ1OSw1LjY1OS0xMS4zMjgsMTUuNzM0LTAuNzk2LDE4LjM2N2MxMi43NjgsMy4xOTMsNDIuMzA3LDcuNzE2LDYyLjI2Ni0yMC4yMjRsLTAuNzk2LDMuMTg4YzUuMzE5LDQuMjYsOS4wNTQsMjcuNzExLDguNDI4LDQ4Ljk2OWMtMC42MjYsMjEuMjU5LTEuMDQ0LDM1Ljg1NCwzLjE0Nyw0Ny4yNTRjNC4xOTEsMTEuNCw4LjM2OCwzNy4wNSw0NC4wNDIsMjkuNDA2YzI5LjgwOS02LjM4OCw0NS4yNTYtMjIuOTQyLDQ3LjQwNS01MC41NTUgICAgYzEuNTI1LTE5LjYzMSw0Ljk3Ni0xNi43MjksNS4xOTQtMzQuMjhsMi43NjgtOC4zMDljMy4xOTItMjYuNjExLDAuNTA3LTM1LjE5NiwxOC44NzItMzEuMjAzbDQuNDYzLDAuMzkyYzEzLjUxNywwLjYxNSwzMS4yMDgtMi4xNzQsNDEuNTkxLTdjMjIuMzU4LTEwLjM3NiwzNS42MTgtMjcuNywxMy41NzMtMjMuMTQ4eiIvPgo8cGF0aCBkPSJNMjE1Ljg2NiwyODYuNDg0Yy0xLjM4NSw0OS41MTYsMC4zNDgsOTkuMzc3LDUuMTkzLDExMS40OTVjNC44NDgsMTIuMTE4LDE1LjIyMywzNS42ODgsNTAuOSwyOC4wNDVjMjkuODA2LTYuMzksNDAuNjUxLTE4Ljc1Niw0NS4zNTctNDYuMDUxYzMuNDY2LTIwLjA4MiwxMC4xNDgtNzUuODU0LDExLjAwNS04Ny4yODEiLz4KPHBhdGggZD0iTTE3My4xMDQsMzguMjU2YzAsMC0xNjEuNTIxLTY2LjAxNi0xNTQuMDEyLDg0LjEwOWMxLjU5NywzMS45MzgsNDUuNzc5LDI0MS42NjQsOTguNDczLDE3OC4zMTZjMTkuMjU2LTIzLjE2NiwzNi42NzEtNDEuMzM1LDM2LjY3MS00MS4zMzUiLz4KPHBhdGggZD0iTTI2MC4zNDksMjYuMjA3Yy01LjU5MSwxLjc1Myw4OS44NDgtMzQuODg5LDE0NC4wODcsMzQuNDE3YzE5LjE1OSwyNC40ODQtMy4wNDMsMTI0LjUxOS01Ni4xNTMsMjAzLjMyOSIvPgo8cGF0aCBzdHlsZT0ic3Ryb2tlLWxpbmVqb2luOmJldmVsOyIgZD0iTTM0OC4yODIsMjYzLjk1M2MwLDAsMy40NjEsMTcuMDM2LDUzLjc2NCw2LjY1M2MyMi4wNC00LjU1Miw4Ljc3NiwxMi43NzQtMTMuNTc3LDIzLjE1NWMtMTguMzQ1LDguNTE0LTU5LjQ3NCwxMC42OTYtNjAuMTQ2LTEuMDY5Yy0xLjcyOS0zMC4zNTUsMjEuNjQ3LTIxLjEzMywxOS45Ni0yOC43MzljLTEuNTI1LTYuODUtMTEuOTc5LTEzLjU3My0xOC44OTQtMzAuMzM4ICAgIGMtNi4wMzctMTQuNjMzLTgyLjc5Ni0xMjYuODQ5LDIxLjI4Ny0xMTAuMTgzYzMuODEzLTAuNzg5LTI3LjE0Ni05OS4wMDItMTI0LjU1My0xMDAuNTk5Yy05Ny4zODUtMS41OTctOTQuMTksMTE5Ljc2Mi05NC4xOSwxMTkuNzYyIi8%2BCjxwYXRoIGQ9Ik0xODguNjA0LDI3NC4zMzRjLTEzLjU3NywxNS4xNjYtOS41ODQsMTcuODI5LTM2LjcyMywyMy40MTdjLTI3LjQ1OSw1LjY2LTExLjMyNiwxNS43MzMtMC43OTcsMTguMzY1YzEyLjc2OCwzLjE5NSw0Mi4zMDcsNy43MTgsNjIuMjY2LTIwLjIyOWM2LjA3OC04LjUwOS0wLjAzNi0yMi4wODYtOC4zODUtMjUuNTQ3Yy00LjAzNC0xLjY3MS05LjQyOC0zLjc2NS0xNi4zNjEsMy45OTR6Ii8%2BCjxwYXRoIGQ9Ik0xODcuNzE1LDI3NC4wNjljLTEuMzY4LTguOTE3LDIuOTMtMTkuNTI4LDcuNTM2LTMxLjk0MmM2LjkyMi0xOC42MjYsMjIuODkzLTM3LjI1NSwxMC4xMTctOTYuMzM5Yy05LjUyMy00NC4wMjktNzMuMzk2LTkuMTYzLTczLjQzNi0zLjE5M2MtMC4wMzksNS45NjgsMi44ODksMzAuMjYtMS4wNjcsNTguNTQ4Yy01LjE2MiwzNi45MTMsMjMuNDg4LDY4LjEzMiw1Ni40NzksNjQuOTM4Ii8%2BCjxwYXRoIHN0eWxlPSJmaWxsOiNGRkZGRkY7c3Ryb2tlLXdpZHRoOjQuMTU1O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyOyIgZD0iTTE3Mi41MTcsMTQxLjdjLTAuMjg4LDIuMDM5LDMuNzMzLDcuNDgsOC45NzYsOC4yMDdjNS4yMzQsMC43Myw5LjcxNC0zLjUyMiw5Ljk5OC01LjU1OWMwLjI4NC0yLjAzOS0zLjczMi00LjI4NS04Ljk3Ny01LjAxNWMtNS4yMzctMC43MzEtOS43MTksMC4zMzMtOS45OTYsMi4zNjd6Ii8%2BCjxwYXRoIHN0eWxlPSJmaWxsOiNGRkZGRkY7c3Ryb2tlLXdpZHRoOjIuMDc3NTtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjsiIGQ9Ik0zMzEuOTQxLDEzNy41NDNjMC4yODQsMi4wMzktMy43MzIsNy40OC04Ljk3Niw4LjIwN2MtNS4yMzgsMC43My05LjcxOC0zLjUyMi0xMC4wMDUtNS41NTljLTAuMjc3LTIuMDM5LDMuNzQtNC4yODUsOC45NzktNS4wMTVjNS4yMzktMC43Myw5LjcxOCwwLjMzMywxMC4wMDIsMi4zNjh6Ii8%2BCjxwYXRoIGQ9Ik0zNTAuNjc2LDEyMy40MzJjMC44NjMsMTUuOTk0LTMuNDQ1LDI2Ljg4OC0zLjk4OCw0My45MTRjLTAuODA0LDI0Ljc0OCwxMS43OTksNTMuMDc0LTcuMTkxLDgxLjQzNSIvPgo8cGF0aCBzdHlsZT0ic3Ryb2tlLXdpZHRoOjM7IiBkPSJNMCw2MC4yMzIiLz4KPC9nPgo8L3N2Zz4K) + ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Frefs%2Fheads%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_integration_rabbitmq_versions.json&style=plastic) ---- diff --git a/app/access/functions/permissions.py b/app/access/functions/permissions.py index 1ba95b50..b1bf83b9 100644 --- a/app/access/functions/permissions.py +++ b/app/access/functions/permissions.py @@ -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 - ) \ No newline at end of file + 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 + ) diff --git a/app/access/urls_api.py b/app/access/urls_api.py index bd9a96cf..6dee2800 100644 --- a/app/access/urls_api.py +++ b/app/access/urls_api.py @@ -34,17 +34,17 @@ router = DefaultRouter(trailing_slash=False) router.register('', access_v2.Index, basename = '_api_v2_access_home') router.register( - prefix = '(?P[company]+)', viewset = entity.ViewSet, + prefix = '/(?P[company]+)', viewset = entity.ViewSet, feature_flag = '2025-00008',basename = '_api_v2_company' ) router.register( - prefix=f'entity/(?P[{entity_type_names}]+)?', viewset = entity.ViewSet, + prefix=f'/entity/(?P[{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[0-9]+)/team', viewset = team_v2.ViewSet, + prefix = '/tenant/(?P[0-9]+)/team', viewset = team_v2.ViewSet, basename = '_api_v2_organization_team' ) @@ -75,13 +75,13 @@ router.register( # ) router.register( - prefix = 'access/tenant/(?P[0-9]+)/team/(?P[0-9]+)/user', + prefix = '/access/tenant/(?P[0-9]+)/team/(?P[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' ) diff --git a/app/accounting/urls.py b/app/accounting/urls.py index 408c9d86..cd6b6732 100644 --- a/app/accounting/urls.py +++ b/app/accounting/urls.py @@ -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[{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[{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 diff --git a/app/api/urls_v2.py b/app/api/urls_v2.py index 087231b7..7e6fc46b 100644 --- a/app/api/urls_v2.py +++ b/app/api/urls_v2.py @@ -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[{history_app_labels}]+)/(?P[{history_type_names} \ + prefix = f'/(?P[{history_app_labels}]+)/(?P[{history_type_names} \ ]+)/(?P[0-9]+)/history', viewset = audit_history.ViewSet, basename = '_api_centurionaudit_sub' ) router.register( - prefix = f'(?P[{notes_app_labels}]+)/(?P[{notes_type_names} \ + prefix = f'/(?P[{notes_app_labels}]+)/(?P[{notes_type_names} \ ]+)/(?P[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')), ] diff --git a/app/assistance/urls_api.py b/app/assistance/urls_api.py index df81aa2b..18f50b01 100644 --- a/app/assistance/urls_api.py +++ b/app/assistance/urls_api.py @@ -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.+)/(?P[0-9]+)/knowledge_base', + prefix = '/(?P.+)/(?P[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' ) diff --git a/app/centurion/settings.py b/app/centurion/settings.py index b3f9de31..280ca65e 100644 --- a/app/centurion/settings.py +++ b/app/centurion/settings.py @@ -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'. diff --git a/app/centurion/tests/integration/test_integration_list_urls.py b/app/centurion/tests/integration/test_integration_list_urls.py new file mode 100644 index 00000000..5cea20b2 --- /dev/null +++ b/app/centurion/tests/integration/test_integration_list_urls.py @@ -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 diff --git a/app/centurion/urls.py b/app/centurion/urls.py index 4970f85e..87b26c2d 100644 --- a/app/centurion/urls.py +++ b/app/centurion/urls.py @@ -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.*)$', 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'), ] diff --git a/app/config_management/urls_api.py b/app/config_management/urls_api.py index 340a6a18..865d5385 100644 --- a/app/config_management/urls_api.py +++ b/app/config_management/urls_api.py @@ -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[0-9]+)/child_group', viewset = config_group_v2.ViewSet, + prefix = '/group/(?P[0-9]+)/child_group', viewset = config_group_v2.ViewSet, basename = '_api_configgroups_child' ) router.register( - prefix = 'group/(?P[0-9]+)/software', + prefix = '/group/(?P[0-9]+)/software', viewset = config_group_software_v2.ViewSet, basename = '_api_configgroupsoftware' ) diff --git a/app/core/urls_api.py b/app/core/urls_api.py index 75cc27ba..4a198f72 100644 --- a/app/core/urls_api.py +++ b/app/core/urls_api.py @@ -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_names}]+)', viewset = ticket.ViewSet, + prefix=f'/ticket/(?P[{ticket_type_names}]+)', viewset = ticket.ViewSet, feature_flag = '2025-00006', basename = '_api_ticketbase_sub' ) router.register( - prefix = 'ticket/(?P[0-9]+)/comment', viewset = ticket_comment.NoDocsViewSet, + prefix = '/ticket/(?P[0-9]+)/comment', viewset = ticket_comment.NoDocsViewSet, feature_flag = '2025-00006', basename = '_api_ticket_comment_base' ) router.register( - prefix = 'ticket/(?P[0-9]+)/comment/(?P[0-9]+)/threads', + prefix = '/ticket/(?P[0-9]+)/comment/(?P[0-9]+)/threads', viewset = ticket_comment.ViewSet, feature_flag = '2025-00006', basename = '_api_ticket_comment_base_thread' ) router.register( - prefix = 'ticket/(?P[0-9]+)/comments', viewset = ticket_comment_depreciated.ViewSet, + prefix = '/ticket/(?P[0-9]+)/comments', viewset = ticket_comment_depreciated.ViewSet, basename = '_api_v2_ticket_comment' ) router.register( - prefix = 'ticket/(?P[0-9]+)/comments/(?P[0-9]+)/threads', + prefix = '/ticket/(?P[0-9]+)/comments/(?P[0-9]+)/threads', viewset = ticket_comment_depreciated.ViewSet, basename = '_api_v2_ticket_comment_threads' ) router.register( - prefix = 'ticket/(?P[0-9]+)/linked_item', viewset = ticket_linked_item.ViewSet, + prefix = '/ticket/(?P[0-9]+)/linked_item', viewset = ticket_linked_item.ViewSet, basename = '_api_v2_ticket_linked_item' ) router.register( - prefix = 'ticket/(?P[0-9]+)/related_ticket', viewset = related_ticket.ViewSet, + prefix = '/ticket/(?P[0-9]+)/related_ticket', viewset = related_ticket.ViewSet, basename = '_api_v2_ticket_related' ) router.register( - prefix=f'ticket/(?P[0-9]+)/(?P[{ticket_comment_names}]+)', + prefix=f'/ticket/(?P[0-9]+)/(?P[{ticket_comment_names}]+)', viewset = ticket_comment.ViewSet, feature_flag = '2025-00006', basename = '_api_ticket_comment_base_sub' ) router.register( - prefix=f'ticket/(?P[0-9]+)/(?P[{ticket_comment_names} \ + prefix=f'/ticket/(?P[0-9]+)/(?P[{ticket_comment_names} \ ]+)/(?P[0-9]+)/threads', viewset = ticket_comment.ViewSet, feature_flag = '2025-00006', basename = '_api_ticket_comment_base_sub_thread' ) router.register( - prefix = '(?P[a-z_]+)/(?P[0-9]+)/item_ticket', + prefix = '/(?P[a-z_]+)/(?P[0-9]+)/item_ticket', viewset = ticket_linked_item.ViewSet, basename = '_api_v2_item_tickets' ) diff --git a/app/devops/urls.py b/app/devops/urls.py index c24962ac..15d5e991 100644 --- a/app/devops/urls.py +++ b/app/devops/urls.py @@ -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(?:/(?Pgitlab|github))?', + prefix = r'/git_repository(?:/(?Pgitlab|github))?', viewset = git_repository.ViewSet, feature_flag = '2025-00001', basename = '_api_gitrepository' ) router.register( - prefix = r'(?Pgithubrepository|gitlabrepository)', + prefix = r'/(?Pgithubrepository|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' ) diff --git a/app/devops/urls_public.py b/app/devops/urls_public.py index 08f68964..3b458aa1 100644 --- a/app/devops/urls_public.py +++ b/app/devops/urls_public.py @@ -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[0-9]+)/flags/(?P[0-9]+)', public_feature_flag.ViewSet, basename='_api_checkin') +router.register('/(?P[0-9]+)/flags/(?P[0-9]+)', public_feature_flag.ViewSet, basename='_api_checkin') urlpatterns = router.urls diff --git a/app/itam/urls_api.py b/app/itam/urls_api.py index 46760edd..ffbcdd56 100644 --- a/app/itam/urls_api.py +++ b/app/itam/urls_api.py @@ -34,57 +34,57 @@ router.register( basename = '_api_v2_itam_home' ) router.register( - prefix = '(?P[itamassetbase]+)', viewset = asset.ViewSet, + prefix = '/(?P[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[0-9]+)/operating_system', + prefix = '/device/(?P[0-9]+)/operating_system', viewset = device_operating_system.ViewSet, basename = '_api_deviceoperatingsystem') router.register( - prefix = 'device/(?P[0-9]+)/software', viewset = device_software_v2.ViewSet, + prefix = '/device/(?P[0-9]+)/software', viewset = device_software_v2.ViewSet, basename = '_api_devicesoftware' ) router.register( - prefix = 'device/(?P[0-9]+)/service', viewset = service_device_v2.ViewSet, + prefix = '/device/(?P[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[0-9]+)/installs', + prefix = '/operating_system/(?P[0-9]+)/installs', viewset = device_operating_system.ViewSet, basename = '_api_v2_operating_system_installs' ) router.register( - prefix = 'operating_system/(?P[0-9]+)/version', + prefix = '/operating_system/(?P[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[0-9]+)/installs', viewset = device_software_v2.ViewSet, + prefix = '/software/(?P[0-9]+)/installs', viewset = device_software_v2.ViewSet, basename = '_api_v2_software_installs' ) router.register( - prefix = 'software/(?P[0-9]+)/version', viewset = software_version_v2.ViewSet, + prefix = '/software/(?P[0-9]+)/version', viewset = software_version_v2.ViewSet, basename = '_api_softwareversion' ) router.register( - prefix = 'software/(?P[0-9]+)/feature_flag', + prefix = '/software/(?P[0-9]+)/feature_flag', viewset = software_enable_feature_flag.ViewSet, basename = '_api_softwareenablefeatureflag' ) diff --git a/app/itim/urls_api.py b/app/itim/urls_api.py index dd92e9d5..935d36d8 100644 --- a/app/itim/urls_api.py +++ b/app/itim/urls_api.py @@ -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[0-9]+)/service', viewset = service_cluster.ViewSet, + prefix = '/cluster/(?P[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' ) diff --git a/app/project_management/tests/unit/project_milestone/test_unit_project_milestone_serializer.py b/app/project_management/tests/unit/project_milestone/test_unit_project_milestone_serializer.py index 82f1da29..dce0d12b 100644 --- a/app/project_management/tests/unit/project_milestone/test_unit_project_milestone_serializer.py +++ b/app/project_management/tests/unit/project_milestone/test_unit_project_milestone_serializer.py @@ -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 diff --git a/app/project_management/urls_api.py b/app/project_management/urls_api.py index 6209e2ab..10bae4e5 100644 --- a/app/project_management/urls_api.py +++ b/app/project_management/urls_api.py @@ -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[0-9]+)/milestone', + prefix = '/project/(?P[0-9]+)/milestone', viewset = project_milestone.ViewSet, basename = '_api_projectmilestone' ) router.register( - prefix = 'project/(?P[0-9]+)/project_task', + prefix = '/project/(?P[0-9]+)/project_task', viewset = project_task.ViewSet, basename = '_api_v2_ticket_project_task' ) diff --git a/app/settings/urls_api.py b/app/settings/urls_api.py index 3333050f..9ddedc46 100644 --- a/app/settings/urls_api.py +++ b/app/settings/urls_api.py @@ -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[0-9]+)/token', viewset = auth_token.ViewSet, + prefix = '/user_(?P[0-9]+)/token', viewset = auth_token.ViewSet, basename = '_api_authtoken' ) diff --git a/app/tests/fixtures/model_projectmilestone.py b/app/tests/fixtures/model_projectmilestone.py index 4fe77234..f5cc6558 100644 --- a/app/tests/fixtures/model_projectmilestone.py +++ b/app/tests/fixtures/model_projectmilestone.py @@ -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') diff --git a/app/tests/fixtures/model_projectstate.py b/app/tests/fixtures/model_projectstate.py index 671dfe6b..a864ae90 100644 --- a/app/tests/fixtures/model_projectstate.py +++ b/app/tests/fixtures/model_projectstate.py @@ -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') diff --git a/app/tests/fixtures/model_projecttype.py b/app/tests/fixtures/model_projecttype.py index 8f130e01..3841ccaf 100644 --- a/app/tests/fixtures/model_projecttype.py +++ b/app/tests/fixtures/model_projecttype.py @@ -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') diff --git a/dockerfile b/dockerfile index eb113034..7df45690 100644 --- a/dockerfile +++ b/dockerfile @@ -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 diff --git a/docs/projects/centurion_erp/index.md b/docs/projects/centurion_erp/index.md index 5eb82aeb..d87c2aed 100644 --- a/docs/projects/centurion_erp/index.md +++ b/docs/projects/centurion_erp/index.md @@ -10,6 +10,11 @@ about: https://gitlab.com/nofusscomputing/infrastructure/configuration-managemen ![Project Status - Active](https://img.shields.io/badge/Project%20Status-Active-green?logo=github&style=plastic) +![Docker Pulls](https://img.shields.io/docker/pulls/nofusscomputing/centurion-erp?style=plastic&logo=docker&color=0db7ed) [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/centurion-erp)](https://artifacthub.io/packages/container/centurion-erp/centurion-erp) + +![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Frefs%2Fheads%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_integration_postgres_versions.json&style=plastic&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iNDMyLjA3MXB0IiBoZWlnaHQ9IjQ0NS4zODNwdCIgdmlld0JveD0iMCAwIDQzMi4wNzEgNDQ1LjM4MyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgaWQ9Im9yZ2luYWwiIHN0eWxlPSJmaWxsLXJ1bGU6bm9uemVybztjbGlwLXJ1bGU6bm9uemVybztzdHJva2U6IzAwMDAwMDtzdHJva2UtbWl0ZXJsaW1pdDo0OyI%2BCgk8L2c%2BCjxnIGlkPSJMYXllcl94MDAyMF8zIiBzdHlsZT0iZmlsbC1ydWxlOm5vbnplcm87Y2xpcC1ydWxlOm5vbnplcm87ZmlsbDpub25lO3N0cm9rZTojRkZGRkZGO3N0cm9rZS13aWR0aDoxMi40NjUxO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDo0OyI%2BCjxwYXRoIHN0eWxlPSJmaWxsOiMwMDAwMDA7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjM3LjM5NTM7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7IiBkPSJNMzIzLjIwNSwzMjQuMjI3YzIuODMzLTIzLjYwMSwxLjk4NC0yNy4wNjIsMTkuNTYzLTIzLjIzOWw0LjQ2MywwLjM5MmMxMy41MTcsMC42MTUsMzEuMTk5LTIuMTc0LDQxLjU4Ny03YzIyLjM2Mi0xMC4zNzYsMzUuNjIyLTI3LjcsMTMuNTcyLTIzLjE0OGMtNTAuMjk3LDEwLjM3Ni01My43NTUtNi42NTUtNTMuNzU1LTYuNjU1YzUzLjExMS03OC44MDMsNzUuMzEzLTE3OC44MzYsNTYuMTQ5LTIwMy4zMjIgICAgQzM1Mi41MTQtNS41MzQsMjYyLjAzNiwyNi4wNDksMjYwLjUyMiwyNi44NjlsLTAuNDgyLDAuMDg5Yy05LjkzOC0yLjA2Mi0yMS4wNi0zLjI5NC0zMy41NTQtMy40OTZjLTIyLjc2MS0wLjM3NC00MC4wMzIsNS45NjctNTMuMTMzLDE1LjkwNGMwLDAtMTYxLjQwOC02Ni40OTgtMTUzLjg5OSw4My42MjhjMS41OTcsMzEuOTM2LDQ1Ljc3NywyNDEuNjU1LDk4LjQ3LDE3OC4zMSAgICBjMTkuMjU5LTIzLjE2MywzNy44NzEtNDIuNzQ4LDM3Ljg3MS00Mi43NDhjOS4yNDIsNi4xNCwyMC4zMDcsOS4yNzIsMzEuOTEyLDguMTQ3bDAuODk3LTAuNzY1Yy0wLjI4MSwyLjg3Ni0wLjE1Nyw1LjY4OSwwLjM1OSw5LjAxOWMtMTMuNTcyLDE1LjE2Ny05LjU4NCwxNy44My0zNi43MjMsMjMuNDE2Yy0yNy40NTcsNS42NTktMTEuMzI2LDE1LjczNC0wLjc5NywxOC4zNjdjMTIuNzY4LDMuMTkzLDQyLjMwNSw3LjcxNiw2Mi4yNjgtMjAuMjI0ICAgIGwtMC43OTUsMy4xODhjNS4zMjUsNC4yNiw0Ljk2NSwzMC42MTksNS43Miw0OS40NTJjMC43NTYsMTguODM0LDIuMDE3LDM2LjQwOSw1Ljg1Niw0Ni43NzFjMy44MzksMTAuMzYsOC4zNjksMzcuMDUsNDQuMDM2LDI5LjQwNmMyOS44MDktNi4zODgsNTIuNi0xNS41ODIsNTQuNjc3LTEwMS4xMDciLz4KPHBhdGggc3R5bGU9ImZpbGw6IzMzNjc5MTtzdHJva2U6bm9uZTsiIGQ9Ik00MDIuMzk1LDI3MS4yM2MtNTAuMzAyLDEwLjM3Ni01My43Ni02LjY1NS01My43Ni02LjY1NWM1My4xMTEtNzguODA4LDc1LjMxMy0xNzguODQzLDU2LjE1My0yMDMuMzI2Yy01Mi4yNy02Ni43ODUtMTQyLjc1Mi0zNS4yLTE0NC4yNjItMzQuMzhsLTAuNDg2LDAuMDg3Yy05LjkzOC0yLjA2My0yMS4wNi0zLjI5Mi0zMy41Ni0zLjQ5NmMtMjIuNzYxLTAuMzczLTQwLjAyNiw1Ljk2Ny01My4xMjcsMTUuOTAyICAgIGMwLDAtMTYxLjQxMS02Ni40OTUtMTUzLjkwNCw4My42M2MxLjU5NywzMS45MzgsNDUuNzc2LDI0MS42NTcsOTguNDcxLDE3OC4zMTJjMTkuMjYtMjMuMTYzLDM3Ljg2OS00Mi43NDgsMzcuODY5LTQyLjc0OGM5LjI0Myw2LjE0LDIwLjMwOCw5LjI3MiwzMS45MDgsOC4xNDdsMC45MDEtMC43NjVjLTAuMjgsMi44NzYtMC4xNTIsNS42ODksMC4zNjEsOS4wMTljLTEzLjU3NSwxNS4xNjctOS41ODYsMTcuODMtMzYuNzIzLDIzLjQxNiAgICBjLTI3LjQ1OSw1LjY1OS0xMS4zMjgsMTUuNzM0LTAuNzk2LDE4LjM2N2MxMi43NjgsMy4xOTMsNDIuMzA3LDcuNzE2LDYyLjI2Ni0yMC4yMjRsLTAuNzk2LDMuMTg4YzUuMzE5LDQuMjYsOS4wNTQsMjcuNzExLDguNDI4LDQ4Ljk2OWMtMC42MjYsMjEuMjU5LTEuMDQ0LDM1Ljg1NCwzLjE0Nyw0Ny4yNTRjNC4xOTEsMTEuNCw4LjM2OCwzNy4wNSw0NC4wNDIsMjkuNDA2YzI5LjgwOS02LjM4OCw0NS4yNTYtMjIuOTQyLDQ3LjQwNS01MC41NTUgICAgYzEuNTI1LTE5LjYzMSw0Ljk3Ni0xNi43MjksNS4xOTQtMzQuMjhsMi43NjgtOC4zMDljMy4xOTItMjYuNjExLDAuNTA3LTM1LjE5NiwxOC44NzItMzEuMjAzbDQuNDYzLDAuMzkyYzEzLjUxNywwLjYxNSwzMS4yMDgtMi4xNzQsNDEuNTkxLTdjMjIuMzU4LTEwLjM3NiwzNS42MTgtMjcuNywxMy41NzMtMjMuMTQ4eiIvPgo8cGF0aCBkPSJNMjE1Ljg2NiwyODYuNDg0Yy0xLjM4NSw0OS41MTYsMC4zNDgsOTkuMzc3LDUuMTkzLDExMS40OTVjNC44NDgsMTIuMTE4LDE1LjIyMywzNS42ODgsNTAuOSwyOC4wNDVjMjkuODA2LTYuMzksNDAuNjUxLTE4Ljc1Niw0NS4zNTctNDYuMDUxYzMuNDY2LTIwLjA4MiwxMC4xNDgtNzUuODU0LDExLjAwNS04Ny4yODEiLz4KPHBhdGggZD0iTTE3My4xMDQsMzguMjU2YzAsMC0xNjEuNTIxLTY2LjAxNi0xNTQuMDEyLDg0LjEwOWMxLjU5NywzMS45MzgsNDUuNzc5LDI0MS42NjQsOTguNDczLDE3OC4zMTZjMTkuMjU2LTIzLjE2NiwzNi42NzEtNDEuMzM1LDM2LjY3MS00MS4zMzUiLz4KPHBhdGggZD0iTTI2MC4zNDksMjYuMjA3Yy01LjU5MSwxLjc1Myw4OS44NDgtMzQuODg5LDE0NC4wODcsMzQuNDE3YzE5LjE1OSwyNC40ODQtMy4wNDMsMTI0LjUxOS01Ni4xNTMsMjAzLjMyOSIvPgo8cGF0aCBzdHlsZT0ic3Ryb2tlLWxpbmVqb2luOmJldmVsOyIgZD0iTTM0OC4yODIsMjYzLjk1M2MwLDAsMy40NjEsMTcuMDM2LDUzLjc2NCw2LjY1M2MyMi4wNC00LjU1Miw4Ljc3NiwxMi43NzQtMTMuNTc3LDIzLjE1NWMtMTguMzQ1LDguNTE0LTU5LjQ3NCwxMC42OTYtNjAuMTQ2LTEuMDY5Yy0xLjcyOS0zMC4zNTUsMjEuNjQ3LTIxLjEzMywxOS45Ni0yOC43MzljLTEuNTI1LTYuODUtMTEuOTc5LTEzLjU3My0xOC44OTQtMzAuMzM4ICAgIGMtNi4wMzctMTQuNjMzLTgyLjc5Ni0xMjYuODQ5LDIxLjI4Ny0xMTAuMTgzYzMuODEzLTAuNzg5LTI3LjE0Ni05OS4wMDItMTI0LjU1My0xMDAuNTk5Yy05Ny4zODUtMS41OTctOTQuMTksMTE5Ljc2Mi05NC4xOSwxMTkuNzYyIi8%2BCjxwYXRoIGQ9Ik0xODguNjA0LDI3NC4zMzRjLTEzLjU3NywxNS4xNjYtOS41ODQsMTcuODI5LTM2LjcyMywyMy40MTdjLTI3LjQ1OSw1LjY2LTExLjMyNiwxNS43MzMtMC43OTcsMTguMzY1YzEyLjc2OCwzLjE5NSw0Mi4zMDcsNy43MTgsNjIuMjY2LTIwLjIyOWM2LjA3OC04LjUwOS0wLjAzNi0yMi4wODYtOC4zODUtMjUuNTQ3Yy00LjAzNC0xLjY3MS05LjQyOC0zLjc2NS0xNi4zNjEsMy45OTR6Ii8%2BCjxwYXRoIGQ9Ik0xODcuNzE1LDI3NC4wNjljLTEuMzY4LTguOTE3LDIuOTMtMTkuNTI4LDcuNTM2LTMxLjk0MmM2LjkyMi0xOC42MjYsMjIuODkzLTM3LjI1NSwxMC4xMTctOTYuMzM5Yy05LjUyMy00NC4wMjktNzMuMzk2LTkuMTYzLTczLjQzNi0zLjE5M2MtMC4wMzksNS45NjgsMi44ODksMzAuMjYtMS4wNjcsNTguNTQ4Yy01LjE2MiwzNi45MTMsMjMuNDg4LDY4LjEzMiw1Ni40NzksNjQuOTM4Ii8%2BCjxwYXRoIHN0eWxlPSJmaWxsOiNGRkZGRkY7c3Ryb2tlLXdpZHRoOjQuMTU1O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyOyIgZD0iTTE3Mi41MTcsMTQxLjdjLTAuMjg4LDIuMDM5LDMuNzMzLDcuNDgsOC45NzYsOC4yMDdjNS4yMzQsMC43Myw5LjcxNC0zLjUyMiw5Ljk5OC01LjU1OWMwLjI4NC0yLjAzOS0zLjczMi00LjI4NS04Ljk3Ny01LjAxNWMtNS4yMzctMC43MzEtOS43MTksMC4zMzMtOS45OTYsMi4zNjd6Ii8%2BCjxwYXRoIHN0eWxlPSJmaWxsOiNGRkZGRkY7c3Ryb2tlLXdpZHRoOjIuMDc3NTtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjsiIGQ9Ik0zMzEuOTQxLDEzNy41NDNjMC4yODQsMi4wMzktMy43MzIsNy40OC04Ljk3Niw4LjIwN2MtNS4yMzgsMC43My05LjcxOC0zLjUyMi0xMC4wMDUtNS41NTljLTAuMjc3LTIuMDM5LDMuNzQtNC4yODUsOC45NzktNS4wMTVjNS4yMzktMC43Myw5LjcxOCwwLjMzMywxMC4wMDIsMi4zNjh6Ii8%2BCjxwYXRoIGQ9Ik0zNTAuNjc2LDEyMy40MzJjMC44NjMsMTUuOTk0LTMuNDQ1LDI2Ljg4OC0zLjk4OCw0My45MTRjLTAuODA0LDI0Ljc0OCwxMS43OTksNTMuMDc0LTcuMTkxLDgxLjQzNSIvPgo8cGF0aCBzdHlsZT0ic3Ryb2tlLXdpZHRoOjM7IiBkPSJNMCw2MC4yMzIiLz4KPC9nPgo8L3N2Zz4K) + ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Frefs%2Fheads%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_integration_rabbitmq_versions.json&style=plastic) + ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nofusscomputing/centurion_erp/ci.yaml?branch=master&style=plastic&logo=github&label=Stable%20Build&color=%23000) ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nofusscomputing/centurion_erp/ci.yaml?branch=development&style=plastic&logo=github&label=Dev%20Build&color=%23000) ![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/nofusscomputing/centurion_erp?style=plastic&logo=github&label=Open%20Issues&color=000) ![GitHub Issues or Pull Requests by label](https://img.shields.io/github/issues/nofusscomputing/centurion_erp/type%3A%3Abug?style=plastic&logo=github&label=Bug%20Fixes%20Required&color=000) @@ -20,8 +25,6 @@ about: https://gitlab.com/nofusscomputing/infrastructure/configuration-managemen ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_functional_test.json) -![Docker Pulls](https://img.shields.io/docker/pulls/nofusscomputing/centurion-erp?style=plastic&logo=docker&color=0db7ed) [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/centurion-erp)](https://artifacthub.io/packages/container/centurion-erp/centurion-erp) - Whilst there are many Enterprise Rescource Planning (ERP) applications, Centurion ERP is being developed to provide an open source option with a large emphasis on the IT Service Management (ITSM) modules. The goal is to provide a system that is not only an IT Information Library (ITIL), but that of which will connect to other ITSM systems, i.e. AWX for automation orchestration. Other common modules that form part of or are normally found within an ERP system, will be added if they relate specifically to any ITSM workflow. We welcome contributions should you desire a feature that does not yet exist. diff --git a/includes/entrypoint.sh b/includes/entrypoint.sh index 1d792ae3..820308f2 100755 --- a/includes/entrypoint.sh +++ b/includes/entrypoint.sh @@ -3,6 +3,7 @@ set -e mkdir -p /etc/supervisor/conf.d; +mkdir -p /var/log/nginx; if [ "$1" == "" ]; then diff --git a/includes/etc/gunicorn.conf.py b/includes/etc/gunicorn.conf.py index 2b8916a8..a49b2308 100644 --- a/includes/etc/gunicorn.conf.py +++ b/includes/etc/gunicorn.conf.py @@ -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() diff --git a/includes/etc/supervisor/conf.source/gunicorn.conf b/includes/etc/supervisor/conf.source/gunicorn.conf index 73bc4ab0..882e2602 100644 --- a/includes/etc/supervisor/conf.source/gunicorn.conf +++ b/includes/etc/supervisor/conf.source/gunicorn.conf @@ -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 diff --git a/makefile b/makefile index 7f780aaa..6a7f9cdb 100644 --- a/makefile +++ b/makefile @@ -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 diff --git a/pyproject.toml b/pyproject.toml index e72db919..b2c51715 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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.", diff --git a/test/docker-compose.yaml b/test/docker-compose.yaml new file mode 100644 index 00000000..b078c969 --- /dev/null +++ b/test/docker-compose.yaml @@ -0,0 +1,89 @@ +--- + +x-app: ¢urion + 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 diff --git a/test/docker/centurion.sql b/test/docker/centurion.sql new file mode 100755 index 00000000..0caab97c --- /dev/null +++ b/test/docker/centurion.sql @@ -0,0 +1,2 @@ +CREATE DATABASE itsm; +GRANT ALL PRIVILEGES ON DATABASE itsm TO admin; diff --git a/test/docker/settings.py b/test/docker/settings.py new file mode 100755 index 00000000..cbaaa5f8 --- /dev/null +++ b/test/docker/settings.py @@ -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 diff --git a/test/page_speed.js b/test/page_speed.js old mode 100644 new mode 100755 diff --git a/test/parameterizedData.json b/test/parameterizedData.json old mode 100644 new mode 100755 diff --git a/test/setup-integration.sh b/test/setup-integration.sh new file mode 100755 index 00000000..d1ccbf39 --- /dev/null +++ b/test/setup-integration.sh @@ -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 + "