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
This commit is contained in:
parent
8df8183a9f
commit
5131816a5b
4 changed files with 133 additions and 0 deletions
12
tests/test_instance_config.py
Normal file
12
tests/test_instance_config.py
Normal file
|
|
@ -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)
|
||||||
41
tests/test_jira_service.py
Normal file
41
tests/test_jira_service.py
Normal file
|
|
@ -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()
|
||||||
|
|
@ -357,6 +357,23 @@ class TestSyncOrchestrator:
|
||||||
tmp_wal = Path(analytics_db + ".tmp.wal")
|
tmp_wal = Path(analytics_db + ".tmp.wal")
|
||||||
assert not tmp_wal.exists(), "Temp WAL file must be cleaned up"
|
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):
|
def test_rejects_malicious_table_name(self, setup_env):
|
||||||
"""Tables with SQL injection names in _meta must be skipped; safe tables still work."""
|
"""Tables with SQL injection names in _meta must be skipped; safe tables still work."""
|
||||||
from src.orchestrator import SyncOrchestrator
|
from src.orchestrator import SyncOrchestrator
|
||||||
|
|
|
||||||
63
tests/test_web_ui.py
Normal file
63
tests/test_web_ui.py
Normal file
|
|
@ -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
|
||||||
Loading…
Reference in a new issue