diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 6544228..a269231 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -14,6 +14,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Build Docker image run: | @@ -43,7 +45,7 @@ jobs: - name: Check deprecation warnings run: | - docker run --rm adguard-cidre:ci python -W error::DeprecationWarning -c "import blocklist_scheduler" 2>&1 | tee deprecation.log || true + 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 @@ -60,4 +62,37 @@ jobs: 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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index cb697c1..32f4c29 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,16 +4,17 @@ ENV TZ=Europe/Paris RUN apk add --no-cache tzdata curl \ && cp /usr/share/zoneinfo/$TZ /etc/localtime \ - && echo $TZ > /etc/timezone \ - && pip install --no-cache-dir requests pyyaml schedule + && echo $TZ > /etc/timezone WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + COPY blocklist_scheduler.py . FROM base AS test -COPY requirements-dev.txt . -RUN pip install --no-cache-dir -r requirements-dev.txt +RUN pip install --no-cache-dir pytest==9.1.1 COPY tests/ tests/ COPY pytest.ini . diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..88c5fb8 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.4.0 diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..254403b --- /dev/null +++ b/renovate.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"], + "timezone": "Europe/Paris", + "schedule": ["before 6am on monday"], + "packageRules": [ + { + "matchManagers": ["pip_requirements"], + "matchUpdateTypes": ["patch", "minor"], + "automerge": true + }, + { + "matchManagers": ["dockerfile"], + "matchUpdateTypes": ["patch"], + "automerge": true + } + ], + "vulnerabilityAlerts": { + "enabled": true + } +} diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 8594477..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,4 +0,0 @@ -requests -pyyaml -schedule -pytest diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1ce82c8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +requests==2.34.2 +pyyaml==6.0.3 +schedule==1.2.2 diff --git a/tests/test_blocklist_scheduler.py b/tests/test_blocklist_scheduler.py index d4a8d26..d937156 100644 --- a/tests/test_blocklist_scheduler.py +++ b/tests/test_blocklist_scheduler.py @@ -1,9 +1,21 @@ 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") @@ -42,6 +54,102 @@ def test_backup_first_start_raises_if_adguard_yaml_missing(tmp_path, monkeypatch 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)