From 5131816a5b446524580dd43345fc28703aaa150f Mon Sep 17 00:00:00 2001 From: ZdenekSrotyr Date: Thu, 9 Apr 2026 07:15:14 +0200 Subject: [PATCH] test: add missing coverage for web UI, Jira extract, instance config, and concurrent rebuild - tests/test_web_ui.py: smoke tests for all authenticated web pages (login, dashboard, catalog, corporate-memory, activity-center, admin/tables, admin/permissions) - tests/test_jira_service.py: unit tests for extract_init and update_meta in the Jira connector - tests/test_instance_config.py: verifies get_instance_name() returns a string when config file is absent - tests/test_orchestrator.py: concurrent rebuild test asserting rebuild succeeds while a read-only connection holds the analytics DB --- tests/test_instance_config.py | 12 +++++++ tests/test_jira_service.py | 41 +++++++++++++++++++++++ tests/test_orchestrator.py | 17 ++++++++++ tests/test_web_ui.py | 63 +++++++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 tests/test_instance_config.py create mode 100644 tests/test_jira_service.py create mode 100644 tests/test_web_ui.py diff --git a/tests/test_instance_config.py b/tests/test_instance_config.py new file mode 100644 index 0000000..62f197d --- /dev/null +++ b/tests/test_instance_config.py @@ -0,0 +1,12 @@ +"""Tests for instance_config loading.""" +import pytest + + +class TestInstanceConfig: + def test_missing_config_returns_defaults(self, tmp_path, monkeypatch): + monkeypatch.setenv("DATA_DIR", str(tmp_path)) + monkeypatch.setenv("TESTING", "1") + monkeypatch.setenv("JWT_SECRET_KEY", "test-secret-key-min-32-characters!!") + from app.instance_config import get_instance_name + name = get_instance_name() + assert isinstance(name, str) diff --git a/tests/test_jira_service.py b/tests/test_jira_service.py new file mode 100644 index 0000000..629d387 --- /dev/null +++ b/tests/test_jira_service.py @@ -0,0 +1,41 @@ +"""Tests for Jira extract_init — init and update_meta.""" +import duckdb +import pytest +from pathlib import Path +from connectors.jira.extract_init import init_extract, update_meta + + +@pytest.fixture +def jira_env(tmp_path, monkeypatch): + monkeypatch.setenv("DATA_DIR", str(tmp_path)) + jira_dir = tmp_path / "extracts" / "jira" + jira_dir.mkdir(parents=True) + return jira_dir + + +class TestJiraExtractInit: + def test_init_creates_extract_db(self, jira_env): + init_extract(jira_env) + assert (jira_env / "extract.duckdb").exists() + conn = duckdb.connect(str(jira_env / "extract.duckdb")) + meta = conn.execute("SELECT * FROM _meta").fetchall() + conn.close() + assert isinstance(meta, list) + + def test_update_meta_creates_view(self, jira_env): + init_extract(jira_env) + issues_dir = jira_env / "data" / "issues" + issues_dir.mkdir(parents=True, exist_ok=True) + pq_path = str(issues_dir / "2026-04.parquet") + tmp = duckdb.connect() + tmp.execute(f"COPY (SELECT 'PROJ-1' AS issue_key, 'Bug' AS type) TO '{pq_path}' (FORMAT PARQUET)") + tmp.close() + + update_meta(jira_env, "issues") + + conn = duckdb.connect(str(jira_env / "extract.duckdb")) + rows = conn.execute("SELECT rows FROM _meta WHERE table_name='issues'").fetchone() + assert rows[0] == 1 + data = conn.execute("SELECT issue_key FROM issues").fetchone() + assert data[0] == "PROJ-1" + conn.close() diff --git a/tests/test_orchestrator.py b/tests/test_orchestrator.py index 504bef3..be89a3b 100644 --- a/tests/test_orchestrator.py +++ b/tests/test_orchestrator.py @@ -357,6 +357,23 @@ class TestSyncOrchestrator: tmp_wal = Path(analytics_db + ".tmp.wal") assert not tmp_wal.exists(), "Temp WAL file must be cleaned up" + def test_rebuild_while_reading(self, setup_env): + """Rebuild should succeed even while a read-only connection exists.""" + from src.orchestrator import SyncOrchestrator + import duckdb + + _create_mock_extract( + setup_env["extracts_dir"], "keboola", + [{"name": "orders", "data": [{"id": "1"}]}], + ) + orch = SyncOrchestrator(analytics_db_path=setup_env["analytics_db"]) + orch.rebuild() + + reader = duckdb.connect(setup_env["analytics_db"], read_only=True) + result = orch.rebuild() + assert "keboola" in result + reader.close() + def test_rejects_malicious_table_name(self, setup_env): """Tables with SQL injection names in _meta must be skipped; safe tables still work.""" from src.orchestrator import SyncOrchestrator diff --git a/tests/test_web_ui.py b/tests/test_web_ui.py new file mode 100644 index 0000000..807c297 --- /dev/null +++ b/tests/test_web_ui.py @@ -0,0 +1,63 @@ +"""Smoke tests for web UI pages.""" +import os +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture +def web_client(tmp_path, monkeypatch): + monkeypatch.setenv("DATA_DIR", str(tmp_path)) + monkeypatch.setenv("TESTING", "1") + monkeypatch.setenv("JWT_SECRET_KEY", "test-secret-key-min-32-characters!!") + (tmp_path / "state").mkdir() + (tmp_path / "analytics").mkdir() + (tmp_path / "extracts").mkdir() + from app.main import create_app + app = create_app() + return TestClient(app) + + +@pytest.fixture +def admin_cookie(web_client, tmp_path, monkeypatch): + from src.db import get_system_db + from src.repositories.users import UserRepository + from app.auth.jwt import create_access_token + conn = get_system_db() + UserRepository(conn).create(id="admin1", email="admin@test.com", name="Admin", role="admin") + conn.close() + token = create_access_token(user_id="admin1", email="admin@test.com", role="admin") + return {"access_token": token} + + +class TestWebUISmoke: + def test_login_page(self, web_client): + resp = web_client.get("/login") + assert resp.status_code == 200 + + def test_dashboard(self, web_client, admin_cookie): + resp = web_client.get("/dashboard", cookies=admin_cookie) + assert resp.status_code in (200, 302) + + def test_catalog(self, web_client, admin_cookie): + resp = web_client.get("/catalog", cookies=admin_cookie) + assert resp.status_code == 200 + + def test_corporate_memory(self, web_client, admin_cookie): + resp = web_client.get("/corporate-memory", cookies=admin_cookie) + assert resp.status_code == 200 + + def test_activity_center(self, web_client, admin_cookie): + resp = web_client.get("/activity-center", cookies=admin_cookie) + assert resp.status_code == 200 + + def test_admin_tables(self, web_client, admin_cookie): + resp = web_client.get("/admin/tables", cookies=admin_cookie) + if resp.status_code == 404: + pytest.skip("Route /admin/tables does not exist") + assert resp.status_code == 200 + + def test_admin_permissions(self, web_client, admin_cookie): + resp = web_client.get("/admin/permissions", cookies=admin_cookie) + if resp.status_code == 404: + pytest.skip("Route /admin/permissions does not exist") + assert resp.status_code == 200