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>
186 lines
6.8 KiB
Python
186 lines
6.8 KiB
Python
"""Tests for scripts/start_kiwix_serve.sh.
|
|
|
|
The script imports every *.zim in the Zim directory into a Kiwix library, then
|
|
starts kiwix-serve. These tests exercise the script directly with fake
|
|
`kiwix-manage`/`kiwix-serve` binaries on PATH so they need no real Kiwix
|
|
installation. Key invariants:
|
|
|
|
- A single corrupt/incomplete zim must never prevent the remaining valid zims
|
|
from being served, nor stop the server from starting.
|
|
- The configured Zim directory (wrolpi.yaml `zims_destination`) is honored.
|
|
- When no functioning zim is found, the default Zim is served as a fallback.
|
|
"""
|
|
import os
|
|
import pathlib
|
|
import stat
|
|
import subprocess
|
|
|
|
import pytest
|
|
|
|
REPO_ROOT = pathlib.Path(__file__).parents[3]
|
|
SCRIPT = REPO_ROOT / 'scripts' / 'start_kiwix_serve.sh'
|
|
|
|
# Fake kiwix-manage: refuses any zim whose contents contain "CORRUPT" (mimicking
|
|
# kiwix-manage's "Cannot add zim ..." rejection of partial/corrupt files),
|
|
# otherwise appends a <book> entry to the library file.
|
|
FAKE_KIWIX_MANAGE = """#!/usr/bin/env bash
|
|
library="$1"; shift
|
|
[ "$1" = "add" ] && shift
|
|
zim="$1"
|
|
if grep -q CORRUPT "${zim}" 2>/dev/null; then
|
|
echo "Cannot add zim ${zim}" >&2
|
|
exit 1
|
|
fi
|
|
if [ ! -f "${library}" ]; then
|
|
printf '<?xml version="1.0" encoding="UTF-8"?>\\n<library version="20110515">\\n' > "${library}"
|
|
fi
|
|
printf ' <book path="%s"/>\\n' "${zim}" >> "${library}"
|
|
exit 0
|
|
"""
|
|
|
|
# Fake kiwix-serve: records that it was started (and with which library) so the
|
|
# test can assert the server came up, then exits 0.
|
|
FAKE_KIWIX_SERVE = """#!/usr/bin/env bash
|
|
library=""
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--library) library="$2"; shift 2;;
|
|
*) shift;;
|
|
esac
|
|
done
|
|
echo "${library}" > "${STARTED_MARKER}"
|
|
exit 0
|
|
"""
|
|
|
|
|
|
@pytest.fixture
|
|
def kiwix_env(tmp_path):
|
|
"""Set up a fake media directory and fake kiwix binaries on PATH."""
|
|
media = tmp_path / 'media'
|
|
zims = media / 'zims'
|
|
zims.mkdir(parents=True)
|
|
(media / 'config').mkdir()
|
|
|
|
bin_dir = tmp_path / 'bin'
|
|
bin_dir.mkdir()
|
|
for name, body in (('kiwix-manage', FAKE_KIWIX_MANAGE), ('kiwix-serve', FAKE_KIWIX_SERVE)):
|
|
p = bin_dir / name
|
|
p.write_text(body)
|
|
p.chmod(p.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
|
|
|
|
started_marker = tmp_path / 'started'
|
|
# Points at a file that does not exist by default (so the fallback is a
|
|
# no-op); the fallback test creates it.
|
|
default_zim = tmp_path / 'blobs' / 'default.zim'
|
|
|
|
def write_config(text: str):
|
|
(media / 'config' / 'wrolpi.yaml').write_text(text)
|
|
|
|
def run():
|
|
env = {
|
|
**os.environ,
|
|
'MEDIA_DIRECTORY': str(media),
|
|
'PROJECT_DIR': str(REPO_ROOT),
|
|
'DEFAULT_ZIM': str(default_zim),
|
|
'PATH': f'{bin_dir}:{os.environ["PATH"]}',
|
|
'STARTED_MARKER': str(started_marker),
|
|
}
|
|
return subprocess.run(['bash', str(SCRIPT)], env=env, capture_output=True, text=True, timeout=60)
|
|
|
|
def library_text():
|
|
lib = zims / 'library.xml'
|
|
return lib.read_text() if lib.exists() else ''
|
|
|
|
return dict(media=media, zims=zims, library=zims / 'library.xml', started_marker=started_marker,
|
|
default_zim=default_zim, write_config=write_config, library_text=library_text, run=run)
|
|
|
|
|
|
def _write_zim(path: pathlib.Path, corrupt: bool = False):
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text('CORRUPT' if corrupt else 'ZIMDATA')
|
|
|
|
|
|
def test_valid_zim_starts_server(kiwix_env):
|
|
"""A valid zim is imported and the server starts."""
|
|
_write_zim(kiwix_env['zims'] / 'wikipedia.zim')
|
|
|
|
result = kiwix_env['run']()
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert kiwix_env['started_marker'].exists(), 'kiwix-serve was not started'
|
|
assert 'wikipedia.zim' in kiwix_env['library'].read_text()
|
|
|
|
|
|
def test_corrupt_zim_does_not_prevent_serving_valid_zims(kiwix_env):
|
|
"""A corrupt zim must be skipped while the valid zims are still served."""
|
|
_write_zim(kiwix_env['zims'] / 'good.zim')
|
|
_write_zim(kiwix_env['zims'] / 'sudan_broken.zim', corrupt=True)
|
|
|
|
result = kiwix_env['run']()
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert kiwix_env['started_marker'].exists(), 'kiwix-serve was not started'
|
|
library = kiwix_env['library'].read_text()
|
|
assert 'good.zim' in library
|
|
assert 'sudan_broken.zim' not in library
|
|
|
|
|
|
def test_only_corrupt_zim_still_starts_server(kiwix_env):
|
|
"""Even when the only zim is corrupt, kiwix must still start."""
|
|
_write_zim(kiwix_env['zims'] / 'sudan_broken.zim', corrupt=True)
|
|
|
|
result = kiwix_env['run']()
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert kiwix_env['started_marker'].exists(), 'kiwix-serve was not started'
|
|
assert kiwix_env['library'].exists()
|
|
|
|
|
|
def test_no_zims_still_starts_server(kiwix_env):
|
|
"""With no zim files at all, kiwix still starts with an empty library."""
|
|
result = kiwix_env['run']()
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert kiwix_env['started_marker'].exists(), 'kiwix-serve was not started'
|
|
|
|
|
|
def test_honors_configured_zims_destination(kiwix_env):
|
|
"""A custom zims_destination in wrolpi.yaml is used instead of the default `zims`."""
|
|
kiwix_env['write_config']('zims_destination: mycustomzims\n')
|
|
_write_zim(kiwix_env['media'] / 'mycustomzims' / 'wikipedia.zim')
|
|
|
|
result = kiwix_env['run']()
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert kiwix_env['started_marker'].exists(), 'kiwix-serve was not started'
|
|
# The library lives in the configured directory and references the zim there.
|
|
library = kiwix_env['media'] / 'mycustomzims' / 'library.xml'
|
|
assert library.exists()
|
|
assert 'mycustomzims/wikipedia.zim' in library.read_text()
|
|
|
|
|
|
def test_traversal_zims_destination_is_rejected(kiwix_env):
|
|
"""A traversal zims_destination must not let Kiwix escape the media root; it falls back to `zims`."""
|
|
kiwix_env['write_config']('zims_destination: ../../escape\n')
|
|
_write_zim(kiwix_env['zims'] / 'wikipedia.zim') # a valid zim in the safe default dir
|
|
|
|
result = kiwix_env['run']()
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert kiwix_env['started_marker'].exists(), 'kiwix-serve was not started'
|
|
# Served from the safe `zims` directory, and no library.xml was created outside media.
|
|
assert kiwix_env['library'].exists()
|
|
assert 'wikipedia.zim' in kiwix_env['library'].read_text()
|
|
assert not (kiwix_env['media'].parent / 'escape').exists()
|
|
|
|
|
|
def test_falls_back_to_default_zim_when_no_usable_zim(kiwix_env):
|
|
"""When no functioning zim is present, the default Zim is served."""
|
|
# No zims in the directory; create the default Zim the script should fall back to.
|
|
_write_zim(kiwix_env['default_zim'])
|
|
|
|
result = kiwix_env['run']()
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert kiwix_env['started_marker'].exists(), 'kiwix-serve was not started'
|
|
assert 'default.zim' in kiwix_env['library'].read_text()
|