fix(backend): supprime les vulnerabilites Sonar du Dockerfile et allege les tests d'exception
This commit is contained in:
@@ -11,13 +11,14 @@ WORKDIR /app
|
|||||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||||
uv sync --locked --no-install-project --no-dev
|
uv sync --locked --no-install-project --no-dev --no-build
|
||||||
|
|
||||||
|
# Le projet lui-meme n'est pas installe (pas de second `uv sync`) : il tourne depuis /app, le
|
||||||
|
# repertoire de travail, et rien ne lit ses metadonnees. L'installer imposerait de le construire
|
||||||
|
# (backend hatchling), donc de retirer `--no-build` de l'etape ci-dessus, qui garantit que
|
||||||
|
# l'installation des dependances n'execute aucun script de build (regle Sonar docker:S8541).
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
|
||||||
uv sync --locked --no-dev
|
|
||||||
|
|
||||||
|
|
||||||
FROM python:3.14-slim AS runtime
|
FROM python:3.14-slim AS runtime
|
||||||
|
|
||||||
|
|||||||
@@ -112,8 +112,10 @@ async def test_duplicate_reading_is_rejected_when_key_matches(
|
|||||||
)
|
)
|
||||||
await data_connection.execute(statement)
|
await data_connection.execute(statement)
|
||||||
|
|
||||||
|
savepoint = data_connection.begin_nested()
|
||||||
|
|
||||||
with pytest.raises(IntegrityError):
|
with pytest.raises(IntegrityError):
|
||||||
async with data_connection.begin_nested():
|
async with savepoint:
|
||||||
await data_connection.execute(statement)
|
await data_connection.execute(statement)
|
||||||
|
|
||||||
|
|
||||||
@@ -147,9 +149,12 @@ async def test_invalid_reading_is_rejected_when_constraints_fail(
|
|||||||
}
|
}
|
||||||
values.update(changes)
|
values.update(changes)
|
||||||
|
|
||||||
|
statement = insert(Reading).values(**values)
|
||||||
|
savepoint = data_connection.begin_nested()
|
||||||
|
|
||||||
with pytest.raises(IntegrityError):
|
with pytest.raises(IntegrityError):
|
||||||
async with data_connection.begin_nested():
|
async with savepoint:
|
||||||
await data_connection.execute(insert(Reading).values(**values))
|
await data_connection.execute(statement)
|
||||||
|
|
||||||
|
|
||||||
async def test_prediction_requires_period_when_energy_is_predicted(
|
async def test_prediction_requires_period_when_energy_is_predicted(
|
||||||
@@ -164,8 +169,10 @@ async def test_prediction_requires_period_when_energy_is_predicted(
|
|||||||
model_reference="test-model/1",
|
model_reference="test-model/1",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
savepoint = data_connection.begin_nested()
|
||||||
|
|
||||||
with pytest.raises(IntegrityError):
|
with pytest.raises(IntegrityError):
|
||||||
async with data_connection.begin_nested():
|
async with savepoint:
|
||||||
await data_connection.execute(statement)
|
await data_connection.execute(statement)
|
||||||
|
|
||||||
|
|
||||||
@@ -212,21 +219,22 @@ async def test_alert_rejects_prediction_when_site_differs(
|
|||||||
)
|
)
|
||||||
).scalar_one()
|
).scalar_one()
|
||||||
|
|
||||||
|
statement = insert(Alert).values(
|
||||||
|
source_alert_id=str(uuid4()),
|
||||||
|
site_id=other_site,
|
||||||
|
source="enervision",
|
||||||
|
timestamp=MOMENT,
|
||||||
|
type="spike",
|
||||||
|
severity="high",
|
||||||
|
message="Test",
|
||||||
|
prediction_id=prediction_id,
|
||||||
|
raw_data={},
|
||||||
|
)
|
||||||
|
savepoint = data_connection.begin_nested()
|
||||||
|
|
||||||
with pytest.raises(IntegrityError):
|
with pytest.raises(IntegrityError):
|
||||||
async with data_connection.begin_nested():
|
async with savepoint:
|
||||||
await data_connection.execute(
|
await data_connection.execute(statement)
|
||||||
insert(Alert).values(
|
|
||||||
source_alert_id=str(uuid4()),
|
|
||||||
site_id=other_site,
|
|
||||||
source="enervision",
|
|
||||||
timestamp=MOMENT,
|
|
||||||
type="spike",
|
|
||||||
severity="high",
|
|
||||||
message="Test",
|
|
||||||
prediction_id=prediction_id,
|
|
||||||
raw_data={},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def test_recommendation_is_unique_when_alert_and_rule_match(
|
async def test_recommendation_is_unique_when_alert_and_rule_match(
|
||||||
@@ -256,6 +264,8 @@ async def test_recommendation_is_unique_when_alert_and_rule_match(
|
|||||||
)
|
)
|
||||||
await data_connection.execute(statement)
|
await data_connection.execute(statement)
|
||||||
|
|
||||||
|
savepoint = data_connection.begin_nested()
|
||||||
|
|
||||||
with pytest.raises(IntegrityError):
|
with pytest.raises(IntegrityError):
|
||||||
async with data_connection.begin_nested():
|
async with savepoint:
|
||||||
await data_connection.execute(statement)
|
await data_connection.execute(statement)
|
||||||
|
|||||||
@@ -106,13 +106,15 @@ def test_validate_source_accepts_valid_dataset():
|
|||||||
def test_validate_source_rejects_missing_column():
|
def test_validate_source_rejects_missing_column():
|
||||||
frame = make_dataframe().drop(columns=["consumption_kwh"])
|
frame = make_dataframe().drop(columns=["consumption_kwh"])
|
||||||
|
|
||||||
|
metadata = make_metadata()
|
||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
ValueError,
|
ValueError,
|
||||||
match="Colonnes obligatoires absentes",
|
match="Colonnes obligatoires absentes",
|
||||||
):
|
):
|
||||||
validate_source(
|
validate_source(
|
||||||
frame,
|
frame,
|
||||||
make_metadata(),
|
metadata,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -124,13 +126,15 @@ def test_validate_source_rejects_duplicates():
|
|||||||
"timestamp",
|
"timestamp",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
metadata = make_metadata()
|
||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
ValueError,
|
ValueError,
|
||||||
match="doublons",
|
match="doublons",
|
||||||
):
|
):
|
||||||
validate_source(
|
validate_source(
|
||||||
frame,
|
frame,
|
||||||
make_metadata(),
|
metadata,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -139,13 +143,15 @@ def test_validate_source_rejects_unknown_site():
|
|||||||
|
|
||||||
frame.loc[1, "site_id"] = "SITE999"
|
frame.loc[1, "site_id"] = "SITE999"
|
||||||
|
|
||||||
|
metadata = make_metadata()
|
||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
ValueError,
|
ValueError,
|
||||||
match="Sites incohérents",
|
match="Sites incohérents",
|
||||||
):
|
):
|
||||||
validate_source(
|
validate_source(
|
||||||
frame,
|
frame,
|
||||||
make_metadata(),
|
metadata,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -49,8 +49,10 @@ async def test_the_database_refuses_to_mutate_the_audit_log(
|
|||||||
) -> None:
|
) -> None:
|
||||||
await une_ligne(session)
|
await une_ligne(session)
|
||||||
|
|
||||||
|
requete = text(instruction)
|
||||||
|
|
||||||
with pytest.raises(DBAPIError, match="ajout seul"):
|
with pytest.raises(DBAPIError, match="ajout seul"):
|
||||||
await session.execute(text(instruction))
|
await session.execute(requete)
|
||||||
await session.rollback()
|
await session.rollback()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -131,11 +131,14 @@ async def test_the_database_refuses_two_tokens_sharing_a_fingerprint(
|
|||||||
user_agent=None,
|
user_agent=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
empreinte = fingerprint_refresh(secret)
|
||||||
|
expiration = datetime.now(UTC) + DUREE
|
||||||
|
|
||||||
with pytest.raises(IntegrityError):
|
with pytest.raises(IntegrityError):
|
||||||
await depot.create(
|
await depot.create(
|
||||||
user_id=compte,
|
user_id=compte,
|
||||||
token_hash=fingerprint_refresh(secret),
|
token_hash=empreinte,
|
||||||
expires_at=datetime.now(UTC) + DUREE,
|
expires_at=expiration,
|
||||||
client_ip=None,
|
client_ip=None,
|
||||||
user_agent=None,
|
user_agent=None,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -178,12 +178,16 @@ async def test_the_database_refuses_two_tokens_sharing_a_fingerprint(
|
|||||||
user_agent=None,
|
user_agent=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
famille = uuid.uuid4()
|
||||||
|
empreinte = fingerprint_refresh(secret)
|
||||||
|
expiration = datetime.now(UTC) + DUREE
|
||||||
|
|
||||||
with pytest.raises(IntegrityError):
|
with pytest.raises(IntegrityError):
|
||||||
await depot.create(
|
await depot.create(
|
||||||
user_id=compte,
|
user_id=compte,
|
||||||
family_id=uuid.uuid4(),
|
family_id=famille,
|
||||||
token_hash=fingerprint_refresh(secret),
|
token_hash=empreinte,
|
||||||
expires_at=datetime.now(UTC) + DUREE,
|
expires_at=expiration,
|
||||||
client_ip=None,
|
client_ip=None,
|
||||||
user_agent=None,
|
user_agent=None,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,14 +31,12 @@ async def test_the_database_refuses_an_email_written_in_upper_case(
|
|||||||
) -> None:
|
) -> None:
|
||||||
saisie = adresse().upper()
|
saisie = adresse().upper()
|
||||||
|
|
||||||
|
requete = text(
|
||||||
|
"insert into app_user (email, password_hash, role) values (:e, '$argon2id$x', 'lecteur')"
|
||||||
|
)
|
||||||
|
|
||||||
with pytest.raises(IntegrityError):
|
with pytest.raises(IntegrityError):
|
||||||
await session.execute(
|
await session.execute(requete, {"e": saisie})
|
||||||
text(
|
|
||||||
"insert into app_user (email, password_hash, role) "
|
|
||||||
"values (:e, '$argon2id$x', 'lecteur')"
|
|
||||||
),
|
|
||||||
{"e": saisie},
|
|
||||||
)
|
|
||||||
await session.rollback()
|
await session.rollback()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -116,13 +116,11 @@ async def test_list_history_normalizes_naive_datetimes_to_utc() -> None:
|
|||||||
async def test_list_history_raises_when_start_is_after_end() -> None:
|
async def test_list_history_raises_when_start_is_after_end() -> None:
|
||||||
service = ReadingService(readings=FakeRepository([]))
|
service = ReadingService(readings=FakeRepository([]))
|
||||||
|
|
||||||
|
debut = datetime(2026, 9, 2, tzinfo=UTC)
|
||||||
|
fin = datetime(2026, 9, 1, tzinfo=UTC)
|
||||||
|
|
||||||
with pytest.raises(FenetreInverseeError):
|
with pytest.raises(FenetreInverseeError):
|
||||||
await service.list_history(
|
await service.list_history(start=debut, end=fin, limit=500, offset=0)
|
||||||
start=datetime(2026, 9, 2, tzinfo=UTC),
|
|
||||||
end=datetime(2026, 9, 1, tzinfo=UTC),
|
|
||||||
limit=500,
|
|
||||||
offset=0,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def test_list_history_raises_when_start_equals_end() -> None:
|
async def test_list_history_raises_when_start_equals_end() -> None:
|
||||||
|
|||||||
@@ -235,5 +235,7 @@ async def test_every_operation_refuses_an_unknown_account(action: str) -> None:
|
|||||||
if action == "set_active":
|
if action == "set_active":
|
||||||
arguments["is_active"] = False
|
arguments["is_active"] = False
|
||||||
|
|
||||||
|
methode = getattr(attirail.service, action)
|
||||||
|
|
||||||
with pytest.raises(UserNotFoundError):
|
with pytest.raises(UserNotFoundError):
|
||||||
await getattr(attirail.service, action)(**arguments)
|
await methode(**arguments)
|
||||||
|
|||||||
@@ -19,13 +19,17 @@ def test_build_parser_reads_the_create_admin_arguments() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_build_parser_requires_a_subcommand() -> None:
|
def test_build_parser_requires_a_subcommand() -> None:
|
||||||
|
parser = cli.build_parser()
|
||||||
|
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
cli.build_parser().parse_args([])
|
parser.parse_args([])
|
||||||
|
|
||||||
|
|
||||||
def test_build_parser_requires_an_email() -> None:
|
def test_build_parser_requires_an_email() -> None:
|
||||||
|
parser = cli.build_parser()
|
||||||
|
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
cli.build_parser().parse_args(["create-admin"])
|
parser.parse_args(["create-admin"])
|
||||||
|
|
||||||
|
|
||||||
def test_read_password_generates_a_long_secret_when_asked(
|
def test_read_password_generates_a_long_secret_when_asked(
|
||||||
|
|||||||
Reference in New Issue
Block a user