testsAndMisc/linux_configuration/tests/test_strip_workout_hosts.py
Krzysztof kuhy Rudnicki f636d2b21b Make focus policy and blocklist usable without root; fix workout CDN unblock
phone_focus_mode is built for a rooted phone: the hosts layer is a read-only
bind mount over /system/etc/hosts, chosen so that even root-in-a-terminal
cannot overwrite it. None of that ports to a stock locked device. This splits
out the parts that are device-independent so one policy can drive both.

python_pkg/focus_policy parses config.sh rather than copying it, so the shell
config stays the source of truth and the two cannot drift. It rejects the
shipped REDACTED_* coordinate placeholder instead of defaulting to (0,0),
which would move "home" to the Atlantic and quietly disable the location gate.

blocklist.py converts the hosts file to domains for a DoT resolver or a local
VpnService. Three properties of the real 194k-line file make the obvious parser
wrong, and all three are handled: generate_hosts_file.sh *comments out*
deliberate unblocks rather than deleting them, so stripping '#' re-blocks
facebook/messenger/linkedin/4chan; the StevenBlack header carries loopback and
IPv6 housekeeping that would blackhole localhost; and ~1060 upstream annotation
tokens ('#[adware.zeno]', '"spam') render as address=/"spam/, which makes
dnsmasq refuse to start -- taking DNS down rather than blocking anything.
Verified with dnsmasq --test over all 185681 generated domains.

strip_workout_hosts.py had the same class of bug on the live rooted phone:
exact set membership meant the allowlisted googlevideo.com never matched the
dynamic r*---sn-*.googlevideo.com hosts YouTube actually streams from, so the
workout unblock was only partly working. Now suffix-matched (17 lines stripped,
was 14). The three new tests were checked against the original code in a
throwaway worktree; two of them fail there, so they pin the fix rather than
passing vacuously.

focus_owner is a device-owner app that deliberately does nothing yet. It ships
clearDeviceOwnerApp() implemented and tested first, because dpm
remove-active-admin does not work on a device owner: an app that locks the
device down before proving it can let go leaves a soft-brick recoverable only
through fastboot on a locked bootloader. Built, installed and launched on the
Pixel 6a; dumpsys afterwards confirms Device Owner Type -1 and no user
restrictions, i.e. the phone is unchanged.

Measured while scoping this, and not yet reflected in any enforcement code:
on Android 16 both pm suspend and pm disable-user are cleared by a reboot, so
shell-level app blocking is per-session only. setApplicationHidden is assumed
to persist under Device Owner but is untested.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL
2026-08-05 09:15:02 +02:00

140 lines
5.0 KiB
Python

"""Tests for phone_focus_mode/strip_workout_hosts.py (hosts workout variant)."""
from __future__ import annotations
from typing import TYPE_CHECKING
import strip_workout_hosts
from strip_workout_hosts import _strip, main
if TYPE_CHECKING:
from pathlib import Path
import pytest
_HOSTS = """\
# header
127.0.0.1 localhost
0.0.0.0 youtube.com www.youtube.com
0.0.0.0 ads.example.com
0.0.0.0 m.youtube.com alias.youtube.com
0.0.0.0 keepme.com
"""
def _src(tmp_path: Path) -> Path:
"""Write the sample hosts file and return its path."""
path = tmp_path / "hosts"
path.write_text(_HOSTS, encoding="utf-8")
return path
class TestStrip:
"""``_strip`` removes lines mapping unblocked domains."""
def test_removes_matching_and_alias_lines(self, tmp_path: Path) -> None:
"""Entries whose name or any alias is unblocked are dropped."""
dest = tmp_path / "out"
_strip(
_src(tmp_path),
dest,
frozenset({"youtube.com", "www.youtube.com", "m.youtube.com"}),
)
result = dest.read_text(encoding="utf-8")
assert "youtube.com" not in result # both youtube entries gone
assert "ads.example.com" in result
assert "keepme.com" in result
assert "localhost" in result
assert "# header" in result
def test_empty_unblock_keeps_everything(self, tmp_path: Path) -> None:
"""An empty unblock set copies the file verbatim."""
dest = tmp_path / "out"
_strip(_src(tmp_path), dest, frozenset())
assert dest.read_text(encoding="utf-8") == _HOSTS
def test_comment_and_blank_lines_preserved(self, tmp_path: Path) -> None:
"""Comment and blank lines are never stripped."""
src = tmp_path / "hosts"
src.write_text("# c\n\n0.0.0.0 block.me\n", encoding="utf-8")
dest = tmp_path / "out"
_strip(src, dest, frozenset({"block.me"}))
assert dest.read_text(encoding="utf-8") == "# c\n\n"
class TestMain:
"""The CLI reads paths from argv and domains from the environment."""
def test_bad_arg_count_returns_2(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""A wrong number of path arguments is a usage error (rc 2)."""
monkeypatch.setattr(
strip_workout_hosts.sys, "argv", ["strip_workout_hosts", "only-one"]
)
assert main() == 2
def test_strips_via_env(
self,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Domains from the env var drive the stripping."""
src = _src(tmp_path)
dest = tmp_path / "out"
monkeypatch.setenv("WORKOUT_UNBLOCK_DOMAINS", "youtube.com m.youtube.com")
monkeypatch.setattr(
strip_workout_hosts.sys,
"argv",
["strip_workout_hosts", str(src), str(dest)],
)
assert main() == 0
result = dest.read_text(encoding="utf-8")
assert "www.youtube.com" not in result
assert "keepme.com" in result
class TestSubdomainMatching:
"""Regression tests for the exact-match bug fixed on 2026-08-05.
The original implementation tested ``name.lower() in unblock``, i.e. exact
set membership. YouTube streams video from dynamically named CDN hosts, so
an allowlist entry of ``googlevideo.com`` released nothing that actually
carried video and playback stayed blocked during workouts.
"""
def test_releases_dynamic_cdn_subdomains(self, tmp_path: Path) -> None:
"""A parent domain releases the CDN hosts beneath it."""
src = tmp_path / "hosts"
src.write_text(
"0.0.0.0 googlevideo.com\n"
"0.0.0.0 r1---sn-4g5e6nls.googlevideo.com\n"
"0.0.0.0 r1---sn-4g5lne7s.googlevideo.com\n"
"0.0.0.0 keepme.com\n",
encoding="utf-8",
)
dest = tmp_path / "out"
_strip(src, dest, frozenset({"googlevideo.com"}))
result = dest.read_text(encoding="utf-8")
assert "googlevideo.com" not in result
assert "keepme.com" in result
def test_does_not_release_lookalike_domains(self, tmp_path: Path) -> None:
"""Suffix matching must not be fooled by substring similarity."""
src = tmp_path / "hosts"
src.write_text(
"0.0.0.0 notgooglevideo.com\n0.0.0.0 googlevideo.com.evil.net\n",
encoding="utf-8",
)
dest = tmp_path / "out"
_strip(src, dest, frozenset({"googlevideo.com"}))
result = dest.read_text(encoding="utf-8")
assert "notgooglevideo.com" in result
assert "googlevideo.com.evil.net" in result
def test_case_and_trailing_dot_are_normalised(self, tmp_path: Path) -> None:
"""Host names are compared case-insensitively and without the root dot."""
src = tmp_path / "hosts"
src.write_text("0.0.0.0 WWW.YouTube.COM.\n", encoding="utf-8")
dest = tmp_path / "out"
_strip(src, dest, frozenset({"youtube.com"}))
assert dest.read_text(encoding="utf-8") == ""