chore: housekeeping - orphan branch + three stale tests (#22)

* test(bootstrap): isolate env vars in test_reads_from_env_file

The test was failing on CT104 because live CENTRAL_DB_DSN
environment variable overrode the test .env file content.

Fix: use monkeypatch.delenv to clear all CENTRAL_* env vars
before creating the Settings object, ensuring the test env
file is the only source of configuration values.

Also add CENTRAL_CSRF_SECRET to test env file since it's
now a required field.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(models): remove stale test_custom_prefix test

The test called subject_for_event(event, prefix="myapp.events")
but the prefix parameter was removed from the API.

The prefix functionality was intentionally removed - subjects
now always use the "central." prefix hardcoded in the function.
Delete the test rather than re-add the parameter.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(nws): update fixtures for new adapter signature and region filtering

NWSAdapter.__init__ signature changed from (config, cursor_db_path)
to (config, config_store, cursor_db_path) with config now being
AdapterConfig with a settings dict instead of NWSAdapterConfig.

Also adapts tests to region-based bbox filtering:
- TestStateFilter now uses region bbox to accept PNW, reject CA
- Add geometry to SAMPLE_FEATURE_OR so it passes region filter
- Other test fixtures use region=None to skip filtering

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
malice 2026-05-17 18:14:58 -06:00 committed by GitHub
commit 8c2e4a358d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 72 additions and 31 deletions

View file

@ -18,6 +18,7 @@ class TestSettingsFromEnv:
monkeypatch.setenv("CENTRAL_NATS_URL", "nats://10.0.0.1:4222") monkeypatch.setenv("CENTRAL_NATS_URL", "nats://10.0.0.1:4222")
monkeypatch.setenv("CENTRAL_MASTER_KEY_PATH", "/tmp/test.key") monkeypatch.setenv("CENTRAL_MASTER_KEY_PATH", "/tmp/test.key")
monkeypatch.setenv("CENTRAL_LOG_LEVEL", "DEBUG") monkeypatch.setenv("CENTRAL_LOG_LEVEL", "DEBUG")
monkeypatch.setenv("CENTRAL_CSRF_SECRET", "testsecret12345678901234567890ab")
settings = Settings() settings = Settings()
@ -29,6 +30,7 @@ class TestSettingsFromEnv:
def test_defaults_applied(self, monkeypatch: pytest.MonkeyPatch) -> None: def test_defaults_applied(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Default values are used when env vars not set.""" """Default values are used when env vars not set."""
monkeypatch.setenv("CENTRAL_DB_DSN", "postgresql://x:y@localhost/db") monkeypatch.setenv("CENTRAL_DB_DSN", "postgresql://x:y@localhost/db")
monkeypatch.setenv("CENTRAL_CSRF_SECRET", "testsecret12345678901234567890ab")
# Clear any existing env vars that might interfere # Clear any existing env vars that might interfere
monkeypatch.delenv("CENTRAL_NATS_URL", raising=False) monkeypatch.delenv("CENTRAL_NATS_URL", raising=False)
monkeypatch.delenv("CENTRAL_MASTER_KEY_PATH", raising=False) monkeypatch.delenv("CENTRAL_MASTER_KEY_PATH", raising=False)
@ -44,13 +46,23 @@ class TestSettingsFromEnv:
class TestSettingsFromFile: class TestSettingsFromFile:
"""Test loading settings from .env file.""" """Test loading settings from .env file."""
def test_reads_from_env_file(self, tmp_path: Path) -> None: def test_reads_from_env_file(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Settings are read from .env file when env vars not present.""" """Settings are read from .env file when env vars not present."""
# Remove any live env vars that would override the test .env file
monkeypatch.delenv("CENTRAL_DB_DSN", raising=False)
monkeypatch.delenv("CENTRAL_NATS_URL", raising=False)
monkeypatch.delenv("CENTRAL_LOG_LEVEL", raising=False)
monkeypatch.delenv("CENTRAL_MASTER_KEY_PATH", raising=False)
monkeypatch.delenv("CENTRAL_CSRF_SECRET", raising=False)
env_file = tmp_path / ".env" env_file = tmp_path / ".env"
env_file.write_text( env_file.write_text(
"CENTRAL_DB_DSN=postgresql://file:pass@localhost/filedb\n" "CENTRAL_DB_DSN=postgresql://file:pass@localhost/filedb\n"
"CENTRAL_NATS_URL=nats://file.local:4222\n" "CENTRAL_NATS_URL=nats://file.local:4222\n"
"CENTRAL_LOG_LEVEL=WARNING\n" "CENTRAL_LOG_LEVEL=WARNING\n"
"CENTRAL_CSRF_SECRET=testsecret12345678901234567890ab\n"
) )
# Create settings pointing to the temp .env file # Create settings pointing to the temp .env file
@ -65,8 +77,12 @@ class TestSettingsFromFile:
) -> None: ) -> None:
"""Environment variables take precedence over .env file.""" """Environment variables take precedence over .env file."""
env_file = tmp_path / ".env" env_file = tmp_path / ".env"
env_file.write_text("CENTRAL_DB_DSN=postgresql://file@localhost/filedb\n") env_file.write_text(
"CENTRAL_DB_DSN=postgresql://file@localhost/filedb\n"
"CENTRAL_CSRF_SECRET=filesecret1234567890123456789012\n"
)
monkeypatch.setenv("CENTRAL_DB_DSN", "postgresql://env@localhost/envdb") monkeypatch.setenv("CENTRAL_DB_DSN", "postgresql://env@localhost/envdb")
monkeypatch.setenv("CENTRAL_CSRF_SECRET", "envsecret12345678901234567890ab")
settings = Settings(_env_file=env_file) settings = Settings(_env_file=env_file)
@ -80,6 +96,7 @@ class TestSettingsValidation:
"""Clear error when required CENTRAL_DB_DSN is missing.""" """Clear error when required CENTRAL_DB_DSN is missing."""
# Ensure no env vars or .env file provides the DSN # Ensure no env vars or .env file provides the DSN
monkeypatch.delenv("CENTRAL_DB_DSN", raising=False) monkeypatch.delenv("CENTRAL_DB_DSN", raising=False)
monkeypatch.delenv("CENTRAL_CSRF_SECRET", raising=False)
with pytest.raises(Exception) as exc_info: with pytest.raises(Exception) as exc_info:
# Use a non-existent .env file path to ensure no fallback # Use a non-existent .env file path to ensure no fallback
@ -91,6 +108,7 @@ class TestSettingsValidation:
def test_invalid_log_level_rejected(self, monkeypatch: pytest.MonkeyPatch) -> None: def test_invalid_log_level_rejected(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Invalid log level values are rejected.""" """Invalid log level values are rejected."""
monkeypatch.setenv("CENTRAL_DB_DSN", "postgresql://x@localhost/db") monkeypatch.setenv("CENTRAL_DB_DSN", "postgresql://x@localhost/db")
monkeypatch.setenv("CENTRAL_CSRF_SECRET", "testsecret12345678901234567890ab")
monkeypatch.setenv("CENTRAL_LOG_LEVEL", "INVALID") monkeypatch.setenv("CENTRAL_LOG_LEVEL", "INVALID")
with pytest.raises(Exception): with pytest.raises(Exception):
@ -103,6 +121,7 @@ class TestGetSettings:
def test_caches_result(self, monkeypatch: pytest.MonkeyPatch) -> None: def test_caches_result(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""get_settings() returns cached instance.""" """get_settings() returns cached instance."""
monkeypatch.setenv("CENTRAL_DB_DSN", "postgresql://cached@localhost/db") monkeypatch.setenv("CENTRAL_DB_DSN", "postgresql://cached@localhost/db")
monkeypatch.setenv("CENTRAL_CSRF_SECRET", "testsecret12345678901234567890ab")
get_settings.cache_clear() get_settings.cache_clear()
s1 = get_settings() s1 = get_settings()
@ -113,6 +132,7 @@ class TestGetSettings:
def test_cache_clear_reloads(self, monkeypatch: pytest.MonkeyPatch) -> None: def test_cache_clear_reloads(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""cache_clear() forces reload on next call.""" """cache_clear() forces reload on next call."""
monkeypatch.setenv("CENTRAL_DB_DSN", "postgresql://first@localhost/db") monkeypatch.setenv("CENTRAL_DB_DSN", "postgresql://first@localhost/db")
monkeypatch.setenv("CENTRAL_CSRF_SECRET", "testsecret12345678901234567890ab")
get_settings.cache_clear() get_settings.cache_clear()
s1 = get_settings() s1 = get_settings()

View file

@ -98,11 +98,6 @@ class TestSubjectForEvent:
subject = subject_for_event(event) subject = subject_for_event(event)
assert subject == "central.wx.alert.us.unknown" assert subject == "central.wx.alert.us.unknown"
def test_custom_prefix(self, sample_event: Event) -> None:
"""Custom prefix is used in subject."""
subject = subject_for_event(sample_event, prefix="myapp.events")
assert subject == "myapp.events.alert.us.id.county.ada"
class TestCloudEventsWire: class TestCloudEventsWire:
"""Tests for CloudEvents wire format.""" """Tests for CloudEvents wire format."""

View file

@ -16,7 +16,7 @@ from central.adapters.nws import (
_compute_bbox, _compute_bbox,
SEVERITY_MAP, SEVERITY_MAP,
) )
from central.config import NWSAdapterConfig from central.config_models import AdapterConfig
from central.models import subject_for_event from central.models import subject_for_event
@ -51,7 +51,7 @@ SAMPLE_FEATURE_ID = {
SAMPLE_FEATURE_OR = { SAMPLE_FEATURE_OR = {
"id": "urn:oid:2.49.0.1.840.0.x1y2z3w4", "id": "urn:oid:2.49.0.1.840.0.x1y2z3w4",
"type": "Feature", "type": "Feature",
"geometry": None, "geometry": {"type": "Point", "coordinates": [-122.7, 45.5]}, # Portland, OR
"properties": { "properties": {
"id": "urn:oid:2.49.0.1.840.0.x1y2z3w4", "id": "urn:oid:2.49.0.1.840.0.x1y2z3w4",
"event": "Winter Storm Warning", "event": "Winter Storm Warning",
@ -181,18 +181,24 @@ class TestBuildRegions:
class TestStateFilter: class TestStateFilter:
"""Tests for state filtering.""" """Tests for region-based filtering."""
@pytest.fixture @pytest.fixture
def adapter(self, tmp_path: Path) -> NWSAdapter: def adapter(self, tmp_path: Path) -> NWSAdapter:
"""Create adapter with ID/OR/WA states.""" """Create adapter with Pacific Northwest region (excludes CA)."""
config = NWSAdapterConfig( config = AdapterConfig(
name="nws",
enabled=True, enabled=True,
cadence_s=60, cadence_s=60,
states=["ID", "OR", "WA", "MT", "WY", "UT", "NV"], settings={
contact_email="test@example.com", "contact_email": "test@example.com",
# Pacific NW region: WA/OR/ID - excludes CA (LA at 34N, region starts at 42N)
"region": {"north": 49.0, "south": 42.0, "east": -104.0, "west": -125.0},
},
updated_at=datetime.now(timezone.utc),
) )
return NWSAdapter(config, tmp_path / "test.db") mock_config_store = MagicMock()
return NWSAdapter(config, mock_config_store, tmp_path / "test.db")
def test_accepts_id_feature(self, adapter: NWSAdapter) -> None: def test_accepts_id_feature(self, adapter: NWSAdapter) -> None:
event = adapter._normalize_feature(SAMPLE_FEATURE_ID) event = adapter._normalize_feature(SAMPLE_FEATURE_ID)
@ -228,13 +234,18 @@ class TestSeverityMapping:
assert SEVERITY_MAP["Unknown"] is None assert SEVERITY_MAP["Unknown"] is None
def test_unknown_severity_in_feature(self, tmp_path: Path) -> None: def test_unknown_severity_in_feature(self, tmp_path: Path) -> None:
config = NWSAdapterConfig( config = AdapterConfig(
name="nws",
enabled=True, enabled=True,
cadence_s=60, cadence_s=60,
states=["WA"], settings={
contact_email="test@example.com", "contact_email": "test@example.com",
# No region = accept all features
},
updated_at=datetime.now(timezone.utc),
) )
adapter = NWSAdapter(config, tmp_path / "test.db") mock_config_store = MagicMock()
adapter = NWSAdapter(config, mock_config_store, tmp_path / "test.db")
event = adapter._normalize_feature(SAMPLE_FEATURE_UNKNOWN_SEVERITY) event = adapter._normalize_feature(SAMPLE_FEATURE_UNKNOWN_SEVERITY)
assert event is not None assert event is not None
assert event.severity is None assert event.severity is None
@ -245,13 +256,18 @@ class TestSubjectDerivation:
@pytest.fixture @pytest.fixture
def adapter(self, tmp_path: Path) -> NWSAdapter: def adapter(self, tmp_path: Path) -> NWSAdapter:
config = NWSAdapterConfig( config = AdapterConfig(
name="nws",
enabled=True, enabled=True,
cadence_s=60, cadence_s=60,
states=["ID", "OR", "WA"], settings={
contact_email="test@example.com", "contact_email": "test@example.com",
# No region = accept all features
},
updated_at=datetime.now(timezone.utc),
) )
return NWSAdapter(config, tmp_path / "test.db") mock_config_store = MagicMock()
return NWSAdapter(config, mock_config_store, tmp_path / "test.db")
def test_county_subject(self, adapter: NWSAdapter) -> None: def test_county_subject(self, adapter: NWSAdapter) -> None:
event = adapter._normalize_feature(SAMPLE_FEATURE_ID) event = adapter._normalize_feature(SAMPLE_FEATURE_ID)
@ -287,13 +303,18 @@ class TestRegionsSorted:
@pytest.fixture @pytest.fixture
def adapter(self, tmp_path: Path) -> NWSAdapter: def adapter(self, tmp_path: Path) -> NWSAdapter:
config = NWSAdapterConfig( config = AdapterConfig(
name="nws",
enabled=True, enabled=True,
cadence_s=60, cadence_s=60,
states=["ID"], settings={
contact_email="test@example.com", "contact_email": "test@example.com",
# No region = accept all features
},
updated_at=datetime.now(timezone.utc),
) )
return NWSAdapter(config, tmp_path / "test.db") mock_config_store = MagicMock()
return NWSAdapter(config, mock_config_store, tmp_path / "test.db")
def test_regions_alphabetically_sorted(self, adapter: NWSAdapter) -> None: def test_regions_alphabetically_sorted(self, adapter: NWSAdapter) -> None:
event = adapter._normalize_feature(SAMPLE_FEATURE_ID) event = adapter._normalize_feature(SAMPLE_FEATURE_ID)
@ -312,13 +333,18 @@ class TestDeduplication:
@pytest.fixture @pytest.fixture
def adapter(self, tmp_path: Path) -> NWSAdapter: def adapter(self, tmp_path: Path) -> NWSAdapter:
config = NWSAdapterConfig( config = AdapterConfig(
name="nws",
enabled=True, enabled=True,
cadence_s=60, cadence_s=60,
states=["ID"], settings={
contact_email="test@example.com", "contact_email": "test@example.com",
# No region = accept all features
},
updated_at=datetime.now(timezone.utc),
) )
return NWSAdapter(config, tmp_path / "test.db") mock_config_store = MagicMock()
return NWSAdapter(config, mock_config_store, tmp_path / "test.db")
def test_same_feature_same_id(self, adapter: NWSAdapter) -> None: def test_same_feature_same_id(self, adapter: NWSAdapter) -> None:
"""Normalizing the same feature twice returns same Event.id.""" """Normalizing the same feature twice returns same Event.id."""