fix: test suite sent real push notifications via the developer's .env
This commit is contained in:
@@ -3,6 +3,13 @@
|
|||||||
All notable changes to this project will be documented here.
|
All notable changes to this project will be documented here.
|
||||||
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **The test suite sent real push notifications to the developer's phone.** `TestSyncCommand` invokes the actual `sync` command, which calls `load_config()`, which calls `load_dotenv()` — so the real `.env` was loaded and its live `NTFY_TOPIC` used for the POST. Every `pytest` run fired three or four pushes, including a fabricated "codex: 3 conversation(s) failed to export" straight out of a fixture, which is worse than noise: it reports a failure that never happened. Nothing appeared in `cache/logs/exporter.log` to explain it, because every test invocation passes `--no-log-file`.
|
||||||
|
|
||||||
|
A `tests/conftest.py` autouse fixture now neutralises the environment for every test: `NTFY_TOPIC`/`NTFY_TOKEN` are emptied, and `NTFY_SERVER` and `JOPLIN_API_URL` are pointed at a closed local port, so a stray topic cannot reach the internet and a test cannot write notes into a real Joplin instance. The values are **emptied rather than deleted** — `load_dotenv(override=False)` skips only keys already present, so deleting one lets `.env` put it back. Verified by instrumenting `requests` across a full run: zero outbound requests, where the same instrumentation without the fixture records POSTs to the live ntfy topic.
|
||||||
|
|
||||||
## [0.9.0] - 2026-08-18
|
## [0.9.0] - 2026-08-18
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""Shared test fixtures.
|
||||||
|
|
||||||
|
The autouse fixture here exists because of a real incident (2026-08-18): the
|
||||||
|
`sync` CLI tests invoke the actual command, which calls `load_config()`, which
|
||||||
|
calls `load_dotenv()` — so the developer's real `.env` was loaded and its
|
||||||
|
`NTFY_TOPIC` used. Every `pytest` run fired real push notifications at the
|
||||||
|
developer's phone, including a fabricated "3 conversations failed to export"
|
||||||
|
from a fixture. Nothing appeared in the log to explain it, because the tests
|
||||||
|
pass `--no-log-file`.
|
||||||
|
|
||||||
|
The lesson generalises past ntfy: any test that exercises a command end to end
|
||||||
|
inherits whatever is in `.env` unless the environment is neutralised first.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _no_outbound_side_effects(monkeypatch):
|
||||||
|
"""Neutralise every environment variable that could reach a real service.
|
||||||
|
|
||||||
|
Set to empty/unroutable values rather than deleted: `load_dotenv` is called
|
||||||
|
with ``override=False``, which only skips keys **already present** in the
|
||||||
|
environment. Deleting a key would let the developer's `.env` put it back.
|
||||||
|
|
||||||
|
Individual tests may still `monkeypatch.setenv` these — that is how the
|
||||||
|
notification tests point at a dead local port on purpose.
|
||||||
|
"""
|
||||||
|
# Notifications: an empty topic disables sending outright (`is_configured`
|
||||||
|
# strips and checks truthiness), and the server is pointed at a closed port
|
||||||
|
# so even a test that sets its own topic cannot reach the internet.
|
||||||
|
monkeypatch.setenv("NTFY_TOPIC", "")
|
||||||
|
monkeypatch.setenv("NTFY_SERVER", "http://127.0.0.1:9")
|
||||||
|
monkeypatch.setenv("NTFY_TOKEN", "")
|
||||||
|
|
||||||
|
# Joplin: a test that reached the developer's running desktop instance would
|
||||||
|
# create or overwrite real notes in their archive.
|
||||||
|
monkeypatch.setenv("JOPLIN_API_URL", "http://127.0.0.1:9")
|
||||||
|
monkeypatch.setenv("JOPLIN_API_TOKEN", "")
|
||||||
@@ -624,3 +624,40 @@ class TestNotifySend:
|
|||||||
monkeypatch.setenv("NTFY_TOPIC", "unit-test-topic")
|
monkeypatch.setenv("NTFY_TOPIC", "unit-test-topic")
|
||||||
monkeypatch.setenv("NTFY_NOTIFY", "off")
|
monkeypatch.setenv("NTFY_NOTIFY", "off")
|
||||||
assert notify_mod.is_configured() is False
|
assert notify_mod.is_configured() is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestNoRealNotificationsDuringTests:
|
||||||
|
"""Regression guard for the 2026-08-18 incident: the suite pushed to the
|
||||||
|
developer's real ntfy topic because `sync` loads `.env` via `load_dotenv`.
|
||||||
|
|
||||||
|
Asserts the conftest neutralisation holds even though a real `.env` with a
|
||||||
|
live NTFY_TOPIC sits beside the tests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_notifications_are_disabled(self):
|
||||||
|
from src import notify as notify_mod
|
||||||
|
|
||||||
|
assert notify_mod.is_configured() is False
|
||||||
|
|
||||||
|
def test_dotenv_cannot_reintroduce_a_topic(self):
|
||||||
|
"""`load_dotenv(override=False)` skips keys already present — including
|
||||||
|
empty ones. Deleting the var instead of emptying it would reopen this."""
|
||||||
|
import os
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
from src import notify as notify_mod
|
||||||
|
|
||||||
|
load_dotenv(override=False)
|
||||||
|
assert os.getenv("NTFY_TOPIC", "").strip() == ""
|
||||||
|
assert notify_mod.is_configured() is False
|
||||||
|
|
||||||
|
def test_send_cannot_reach_the_public_server(self, monkeypatch):
|
||||||
|
"""Even a test that sets its own topic is pinned to a dead local port."""
|
||||||
|
import os
|
||||||
|
|
||||||
|
from src import notify as notify_mod
|
||||||
|
|
||||||
|
monkeypatch.setenv("NTFY_TOPIC", "some-topic")
|
||||||
|
assert "127.0.0.1" in os.getenv("NTFY_SERVER", "")
|
||||||
|
assert notify_mod.send("t", "m") is False
|
||||||
|
|||||||
Reference in New Issue
Block a user