mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 19:47:12 +01:00
The network layer was the wrong place for this. Tested remotely today: opening RethinkDNS and switching off its device blocklists un-blocked YouTube in a few taps, and no device owner API can stop that -- an on-device filter app always owns its own rules. App hiding is different. 'pm unhide' from adb shell throws a SecurityException and the package stays hidden; only the device owner can reverse it, and reaching that costs a factory reset. So the block moves there, scoped to an explicit allowlist and the home geofence. Both whitelists are rewritten from the apps that actually exist on this phone. The old ones described the rooted Blackview. Every browser is excluded now, so at home a link does not open at all -- confirmed as intended. LAUNCHER_PACKAGE was still the Blackview's launcher, which is not installed here. The exporter refused to render the policy twice over it -- precisely what that check is for. The network layer stays a soft deterrent by choice: pinned and uninstall-blocked, but without lockdown, and its blocklists remain switchable. Private DNS is implemented but ships disabled. stunnel logged zero connections from the handset across two pin attempts, so the router does not forward 853. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DqEd1uUhyT52KjaNeLanC
222 lines
8.0 KiB
Python
222 lines
8.0 KiB
Python
"""Tests for the JSON policy exporter.
|
|
|
|
The guarantee under test is the one that cannot be recovered from on the device
|
|
itself: the enforcing app must never be rendered into a policy that would hide
|
|
it. Everything else here is shape, not safety.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import datetime as dt
|
|
|
|
from python_pkg.focus_policy.export import (
|
|
ENFORCER_PACKAGE,
|
|
policy_to_dict,
|
|
policy_to_json,
|
|
)
|
|
from python_pkg.focus_policy.model import CurfewWindow, FocusPolicy, HomeLocation
|
|
|
|
_DEFAULT_CURFEW = CurfewWindow(start=dt.time(23, 0), end=dt.time(5, 0))
|
|
|
|
|
|
def _policy(
|
|
*,
|
|
# The launcher must be allowed or the model rejects the policy outright.
|
|
allowed_packages: frozenset[str] = frozenset({"pl.mbank", "com.launcher"}),
|
|
night_allowed_packages: frozenset[str] = frozenset({"pl.mbank", "com.launcher"}),
|
|
curfew: CurfewWindow | None = _DEFAULT_CURFEW,
|
|
never_disable_prefixes: tuple[str, ...] = ("com.android.",),
|
|
) -> FocusPolicy:
|
|
"""Build a policy with the enforcer deliberately absent from both lists."""
|
|
return FocusPolicy(
|
|
home=HomeLocation(
|
|
latitude=52.2297,
|
|
longitude=21.0122,
|
|
radius_m=150.0,
|
|
hysteresis_m=30.0,
|
|
),
|
|
curfew=curfew,
|
|
launcher_package="com.launcher",
|
|
allowed_packages=allowed_packages,
|
|
night_allowed_packages=night_allowed_packages,
|
|
never_disable_prefixes=never_disable_prefixes,
|
|
workout_unblock_domains=frozenset({"youtube.com"}),
|
|
browser_packages=frozenset({"org.mozilla.firefox"}),
|
|
)
|
|
|
|
|
|
def test_enforcer_is_injected_into_both_allowlists() -> None:
|
|
"""The app must be allowed even when the source config omits it.
|
|
|
|
Regression guard for the on-device finding of 2026-08-09: the enforcer was
|
|
in neither allowlist, so the decision layer placed it in the hide set on
|
|
every enforcing pass, leaving one in-app check as the sole thing preventing
|
|
the app from hiding itself.
|
|
"""
|
|
payload = policy_to_dict(_policy())
|
|
|
|
assert ENFORCER_PACKAGE in payload["allowed_packages"]
|
|
assert ENFORCER_PACKAGE in payload["night_allowed_packages"]
|
|
|
|
|
|
def test_enforcer_is_not_duplicated_when_already_present() -> None:
|
|
"""Injection is a set union, so an explicit entry stays single."""
|
|
payload = policy_to_dict(
|
|
_policy(
|
|
allowed_packages=frozenset({"pl.mbank", "com.launcher", ENFORCER_PACKAGE}),
|
|
night_allowed_packages=frozenset({"com.launcher", ENFORCER_PACKAGE}),
|
|
),
|
|
)
|
|
|
|
assert payload["allowed_packages"].count(ENFORCER_PACKAGE) == 1
|
|
assert payload["night_allowed_packages"].count(ENFORCER_PACKAGE) == 1
|
|
|
|
|
|
def test_blockable_system_packages_names_the_distraction_apps() -> None:
|
|
"""The sweep is default-deny for system apps; this is the opt-in.
|
|
|
|
YouTube and Chrome ship with FLAG_SYSTEM, so without this list the Device
|
|
Owner sweep skips exactly the apps it exists to block. Play is included
|
|
because leaving it reachable makes every other removal a one-tap undo.
|
|
"""
|
|
blockable = policy_to_dict(_policy())["blockable_system_packages"]
|
|
|
|
assert "com.google.android.youtube" in blockable
|
|
assert "com.android.chrome" in blockable
|
|
assert "com.android.vending" in blockable
|
|
|
|
|
|
def test_blockable_system_packages_never_contradicts_the_allowlist() -> None:
|
|
"""An allowed package must not also be listed as blockable.
|
|
|
|
Otherwise the asset would state both at once and the outcome would depend
|
|
on which check the runner happened to apply first.
|
|
"""
|
|
payload = policy_to_dict(
|
|
_policy(
|
|
allowed_packages=frozenset(
|
|
{"pl.mbank", "com.launcher", "com.android.chrome"},
|
|
),
|
|
),
|
|
)
|
|
|
|
assert "com.android.chrome" not in payload["blockable_system_packages"]
|
|
assert "com.android.chrome" in payload["allowed_packages"]
|
|
|
|
|
|
def test_always_blocked_exempts_youtube_and_chrome_from_the_geofence() -> None:
|
|
"""The geofence must not become an off switch for these.
|
|
|
|
Everything else is restored on the AWAY branch, which makes leaving the
|
|
house a way to switch enforcement off -- the specific thing Device Owner
|
|
was provisioned to remove. Play is deliberately absent: it stays geofenced
|
|
so apps can still be installed away from home.
|
|
"""
|
|
always = policy_to_dict(_policy())["always_blocked_packages"]
|
|
|
|
assert "com.google.android.youtube" in always
|
|
assert "com.google.android.apps.youtube.music" in always
|
|
assert "com.android.chrome" in always
|
|
assert "com.android.vending" not in always
|
|
|
|
|
|
def test_always_on_vpn_requires_a_sweep_protected_provider() -> None:
|
|
"""Pinning a package the enforcer can hide is the worst failure mode.
|
|
|
|
DISALLOW_CONFIG_VPN pointing at a hidden app costs general connectivity,
|
|
not just the filter, so the field is emitted only when the provider is
|
|
protected.
|
|
"""
|
|
unprotected = policy_to_dict(_policy())["always_on_vpn_package"]
|
|
protected = policy_to_dict(
|
|
_policy(never_disable_prefixes=("com.celzero.bravedns",)),
|
|
)["always_on_vpn_package"]
|
|
|
|
assert unprotected == ""
|
|
assert protected == "com.celzero.bravedns"
|
|
|
|
|
|
def test_private_dns_host_is_exported_for_pinning() -> None:
|
|
"""The domain rules live on this resolver, not in the VPN app.
|
|
|
|
Measured on 2026-08-11: RethinkDNS's blocklists can be switched off from
|
|
inside that app in a few taps, and no device owner API prevents it. A
|
|
pinned Private DNS host moves the rules somewhere the phone cannot edit.
|
|
"""
|
|
payload = policy_to_dict(_policy())
|
|
|
|
# Empty until the resolver is reachable from the phone: pinning an
|
|
# unreachable host means no DNS at all, and Android refuses it anyway
|
|
# (PRIVATE_DNS_SET_ERROR_HOST_NOT_SERVING).
|
|
assert payload["private_dns_host"] == ""
|
|
assert payload["vpn_lockdown"] is True
|
|
|
|
|
|
def test_always_blocked_is_a_subset_of_blockable() -> None:
|
|
"""An always-blocked system app the sweep cannot see would silently no-op.
|
|
|
|
The runner filters FLAG_SYSTEM packages out of ``installedPackages``
|
|
unless they are opted in, so the decision layer would never receive them.
|
|
"""
|
|
payload = policy_to_dict(_policy())
|
|
|
|
assert set(payload["always_blocked_packages"]) <= set(
|
|
payload["blockable_system_packages"],
|
|
)
|
|
|
|
|
|
def test_always_blocked_never_contradicts_the_allowlist() -> None:
|
|
"""Allowing a package wins over always-blocking it.
|
|
|
|
The asset must not state both at once; the subtraction keeps the two
|
|
fields consistent no matter what config.sh says.
|
|
"""
|
|
payload = policy_to_dict(
|
|
_policy(
|
|
allowed_packages=frozenset(
|
|
{"pl.mbank", "com.launcher", "com.android.chrome"},
|
|
),
|
|
),
|
|
)
|
|
|
|
assert "com.android.chrome" not in payload["always_blocked_packages"]
|
|
assert "com.android.chrome" in payload["allowed_packages"]
|
|
|
|
|
|
def test_lists_are_sorted_for_stable_diffs() -> None:
|
|
"""Sorted output keeps a committed asset's diff meaningful."""
|
|
payload = policy_to_dict(_policy())
|
|
|
|
for key in (
|
|
"allowed_packages",
|
|
"night_allowed_packages",
|
|
"never_disable_prefixes",
|
|
"workout_unblock_domains",
|
|
"browser_packages",
|
|
):
|
|
assert payload[key] == sorted(payload[key])
|
|
|
|
|
|
def test_curfew_is_rendered_as_wall_clock_strings() -> None:
|
|
"""Kotlin parses `HH:MM`, not an ISO time."""
|
|
assert policy_to_dict(_policy())["curfew"] == {"start": "23:00", "end": "05:00"}
|
|
|
|
|
|
def test_absent_curfew_renders_as_null() -> None:
|
|
"""A policy with no curfew must not fabricate one."""
|
|
assert policy_to_dict(_policy(curfew=None))["curfew"] is None
|
|
|
|
|
|
def test_redact_home_blanks_coordinates_but_keeps_geometry() -> None:
|
|
"""A committed asset discloses the radius, never the location."""
|
|
payload = policy_to_json(_policy(), redact_home=True)
|
|
|
|
assert '"latitude": null' in payload
|
|
assert '"longitude": null' in payload
|
|
assert '"radius_m": 150.0' in payload
|
|
|
|
|
|
def test_coordinates_survive_when_not_redacted() -> None:
|
|
"""The provisioning path still needs the real values."""
|
|
assert '"latitude": 52.2297' in policy_to_json(_policy())
|