Files
team-tryouts/tests/test_filesystem_roots.py
T
GGThedandClaude Opus 5 bda23dbb67 fix(ops): la sauvegarde des contrats archivait le mauvais repertoire
OBS-006. Trois racines etaient baties sur os.getcwd() : le magasin de
documents, les journaux et les sauvegardes. La vague G a corrige la
premiere, parce qu'elle bloquait aussi OPS-011, et a laisse les deux autres.
C'est la lecon deja consignee deux fois : un motif fautif corrige dans une
seule couche reste dans les autres.

Le plus serieux n'est pas le motif, c'est l'ecart qu'il a ouvert.
backup.py gardait sa propre constante DOCUMENTS_DIR sur os.getcwd(), donc
il ignorait DOCUMENTS_ROOT -- la variable que la vague G a introduite et que
docs/deployment.md dit maintenant de regler pour sortir les televersements
des repertoires de version. Des qu'un exploitant suit cette consigne, le
script archive un repertoire ou l'application n'a jamais rien ecrit. Et
comme il repond a un repertoire absent par une ligne d'information et un
code de sortie 0, une tache planifiee qui surveille le code de sortie voit
vert indefiniment.

Autrement dit : plus l'exploitant suivait correctement la documentation de
deploiement, plus surement ses sauvegardes de contrats etaient vides.

Les trois racines viennent desormais d'app/storage.py, resolues a l'appel et
non a l'import, et la sauvegarde imprime la source qu'elle a utilisee. Le
message d'absence nomme le chemin ou elle a cherche : "No documents
directory found" se lisait comme "il n'y a pas de documents" plutot que
comme "je regarde au mauvais endroit".

Le test qui porte est celui qui ouvre l'archive : un zip vide est un fichier
de taille non nulle, donc verifier qu'un fichier a ete produit ne prouvait
rien. Verifie par mutation.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-11 18:34:52 -04:00

145 lines
5.5 KiB
Python

"""Where the application puts its files, and who agrees about it.
OBS-006. Three roots were built as `os.path.join(os.getcwd(), …)`: the
document store, the log directory and the backup directory. Each therefore
moved with whatever directory the process was started from — and the
processes that start this application are Waitress under Pterodactyl, a task
scheduler, pytest, and a person in a shell. Only one of the four is reliably
in the project directory.
The document store was fixed in wave G. The other two were not, and the gap
that opened between them is the reason this file exists: `backup.py` kept
archiving `./documents` while the application wrote to `DOCUMENTS_ROOT`, and
the script's answer to a missing directory is to print a line and exit 0.
Following the deployment documentation was what broke it.
"""
import os
import pytest
from app.storage import backups_root, documents_root, logs_root, project_root
class TestTheProjectAnchor:
def test_it_is_the_directory_holding_the_app_package(self):
assert os.path.isdir(os.path.join(project_root(), 'app'))
def test_it_does_not_follow_the_working_directory(self, tmp_path, monkeypatch):
"""The whole point. Every root below inherits this."""
before = project_root()
monkeypatch.chdir(tmp_path)
assert project_root() == before
@pytest.mark.parametrize(
('root', 'env_name', 'default_name'),
[
(documents_root, 'DOCUMENTS_ROOT', 'documents'),
(logs_root, 'LOG_DIR', 'logs'),
(backups_root, 'BACKUP_DIR', 'backups'),
],
)
class TestEachRoot:
def test_it_defaults_beside_the_project(self, root, env_name, default_name, monkeypatch):
monkeypatch.delenv(env_name, raising=False)
assert root() == os.path.join(project_root(), default_name)
def test_it_ignores_the_working_directory(
self, root, env_name, default_name, tmp_path, monkeypatch
):
monkeypatch.delenv(env_name, raising=False)
monkeypatch.chdir(tmp_path)
assert root() == os.path.join(project_root(), default_name)
assert not root().startswith(str(tmp_path))
def test_the_environment_wins(self, root, env_name, default_name, tmp_path, monkeypatch):
monkeypatch.setenv(env_name, str(tmp_path / 'elsewhere'))
assert root() == os.path.abspath(str(tmp_path / 'elsewhere'))
class TestTheBackupScriptAgreesWithTheApplication:
"""The regression this file was written for.
`backup.py` held its own `os.getcwd()`-based constant, so the two
answers to "where are the contracts" could differ — and did, for anyone
who set DOCUMENTS_ROOT because docs/deployment.md said to.
"""
def test_it_archives_the_directory_the_application_writes_to(self, tmp_path, monkeypatch):
from app.supporting_scripts import backup
store = tmp_path / 'somewhere-else' / 'documents'
(store / 'contrats signés').mkdir(parents=True)
(store / 'contrats signés' / 'a.pdf').write_bytes(b'%PDF-1.4 test')
monkeypatch.setenv('DOCUMENTS_ROOT', str(store))
monkeypatch.setattr(backup, 'BACKUP_DIR', str(tmp_path / 'backups'))
(tmp_path / 'backups').mkdir()
archive = backup.backup_documents()
assert archive is not None, (
'the script skipped, which is what it did on production while reporting success'
)
assert os.path.getsize(archive) > 0
def test_the_archive_actually_contains_the_contract(self, tmp_path, monkeypatch):
"""An empty zip is still a file of non-zero size. This is the
assertion that would have caught the original defect."""
import zipfile
from app.supporting_scripts import backup
store = tmp_path / 'documents'
(store / 'contrats signés').mkdir(parents=True)
(store / 'contrats signés' / 'contrat.pdf').write_bytes(b'%PDF-1.4 test')
monkeypatch.setenv('DOCUMENTS_ROOT', str(store))
monkeypatch.setattr(backup, 'BACKUP_DIR', str(tmp_path / 'backups'))
(tmp_path / 'backups').mkdir()
archive = backup.backup_documents()
with zipfile.ZipFile(archive) as zf:
assert any(name.endswith('contrat.pdf') for name in zf.namelist())
def test_a_missing_store_names_the_path_it_looked_in(self, tmp_path, monkeypatch, capsys):
""" "No documents directory found" read as "there are no documents"
rather than "I am looking in the wrong place"."""
from app.supporting_scripts import backup
missing = tmp_path / 'not-here'
monkeypatch.setenv('DOCUMENTS_ROOT', str(missing))
assert backup.backup_documents() is None
assert str(missing) in capsys.readouterr().out
class TestLogsFollowTheSameRule:
def test_configure_logging_writes_where_logs_root_says(self, tmp_path, monkeypatch):
from app.app import create_app
target = tmp_path / 'var' / 'log'
monkeypatch.setenv('LOG_DIR', str(target))
create_app(
{
'SECRET_KEY': 'test-secret-not-used-anywhere-real',
'SQLALCHEMY_DATABASE_URI': f'sqlite:///{tmp_path / "t.sqlite"}',
'TESTING': True,
'WTF_CSRF_ENABLED': False,
'FORCE_HTTPS': False,
'SESSION_COOKIE_SECURE': False,
'ENABLE_DISCORD_BOT': False,
'AUTO_CREATE_TABLES': True,
'CORS_ALLOWED_ORIGINS': '',
'RATELIMIT_ENABLED': False,
}
)
assert target.is_dir(), 'the log directory is created where LOG_DIR points'
assert (target / 'app.log').exists()