From fb08fcaa07a6ad98637c4de6ec8c809c08aa84a5 Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Fri, 18 Jul 2025 02:16:14 -0500 Subject: [PATCH] Fix SQLite view creation and test domain filter --- scripts/generate_reports.py | 30 ++++++++++++++++------ tests/test_reports.py | 50 ++++++++++++++++++++++++++++++++++--- 2 files changed, 68 insertions(+), 12 deletions(-) diff --git a/scripts/generate_reports.py b/scripts/generate_reports.py index f099d17..7ac103a 100644 --- a/scripts/generate_reports.py +++ b/scripts/generate_reports.py @@ -36,6 +36,7 @@ def _get_domains() -> List[str]: conn.close() return domains + def _load_config() -> List[Dict]: if not REPORT_CONFIG.exists(): typer.echo(f"Config file not found: {REPORT_CONFIG}") @@ -47,10 +48,12 @@ def _load_config() -> List[Dict]: raise typer.Exit(1) return data + def _save_json(path: Path, data: List[Dict]) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(data, indent=2)) + def _render_html(interval: str, reports: List[Dict], out_path: Path) -> None: env = Environment(loader=FileSystemLoader(TEMPLATE_DIR)) template = env.get_template("report.html") @@ -65,6 +68,7 @@ def _bucket_expr(interval: str) -> str: raise typer.Exit(1) return f"strftime('{fmt}', datetime(time))" + def _generate_interval(interval: str, domain: Optional[str] = None) -> None: cfg = _load_config() if not cfg: @@ -79,9 +83,12 @@ def _generate_interval(interval: str, domain: Optional[str] = None) -> None: # Create a temporary view so queries can easily be filtered by domain cur.execute("DROP VIEW IF EXISTS logs_view") if domain: + # Parameters are not allowed in CREATE VIEW statements, so we must + # safely interpolate the domain value ourselves. Escape any single + # quotes to prevent malformed queries. + safe_domain = domain.replace("'", "''") cur.execute( - "CREATE TEMP VIEW logs_view AS SELECT * FROM logs WHERE host = ?", - (domain,), + f"CREATE TEMP VIEW logs_view AS SELECT * FROM logs WHERE host = '{safe_domain}'" ) out_dir = OUTPUT_DIR / domain / interval else: @@ -101,12 +108,14 @@ def _generate_interval(interval: str, domain: Optional[str] = None) -> None: data = [dict(zip(headers, row)) for row in rows] json_path = out_dir / f"{name}.json" _save_json(json_path, data) - report_list.append({ - "name": name, - "label": definition.get("label", name.title()), - "chart": definition.get("chart", "line"), - "json": f"{name}.json", - }) + report_list.append( + { + "name": name, + "label": definition.get("label", name.title()), + "chart": definition.get("chart", "line"), + "json": f"{name}.json", + } + ) _save_json(out_dir / "reports.json", report_list) _render_html(interval, report_list, out_dir / "index.html") @@ -118,6 +127,7 @@ def _generate_all_domains(interval: str) -> None: for domain in _get_domains(): _generate_interval(interval, domain) + @app.command() def hourly( domain: Optional[str] = typer.Option( @@ -133,6 +143,7 @@ def hourly( else: _generate_interval("hourly", domain) + @app.command() def daily( domain: Optional[str] = typer.Option( @@ -148,6 +159,7 @@ def daily( else: _generate_interval("daily", domain) + @app.command() def weekly( domain: Optional[str] = typer.Option( @@ -163,6 +175,7 @@ def weekly( else: _generate_interval("weekly", domain) + @app.command() def monthly( domain: Optional[str] = typer.Option( @@ -178,5 +191,6 @@ def monthly( else: _generate_interval("monthly", domain) + if __name__ == "__main__": app() diff --git a/tests/test_reports.py b/tests/test_reports.py index 8319828..8905960 100644 --- a/tests/test_reports.py +++ b/tests/test_reports.py @@ -32,11 +32,31 @@ def setup_db(path: Path): ) cur.execute( "INSERT INTO logs (ip, host, time, request, status, bytes_sent, referer, user_agent, cache_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", - ("127.0.0.1", "example.com", "2024-01-01 10:00:00", "GET / HTTP/1.1", 200, 100, "-", "curl", "MISS"), + ( + "127.0.0.1", + "example.com", + "2024-01-01 10:00:00", + "GET / HTTP/1.1", + 200, + 100, + "-", + "curl", + "MISS", + ), ) cur.execute( "INSERT INTO logs (ip, host, time, request, status, bytes_sent, referer, user_agent, cache_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", - ("127.0.0.1", "example.com", "2024-01-01 10:05:00", "GET /err HTTP/1.1", 500, 100, "-", "curl", "MISS"), + ( + "127.0.0.1", + "example.com", + "2024-01-01 10:05:00", + "GET /err HTTP/1.1", + 500, + 100, + "-", + "curl", + "MISS", + ), ) conn.commit() conn.close() @@ -72,14 +92,36 @@ def test_generate_interval(tmp_path, sample_reports, monkeypatch): monkeypatch.setattr(gr, "DB_PATH", db_path) monkeypatch.setattr(gr, "OUTPUT_DIR", tmp_path / "output") monkeypatch.setattr(gr, "REPORT_CONFIG", sample_reports) - monkeypatch.setattr(gr, "TEMPLATE_DIR", Path(__file__).resolve().parents[1] / "templates") + monkeypatch.setattr( + gr, "TEMPLATE_DIR", Path(__file__).resolve().parents[1] / "templates" + ) gr._generate_interval("hourly") hits = json.loads((tmp_path / "output" / "hourly" / "hits.json").read_text()) assert hits[0]["value"] == 2 - error_rate = json.loads((tmp_path / "output" / "hourly" / "error_rate.json").read_text()) + error_rate = json.loads( + (tmp_path / "output" / "hourly" / "error_rate.json").read_text() + ) assert error_rate[0]["value"] == pytest.approx(50.0) reports = json.loads((tmp_path / "output" / "hourly" / "reports.json").read_text()) assert {r["name"] for r in reports} == {"hits", "error_rate"} + +def test_generate_interval_domain_filter(tmp_path, sample_reports, monkeypatch): + db_path = tmp_path / "database" / "ngxstat.db" + setup_db(db_path) + + monkeypatch.setattr(gr, "DB_PATH", db_path) + monkeypatch.setattr(gr, "OUTPUT_DIR", tmp_path / "output") + monkeypatch.setattr(gr, "REPORT_CONFIG", sample_reports) + monkeypatch.setattr( + gr, "TEMPLATE_DIR", Path(__file__).resolve().parents[1] / "templates" + ) + + gr._generate_interval("hourly", "example.com") + + hits = json.loads( + (tmp_path / "output" / "example.com" / "hourly" / "hits.json").read_text() + ) + assert hits[0]["value"] == 2