From 435a0687d7788e8e4b59f7605098778b38e12bc8 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Tue, 29 Sep 2026 10:04:12 -0400 Subject: [PATCH] test: isole le garde SSRF dans les tests fetch_url - plus de dependance au DNS reel BUG-092 --- CHANGELOG.md | 17 ++++++++++++++++- README.fr.md | 6 +++--- README.md | 6 +++--- VERSION | 2 +- desktop/Cargo.lock | 2 +- desktop/Cargo.toml | 2 +- desktop/tauri.conf.json | 2 +- docs/ISSUES_TODOLIST.md | 1 + docs/ROADMAP.md | 2 +- package.json | 2 +- tests/test_web_tools.py | 17 +++++++++++++++-- tests/test_webrender.py | 19 ++++++++++++++++--- 12 files changed, 60 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 761be74..f6f8229 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/), et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). > **En cours de développement** : les changements à venir sont listés dans la section -> [Unreleased](#unreleased). La dernière version livrée est **2.39.4**. +> [Unreleased](#unreleased). La dernière version livrée est **2.39.5**. --- @@ -14,6 +14,21 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [2.39.5] — 2026-09-29 + +### Correction + +- **BUG-092 — les tests réseau ne dépendaient plus du DNS réel.** Trois tests + de `fetch_url` mockaient `httpx` mais laissaient le garde SSRF résoudre + `example.com` pour de vrai : sur un runner au DNS instable, le job CI + `test` échouait en `dns_error` au lieu d'atteindre la couche testée. Les + tests isolent désormais le garde — y compris la référence importée dans + `webrender`, qui échappait au premier correctif — et les tests de garde + SSRF continuent de traverser le vrai chemin. Contre-preuve : DNS coupé + globalement, la suite passe (1474 tests). + +--- + ## [2.39.4] — 2026-09-29 --- diff --git a/README.fr.md b/README.fr.md index 94cbc84..4b4fc99 100644 --- a/README.fr.md +++ b/README.fr.md @@ -4,7 +4,7 @@ **Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive. -[![Version](https://img.shields.io/badge/Version-2.39.4-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.39.5-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -976,8 +976,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l ## 📝 Changelog -Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.39.4). +Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.39.5). --- -*Projet : ObsiGate | Version : 2.39.4 | Dernière mise à jour : Septembre 2026* +*Projet : ObsiGate | Version : 2.39.5 | Dernière mise à jour : Septembre 2026* diff --git a/README.md b/README.md index 12bfc18..ac384ca 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface. -[![Version](https://img.shields.io/badge/Version-2.39.4-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.39.5-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -1151,8 +1151,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) ## 📝 Changelog -See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.39.4). +See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.39.5). --- -*Project: ObsiGate | Version: 2.39.4 | Last updated: September 2026* +*Project: ObsiGate | Version: 2.39.5 | Last updated: September 2026* diff --git a/VERSION b/VERSION index 1bf302a..21b5230 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.39.4 +2.39.5 diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index 8c63825..27b2ec0 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.39.4" +version = "2.39.5" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index e1fc12f..38ff344 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.39.4" +version = "2.39.5" description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian" authors = ["Bruno Charest"] edition = "2021" diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index d558151..745d971 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json", "productName": "ObsiGate", - "version": "2.39.4", + "version": "2.39.5", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/ISSUES_TODOLIST.md b/docs/ISSUES_TODOLIST.md index 2d859cd..77fb978 100644 --- a/docs/ISSUES_TODOLIST.md +++ b/docs/ISSUES_TODOLIST.md @@ -216,6 +216,7 @@ Avant de corriger quoi que ce soit, un agent IA doit : | Date | ID(s) traité(s) | Action | Fichiers modifiés | Résumé | Statut après | |---|---|---|---|---|---| | 2026-09-28 | BUG-090 (#153 A8 + A9) | Correction + feature | `backend/xlsx_reader.py`, `backend/routers/files_read.py`, `backend/schemas.py`, `backend/openapi_docs.py`, `frontend/js/viewer.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `tests/test_xlsx_viewer.py`, `tests/frontend/xlsx-viewer.test.mjs`, `tests/e2e/xlsx-viewer.spec.js`, `test_vault/sample-xlsx-large.xlsx` | **La troncature d'une feuille est annoncée et les lignes cachées restent accessibles** : (BUG-090/A8) `render_sheets()` renvoie `total_rows`/`total_cols`/`max_rows`/`max_cols`/`truncated`, la visionneuse affiche un bandeau « Feuille tronquée » (i18n FR/EN, axes lignes et colonnes) et la ligne d'en-têtes devient `sticky` (`top: auto` sur les numéros de ligne pour éviter l'empilement) ; (A9) `GET /api/file/{vault}/xlsx/sheet?sheet=&offset=&limit=` (`XlsxSheetWindowResponse`, plafond 1 000 lignes/requête, 404 feuille inconnue, 415 non-xlsx) sert une fenêtre avec les **vraies** coordonnées A1 et le `has_more` de pagination. Contre-preuves : neutraliser `truncated` → 2 tests échouent ; neutraliser l'offset → 3 tests échouent. Vérifié : `test_xlsx_viewer.py` 58 passed, xlsx-viewer.test.mjs 14/14, E2E 7/7 (3 nouveaux + fixture `sample-xlsx-large.xlsx` 520 lignes), suite 1417 passed / 6 skipped, ruff 0, mypy 0, i18n parity, validate-imports 40 modules | 🟢 corrigé (en attente vérif utilisateur) | +| 2026-09-29 | BUG-092 (job CI `test`, #153) | Correction tests | `tests/test_webrender.py`, `tests/test_web_tools.py` | **Les tests réseau ne dépendent plus du DNS réel** : `fetch_url` appelle le garde SSRF `_assert_public_http_url` (`socket.getaddrinfo`) *avant* le traitement, et seule la couche httpx était mockée. Sur le runner au DNS instable, `tests/test_webrender.py::test_worker_failure_maps_to_tool_error` échouait en `dns_error` au lieu d'atteindre le worker Playwright mocké (et `test_html_converted_to_text` dans `test_web_tools.py` de la même façon). Correctif : fixture `no_dns` mockant les **deux** références du garde (`web._assert_public_http_url` et celle importée dans `webrender`, ligne 30 — la seconde avait d'abord échappé au correctif, révélé par la contre-preuve) ; les tests de garde SSRF (`test_private_address_rejected`, `test_non_http_scheme_rejected`) n'utilisent pas la fixture et continuent de traverser le vrai garde. Contre-preuve : DNS cassé globalement (`socket.getaddrinfo` → `gaierror`) → avant 1 échec, après **1474 passed / 6 skipped** | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-29 | BUG-091 (#153, runs CI #1641-#1642) | Correction CI | `.gitea/workflows/ci.yml`, `backend/requirements.txt`, `docs/ISSUES_TODOLIST.md`, `CHANGELOG.md` | **Le job `security` est réparé définitivement** : (1) le binaire semgrep non épinglé exige depuis 1.158.0 un CPU x86-64-v2 que le runner Gitea ne fournit pas (`libs/libresolv.so.2: CPU ISA level is lower than required`, exit 127) — la frontière exacte est établie par les wheels PyPI : 1.157.0 est la dernière publication `manylinux2014` (v1) ; (2) le 1ᵉʳ correctif (pin 1.174.0, v2.39.2) échouait car cette version ne publie qu'en `manylinux_2_34` ; (3) semgrep vit désormais dans un venv isolé du job (`/tmp/semgrep-venv`, pin 1.157.0) car ses dépendances contredisent l'env principal (`tomli~=2.0.1` vs pip-audit ≥ 2.10, `pyjwt~=2.12.0` vs PYSEC-2026-178) ; (4) plancher `pyjwt[crypto]>=2.13.0` dans requirements.txt (transitif de mcp) et `pip install -U pip setuptools` dans le job (nouveaux advisories pip PYSEC-2026-3721, setuptools PYSEC-2026-3447). Validation : environnement frais reconstitué en local → résolution sans conflit (pyjwt 2.15.1), pip-audit exit 0, semgrep 1.157.0 exit 0 sur `semgrep-rules/`. Au passage documenté : security échouait déjà avant ce push (v2.31.0/v2.32.0 rouges) et les commits de features n'ont déclenché aucun run (Gitea : commit de tête uniquement) | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-28 | #153 A6 → A17 (v2.33.0 → v2.39.0) | Feature + clôture documentaire (aucun bug nouveau) | `CHANGELOG.md`, `docs/features/xlsx-viewer.md`, `docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md`, `README.md`, `README.fr.md` | **Clôture du backlog #153** : entrées CHANGELOG des 7 sous-tâches, fiche `features/xlsx-viewer.md` (statut terminé, cases A6-A17 cochées, historique), section 6 du guide utilisateur étendue (barre de formule, navigation clavier, tri/filtre/recherche/export CSV, structure, styles, formats `.xlsm`/`.xls`/`.ods`/`.csv`, tableau de bord) et bullets README FR/EN. Code livré : v2.33.0 A6 (outils IA `backend/tools/spreadsheets.py`), v2.34.0 A7 (clavier + barre de formule), v2.35.0 A13 (tri/filtre/recherche/export), v2.36.0 A14 (structure `PUT …/xlsx/structure`), v2.37.0 A15 (styles/fusions/volets figés), v2.38.0 A16 (`.xlsm` éditable, `.xls`/`.ods` lecture seule, `.csv` RFC 4180), v2.39.0 A17 (dashboard `GET …/xlsx/dashboard`). Vérifié : suite xlsx 116 passed, xlsx-viewer.test.mjs 35/35, ruff/mypy 0, i18n parity, validate-imports 40 modules | ✅ livré (en attente vérif utilisateur) | | 2026-09-28 | BUG-089 (#153 A5, A10, A12) | Correction | `backend/xlsx_reader.py`, `backend/indexer.py`, `backend/search.py`, `backend/services/mutations.py`, `frontend/js/viewer.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `tests/test_xlsx_viewer.py` | **Les tableurs deviennent visibles ettypés** : (A5) `extract_indexable_text()` indexe noms de feuilles + 20 premières lignes (plafond 5 k caractères) dans le TF-IDF et la recherche sémantique — un mot tapé dans une cellule rend le fichier trouvable ; (A10) `_coerce_xlsx_value()` reconnaît désormais les booléens (`TRUE`/`FAUX`/`OUI`/`NON`) et les dates FR `JJ/MM/AAAA` (jour-first : `01/02/2026` = 1er février), symétrique avec l'affichage ; (A12) la valeur calculée en cache s'affiche sous la formule (``, 2ᵉ lecture `data_only=True` uniquement si l'archive contient un ``), info-bulle traduite via `xlsx.cached_value_title` FR/EN. (BUG-089) un reindex manuel reconstruisait mal l'index inversé et `backend/search.py` lisait l'index par valeur. Contre-preuves vérifiées pour A5, A10 et A12. Vérifié : `test_xlsx_viewer.py` 43 passed, suite 1402 passed / 6 skipped, ruff 0, mypy 0, i18n parity, validate-imports 40 modules, xlsx-viewer.test.mjs 10/10 | 🟢 corrigé (en attente vérif utilisateur) | diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 0d5117f..16c009f 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # ObsiGate — Roadmap -> **Version :** 2.39.4 | **Dernière mise à jour :** 2026-09-29 +> **Version :** 2.39.5 | **Dernière mise à jour :** 2026-09-29 > **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact > vers les fonctionnalités livrées. > - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)** diff --git a/package.json b/package.json index ae4c21d..bbcfda5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsigate", - "version": "2.39.4", + "version": "2.39.5", "description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.", "main": "patch.js", "directories": { diff --git a/tests/test_web_tools.py b/tests/test_web_tools.py index 71411c6..17ec662 100644 --- a/tests/test_web_tools.py +++ b/tests/test_web_tools.py @@ -10,6 +10,19 @@ from backend.tools.context import ToolContext, ToolError, ToolMode, ToolRisk from backend.tools.registry import get_tool +@pytest.fixture +def no_dns(monkeypatch): + """Neutralise la résolution DNS réelle du garde SSRF (runner au réseau fragile). + + Seuls les tests qui vérifient l'extraction HTML mockent ``httpx.get`` ; sans + ce mock, ``_assert_public_http_url`` résolvait ``example.com`` pour de vrai et + le test échouait en ``dns_error`` sur un runner dont le DNS est instable. + Les tests de garde SSRF (``test_private_address_rejected``) n'utilisent PAS + la fixture : ils doivent au contraire traverser le vrai garde. + """ + monkeypatch.setattr(web, "_assert_public_http_url", lambda url: url) + + class FakeResponse: def __init__(self, payload: Any = None, json_data: Any = None, status_code: int = 200, content: bytes = b"", headers: dict | None = None, url: str = "https://example.com/x"): @@ -171,7 +184,7 @@ class TestWebSearch: class TestFetchUrl: - def test_html_converted_to_text(self, monkeypatch): + def test_html_converted_to_text(self, monkeypatch, no_dns): html = (b"T&" b"

hello

    " b"
  • one
  • two
") @@ -196,7 +209,7 @@ class TestFetchUrl: web.fetch_url(_ctx(), web.FetchUrlInput(url="file:///etc/passwd")) assert ei.value.code == "invalid_scheme" - def test_binary_content_rejected(self, monkeypatch): + def test_binary_content_rejected(self, monkeypatch, no_dns): monkeypatch.setattr(web.httpx, "get", lambda *a, **k: FakeResponse(content=b"%PDF-1.4...", headers={"content-type": "application/pdf"})) diff --git a/tests/test_webrender.py b/tests/test_webrender.py index c0df20d..a78b0e4 100644 --- a/tests/test_webrender.py +++ b/tests/test_webrender.py @@ -20,7 +20,7 @@ class TestRegistration: class TestRenderUnavailable: - def test_missing_playwright_clear_error(self, monkeypatch): + def test_missing_playwright_clear_error(self, monkeypatch, no_dns): monkeypatch.setattr(webrender, "_playwright_available", lambda: False) with pytest.raises(ToolError) as ei: web.fetch_url(_ctx(), web.FetchUrlInput( @@ -35,8 +35,21 @@ class TestRenderUnavailable: assert ei.value.code in ("ssrf_blocked", "dns_error") +@pytest.fixture +def no_dns(monkeypatch): + """Neutralise la résolution DNS réelle du garde SSRF. + + ``fetch_url`` appelle ``_assert_public_http_url`` (getaddrinfo) *avant* le + rendu : sur un runner au DNS instable le test échouait en ``dns_error`` + au lieu d'atteindre le worker Playwright mocké. ``webrender`` importe la + fonction dans son propre namespace : les deux références sont mockées. + """ + monkeypatch.setattr(web, "_assert_public_http_url", lambda url: url) + monkeypatch.setattr(webrender, "_assert_public_http_url", lambda url: url) + + class TestRenderSuccess: - def test_fetch_url_delegates_to_worker(self, monkeypatch): + def test_fetch_url_delegates_to_worker(self, monkeypatch, no_dns): captured = {} def fake_render(url): @@ -51,7 +64,7 @@ class TestRenderSuccess: assert out["rendered"] is True assert "dynamic content" in out["text"] - def test_worker_failure_maps_to_tool_error(self, monkeypatch): + def test_worker_failure_maps_to_tool_error(self, monkeypatch, no_dns): monkeypatch.setattr(webrender, "_playwright_available", lambda: True) def boom(url):