`youtube.com` on the block list catches `music.youtube.com` — a plain entry covers its subtree. `youtube.com` on the *allow* list caught only itself, because exceptions were glob-matched whatever they looked like and a plain name has no metacharacters. The same text in two boxes of the same form meant a subtree in one and a single hostname in the other, with nothing in the UI saying so. A plain exception now covers its subdomains; a masked one is glob-matched exactly as before, which is still how you spare something narrower than a whole subtree (`docs.*`, `*.docs.google.com`). Three enforcers decide this and all three had to move together, or an exception would work in the browser and not at the resolver: `blocker_service.is_allowed`, `:core`'s `isAllowed`, and the desktop DNS stub's own allow check, which was reusing the generic glob matcher. Parity tests on both sides, plus the two sentences in the web UI and the README that promised the old behaviour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
192 lines
8.2 KiB
Python
192 lines
8.2 KiB
Python
"""Blocking policy: when it is on, and what is on the list."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from app.services import blocker_service, pomodoro, settings_service
|
|
|
|
|
|
def test_disabled_blocker_blocks_nothing(session, settings):
|
|
pomodoro.start(session, settings, kind="work")
|
|
policy = blocker_service.policy(session, settings)
|
|
assert policy["active"] is False and policy["domains"] == []
|
|
|
|
|
|
def test_during_work_mode_follows_the_timer(session, settings):
|
|
settings.blocker.enabled = True
|
|
settings.blocker.blocked_domains = ["youtube.com"]
|
|
settings_service.save_settings(session, settings)
|
|
|
|
assert blocker_service.policy(session, settings)["active"] is False
|
|
|
|
pomodoro.start(session, settings, kind="work")
|
|
policy = blocker_service.policy(session, settings)
|
|
assert policy["active"] is True
|
|
assert "www.youtube.com" in policy["domains"]
|
|
|
|
pomodoro.complete(session, settings, auto_next=False)
|
|
assert blocker_service.policy(session, settings)["active"] is False
|
|
|
|
|
|
def test_breaks_do_not_block_unless_asked(session, settings):
|
|
settings.blocker.enabled = True
|
|
settings_service.save_settings(session, settings)
|
|
pomodoro.start(session, settings, kind="short_break")
|
|
assert blocker_service.policy(session, settings)["active"] is False
|
|
|
|
settings.blocker.block_during_breaks = True
|
|
settings_service.save_settings(session, settings)
|
|
assert blocker_service.policy(session, settings)["active"] is True
|
|
|
|
|
|
def test_always_mode_ignores_the_timer(session, settings):
|
|
settings.blocker.enabled = True
|
|
settings.blocker.mode = "always"
|
|
settings_service.save_settings(session, settings)
|
|
assert blocker_service.policy(session, settings)["active"] is True
|
|
|
|
|
|
def test_allowlist_wins_over_the_blocklist(session, settings):
|
|
settings.blocker.blocked_domains = ["youtube.com"]
|
|
settings.blocker.allowed_domains = ["m.youtube.com"]
|
|
hosts = blocker_service.expand_domains(settings)
|
|
assert "youtube.com" in hosts
|
|
assert "m.youtube.com" not in hosts
|
|
|
|
|
|
def test_events_are_grouped_for_the_report(session, settings):
|
|
for _ in range(3):
|
|
blocker_service.record_event(session, settings, kind="domain", target="YouTube.com")
|
|
blocker_service.record_event(session, settings, kind="app", target="steam")
|
|
summary = blocker_service.events_for_day(session, settings)
|
|
assert summary["total"] == 4
|
|
assert summary["by_target"][0] == {"target": "youtube.com", "count": 3}
|
|
|
|
|
|
def test_masked_subdomains_are_expanded(session, settings):
|
|
"""A hosts file has no wildcards, so `*.x` has to become real names."""
|
|
settings.blocker.blocked_domains = ["*.google.com"]
|
|
settings.blocker.subdomain_expansions = ["www", "mail"]
|
|
hosts = blocker_service.expand_domains(settings)
|
|
assert hosts == ["google.com", "www.google.com", "mail.google.com"]
|
|
|
|
|
|
def test_a_plain_exception_covers_its_subdomains_like_a_plain_block_does(session, settings):
|
|
"""The two halves of one list have to read the same way.
|
|
|
|
`youtube.com` on the block list catches `music.youtube.com`. For a long time
|
|
`youtube.com` on the *allow* list caught only itself, because exceptions were
|
|
glob-matched — so the same text meant a subtree in one box and one hostname
|
|
in the other, with nothing saying so.
|
|
"""
|
|
settings.blocker.blocked_domains = ["*.youtube.com", "reddit.com"]
|
|
settings.blocker.subdomain_expansions = ["www", "m", "music"]
|
|
settings.blocker.allowed_domains = ["youtube.com"]
|
|
|
|
hosts = blocker_service.expand_domains(settings)
|
|
|
|
assert hosts == ["reddit.com", "www.reddit.com", "m.reddit.com"]
|
|
assert blocker_service.is_allowed("music.youtube.com", ["youtube.com"])
|
|
assert not blocker_service.is_allowed("notyoutube.com", ["youtube.com"])
|
|
|
|
|
|
def test_a_masked_exception_is_still_a_glob(session, settings):
|
|
"""Which is how you allow something narrower than a whole subtree."""
|
|
assert blocker_service.is_allowed("docs.google.com", ["docs.*"])
|
|
assert blocker_service.is_allowed("a.docs.google.com", ["*.docs.google.com"])
|
|
assert not blocker_service.is_allowed("sheets.google.com", ["docs.*"])
|
|
|
|
|
|
def test_allow_masks_carve_holes_in_block_masks(session, settings):
|
|
settings.blocker.blocked_domains = ["*.google.com", "reddit.com"]
|
|
settings.blocker.subdomain_expansions = ["www", "docs"]
|
|
settings.blocker.allowed_domains = ["docs.*", "m.reddit.com"]
|
|
hosts = blocker_service.expand_domains(settings)
|
|
assert "docs.google.com" not in hosts
|
|
assert "m.reddit.com" not in hosts
|
|
assert "www.google.com" in hosts and "reddit.com" in hosts
|
|
|
|
|
|
def test_patterns_a_hosts_file_cannot_express_are_reported(session, settings):
|
|
settings.blocker.blocked_domains = ["*gram*", "youtube.com", "*.x.com"]
|
|
assert blocker_service.unexpandable_masks(settings) == ["*gram*"]
|
|
hosts = blocker_service.expand_domains(settings)
|
|
assert "youtube.com" in hosts and "x.com" in hosts
|
|
|
|
|
|
def test_app_masks_are_passed_through_untouched(session, settings):
|
|
"""Process matching happens on the desktop, where globs work directly."""
|
|
settings.blocker.enabled = True
|
|
settings.blocker.mode = "always"
|
|
settings.blocker.blocked_apps = ["steam*", "*discord*"]
|
|
settings_service.save_settings(session, settings)
|
|
policy = blocker_service.policy(session, settings)
|
|
assert policy["apps"] == ["steam*", "*discord*"]
|
|
|
|
|
|
def test_a_wildcard_means_every_subdomain(session, settings):
|
|
"""`*` in the expansion list is the user asking for all of them.
|
|
|
|
It used to be refused (it is not a DNS label), which left `*.telegram.org`
|
|
expanded to just the bare domain with a red banner beside it. Now the labels
|
|
still expand, the pattern is handed to the clients that can match masks, and
|
|
only entries that are neither a label nor `*` are refused.
|
|
"""
|
|
settings.blocker.blocked_domains = ["*.telegram.org"]
|
|
settings.blocker.subdomain_expansions = ["*", "web", "", "not a label"]
|
|
|
|
expansion = blocker_service.parse_expansions(settings.blocker.subdomain_expansions)
|
|
assert expansion.labels == ["web"]
|
|
assert expansion.wildcard is True
|
|
assert expansion.rejected == ["not a label"]
|
|
|
|
hosts = blocker_service.expand_domains(settings)
|
|
assert hosts == ["telegram.org", "web.telegram.org"]
|
|
assert not any("*" in host for host in hosts)
|
|
|
|
# The pattern comes back too: a hosts file cannot say "every subdomain", so
|
|
# the desktop app's resolver stub and the phone's URL matcher take it.
|
|
assert blocker_service.unexpandable_masks(settings) == ["*.telegram.org"]
|
|
|
|
|
|
def test_without_a_wildcard_the_pattern_stays_in_the_expansion_list(session, settings):
|
|
"""`*.x.com` alone means the configured labels, not a wildcard — nothing else."""
|
|
settings.blocker.blocked_domains = ["*.google.com"]
|
|
settings.blocker.subdomain_expansions = ["www", "mail"]
|
|
|
|
assert blocker_service.unexpandable_masks(settings) == []
|
|
assert blocker_service.expand_domains(settings) == [
|
|
"google.com",
|
|
"www.google.com",
|
|
"mail.google.com",
|
|
]
|
|
|
|
|
|
def test_the_policy_reports_expansions_it_had_to_refuse(session, settings):
|
|
settings.blocker.enabled = True
|
|
settings.blocker.mode = "always"
|
|
settings.blocker.blocked_domains = ["*.telegram.org"]
|
|
settings.blocker.subdomain_expansions = ["*", ".."]
|
|
|
|
result = blocker_service.policy(session, settings)
|
|
assert result["invalid_expansions"] == [".."]
|
|
assert result["domains"] == ["telegram.org"]
|
|
assert result["unexpandable"] == ["*.telegram.org"]
|
|
|
|
|
|
def test_the_policy_carries_the_allow_masks_for_the_stub(session, settings):
|
|
"""The desktop app answers DNS for a whole zone, so it needs the exceptions.
|
|
|
|
Without them the resolver stub would sinkhole `music.youtube.com` along with
|
|
everything else under a wildcarded domain — an exception that works over the
|
|
hosts file but not over DNS is an exception nobody will trust.
|
|
"""
|
|
settings.blocker.enabled = True
|
|
settings.blocker.mode = "always"
|
|
settings.blocker.blocked_domains = ["*.youtube.com"]
|
|
settings.blocker.allowed_domains = [" music.youtube.com ", "*.docs.google.com"]
|
|
|
|
result = blocker_service.policy(session, settings)
|
|
assert result["allowed"] == ["music.youtube.com", "*.docs.google.com"]
|
|
assert "music.youtube.com" not in result["domains"]
|
|
assert "m.youtube.com" in result["all_domains"]
|