bottleneckcrm/backend/common/tests/test_packaging.py
2026-10-20 21:48:14 +05:30

145 lines
5.6 KiB
Python

"""The built wheel ships every app the settings install, and every template.
``pip install django-crm`` gets whatever ``[tool.hatch.build.targets.wheel]``
lists in ``pyproject.toml`` and nothing else. That list is hand-maintained, and
a source checkout never exercises it: the editable install used in development
is a single ``.pth`` file adding all of ``backend/`` to ``sys.path``, so every
app imports whether or not it was ever packaged.
Both halves of that gap were live. ``business_hours`` and ``macros`` were in
``INSTALLED_APPS`` and absent from the wheel, so an installed copy died at
startup with ``ModuleNotFoundError``. The magic-link login emails lived in a
project-level ``templates/`` directory reached through ``TEMPLATES[0]["DIRS"]``
via ``BASE_DIR``, which resolves to ``site-packages`` once installed, so they
would not have rendered either.
Derived from the app registry rather than a second hand-written list here,
because a list of "apps to check" fails exactly the way the list it checks
does. See ``test_org_index_coverage.py`` for the same reasoning.
"""
from __future__ import annotations
import tomllib
from pathlib import Path
import pytest
from django.apps import apps
from django.conf import settings
from django.template import TemplateDoesNotExist
from django.template.loader import get_template
from common.packs.loader import PACKS_DIR
# The project package. Holds settings/urls/celery/wsgi, so it is required at
# runtime, but it is not an app and never appears in INSTALLED_APPS.
PROJECT_PACKAGE = "crm"
# Templates that used to sit in the project-level templates/ directory. Each is
# rendered by name from application code, so the name is the contract: moving a
# file between app template directories is fine, dropping it out of a packaged
# app is not.
MOVED_TEMPLATES = [
"assigned_to/account_assigned.html",
"assigned_to/cases_assigned.html",
"assigned_to/contact_assigned.html",
"assigned_to/leads_assigned.html",
"assigned_to/opportunity_assigned.html",
"healthz.html",
"magic_link_code_email.html",
"magic_link_email.html",
"opportunity/goal_milestone.html",
"opportunity/stale_deals_alert.html",
"root_email_template_new.html",
]
def _backend_root() -> Path:
return Path(settings.BASE_DIR)
def _first_party_apps() -> set[str]:
"""Installed apps whose source lives in this repo, not in site-packages."""
root = _backend_root()
return {
config.label
for config in apps.get_app_configs()
if Path(config.path).resolve().parent == root.resolve()
}
def _build_config() -> dict:
with open(_backend_root() / "pyproject.toml", "rb") as handle:
return tomllib.load(handle)["tool"]["hatch"]["build"]["targets"]
def test_wheel_packages_match_installed_apps():
"""Every first-party app is packaged, and nothing packaged has been deleted."""
packaged = set(_build_config()["wheel"]["packages"])
expected = _first_party_apps() | {PROJECT_PACKAGE}
assert packaged == expected, (
"pyproject.toml [tool.hatch.build.targets.wheel].packages has drifted "
f"from INSTALLED_APPS. Missing from the wheel: "
f"{sorted(expected - packaged)}. Packaged but not installed: "
f"{sorted(packaged - expected)}."
)
def test_sdist_includes_every_wheel_package():
"""The sdist carries at least what the wheel does, as a trailing-slash path."""
targets = _build_config()
included = set(targets["sdist"]["include"])
missing = [
f"{name}/"
for name in targets["wheel"]["packages"]
if f"{name}/" not in included
]
assert not missing, (
f"pyproject.toml [tool.hatch.build.targets.sdist].include is missing {missing}. "
"An sdist that omits an installed app builds a wheel that cannot start."
)
def test_pack_manifests_ship_in_the_wheel():
"""Every vertical-pack manifest sits inside a packaged app.
The manifests used to live in a top-level ``backend/packs/`` directory that
no wheel package covers. A source checkout found them; an installed copy
looked in ``site-packages/packs``, found nothing, and ``GET /api/packs/``
answered an empty list, so the org-creation page hid its starter picker
without logging a thing.
"""
root = _backend_root().resolve()
manifests = sorted(PACKS_DIR.glob("*.json"))
assert manifests, f"No pack manifests found in {PACKS_DIR}."
packaged = set(_build_config()["wheel"]["packages"])
unpackaged = [
str(path.resolve().relative_to(root))
for path in manifests
if path.resolve().relative_to(root).parts[0] not in packaged
]
assert not unpackaged, (
f"Pack manifests outside every wheel package: {unpackaged}. An installed "
"copy would load no packs. Keep them inside a package listed in "
"[tool.hatch.build.targets.wheel].packages."
)
@pytest.mark.parametrize("name", MOVED_TEMPLATES)
def test_template_resolves_through_an_app_directory(name):
"""Resolvable with TEMPLATES[0]["DIRS"] empty, so it ships as package data."""
assert settings.TEMPLATES[0]["DIRS"] == [], (
"A project-level template directory resolves to site-packages/templates "
"on an installed copy, where nothing is written. Keep templates in the "
"owning app's templates/ directory so APP_DIRS finds them."
)
try:
get_template(name)
except TemplateDoesNotExist: # pragma: no cover - the assertion is the report
pytest.fail(
f"{name} is not reachable through any installed app's templates/ "
"directory, so it will be absent from the wheel."
)