mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
Triage of Greptile findings on the default-Zim / configurable-directory PR: - Stale config on restart (P1): settings update() only queues an async background_save, and the restart switch has no ordering guarantee (the switch worker pops LIFO), so start_kiwix_serve.sh could read the old zims_destination. Save the config synchronously before activating the restart switch. - YAML null (P1): read_config_value.sh now treats an unquoted null/~ as unset (returns the default), matching PyYAML/the Python config layer. A quoted "null" remains the literal string. - Path traversal (P1/security): the settings API now rejects a zims_destination containing "..", and start_kiwix_serve.sh defends in depth by falling back to `zims` for any absolute/traversal/empty value, so Kiwix can never scan or rewrite library.xml outside the media root. - Reproducible build (P2): pin the zim-tools image to 3.7.0 instead of :latest. Tests added for null handling, traversal rejection (API + script). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
84 lines
2.9 KiB
Python
84 lines
2.9 KiB
Python
"""Tests for wrolpi/scripts/read_config_value.sh.
|
|
|
|
The reader extracts a top-level scalar from wrolpi.yaml with zero dependencies
|
|
so non-Python scripts (e.g. start_kiwix_serve.sh, the Docker Zim entrypoint) can
|
|
read config values. Exercised directly via bash.
|
|
"""
|
|
import os
|
|
import pathlib
|
|
import subprocess
|
|
|
|
import pytest
|
|
|
|
REPO_ROOT = pathlib.Path(__file__).parents[3]
|
|
READER = REPO_ROOT / 'wrolpi' / 'scripts' / 'read_config_value.sh'
|
|
|
|
|
|
def _read(media: pathlib.Path, key: str, default: str = '') -> str:
|
|
env = {**os.environ, 'MEDIA_DIRECTORY': str(media)}
|
|
result = subprocess.run(['bash', str(READER), key, default], env=env,
|
|
capture_output=True, text=True, timeout=30)
|
|
assert result.returncode == 0, result.stderr
|
|
return result.stdout.strip('\n')
|
|
|
|
|
|
@pytest.fixture
|
|
def media_with_config(tmp_path):
|
|
media = tmp_path / 'media'
|
|
(media / 'config').mkdir(parents=True)
|
|
|
|
def write(text: str):
|
|
(media / 'config' / 'wrolpi.yaml').write_text(text)
|
|
|
|
return media, write
|
|
|
|
|
|
def test_reads_scalar_value(media_with_config):
|
|
media, write = media_with_config
|
|
write('zims_destination: mycustomzims\nvideos_destination: videos\n')
|
|
assert _read(media, 'zims_destination', 'zims') == 'mycustomzims'
|
|
|
|
|
|
def test_strips_surrounding_quotes(media_with_config):
|
|
media, write = media_with_config
|
|
write('videos_destination: "quoted videos"\n')
|
|
assert _read(media, 'videos_destination', 'x') == 'quoted videos'
|
|
|
|
|
|
def test_missing_key_returns_default(media_with_config):
|
|
media, write = media_with_config
|
|
write('videos_destination: videos\n')
|
|
assert _read(media, 'zims_destination', 'zims') == 'zims'
|
|
|
|
|
|
def test_empty_value_returns_default(media_with_config):
|
|
media, write = media_with_config
|
|
write('zims_destination:\n')
|
|
assert _read(media, 'zims_destination', 'zims') == 'zims'
|
|
|
|
|
|
@pytest.mark.parametrize('null_repr', ['null', 'Null', 'NULL', '~'])
|
|
def test_yaml_null_returns_default(media_with_config, null_repr):
|
|
"""An unquoted YAML null means "unset" -> default, matching the Python config layer."""
|
|
media, write = media_with_config
|
|
write(f'zims_destination: {null_repr}\n')
|
|
assert _read(media, 'zims_destination', 'zims') == 'zims'
|
|
|
|
|
|
def test_quoted_null_is_literal_string(media_with_config):
|
|
"""A quoted "null" is the string null, not YAML null."""
|
|
media, write = media_with_config
|
|
write('zims_destination: "null"\n')
|
|
assert _read(media, 'zims_destination', 'zims') == 'null'
|
|
|
|
|
|
def test_missing_config_file_returns_default(tmp_path):
|
|
# No config directory/file at all (fresh install) -> default, no error.
|
|
assert _read(tmp_path / 'media', 'zims_destination', 'zims') == 'zims'
|
|
|
|
|
|
def test_ignores_indented_nested_keys(media_with_config):
|
|
media, write = media_with_config
|
|
# A nested key with the same name must not be matched at the top level.
|
|
write('some_section:\n zims_destination: nested\ntop: value\n')
|
|
assert _read(media, 'zims_destination', 'zims') == 'zims'
|