diff --git a/controller/__init__.py b/controller/__init__.py new file mode 100644 index 0000000..b1b6456 --- /dev/null +++ b/controller/__init__.py @@ -0,0 +1 @@ +"""The controller app for the drunc_ui project.""" diff --git a/controller/admin.py b/controller/admin.py new file mode 100644 index 0000000..751c975 --- /dev/null +++ b/controller/admin.py @@ -0,0 +1,3 @@ +"""Admin module for the controller app.""" + +# Register your models here. diff --git a/controller/apps.py b/controller/apps.py new file mode 100644 index 0000000..38baaf4 --- /dev/null +++ b/controller/apps.py @@ -0,0 +1,10 @@ +"""Apps module for the controller app.""" + +from django.apps import AppConfig + + +class ControllerConfig(AppConfig): + """The app config for the controller app.""" + + default_auto_field = "django.db.models.BigAutoField" + name = "controller" diff --git a/controller/migrations/__init__.py b/controller/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/controller/models.py b/controller/models.py new file mode 100644 index 0000000..1873bc5 --- /dev/null +++ b/controller/models.py @@ -0,0 +1,3 @@ +"""Models module for the controller app.""" + +# Create your models here. diff --git a/controller/templates/controller/index.html b/controller/templates/controller/index.html new file mode 100644 index 0000000..69f65f2 --- /dev/null +++ b/controller/templates/controller/index.html @@ -0,0 +1,4 @@ +{% extends "main/base.html" %} +{% block title %} + Controller +{% endblock title %} diff --git a/controller/urls.py b/controller/urls.py new file mode 100644 index 0000000..2bf73a5 --- /dev/null +++ b/controller/urls.py @@ -0,0 +1,10 @@ +"""Urls module for the controller app.""" + +from django.urls import path + +from . import views + +app_name = "controller" +urlpatterns = [ + path("", views.index, name="index"), +] diff --git a/controller/views.py b/controller/views.py new file mode 100644 index 0000000..f85e30c --- /dev/null +++ b/controller/views.py @@ -0,0 +1,11 @@ +"""Views module for the controller app.""" + +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest, HttpResponse +from django.shortcuts import render + + +@login_required +def index(request: HttpRequest) -> HttpResponse: + """View that renders the index/home page.""" + return render(request=request, template_name="controller/index.html") diff --git a/drunc_ui/settings/settings.py b/drunc_ui/settings/settings.py index 965e1cd..e36a999 100644 --- a/drunc_ui/settings/settings.py +++ b/drunc_ui/settings/settings.py @@ -130,7 +130,7 @@ # Custom settings -INSTALLED_APPS += ["main", "django_tables2"] +INSTALLED_APPS += ["main", "process_manager", "controller", "django_tables2"] MIDDLEWARE.insert(1, "whitenoise.middleware.WhiteNoiseMiddleware") diff --git a/drunc_ui/urls.py b/drunc_ui/urls.py index 5772129..40144ff 100644 --- a/drunc_ui/urls.py +++ b/drunc_ui/urls.py @@ -21,4 +21,6 @@ urlpatterns = [ path("admin/", admin.site.urls), path("", include("main.urls")), + path("process_manager/", include("process_manager.urls")), + path("controller/", include("controller.urls")), ] diff --git a/main/templates/main/base.html b/main/templates/main/base.html index 898a813..61173fb 100644 --- a/main/templates/main/base.html +++ b/main/templates/main/base.html @@ -18,11 +18,7 @@ - + {% include "main/navbar.html" %}
{% block content %} {% endblock content %} diff --git a/main/templates/main/index.html b/main/templates/main/index.html index d75d738..8b2edf0 100644 --- a/main/templates/main/index.html +++ b/main/templates/main/index.html @@ -2,51 +2,11 @@ {% block title %} Home {% endblock title %} -{% block extra_css %} - -{% endblock extra_css %} {% block content %}
-
- {% csrf_token %} - Boot - - - -
-
-
-
-
-
Messages
-
-
    - {% if messages %} - {% for message in messages %}
  • {{ message }}
  • {% endfor %} - {% endif %} -
-
-
+ Process Manager + Controller
{% endblock content %} diff --git a/main/templates/main/navbar.html b/main/templates/main/navbar.html new file mode 100644 index 0000000..256741c --- /dev/null +++ b/main/templates/main/navbar.html @@ -0,0 +1,27 @@ + + diff --git a/main/urls.py b/main/urls.py index 70b2f98..33fa5d0 100644 --- a/main/urls.py +++ b/main/urls.py @@ -5,16 +5,7 @@ from . import views app_name = "main" - -partial_urlpatterns = [ - path("process_table/", views.process_table, name="process_table"), -] - urlpatterns = [ path("", views.index, name="index"), path("accounts/", include("django.contrib.auth.urls")), - path("process_action/", views.process_action, name="process_action"), - path("logs/", views.logs, name="logs"), - path("boot_process/", views.BootProcessView.as_view(), name="boot_process"), - path("partials/", include(partial_urlpatterns)), ] diff --git a/main/views.py b/main/views.py index dce1dc2..d859bf5 100644 --- a/main/views.py +++ b/main/views.py @@ -1,218 +1,11 @@ """Views for the main app.""" -import asyncio -import uuid -from enum import Enum - -import django_tables2 -from django.conf import settings -from django.contrib.auth.decorators import login_required, permission_required -from django.contrib.auth.mixins import PermissionRequiredMixin -from django.db import transaction -from django.http import HttpRequest, HttpResponse, HttpResponseRedirect +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest, HttpResponse from django.shortcuts import render -from django.urls import reverse, reverse_lazy -from django.views.generic.edit import FormView -from drunc.process_manager.process_manager_driver import ProcessManagerDriver -from drunc.utils.shell_utils import DecodedResponse, create_dummy_token_from_uname -from druncschema.process_manager_pb2 import ( - LogRequest, - ProcessInstance, - ProcessInstanceList, - ProcessQuery, - ProcessUUID, -) - -from .forms import BootProcessForm -from .tables import ProcessTable - - -def get_process_manager_driver() -> ProcessManagerDriver: - """Get a ProcessManagerDriver instance.""" - token = create_dummy_token_from_uname() - return ProcessManagerDriver( - settings.PROCESS_MANAGER_URL, token=token, aio_channel=True - ) - - -async def get_session_info() -> ProcessInstanceList: - """Get info about all sessions from process manager.""" - pmd = get_process_manager_driver() - query = ProcessQuery(names=[".*"]) - return await pmd.ps(query) @login_required def index(request: HttpRequest) -> HttpResponse: """View that renders the index/home page.""" - with transaction.atomic(): - # atomic to avoid race condition with kafka consumer - messages = request.session.load().get("messages", []) - request.session.pop("messages", []) - request.session.save() - - context = {"messages": messages} - return render(request=request, context=context, template_name="main/index.html") - - -@login_required -def process_table(request: HttpRequest) -> HttpResponse: - """Renders the process table. - - This view may be called using either GET or POST methods. GET renders the table with - no check boxes selected. POST renders the table with checked boxes for any table row - with a uuid provided in the select key of the request data. - """ - selected_rows = request.POST.getlist("select", []) - session_info = asyncio.run(get_session_info()) - - status_enum_lookup = dict(item[::-1] for item in ProcessInstance.StatusCode.items()) - - table_data = [] - process_instances = session_info.data.values - for process_instance in process_instances: - metadata = process_instance.process_description.metadata - uuid = process_instance.uuid.uuid - table_data.append( - { - "uuid": uuid, - "name": metadata.name, - "user": metadata.user, - "session": metadata.session, - "status_code": status_enum_lookup[process_instance.status_code], - "exit_code": process_instance.return_code, - "checked": (uuid in selected_rows), - } - ) - table = ProcessTable(table_data) - - # sort table data based on request parameters - table_configurator = django_tables2.RequestConfig(request) - table_configurator.configure(table) - - return render( - request=request, - context=dict(table=table), - template_name="main/partials/process_table.html", - ) - - -# an enum for process actions -class ProcessAction(Enum): - """Enum for process actions.""" - - RESTART = "restart" - KILL = "kill" - FLUSH = "flush" - - -async def _process_call(uuids: list[str], action: ProcessAction) -> None: - """Perform an action on a process with a given UUID. - - Args: - uuids: List of UUIDs of the process to be actioned. - action: Action to be performed {restart,flush,kill}. - """ - pmd = get_process_manager_driver() - uuids_ = [ProcessUUID(uuid=u) for u in uuids] - - match action: - case ProcessAction.RESTART: - for uuid_ in uuids_: - query = ProcessQuery(uuids=[uuid_]) - await pmd.restart(query) - case ProcessAction.KILL: - query = ProcessQuery(uuids=uuids_) - await pmd.kill(query) - case ProcessAction.FLUSH: - query = ProcessQuery(uuids=uuids_) - await pmd.flush(query) - - -@login_required -@permission_required("main.can_modify_processes", raise_exception=True) -def process_action(request: HttpRequest) -> HttpResponse: - """Perform an action on the selected processes. - - Both the action and the selected processes are retrieved from the request. - - Args: - request: Django HttpRequest object. - - Returns: - HttpResponse redirecting to the index page. - """ - try: - action = request.POST.get("action", "") - action_enum = ProcessAction(action.lower()) - except ValueError: - return HttpResponseRedirect(reverse("main:index")) - - if uuids_ := request.POST.getlist("select"): - asyncio.run(_process_call(uuids_, action_enum)) - return HttpResponseRedirect(reverse("main:index")) - - -async def _get_process_logs(uuid: str) -> list[DecodedResponse]: - """Retrieve logs for a process from the process manager. - - Args: - uuid: UUID of the process. - - Returns: - The process logs. - """ - pmd = get_process_manager_driver() - query = ProcessQuery(uuids=[ProcessUUID(uuid=uuid)]) - request = LogRequest(query=query, how_far=100) - return [item async for item in pmd.logs(request)] - - -@login_required -@permission_required("main.can_view_process_logs", raise_exception=True) -def logs(request: HttpRequest, uuid: uuid.UUID) -> HttpResponse: - """Display the logs of a process. - - Args: - request: the triggering request. - uuid: identifier for the process. - - Returns: - The rendered page. - """ - logs_response = asyncio.run(_get_process_logs(str(uuid))) - context = dict(log_text="\n".join(val.data.line for val in logs_response)) - return render(request=request, context=context, template_name="main/logs.html") - - -async def _boot_process(user: str, data: dict[str, str | int]) -> None: - """Boot a process with the given data. - - Args: - user: the user to boot the process as. - data: the data for the process. - """ - pmd = get_process_manager_driver() - async for item in pmd.dummy_boot(user=user, **data): - pass - - -class BootProcessView(PermissionRequiredMixin, FormView): # type: ignore [type-arg] - """View for the BootProcess form.""" - - template_name = "main/boot_process.html" - form_class = BootProcessForm - success_url = reverse_lazy("main:index") - permission_required = "main.can_modify_processes" - - def form_valid(self, form: BootProcessForm) -> HttpResponse: - """Boot a Process when valid form data has been POSTed. - - Args: - form: the form instance that has been validated. - - Returns: - A redirect to the index page. - """ - asyncio.run(_boot_process("root", form.cleaned_data)) - return super().form_valid(form) + return render(request=request, template_name="main/index.html") diff --git a/process_manager/__init__.py b/process_manager/__init__.py new file mode 100644 index 0000000..0cb9b3c --- /dev/null +++ b/process_manager/__init__.py @@ -0,0 +1 @@ +"""The process_manager app for the drunc_ui project.""" diff --git a/process_manager/admin.py b/process_manager/admin.py new file mode 100644 index 0000000..eb1b949 --- /dev/null +++ b/process_manager/admin.py @@ -0,0 +1,3 @@ +"""Admin module for the process_manager app.""" + +# Register your models here. diff --git a/process_manager/apps.py b/process_manager/apps.py new file mode 100644 index 0000000..3736c31 --- /dev/null +++ b/process_manager/apps.py @@ -0,0 +1,10 @@ +"""Apps module for the process_manager app.""" + +from django.apps import AppConfig + + +class ProcessManagerConfig(AppConfig): + """The app config for the process_manager app.""" + + default_auto_field = "django.db.models.BigAutoField" + name = "process_manager" diff --git a/main/forms.py b/process_manager/forms.py similarity index 85% rename from main/forms.py rename to process_manager/forms.py index b487902..457ad0d 100644 --- a/main/forms.py +++ b/process_manager/forms.py @@ -1,4 +1,4 @@ -"""Forms for the main app.""" +"""Forms for the process_manager app.""" from django import forms diff --git a/process_manager/migrations/__init__.py b/process_manager/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/process_manager/models.py b/process_manager/models.py new file mode 100644 index 0000000..86b8943 --- /dev/null +++ b/process_manager/models.py @@ -0,0 +1,3 @@ +"""Models module for the process_manager app.""" + +# Create your models here. diff --git a/main/tables.py b/process_manager/tables.py similarity index 75% rename from main/tables.py rename to process_manager/tables.py index d57b282..8dd6b60 100644 --- a/main/tables.py +++ b/process_manager/tables.py @@ -1,10 +1,10 @@ -"""Tables for the main app.""" +"""Tables for the process_manager app.""" import django_tables2 as tables restart_column_template = ( "{text}".format( - href="\"{% url 'main:restart' record.uuid%}\"", + href="\"{% url 'process_manager:restart' record.uuid%}\"", message="You are about to restart process {{record.uuid}}. Are you sure?", text="RESTART", ) @@ -12,15 +12,19 @@ kill_column_template = ( "{text}".format( - href="\"{% url 'main:kill' record.uuid%}\"", + href="\"{% url 'process_manager:kill' record.uuid%}\"", message="You are about to kill process {{record.uuid}}. Are you sure?", text="KILL", ) ) -flush_column_template = "FLUSH" +flush_column_template = ( + "FLUSH" +) -logs_column_template = "LOGS" +logs_column_template = ( + "LOGS" +) class ProcessTable(tables.Table): diff --git a/main/templates/main/boot_process.html b/process_manager/templates/process_manager/boot_process.html similarity index 80% rename from main/templates/main/boot_process.html rename to process_manager/templates/process_manager/boot_process.html index 4eef2b8..3c25c35 100644 --- a/main/templates/main/boot_process.html +++ b/process_manager/templates/process_manager/boot_process.html @@ -5,7 +5,7 @@ Boot Process {% endblock title %} {% block content %} -
+ {% csrf_token %} {{ form|crispy }} {% bootstrap_button button_type="submit" content="Submit" %} diff --git a/process_manager/templates/process_manager/index.html b/process_manager/templates/process_manager/index.html new file mode 100644 index 0000000..c7487f8 --- /dev/null +++ b/process_manager/templates/process_manager/index.html @@ -0,0 +1,53 @@ +{% extends "main/base.html" %} +{% block title %} + Home +{% endblock title %} +{% block extra_css %} + +{% endblock extra_css %} +{% block content %} +
+
+ + {% csrf_token %} + Boot + + + +
+ +
+
+
+
Messages
+
+
    + {% if messages %} + {% for message in messages %}
  • {{ message }}
  • {% endfor %} + {% endif %} +
+
+
+
+
+{% endblock content %} diff --git a/main/templates/main/logs.html b/process_manager/templates/process_manager/logs.html similarity index 77% rename from main/templates/main/logs.html rename to process_manager/templates/process_manager/logs.html index a509361..37af64f 100644 --- a/main/templates/main/logs.html +++ b/process_manager/templates/process_manager/logs.html @@ -6,5 +6,5 @@
{{ log_text }}
- Return to table + Return to table {% endblock content %} diff --git a/main/templates/main/partials/process_table.html b/process_manager/templates/process_manager/partials/process_table.html similarity index 72% rename from main/templates/main/partials/process_table.html rename to process_manager/templates/process_manager/partials/process_table.html index 26a2d7c..0963f97 100644 --- a/main/templates/main/partials/process_table.html +++ b/process_manager/templates/process_manager/partials/process_table.html @@ -1,4 +1,4 @@ {% load render_table from django_tables2 %} -
{% render_table table %}
diff --git a/process_manager/urls.py b/process_manager/urls.py new file mode 100644 index 0000000..408cf0b --- /dev/null +++ b/process_manager/urls.py @@ -0,0 +1,19 @@ +"""Urls module for the process_manager app.""" + +from django.urls import include, path + +from . import views + +app_name = "process_manager" + +partial_urlpatterns = [ + path("process_table/", views.process_table, name="process_table"), +] + +urlpatterns = [ + path("", views.index, name="index"), + path("process_action/", views.process_action, name="process_action"), + path("logs/", views.logs, name="logs"), + path("boot_process/", views.BootProcessView.as_view(), name="boot_process"), + path("partials/", include(partial_urlpatterns)), +] diff --git a/process_manager/views.py b/process_manager/views.py new file mode 100644 index 0000000..712c59f --- /dev/null +++ b/process_manager/views.py @@ -0,0 +1,221 @@ +"""Views for the process_manager app.""" + +import asyncio +import uuid +from enum import Enum + +import django_tables2 +from django.conf import settings +from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.db import transaction +from django.http import HttpRequest, HttpResponse, HttpResponseRedirect +from django.shortcuts import render +from django.urls import reverse, reverse_lazy +from django.views.generic.edit import FormView +from drunc.process_manager.process_manager_driver import ProcessManagerDriver +from drunc.utils.shell_utils import DecodedResponse, create_dummy_token_from_uname +from druncschema.process_manager_pb2 import ( + LogRequest, + ProcessInstance, + ProcessInstanceList, + ProcessQuery, + ProcessUUID, +) + +from .forms import BootProcessForm +from .tables import ProcessTable + + +def get_process_manager_driver() -> ProcessManagerDriver: + """Get a ProcessManagerDriver instance.""" + token = create_dummy_token_from_uname() + return ProcessManagerDriver( + settings.PROCESS_MANAGER_URL, token=token, aio_channel=True + ) + + +async def get_session_info() -> ProcessInstanceList: + """Get info about all sessions from process manager.""" + pmd = get_process_manager_driver() + query = ProcessQuery(names=[".*"]) + return await pmd.ps(query) + + +@login_required +def index(request: HttpRequest) -> HttpResponse: + """View that renders the index/home page.""" + with transaction.atomic(): + # atomic to avoid race condition with kafka consumer + messages = request.session.load().get("messages", []) + request.session.pop("messages", []) + request.session.save() + + context = {"messages": messages} + return render( + request=request, context=context, template_name="process_manager/index.html" + ) + + +@login_required +def process_table(request: HttpRequest) -> HttpResponse: + """Renders the process table. + + This view may be called using either GET or POST methods. GET renders the table with + no check boxes selected. POST renders the table with checked boxes for any table row + with a uuid provided in the select key of the request data. + """ + selected_rows = request.POST.getlist("select", []) + session_info = asyncio.run(get_session_info()) + + status_enum_lookup = dict(item[::-1] for item in ProcessInstance.StatusCode.items()) + + table_data = [] + process_instances = session_info.data.values + for process_instance in process_instances: + metadata = process_instance.process_description.metadata + uuid = process_instance.uuid.uuid + table_data.append( + { + "uuid": uuid, + "name": metadata.name, + "user": metadata.user, + "session": metadata.session, + "status_code": status_enum_lookup[process_instance.status_code], + "exit_code": process_instance.return_code, + "checked": (uuid in selected_rows), + } + ) + table = ProcessTable(table_data) + + # sort table data based on request parameters + table_configurator = django_tables2.RequestConfig(request) + table_configurator.configure(table) + + return render( + request=request, + context=dict(table=table), + template_name="process_manager/partials/process_table.html", + ) + + +class ProcessAction(Enum): + """Enum for process actions.""" + + RESTART = "restart" + KILL = "kill" + FLUSH = "flush" + + +async def _process_call(uuids: list[str], action: ProcessAction) -> None: + """Perform an action on a process with a given UUID. + + Args: + uuids: List of UUIDs of the process to be actioned. + action: Action to be performed {restart,flush,kill}. + """ + pmd = get_process_manager_driver() + uuids_ = [ProcessUUID(uuid=u) for u in uuids] + + match action: + case ProcessAction.RESTART: + for uuid_ in uuids_: + query = ProcessQuery(uuids=[uuid_]) + await pmd.restart(query) + case ProcessAction.KILL: + query = ProcessQuery(uuids=uuids_) + await pmd.kill(query) + case ProcessAction.FLUSH: + query = ProcessQuery(uuids=uuids_) + await pmd.flush(query) + + +@login_required +@permission_required("main.can_modify_processes", raise_exception=True) +def process_action(request: HttpRequest) -> HttpResponse: + """Perform an action on the selected processes. + + Both the action and the selected processes are retrieved from the request. + + Args: + request: Django HttpRequest object. + + Returns: + HttpResponse redirecting to the index page. + """ + try: + action = request.POST.get("action", "") + action_enum = ProcessAction(action.lower()) + except ValueError: + return HttpResponseRedirect(reverse("process_manager:index")) + + if uuids_ := request.POST.getlist("select"): + asyncio.run(_process_call(uuids_, action_enum)) + return HttpResponseRedirect(reverse("process_manager:index")) + + +async def _get_process_logs(uuid: str) -> list[DecodedResponse]: + """Retrieve logs for a process from the process manager. + + Args: + uuid: UUID of the process. + + Returns: + The process logs. + """ + pmd = get_process_manager_driver() + query = ProcessQuery(uuids=[ProcessUUID(uuid=uuid)]) + request = LogRequest(query=query, how_far=100) + return [item async for item in pmd.logs(request)] + + +@login_required +@permission_required("main.can_view_process_logs", raise_exception=True) +def logs(request: HttpRequest, uuid: uuid.UUID) -> HttpResponse: + """Display the logs of a process. + + Args: + request: the triggering request. + uuid: identifier for the process. + + Returns: + The rendered page. + """ + logs_response = asyncio.run(_get_process_logs(str(uuid))) + context = dict(log_text="\n".join(val.data.line for val in logs_response)) + return render( + request=request, context=context, template_name="process_manager/logs.html" + ) + + +async def _boot_process(user: str, data: dict[str, str | int]) -> None: + """Boot a process with the given data. + + Args: + user: the user to boot the process as. + data: the data for the process. + """ + pmd = get_process_manager_driver() + async for item in pmd.dummy_boot(user=user, **data): + pass + + +class BootProcessView(PermissionRequiredMixin, FormView): # type: ignore [type-arg] + """View for the BootProcess form.""" + + template_name = "process_manager/boot_process.html" + form_class = BootProcessForm + success_url = reverse_lazy("process_manager:index") + permission_required = "main.can_modify_processes" + + def form_valid(self, form: BootProcessForm) -> HttpResponse: + """Boot a Process when valid form data has been POSTed. + + Args: + form: the form instance that has been validated. + + Returns: + A redirect to the index page. + """ + asyncio.run(_boot_process("root", form.cleaned_data)) + return super().form_valid(form) diff --git a/pyproject.toml b/pyproject.toml index 763768c..fb0641b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ disallow_any_generics = true warn_unreachable = true warn_unused_ignores = true disallow_untyped_defs = true -exclude = [".venv/", "manage.py", "main/migrations/"] +exclude = [".venv/", "manage.py", "*/migrations/"] plugins = ["mypy_django_plugin.main"] [[tool.mypy.overrides]] @@ -60,7 +60,7 @@ DJANGO_SETTINGS_MODULE = "drunc_ui.settings" FAIL_INVALID_TEMPLATE_VARS = true [tool.ruff] -exclude = ["main/migrations"] +exclude = ["*/migrations"] target-version = "py312" [tool.ruff.lint] diff --git a/tests/__init__.py b/tests/__init__.py index f513847..dc33d24 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ -"""Unit tests for MyProject.""" +"""Unit tests for drunc_ui.""" diff --git a/tests/controller/__init__.py b/tests/controller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/controller/test_views.py b/tests/controller/test_views.py new file mode 100644 index 0000000..a2ca970 --- /dev/null +++ b/tests/controller/test_views.py @@ -0,0 +1,18 @@ +from http import HTTPStatus + +from django.urls import reverse +from pytest_django.asserts import assertTemplateUsed + +from ..utils import LoginRequiredTest + + +class TestIndexView(LoginRequiredTest): + """Tests for the index view.""" + + endpoint = reverse("controller:index") + + def test_index_view_authenticated(self, auth_client, mocker): + """Test the index view for an authenticated user.""" + with assertTemplateUsed(template_name="controller/index.html"): + response = auth_client.get(self.endpoint) + assert response.status_code == HTTPStatus.OK diff --git a/tests/main/__init__.py b/tests/main/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/main/test_views.py b/tests/main/test_views.py new file mode 100644 index 0000000..efa8274 --- /dev/null +++ b/tests/main/test_views.py @@ -0,0 +1,40 @@ +from http import HTTPStatus + +from django.urls import reverse +from pytest_django.asserts import assertContains, assertNotContains, assertTemplateUsed + +from ..utils import LoginRequiredTest + + +class TestIndexView(LoginRequiredTest): + """Tests for the index view.""" + + endpoint = reverse("main:index") + + def test_no_nav_links(self, client): + """Test that the navbar does not have any nav links when not authenticated.""" + response = client.get(self.endpoint, follow=True) + assertNotContains(response, "nav-link") + + def test_index_view_authenticated(self, auth_client): + """Test the index view for an authenticated user.""" + with assertTemplateUsed(template_name="main/index.html"): + response = auth_client.get(self.endpoint) + assert response.status_code == HTTPStatus.OK + + assertContains( + response, + f'Process Manager', # noqa: E501 + ) + assertContains( + response, + f'Process Manager', # noqa: E501 + ) + assertContains( + response, + f'Controller', + ) + assertContains( + response, + f'Controller', # noqa: E501 + ) diff --git a/tests/process_manager/__init__.py b/tests/process_manager/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_forms.py b/tests/process_manager/test_forms.py similarity index 95% rename from tests/test_forms.py rename to tests/process_manager/test_forms.py index 2328fc0..e6a0190 100644 --- a/tests/test_forms.py +++ b/tests/process_manager/test_forms.py @@ -1,6 +1,6 @@ from django import forms -from main.forms import BootProcessForm +from process_manager.forms import BootProcessForm def test_boot_form_empty(): diff --git a/tests/test_views.py b/tests/process_manager/test_views.py similarity index 75% rename from tests/test_views.py rename to tests/process_manager/test_views.py index 448a075..24e880a 100644 --- a/tests/test_views.py +++ b/tests/process_manager/test_views.py @@ -4,41 +4,30 @@ import pytest from django.urls import reverse -from pytest_django.asserts import assertContains, assertRedirects, assertTemplateUsed +from pytest_django.asserts import assertContains, assertTemplateUsed -from main.tables import ProcessTable -from main.views import ProcessAction +from process_manager.tables import ProcessTable +from process_manager.views import ProcessAction - -class LoginRequiredTest: - """Tests for views that require authentication.""" - - endpoint: str - - def test_login_redirect(self, client): - """Test that the view redirects to the login page.""" - response = client.get(self.endpoint) - assert response.status_code == HTTPStatus.FOUND - - assertRedirects(response, reverse("main:login") + f"?next={self.endpoint}") +from ..utils import LoginRequiredTest class TestIndexView(LoginRequiredTest): """Tests for the index view.""" - endpoint = reverse("main:index") + endpoint = reverse("process_manager:index") def test_index_view_authenticated(self, auth_client, mocker): """Test the index view for an authenticated user.""" - mocker.patch("main.views.get_session_info") - with assertTemplateUsed(template_name="main/index.html"): + mocker.patch("process_manager.views.get_session_info") + with assertTemplateUsed(template_name="process_manager/index.html"): response = auth_client.get(self.endpoint) assert response.status_code == HTTPStatus.OK def test_index_view_admin(self, admin_client, mocker): """Test the index view for an admin user.""" - mocker.patch("main.views.get_session_info") - with assertTemplateUsed(template_name="main/index.html"): + mocker.patch("process_manager.views.get_session_info") + with assertTemplateUsed(template_name="process_manager/index.html"): response = admin_client.get(self.endpoint) assert response.status_code == HTTPStatus.OK assertContains(response, "Boot") @@ -48,7 +37,7 @@ def test_session_messages(self, auth_client, mocker): from django.contrib.sessions.backends.db import SessionStore from django.contrib.sessions.models import Session - mocker.patch("main.views.get_session_info") + mocker.patch("process_manager.views.get_session_info") session = Session.objects.get() message_data = ["message 1", "message 2"] store = SessionStore(session_key=session.session_key) @@ -67,7 +56,7 @@ class TestLogsView(LoginRequiredTest): """Tests for the logs view.""" uuid = uuid4() - endpoint = reverse("main:logs", kwargs=dict(uuid=uuid)) + endpoint = reverse("process_manager:logs", kwargs=dict(uuid=uuid)) def test_logs_view_unprivileged(self, auth_client): """Test the logs view for an unprivileged user.""" @@ -76,8 +65,8 @@ def test_logs_view_unprivileged(self, auth_client): def test_logs_view_privileged(self, auth_logs_client, mocker): """Test the logs view for a privileged user.""" - mock = mocker.patch("main.views._get_process_logs") - with assertTemplateUsed(template_name="main/logs.html"): + mock = mocker.patch("process_manager.views._get_process_logs") + with assertTemplateUsed(template_name="process_manager/logs.html"): response = auth_logs_client.get(self.endpoint) assert response.status_code == HTTPStatus.OK @@ -88,13 +77,13 @@ def test_logs_view_privileged(self, auth_logs_client, mocker): class TestProcessActionView(LoginRequiredTest): """Tests for the process_action view.""" - endpoint = reverse("main:process_action") + endpoint = reverse("process_manager:process_action") def test_process_action_no_action(self, auth_process_client): """Test process_action view with no action provided.""" response = auth_process_client.post(self.endpoint, data={}) assert response.status_code == HTTPStatus.FOUND - assert response.url == reverse("main:index") + assert response.url == reverse("process_manager:index") def test_process_action_invalid_action(self, auth_process_client): """Test process_action view with an invalid action.""" @@ -102,52 +91,54 @@ def test_process_action_invalid_action(self, auth_process_client): self.endpoint, data={"action": "invalid_action"} ) assert response.status_code == HTTPStatus.FOUND - assert response.url == reverse("main:index") + assert response.url == reverse("process_manager:index") @pytest.mark.parametrize("action", ["kill", "restart", "flush"]) def test_process_action_valid_action(self, action, auth_process_client, mocker): """Test process_action view with a valid action.""" - mock = mocker.patch("main.views._process_call") + mock = mocker.patch("process_manager.views._process_call") uuids_ = [str(uuid4()), str(uuid4())] response = auth_process_client.post( self.endpoint, data={"action": action, "select": uuids_} ) assert response.status_code == HTTPStatus.FOUND - assert response.url == reverse("main:index") + assert response.url == reverse("process_manager:index") mock.assert_called_once_with(uuids_, ProcessAction(action)) def test_process_action_get_unprivileged(self, auth_client): """Test the GET request for the process_action view (unprivileged).""" - response = auth_client.get(reverse("main:boot_process")) + response = auth_client.get(reverse("process_manager:boot_process")) assert response.status_code == HTTPStatus.FORBIDDEN class TestBootProcess(LoginRequiredTest): """Grouping the tests for the BootProcess view.""" - template_name = "main/boot_process.html" - endpoint = reverse("main:boot_process") + template_name = "process_manager/boot_process.html" + endpoint = reverse("process_manager:boot_process") def test_boot_process_get_unprivileged(self, auth_client): """Test the GET request for the BootProcess view (unprivileged).""" - response = auth_client.get(reverse("main:boot_process")) + response = auth_client.get(reverse("process_manager:boot_process")) assert response.status_code == HTTPStatus.FORBIDDEN def test_boot_process_get_privileged(self, auth_process_client): """Test the GET request for the BootProcess view (privileged).""" with assertTemplateUsed(template_name=self.template_name): - response = auth_process_client.get(reverse("main:boot_process")) + response = auth_process_client.get(reverse("process_manager:boot_process")) assert response.status_code == HTTPStatus.OK assert "form" in response.context - assertContains(response, f'form action="{reverse("main:boot_process")}"') + assertContains( + response, f'form action="{reverse("process_manager:boot_process")}"' + ) def test_boot_process_post_invalid(self, auth_process_client): """Test the POST request for the BootProcess view with invalid data.""" with assertTemplateUsed(template_name=self.template_name): response = auth_process_client.post( - reverse("main:boot_process"), data=dict() + reverse("process_manager:boot_process"), data=dict() ) assert response.status_code == HTTPStatus.OK @@ -157,13 +148,13 @@ def test_boot_process_post_valid( self, auth_process_client, mocker, dummy_session_data ): """Test the POST request for the BootProcess view.""" - mock = mocker.patch("main.views._boot_process") + mock = mocker.patch("process_manager.views._boot_process") response = auth_process_client.post( - reverse("main:boot_process"), data=dummy_session_data + reverse("process_manager:boot_process"), data=dummy_session_data ) assert response.status_code == HTTPStatus.FOUND - assert response.url == reverse("main:index") + assert response.url == reverse("process_manager:index") mock.assert_called_once_with("root", dummy_session_data) @@ -171,9 +162,9 @@ def test_boot_process_post_valid( @pytest.mark.asyncio async def test_boot_process(mocker, dummy_session_data): """Test the _boot_process function.""" - from main.views import _boot_process + from process_manager.views import _boot_process - mock = mocker.patch("main.views.get_process_manager_driver") + mock = mocker.patch("process_manager.views.get_process_manager_driver") await _boot_process("root", dummy_session_data) mock.assert_called_once() mock.return_value.dummy_boot.assert_called_once_with( @@ -182,9 +173,9 @@ async def test_boot_process(mocker, dummy_session_data): class TestProcessTableView(LoginRequiredTest): - """Test the main.views.process_table view function.""" + """Test the process_manager.views.process_table view function.""" - endpoint = reverse("main:process_table") + endpoint = reverse("process_manager:process_table") @pytest.mark.parametrize("method", ("get", "post")) def test_method(self, method, auth_client, mocker): @@ -196,7 +187,7 @@ def test_method(self, method, auth_client, mocker): def _mock_session_info(self, mocker, uuids): """Mocks views.get_session_info with ProcessInstanceList like data.""" - mock = mocker.patch("main.views.get_session_info") + mock = mocker.patch("process_manager.views.get_session_info") instance_mocks = [MagicMock() for uuid in uuids] for instance_mock, uuid in zip(instance_mocks, uuids): instance_mock.uuid.uuid = str(uuid) diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..000b0ce --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,17 @@ +from http import HTTPStatus + +from django.urls import reverse +from pytest_django.asserts import assertRedirects + + +class LoginRequiredTest: + """Tests for views that require authentication.""" + + endpoint: str + + def test_login_redirect(self, client): + """Test that the view redirects to the login page.""" + response = client.get(self.endpoint) + assert response.status_code == HTTPStatus.FOUND + + assertRedirects(response, reverse("main:login") + f"?next={self.endpoint}")