diff --git a/paas_app_charmer/django/charm.py b/paas_app_charmer/django/charm.py index 9fbe18e..d492721 100644 --- a/paas_app_charmer/django/charm.py +++ b/paas_app_charmer/django/charm.py @@ -71,6 +71,29 @@ def get_cos_dir(self) -> str: """ return str((pathlib.Path(__file__).parent / "cos").absolute()) + def is_ready(self) -> bool: + """Check if the charm is ready to start the workload application. + + For Django, at least one database is needed. Migrations will be run on startup + and without that integration it will fail. + + Returns: + True if the charm is ready to start the workload application. + """ + if not super().is_ready(): + return False + + # At this point all integrations are correctly configured. If there is no database uri, + # it means that there is no integration for databases or they are optional and no one + # is set. + charm_state = self._create_charm_state() + if not charm_state.integrations.databases_uris: + self.update_app_and_unit_status( + ops.BlockedStatus("Django requires a database integration to work") + ) + return False + return True + def _on_create_superuser_action(self, event: ops.ActionEvent) -> None: """Handle the create-superuser action. diff --git a/tests/unit/django/conftest.py b/tests/unit/django/conftest.py index 33655f4..e6ed886 100644 --- a/tests/unit/django/conftest.py +++ b/tests/unit/django/conftest.py @@ -5,6 +5,7 @@ import os import pathlib import shlex +import textwrap import typing import unittest.mock @@ -28,7 +29,67 @@ def cwd(): @pytest.fixture(name="harness") def harness_fixture() -> typing.Generator[Harness, None, None]: """Ops testing framework harness fixture.""" - harness = Harness(DjangoCharm) + harness = _build_harness() + yield harness + harness.cleanup() + + +@pytest.fixture(name="harness_no_integrations") +def harness_no_integrations_fixture() -> typing.Generator[Harness, None, None]: + """Ops testing framework harness fixture without a database.""" + meta = textwrap.dedent( + """ + name: django-k8s + + bases: + - build-on: + - name: ubuntu + channel: "22.04" + run-on: + - name: ubuntu + channel: "22.04" + + summary: An example Django application. + + description: An example Django application. + + containers: + django-app: + resource: django-app-image + + peers: + secret-storage: + interface: secret-storage + provides: + grafana-dashboard: + interface: grafana_dashboard + metrics-endpoint: + interface: prometheus_scrape + requires: + ingress: + interface: ingress + limit: 1 + logging: + interface: loki_push_api + """ + ) + harness = _build_harness(meta) + yield harness + harness.cleanup() + + +@pytest.fixture +def database_migration_mock(): + """Create a mock instance for the DatabaseMigration class.""" + mock = unittest.mock.MagicMock() + mock.status = DatabaseMigrationStatus.PENDING + mock.script = None + return mock + + +def _build_harness(meta=None): + """Create a harness instance with the specified metadata.""" + harness = Harness(DjangoCharm, meta=meta) harness.set_leader() container = "django-app" root = harness.get_filesystem_root(container) @@ -51,15 +112,4 @@ def check_config_handler(_): check_config_command, handler=check_config_handler, ) - - yield harness - harness.cleanup() - - -@pytest.fixture -def database_migration_mock(): - """Create a mock instance for the DatabaseMigration class.""" - mock = unittest.mock.MagicMock() - mock.status = DatabaseMigrationStatus.PENDING - mock.script = None - return mock + return harness diff --git a/tests/unit/django/test_charm.py b/tests/unit/django/test_charm.py index 9701bb7..c872da7 100644 --- a/tests/unit/django/test_charm.py +++ b/tests/unit/django/test_charm.py @@ -1,16 +1,18 @@ # Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. -"""Flask charm unit tests.""" +"""Django charm unit tests.""" # this is a unit test file # pylint: disable=protected-access import unittest.mock +import ops import pytest from ops.testing import ExecArgs, ExecResult, Harness +from examples.django.charm.src.charm import DjangoCharm from paas_app_charmer._gunicorn.webserver import GunicornWebserver, WebserverConfig from paas_app_charmer._gunicorn.workload_config import create_workload_config from paas_app_charmer._gunicorn.wsgi_app import WsgiApp @@ -43,7 +45,7 @@ def test_django_config(harness: Harness, config: dict, env: dict) -> None: """ arrange: none act: start the django charm and set django-app container to be ready. - assert: flask charm should submit the correct flaks pebble layer to pebble. + assert: django charm should submit the correct pebble layer to pebble. """ harness.begin() container = harness.charm.unit.get_container("django-app") @@ -124,3 +126,19 @@ def handler(args: ExecArgs) -> None | ExecResult: ) assert "password" in output.results assert output.results["password"] == password + + +def test_required_database_integration(harness_no_integrations: Harness): + """ + arrange: Start the Django charm with no integrations specified in the charm. + act: Start the django charm and set django-app container to be ready. + assert: The charm should be blocked, as Django requires a database to work. + """ + harness = harness_no_integrations + container = harness.model.unit.get_container("django-app") + container.add_layer("a_layer", DEFAULT_LAYER) + + harness.begin_with_initial_hooks() + assert harness.model.unit.status == ops.BlockedStatus( + "Django requires a database integration to work" + )