Files
kadence/backend/tests/test_blocker.py
T
timka123andClaude Opus 5 5facf41f10 blocker: an exception reads like a block, so make it behave like one
`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>
2026-10-22 17:24:17 +03:00

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"]