Files
drive/tests/test_deployment.py
2026-10-18 23:48:22 +05:30

78 lines
3.3 KiB
Python

"""Static deployment-readiness checks (these do NOT deploy anything to Heroku)."""
import json
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
# Top-level keys documented for app.json (https://devcenter.heroku.com/articles/app-json-schema)
APP_JSON_KEYS = {
"name", "description", "keywords", "website", "repository", "logo", "success_url", "scripts", "env",
"formation", "image", "addons", "buildpacks", "environments", "stack",
}
ENV_KEYS = {"description", "value", "required", "generator"}
def _config_vars() -> set[str]:
source = (ROOT / "config.py").read_text()
return set(re.findall(r'r\.\w+\(\s*"([A-Z0-9_]+)"', source)) | set(re.findall(r'env\.get\("([A-Z0-9_]+)"\)', source))
def _app_json() -> dict:
return json.loads((ROOT / "app.json").read_text())
def test_app_json_structure():
manifest = _app_json()
assert set(manifest) <= APP_JSON_KEYS
assert len(manifest["name"]) <= 30 and manifest["description"]
assert manifest["buildpacks"] == [{"url": "heroku/python"}]
assert manifest["formation"] == {"worker": {"quantity": 1, "size": "basic"}}
assert manifest["addons"][0]["plan"].startswith("heroku-postgresql:")
for name, spec in manifest["env"].items():
assert set(spec) <= ENV_KEYS, name
assert spec["description"], name
assert isinstance(spec.get("required", False), bool), name
if "value" in spec:
assert isinstance(spec["value"], str), name
def test_app_json_only_lists_supported_variables():
supported = _config_vars()
unsupported = set(_app_json()["env"]) - supported
assert not unsupported, f"app.json lists variables the app does not read: {unsupported}"
def test_env_example_documents_every_variable():
documented = set(re.findall(r"^([A-Z0-9_]+)=", (ROOT / ".env.example").read_text(), re.MULTILINE))
supported = _config_vars() - {"DYNO"} # DYNO is set by Heroku itself
assert supported - documented == set()
assert documented - supported == set()
def test_procfile_matches_entry_point():
procfile = (ROOT / "Procfile").read_text().strip().splitlines()
assert procfile == ["worker: python main.py run"]
assert set(_app_json()["formation"]) == {line.split(":")[0] for line in procfile}
main_source = (ROOT / "main.py").read_text()
assert 'sub.add_parser("run"' in main_source and "signal.SIGTERM" in main_source
def test_runtime_and_requirements():
assert (ROOT / ".python-version").read_text().strip() == "3.12"
assert not (ROOT / "runtime.txt").exists() # deprecated in favour of .python-version
reqs = (ROOT / "requirements.txt").read_text()
for package in ("Telethon", "google-api-python-client", "google-auth", "google-auth-oauthlib", "psycopg[binary]", "python-dotenv"):
assert re.search(rf"^{re.escape(package)}==", reqs, re.MULTILINE), package
def test_gitignore_protects_secrets_and_state():
ignored = (ROOT / ".gitignore").read_text().splitlines()
for pattern in (".env", "*.session", "credentials/", "token.json", "session_string.txt", "data/", "downloads/", "*.db"):
assert pattern in ignored, pattern
def test_readme_deploy_button():
readme = (ROOT / "README.md").read_text()
assert "https://www.herokucdn.com/deploy/button.svg" in readme
assert re.search(r"\]\(https://heroku\.com/deploy\?template=https://github\.com/[^)\s]+\)", readme)