15 Commits
Author SHA1 Message Date
Djeex b865da38f3 Add labels to Renovate PRs based on update type and vulnerability status
CI / build-and-scan (push) Successful in 33s
2026-08-22 13:02:27 +02:00
Djeex 67b4984664 Temporarily remove Renovate schedule restriction for manual testing
CI / build-and-scan (push) Successful in 30s
2026-08-22 12:44:29 +02:00
Djeex 5eafd7c7cc Add Renovate config, versioned image publishing, and pinned prod requirements
CI / build-and-scan (push) Successful in 43s
2026-08-22 12:17:59 +02:00
Djeex a72b486b3b Fix weekly scheduling crash: schedule library uses full weekday names, not 3-letter abbreviations 2026-08-22 12:17:09 +02:00
Djeex d051d9deb7 Fix CI unit test step with a dedicated Docker build stage
CI / build-and-scan (push) Successful in 34s
2026-08-22 11:11:41 +02:00
Djeex 267d9e52e0 Add unit tests for backup_first_start and run them in CI
CI / build-and-scan (push) Failing after 40s
2026-08-22 11:09:24 +02:00
Djeex 4753d80891 CI workflow with smoke test
CI / build-and-scan (push) Successful in 48s
2026-08-22 10:52:15 +02:00
Djeex 36611ef30b Add CI workflow
CI / build-and-scan (push) Successful in 17s
2026-08-22 10:00:59 +02:00
Djeex 5272796d71 python 3.13 2025-09-19 22:39:27 +00:00
Djeex 157a0f7830 Removed french discord link 2025-07-07 14:05:24 +00:00
Djeex 13379a3419 Merge branch 'wip-python' 2025-06-06 12:40:32 +00:00
Djeex fedbe1e227 Fixed wrong repo url 2025-06-06 12:39:59 +00:00
Djeex 15ba895b04 Merge pull request 'v1.3 - Excluding list (all countries except the specified ones)' (#6) from wip-python into main
Reviewed-on: #6
2025-06-06 14:17:11 +02:00
Djeex 7676d34a39 Excluding list 2025-06-06 12:09:41 +00:00
Djeex c8451688ca Github notice 2025-06-06 11:12:15 +00:00
10 changed files with 425 additions and 29 deletions
+98
View File
@@ -0,0 +1,98 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: "0 6 * * 1"
jobs:
build-and-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build Docker image
run: |
docker build -t adguard-cidre:ci . 2>&1 | tee build.log
if grep -q "Building wheel for" build.log; then
echo "::warning::A dependency was built from source — check Python/Alpine compatibility"
fi
- name: Smoke test (syntax check)
run: |
docker run --rm --entrypoint python adguard-cidre:ci -c "
import ast
with open('blocklist_scheduler.py') as f:
source = f.read()
try:
ast.parse(source)
print('OK: syntax is valid')
except SyntaxError as e:
print(f'::error::Syntax error: {e}')
exit(1)
"
- name: Run unit tests
run: |
docker build --target test -t adguard-cidre:test .
docker run --rm adguard-cidre:test pytest -v
- name: Check deprecation warnings
run: |
docker run --rm --entrypoint python adguard-cidre:ci -W error::DeprecationWarning -c "import blocklist_scheduler" 2>&1 | tee deprecation.log || true
if grep -qi "deprecat" deprecation.log; then
echo "::warning::Deprecation warning detected, check logs"
fi
- name: Scan with Trivy (critical - blocking)
run: |
docker run --rm \
-e DOCKER_HOST=tcp://dockerhost:2375 \
--add-host=dockerhost:host-gateway \
aquasec/trivy:0.74.0 image --exit-code 1 --severity CRITICAL adguard-cidre:ci
- name: Scan with Trivy (high - informative)
run: |
docker run --rm \
-e DOCKER_HOST=tcp://dockerhost:2375 \
--add-host=dockerhost:host-gateway \
aquasec/trivy:0.74.0 image --exit-code 0 --severity HIGH adguard-cidre:ci
- name: Publish tagged image
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: |
BEFORE="${{ github.event.before }}"
if [ -n "$BEFORE" ] && [ "$BEFORE" != "0000000000000000000000000000000000000000" ] && git cat-file -e "$BEFORE" 2>/dev/null; then
CHANGED=$(git diff --name-only "$BEFORE" "${{ github.sha }}")
else
CHANGED=$(git diff --name-only HEAD~1 HEAD)
fi
echo "Changed files:"
echo "$CHANGED"
if ! echo "$CHANGED" | grep -qE '^(Dockerfile|blocklist_scheduler\.py|VERSION)$'; then
echo "No container-relevant file changed, skipping publish."
exit 0
fi
IMAGE=git.djeex.fr/djeex/adguard-cidre
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.djeex.fr -u Djeex --password-stdin
# Retag the already-built, already-scanned image — never rebuild for publish,
# so what ships is byte-for-byte what Trivy just scanned.
docker tag adguard-cidre:ci "$IMAGE:latest"
docker push "$IMAGE:latest"
if echo "$CHANGED" | grep -qE '^VERSION$'; then
VERSION=$(tr -d '[:space:]' < VERSION)
docker tag adguard-cidre:ci "$IMAGE:$VERSION"
docker push "$IMAGE:$VERSION"
else
echo "VERSION unchanged, skipping versioned tag to avoid overwriting an existing release."
fi
+2
View File
@@ -1,2 +1,4 @@
/adguard/*.log /adguard/*.log
/tmp/ /tmp/
__pycache__/
.pytest_cache/
+11 -3
View File
@@ -1,14 +1,22 @@
FROM python:3.11-alpine FROM python:3.13-alpine AS base
ENV TZ=Europe/Paris ENV TZ=Europe/Paris
RUN apk add --no-cache tzdata curl \ RUN apk add --no-cache tzdata curl \
&& cp /usr/share/zoneinfo/$TZ /etc/localtime \ && cp /usr/share/zoneinfo/$TZ /etc/localtime \
&& echo $TZ > /etc/timezone \ && echo $TZ > /etc/timezone
&& pip install --no-cache-dir requests pyyaml schedule
WORKDIR /app WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY blocklist_scheduler.py . COPY blocklist_scheduler.py .
FROM base AS test
RUN pip install --no-cache-dir pytest==9.1.1
COPY tests/ tests/
COPY pytest.ini .
FROM base
ENTRYPOINT ["python3", "blocklist_scheduler.py"] ENTRYPOINT ["python3", "blocklist_scheduler.py"]
+5 -7
View File
@@ -1,13 +1,11 @@
<h1 align="center"> Adguard CIDRE Sync</h1> <h1 align="center"> Adguard CIDRE Sync</h1>
<div align="center">
<a href="https://discord.gg/gxffg3GA96">
<img src="https://img.shields.io/badge/JV%20hardware-rejoindre-green?style=flat-square&logo=discord&logoColor=%23fff" alt="JV Hardware">
</a>
</div>
**Adguard CIDRE Sync** - A bot to synchronize adguard clients disallow list with countries CIDR list of your choices. **Adguard CIDRE Sync** - A bot to synchronize adguard clients disallow list with countries CIDR list of your choices.
*The code is partially generated by AI* > [!NOTE]
>_The code was partially written and structured using a generative AI._
>
>_Github repo is a mirror of https://git.djeex.fr/Djeex/adguard-cidre. You'll find full package, history and release note there._
## Sommaire ## Sommaire
@@ -34,7 +32,7 @@
| Variable | Description | Example | Possible Values | | Variable | Description | Example | Possible Values |
|--------------------------|--------------------------------------------------------------------------|-----------------------------|---------------------------------------------| |--------------------------|--------------------------------------------------------------------------|-----------------------------|---------------------------------------------|
| `TZ` | Timezone of the container to correctly schedule updates | `Europe/Paris` | Any valid timezone (e.g., `UTC`, `America/New_York`, etc.) | | `TZ` | Timezone of the container to correctly schedule updates | `Europe/Paris` | Any valid timezone (e.g., `UTC`, `America/New_York`, etc.) |
| `BLOCK_COUNTRIES` | List of country codes for CIDR lists, separated by commas | `cn,ru,ir` | ISO 2-letter country codes | | `BLOCK_COUNTRIES` | List of country codes for CIDR lists, separated by commas. You can also define an exclude list (all countries except the specified ones) by prefixing each country code with !. Mixing inclusion and exclusion codes is not supported. | including list : `cn,ru,ir`, excluding list : `!cn,!ru,!ir` | ISO 2-letter country codes |
| `BLOCKLIST_CRON_TYPE` | Scheduling type: `daily` or `weekly` | `daily` | `daily`, `weekly` | | `BLOCKLIST_CRON_TYPE` | Scheduling type: `daily` or `weekly` | `daily` | `daily`, `weekly` |
| `BLOCKLIST_CRON_TIME` | Time to run update in `HH:MM` 24-hour format | `06:00` | 24-hour time format | | `BLOCKLIST_CRON_TIME` | Time to run update in `HH:MM` 24-hour format | `06:00` | 24-hour time format |
| `BLOCKLIST_CRON_DAY` | Day of the week for weekly schedule (e.g., `mon`, `tue`, etc.) | `mon` | `mon`, `tue`, `wed`, `thu`, `fri`, `sat`, `sun` | | `BLOCKLIST_CRON_DAY` | Day of the week for weekly schedule (e.g., `mon`, `tue`, etc.) | `mon` | `mon`, `tue`, `wed`, `thu`, `fri`, `sat`, `sun` |
+1
View File
@@ -0,0 +1 @@
1.4.0
+63 -19
View File
@@ -6,6 +6,7 @@ import requests
import yaml import yaml
import schedule import schedule
import time import time
import re
from pathlib import Path from pathlib import Path
logging.basicConfig( logging.basicConfig(
@@ -18,14 +19,15 @@ ADGUARD_YAML = Path("/adguard/AdGuardHome.yaml")
TMP_YAML = ADGUARD_YAML.parent / (ADGUARD_YAML.name + ".tmp") TMP_YAML = ADGUARD_YAML.parent / (ADGUARD_YAML.name + ".tmp")
MANUAL_IPS_FILE = Path("/adguard/manually_blocked_ips.conf") MANUAL_IPS_FILE = Path("/adguard/manually_blocked_ips.conf")
CIDR_BASE_URL = "https://raw.githubusercontent.com/vulnebify/cidre/main/output/cidr/ipv4" CIDR_BASE_URL = "https://raw.githubusercontent.com/vulnebify/cidre/main/output/cidr/ipv4"
COUNTRY_LIST_URL = "https://raw.githubusercontent.com/vulnebify/cidre/refs/heads/main/cidre/countries.py"
FIRST_BACKUP = ADGUARD_YAML.parent / "AdGuardHome.yaml.first-start.bak" FIRST_BACKUP = ADGUARD_YAML.parent / "AdGuardHome.yaml.first-start.bak"
LAST_UPDATE_BACKUP = ADGUARD_YAML.parent / "AdGuardHome.yaml.last-update.bak" LAST_UPDATE_BACKUP = ADGUARD_YAML.parent / "AdGuardHome.yaml.last-update.bak"
BLOCK_COUNTRIES = os.getenv("BLOCK_COUNTRIES", "") BLOCK_COUNTRIES = os.getenv("BLOCK_COUNTRIES", "")
BLOCKLIST_CRON_TYPE = os.getenv("BLOCKLIST_CRON_TYPE", "daily").lower() # daily or weekly BLOCKLIST_CRON_TYPE = os.getenv("BLOCKLIST_CRON_TYPE", "daily").lower()
BLOCKLIST_CRON_TIME = os.getenv("BLOCKLIST_CRON_TIME", "06:00") # HH:MM format BLOCKLIST_CRON_TIME = os.getenv("BLOCKLIST_CRON_TIME", "06:00")
BLOCKLIST_CRON_DAY = os.getenv("BLOCKLIST_CRON_DAY", "mon").lower() # only if weekly BLOCKLIST_CRON_DAY = os.getenv("BLOCKLIST_CRON_DAY", "mon").lower()
ADGUARD_CONTAINER_NAME = os.getenv("ADGUARD_CONTAINER_NAME", "adguardhome") ADGUARD_CONTAINER_NAME = os.getenv("ADGUARD_CONTAINER_NAME", "adguardhome")
DOCKER_API_URL = os.getenv("DOCKER_API_URL", "http://socket-proxy-adguard:2375") DOCKER_API_URL = os.getenv("DOCKER_API_URL", "http://socket-proxy-adguard:2375")
@@ -41,6 +43,44 @@ def backup_last_update():
logging.info(f"Creating last update backup: {LAST_UPDATE_BACKUP}") logging.info(f"Creating last update backup: {LAST_UPDATE_BACKUP}")
LAST_UPDATE_BACKUP.write_text(ADGUARD_YAML.read_text()) LAST_UPDATE_BACKUP.write_text(ADGUARD_YAML.read_text())
def fetch_all_country_codes():
try:
resp = requests.get(COUNTRY_LIST_URL, timeout=15)
resp.raise_for_status()
matches = re.findall(r'"([A-Z]{2})"', resp.text)
return set(code.lower() for code in matches)
except Exception as e:
logging.error(f"Failed to fetch available country codes: {e}")
return set()
def get_selected_countries():
if not BLOCK_COUNTRIES:
logging.error("BLOCK_COUNTRIES is not set. Skipping update.")
return []
raw_codes = [c.strip() for c in BLOCK_COUNTRIES.split(",") if c.strip()]
if not raw_codes:
logging.error("No valid country codes provided.")
return []
is_exclusion = all(c.startswith("!") for c in raw_codes)
is_inclusion = all(not c.startswith("!") for c in raw_codes)
if not (is_exclusion or is_inclusion):
logging.error("Mixed syntax in BLOCK_COUNTRIES. Use only inclusion (e.g. 'fr,de') or only exclusion (e.g. '!fr,!de').")
sys.exit(1)
available = fetch_all_country_codes()
selected = {c.lstrip("!") for c in raw_codes}
unknown = selected - available
if unknown:
logging.warning(f"Unknown country codes: {', '.join(sorted(unknown))}")
if is_exclusion:
return sorted(available - selected)
else:
return sorted(selected & available)
def download_cidr_lists(countries): def download_cidr_lists(countries):
combined_ips = [] combined_ips = []
for code in countries: for code in countries:
@@ -76,12 +116,15 @@ def update_yaml_with_ips(ips):
logging.error(f"{ADGUARD_YAML} does not exist. Cannot update.") logging.error(f"{ADGUARD_YAML} does not exist. Cannot update.")
return False return False
data = None try:
with ADGUARD_YAML.open() as f: with ADGUARD_YAML.open() as f:
data = yaml.safe_load(f) data = yaml.safe_load(f)
except Exception as e:
logging.error(f"Failed to parse YAML file: {e}")
return False
if data is None: if not isinstance(data, dict):
logging.error(f"Failed to parse YAML file {ADGUARD_YAML}") logging.error("Invalid YAML format.")
return False return False
data['dns']['disallowed_clients'] = ips data['dns']['disallowed_clients'] = ips
@@ -106,12 +149,12 @@ def restart_adguard_container():
logging.error(f"Error restarting container: {e}") logging.error(f"Error restarting container: {e}")
def update_blocklist(): def update_blocklist():
if not BLOCK_COUNTRIES: countries = get_selected_countries()
logging.error("No countries specified in BLOCK_COUNTRIES environment variable. Skipping update.") if not countries:
logging.error("No valid countries to process. Skipping update.")
return return
countries_list = [c.strip() for c in BLOCK_COUNTRIES.split(",") if c.strip()] cidr_ips = download_cidr_lists(countries)
cidr_ips = download_cidr_lists(countries_list)
manual_ips = read_manual_ips() manual_ips = read_manual_ips()
combined_ips = cidr_ips + manual_ips combined_ips = cidr_ips + manual_ips
@@ -132,13 +175,16 @@ def schedule_job():
schedule.every().day.at(f"{hour:02d}:{minute:02d}").do(update_blocklist) schedule.every().day.at(f"{hour:02d}:{minute:02d}").do(update_blocklist)
logging.info(f"Scheduled daily update at {hour:02d}:{minute:02d}") logging.info(f"Scheduled daily update at {hour:02d}:{minute:02d}")
elif BLOCKLIST_CRON_TYPE == "weekly": elif BLOCKLIST_CRON_TYPE == "weekly":
valid_days = ["mon","tue","wed","thu","fri","sat","sun"] day_names = {
"mon": "monday", "tue": "tuesday", "wed": "wednesday", "thu": "thursday",
"fri": "friday", "sat": "saturday", "sun": "sunday",
}
day = BLOCKLIST_CRON_DAY[:3] day = BLOCKLIST_CRON_DAY[:3]
if day not in valid_days: if day not in day_names:
logging.error(f"Invalid BLOCKLIST_CRON_DAY '{BLOCKLIST_CRON_DAY}', must be one of {valid_days}. Defaulting to Monday.") logging.error(f"Invalid BLOCKLIST_CRON_DAY '{BLOCKLIST_CRON_DAY}', must be one of {list(day_names)}. Defaulting to Monday.")
day = "mon" day = "mon"
getattr(schedule.every(), day).at(f"{hour:02d}:{minute:02d}").do(update_blocklist) getattr(schedule.every(), day_names[day]).at(f"{hour:02d}:{minute:02d}").do(update_blocklist)
logging.info(f"Scheduled weekly update on {day.capitalize()} at {hour:02d}:{minute:02d}") logging.info(f"Scheduled weekly update on {day_names[day].capitalize()} at {hour:02d}:{minute:02d}")
else: else:
logging.error(f"Invalid BLOCKLIST_CRON_TYPE '{BLOCKLIST_CRON_TYPE}', must be 'daily' or 'weekly'. Defaulting to daily.") logging.error(f"Invalid BLOCKLIST_CRON_TYPE '{BLOCKLIST_CRON_TYPE}', must be 'daily' or 'weekly'. Defaulting to daily.")
schedule.every().day.at(f"{hour:02d}:{minute:02d}").do(update_blocklist) schedule.every().day.at(f"{hour:02d}:{minute:02d}").do(update_blocklist)
@@ -146,9 +192,7 @@ def schedule_job():
def main(): def main():
logging.info("Starting blocklist scheduler...") logging.info("Starting blocklist scheduler...")
backup_first_start() backup_first_start()
update_blocklist() update_blocklist()
schedule_job() schedule_job()
while True: while True:
+2
View File
@@ -0,0 +1,2 @@
[pytest]
pythonpath = .
+30
View File
@@ -0,0 +1,30 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"timezone": "Europe/Paris",
"labels": ["bot"],
"packageRules": [
{
"matchManagers": ["pip_requirements"],
"matchUpdateTypes": ["patch", "minor"],
"automerge": true
},
{
"matchManagers": ["dockerfile"],
"matchUpdateTypes": ["patch"],
"automerge": true
},
{
"matchUpdateTypes": ["major"],
"addLabels": ["major"]
},
{
"matchUpdateTypes": ["minor"],
"addLabels": ["minor"]
}
],
"vulnerabilityAlerts": {
"enabled": true,
"addLabels": ["bug"]
}
}
+3
View File
@@ -0,0 +1,3 @@
requests==2.34.2
pyyaml==6.0.3
schedule==1.2.2
+210
View File
@@ -0,0 +1,210 @@
import pytest
import schedule as schedule_lib
import yaml
import blocklist_scheduler as bs
class FakeResponse:
def __init__(self, text="", status_code=200, raise_exc=None):
self.text = text
self.status_code = status_code
self._raise_exc = raise_exc
def raise_for_status(self):
if self._raise_exc:
raise self._raise_exc
def test_backup_first_start_creates_backup_when_missing(tmp_path, monkeypatch):
adguard_yaml = tmp_path / "AdGuardHome.yaml"
adguard_yaml.write_text("original: config\n")
first_backup = tmp_path / "AdGuardHome.yaml.first-start.bak"
monkeypatch.setattr(bs, "ADGUARD_YAML", adguard_yaml)
monkeypatch.setattr(bs, "FIRST_BACKUP", first_backup)
bs.backup_first_start()
assert first_backup.read_text() == "original: config\n"
def test_backup_first_start_does_not_overwrite_existing_backup(tmp_path, monkeypatch):
adguard_yaml = tmp_path / "AdGuardHome.yaml"
adguard_yaml.write_text("new: config\n")
first_backup = tmp_path / "AdGuardHome.yaml.first-start.bak"
first_backup.write_text("pristine: original\n")
monkeypatch.setattr(bs, "ADGUARD_YAML", adguard_yaml)
monkeypatch.setattr(bs, "FIRST_BACKUP", first_backup)
bs.backup_first_start()
assert first_backup.read_text() == "pristine: original\n"
def test_backup_first_start_raises_if_adguard_yaml_missing(tmp_path, monkeypatch):
adguard_yaml = tmp_path / "AdGuardHome.yaml"
first_backup = tmp_path / "AdGuardHome.yaml.first-start.bak"
monkeypatch.setattr(bs, "ADGUARD_YAML", adguard_yaml)
monkeypatch.setattr(bs, "FIRST_BACKUP", first_backup)
with pytest.raises(FileNotFoundError):
bs.backup_first_start()
# --- update_yaml_with_ips (pyyaml) ---
def test_update_yaml_with_ips_writes_disallowed_clients(tmp_path, monkeypatch):
adguard_yaml = tmp_path / "AdGuardHome.yaml"
adguard_yaml.write_text("dns:\n bind_hosts:\n - 0.0.0.0\n")
tmp_yaml = tmp_path / "AdGuardHome.yaml.tmp"
monkeypatch.setattr(bs, "ADGUARD_YAML", adguard_yaml)
monkeypatch.setattr(bs, "TMP_YAML", tmp_yaml)
result = bs.update_yaml_with_ips(["1.2.3.0/24", "5.6.7.8"])
assert result is True
data = yaml.safe_load(adguard_yaml.read_text())
assert data["dns"]["disallowed_clients"] == ["1.2.3.0/24", "5.6.7.8"]
assert not tmp_yaml.exists()
def test_update_yaml_with_ips_missing_file_returns_false(tmp_path, monkeypatch):
adguard_yaml = tmp_path / "AdGuardHome.yaml"
monkeypatch.setattr(bs, "ADGUARD_YAML", adguard_yaml)
assert bs.update_yaml_with_ips(["1.2.3.4"]) is False
def test_update_yaml_with_ips_invalid_yaml_returns_false(tmp_path, monkeypatch):
adguard_yaml = tmp_path / "AdGuardHome.yaml"
adguard_yaml.write_text("key: [unclosed\n")
monkeypatch.setattr(bs, "ADGUARD_YAML", adguard_yaml)
assert bs.update_yaml_with_ips(["1.2.3.4"]) is False
def test_update_yaml_with_ips_missing_dns_key_raises(tmp_path, monkeypatch):
adguard_yaml = tmp_path / "AdGuardHome.yaml"
adguard_yaml.write_text("some_other_key: true\n")
monkeypatch.setattr(bs, "ADGUARD_YAML", adguard_yaml)
with pytest.raises(KeyError):
bs.update_yaml_with_ips(["1.2.3.4"])
# --- fetch_all_country_codes / download_cidr_lists / restart_adguard_container (requests) ---
def test_fetch_all_country_codes_parses_codes(monkeypatch):
monkeypatch.setattr(bs.requests, "get", lambda *a, **k: FakeResponse(text='COUNTRIES = ["FR", "DE", "US"]\n'))
assert bs.fetch_all_country_codes() == {"fr", "de", "us"}
def test_fetch_all_country_codes_returns_empty_set_on_error(monkeypatch):
def raise_error(*a, **k):
raise bs.requests.exceptions.ConnectionError("boom")
monkeypatch.setattr(bs.requests, "get", raise_error)
assert bs.fetch_all_country_codes() == set()
def test_download_cidr_lists_combines_successful_countries_and_skips_failures(monkeypatch):
def fake_get(url, timeout=None):
if "/fr.cidr" in url:
return FakeResponse(text="1.1.1.0/24\n1.1.2.0/24\n")
raise bs.requests.exceptions.ConnectionError("boom")
monkeypatch.setattr(bs.requests, "get", fake_get)
result = bs.download_cidr_lists(["fr", "de"])
assert result == ["1.1.1.0/24", "1.1.2.0/24"]
def test_restart_adguard_container_success_does_not_raise(monkeypatch):
monkeypatch.setattr(bs.requests, "post", lambda *a, **k: FakeResponse(status_code=204))
bs.restart_adguard_container()
def test_restart_adguard_container_error_status_does_not_raise(monkeypatch):
monkeypatch.setattr(bs.requests, "post", lambda *a, **k: FakeResponse(status_code=500, text="err"))
bs.restart_adguard_container()
def test_restart_adguard_container_network_error_does_not_raise(monkeypatch):
def raise_error(*a, **k):
raise bs.requests.exceptions.ConnectionError("boom")
monkeypatch.setattr(bs.requests, "post", raise_error)
bs.restart_adguard_container()
# --- schedule_job (schedule) ---
@pytest.fixture(autouse=True)
def clear_schedule():
yield
schedule_lib.clear()
def test_schedule_job_daily(monkeypatch):
monkeypatch.setattr(bs, "BLOCKLIST_CRON_TYPE", "daily")
monkeypatch.setattr(bs, "BLOCKLIST_CRON_TIME", "06:00")
bs.schedule_job()
assert len(schedule_lib.jobs) == 1
job = schedule_lib.jobs[0]
assert job.unit == "days"
assert str(job.at_time) == "06:00:00"
assert job.job_func.func is bs.update_blocklist
def test_schedule_job_weekly_valid_day(monkeypatch):
monkeypatch.setattr(bs, "BLOCKLIST_CRON_TYPE", "weekly")
monkeypatch.setattr(bs, "BLOCKLIST_CRON_TIME", "18:30")
monkeypatch.setattr(bs, "BLOCKLIST_CRON_DAY", "wed")
bs.schedule_job()
job = schedule_lib.jobs[0]
assert job.unit == "weeks"
assert job.start_day == "wednesday"
assert str(job.at_time) == "18:30:00"
def test_schedule_job_weekly_invalid_day_defaults_to_monday(monkeypatch):
monkeypatch.setattr(bs, "BLOCKLIST_CRON_TYPE", "weekly")
monkeypatch.setattr(bs, "BLOCKLIST_CRON_DAY", "xxx")
bs.schedule_job()
assert schedule_lib.jobs[0].start_day == "monday"
def test_schedule_job_invalid_time_defaults_to_six_am(monkeypatch):
monkeypatch.setattr(bs, "BLOCKLIST_CRON_TYPE", "daily")
monkeypatch.setattr(bs, "BLOCKLIST_CRON_TIME", "not-a-time")
bs.schedule_job()
assert str(schedule_lib.jobs[0].at_time) == "06:00:00"
def test_schedule_job_invalid_type_defaults_to_daily(monkeypatch):
monkeypatch.setattr(bs, "BLOCKLIST_CRON_TYPE", "bogus")
bs.schedule_job()
assert schedule_lib.jobs[0].unit == "days"