From 51b8bf6ecb2178062e186e0c759cb2a9560155e4 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 30 Sep 2026 19:13:02 +0100 Subject: [PATCH] Make SpaceDevs cache files readable by the web server --- agenda/thespacedevs.py | 2 ++ tests/test_spacedevs_cache.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/agenda/thespacedevs.py b/agenda/thespacedevs.py index 0066b73..7bcb57d 100644 --- a/agenda/thespacedevs.py +++ b/agenda/thespacedevs.py @@ -34,6 +34,8 @@ def write_json_cache(filename: str, payload: StrDict) -> None: temporary = cache.name cache.write(contents) cache.flush() + # The updater and web server run as different users. + os.fchmod(cache.fileno(), 0o644) os.fsync(cache.fileno()) os.replace(temporary, filename) finally: diff --git a/tests/test_spacedevs_cache.py b/tests/test_spacedevs_cache.py index 0e9ec0d..0792725 100644 --- a/tests/test_spacedevs_cache.py +++ b/tests/test_spacedevs_cache.py @@ -2,6 +2,7 @@ import errno import json +import typing from datetime import datetime, timedelta from pathlib import Path from unittest.mock import Mock @@ -64,3 +65,34 @@ def test_all_empty_launch_files_return_no_cache(tmp_path: Path) -> None: (tmp_path / "2026-09-30_12:00:00.json").touch() assert thespacedevs.load_cached_launches(str(tmp_path)) is None assert not thespacedevs.is_launches_cache_fresh(str(tmp_path)) + + +@pytest.mark.parametrize("existing_mode", [None, 0o600, 0o644]) +def test_published_cache_is_readable_by_web_server( + tmp_path: Path, existing_mode: int | None +) -> None: + filename = tmp_path / "cache.json" + if existing_mode is not None: + filename.touch(mode=existing_mode) + thespacedevs.write_json_cache(str(filename), {"results": []}) + assert filename.stat().st_mode & 0o777 == 0o644 + + +def test_unreadable_cache_falls_back( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + older = tmp_path / "2026-09-29_12:00:00.json" + newer = tmp_path / "2026-09-30_12:00:00.json" + older.write_text('{"results": []}') + newer.write_text('{"results": []}') + import builtins + + original_open = builtins.open + + def open_cache(filename: str) -> typing.TextIO: + if filename == str(newer): + raise PermissionError(errno.EACCES, "Permission denied", filename) + return original_open(filename) + + monkeypatch.setattr("agenda.thespacedevs.open", open_cache, raising=False) + assert thespacedevs.load_cached_launches(str(tmp_path)) == {"results": []}