diff --git a/.Jules/test_coverage_fix.md b/.Jules/test_coverage_fix.md
new file mode 100644
index 00000000..2e978f81
--- /dev/null
+++ b/.Jules/test_coverage_fix.md
@@ -0,0 +1,7 @@
+## 2024-05-30 - Fix Test Coverage
+
+**Learning:**
+In order to achieve 100% test coverage we must provide coverage for all files. `app/db.py` was missing test logic so I mocked out connections and pooler behavior to simulate various edge cases. The `GroupModal.tsx` React component was also missing coverage for form validations on the client which I addressed via React Testing Library by simulating `submit` events.
+
+**Action:**
+Created new test logic for missing files to hit all branches and verify coverage using tools like `pytest` and `vitest`.
diff --git a/backend/tests/test_db.py b/backend/tests/test_db.py
new file mode 100644
index 00000000..efbe704e
--- /dev/null
+++ b/backend/tests/test_db.py
@@ -0,0 +1,199 @@
+from __future__ import annotations
+
+import time
+import asyncio
+from unittest.mock import patch, MagicMock
+
+import pytest
+
+from app.db import (
+ get_sync_database_url,
+ _probe_pooler_admin_console,
+ get_pooler_detection,
+ get_session,
+ get_read_session,
+)
+from app.pooler import PoolerDetectionResult, PoolerKind
+from app.settings import settings
+
+
+def test_get_sync_database_url():
+ with patch("app.db.settings", database_url="postgresql+asyncpg://user:pass@host/db"):
+ assert get_sync_database_url() == "postgresql+psycopg://user:pass@host/db"
+
+ with patch("app.db.settings", database_url="sqlite:///test.db"):
+ assert get_sync_database_url() == "sqlite:///test.db"
+
+@pytest.mark.asyncio
+async def test_probe_pooler_admin_console_timeout():
+ with patch("app.db.settings", db_pooler_probe_timeout_seconds=0.0, database_url="postgresql+asyncpg://user:pass@host/db"):
+ res = await _probe_pooler_admin_console("pgbouncer")
+ assert res is None
+
+@pytest.mark.asyncio
+async def test_probe_pooler_admin_console_success():
+ class DummyCursor:
+ def execute(self, q): pass
+ def fetchone(self): return ("PgBouncer 1.21.0",)
+ def __enter__(self): return self
+ def __exit__(self, *args): pass
+
+ class DummyConn:
+ def cursor(self): return DummyCursor()
+ def __enter__(self): return self
+ def __exit__(self, *args): pass
+
+ with patch("app.db.settings", db_pooler_probe_timeout_seconds=2.0, database_url="postgresql+asyncpg://user:pass@host/db"):
+ with patch("psycopg.connect", return_value=DummyConn()):
+ res = await _probe_pooler_admin_console("pgbouncer")
+ assert res == "PgBouncer 1.21.0"
+
+@pytest.mark.asyncio
+async def test_probe_pooler_admin_console_none():
+ class DummyCursor:
+ def execute(self, q): pass
+ def fetchone(self): return None
+ def __enter__(self): return self
+ def __exit__(self, *args): pass
+
+ class DummyConn:
+ def cursor(self): return DummyCursor()
+ def __enter__(self): return self
+ def __exit__(self, *args): pass
+
+ with patch("app.db.settings", db_pooler_probe_timeout_seconds=2.0, database_url="postgresql+asyncpg://user:pass@host/db"):
+ with patch("psycopg.connect", return_value=DummyConn()):
+ res = await _probe_pooler_admin_console("pgbouncer")
+ assert res is None
+
+@pytest.mark.asyncio
+async def test_probe_pooler_admin_console_exception():
+ with patch("app.db.settings", db_pooler_probe_timeout_seconds=2.0, database_url="postgresql+asyncpg://user:pass@host/db"):
+ with patch("psycopg.connect", side_effect=Exception("Timeout or fail")):
+ res = await _probe_pooler_admin_console("pgbouncer")
+ assert res is None
+
+
+@pytest.fixture(autouse=True)
+def reset_pooler_cache():
+ import app.db
+ app.db._pooler_cache = None
+ app.db._pooler_cache_at = 0.0
+ yield
+ app.db._pooler_cache = None
+ app.db._pooler_cache_at = 0.0
+
+@pytest.mark.asyncio
+async def test_get_pooler_detection_explicit():
+ with patch("app.db.settings", db_pooler_kind="pgbouncer"):
+ res = await get_pooler_detection()
+ assert res.kind == PoolerKind.PGBOUNCER
+ assert res.detected is True
+
+@pytest.mark.asyncio
+async def test_get_pooler_detection_cached():
+ import app.db
+ app.db._pooler_cache = PoolerDetectionResult(PoolerKind.PGCAT, True, "PgCat 0.1")
+ app.db._pooler_cache_at = time.monotonic()
+
+ with patch("app.db.settings", db_pooler_kind=None):
+ res = await get_pooler_detection()
+ assert res.kind == PoolerKind.PGCAT
+
+@pytest.mark.asyncio
+async def test_get_pooler_detection_pgbouncer():
+ with patch("app.db.settings", db_pooler_kind=None):
+ with patch("app.db._probe_pooler_admin_console", side_effect=lambda db: "PgBouncer 1.21.0" if db == "pgbouncer" else None):
+ res = await get_pooler_detection()
+ assert res.kind == PoolerKind.PGBOUNCER
+
+@pytest.mark.asyncio
+async def test_get_pooler_detection_pgcat():
+ with patch("app.db.settings", db_pooler_kind=None):
+ with patch("app.db._probe_pooler_admin_console", side_effect=lambda db: "PgCat 0.10.0" if db == "pgcat" else None):
+ res = await get_pooler_detection()
+ assert res.kind == PoolerKind.PGCAT
+
+@pytest.mark.asyncio
+async def test_get_pooler_detection_unknown():
+ with patch("app.db.settings", db_pooler_kind=None):
+ with patch("app.db._probe_pooler_admin_console", return_value=None):
+ res = await get_pooler_detection()
+ assert res.kind == PoolerKind.UNKNOWN
+
+@pytest.mark.asyncio
+async def test_get_session():
+ async for session in get_session():
+ assert session is not None
+ break
+
+@pytest.mark.asyncio
+async def test_get_read_session_no_readonly():
+ import app.db
+ orig = app.db.ReadOnlySessionLocal
+ app.db.ReadOnlySessionLocal = None
+ try:
+ async for session in get_read_session():
+ assert session is not None
+ break
+ finally:
+ app.db.ReadOnlySessionLocal = orig
+
+@pytest.mark.asyncio
+async def test_get_read_session_with_readonly():
+ import app.db
+ app.db.ReadOnlySessionLocal = MagicMock()
+ app.db.ReadOnlySessionLocal.return_value.__aenter__.return_value = "readonly_session"
+ app.db.SessionLocal = MagicMock()
+ app.db.SessionLocal.return_value.__aenter__.return_value = "primary_session"
+
+ with patch("app.db.get_pooler_detection", return_value=PoolerDetectionResult(PoolerKind.PGBOUNCER, True, "PgBouncer")):
+ with patch("app.db.should_route_reads_to_read_only", return_value=True):
+ async for session in get_read_session():
+ assert session == "readonly_session"
+ break
+
+ with patch("app.db.should_route_reads_to_read_only", return_value=False):
+ async for session in get_read_session():
+ assert session == "primary_session"
+ break
+
+@pytest.mark.asyncio
+async def test_get_pooler_detection_locked_cache():
+ import app.db
+ with patch("app.db.settings", db_pooler_kind=None):
+ with patch("app.db._probe_pooler_admin_console", return_value="PgBouncer 1.21.0"):
+ async def fast_cache():
+ app.db._pooler_cache = PoolerDetectionResult(PoolerKind.PGCAT, True, "PgCat 0.1")
+ app.db._pooler_cache_at = time.monotonic()
+
+ # Pretend that by the time lock is acquired, cache is populated
+ original_lock = app.db._pooler_lock
+ class MockLock:
+ async def __aenter__(self):
+ await fast_cache()
+ return self
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
+ pass
+
+ app.db._pooler_lock = MockLock()
+ try:
+ res = await get_pooler_detection()
+ assert res.kind == PoolerKind.PGCAT
+ finally:
+ app.db._pooler_lock = original_lock
+
+@pytest.mark.asyncio
+async def test_get_read_session_return():
+ import app.db
+ orig = app.db.ReadOnlySessionLocal
+ app.db.ReadOnlySessionLocal = None
+ try:
+ gen = get_read_session()
+ await gen.__anext__()
+ try:
+ await gen.__anext__()
+ except StopAsyncIteration:
+ pass
+ finally:
+ app.db.ReadOnlySessionLocal = orig
diff --git a/frontend/src/components/modals/GroupModal.test.tsx b/frontend/src/components/modals/GroupModal.test.tsx
index 997d887f..799cca2f 100644
--- a/frontend/src/components/modals/GroupModal.test.tsx
+++ b/frontend/src/components/modals/GroupModal.test.tsx
@@ -1,10 +1,13 @@
import '@testing-library/jest-dom/vitest';
import { describe, expect, it, vi } from 'vitest';
-import { render, screen } from '@testing-library/react';
+import { render, screen, fireEvent, cleanup } from '@testing-library/react';
+import { afterEach } from 'vitest';
import { GroupModal } from './GroupModal';
describe('GroupModal', () => {
+ afterEach(cleanup);
+
it('exposes truncated assignment table names accessibly', () => {
const tableName = 'analytics.extremely_long_customer_activity_table';
@@ -42,4 +45,54 @@ describe('GroupModal', () => {
expect(tableLabel).toHaveAttribute('title', tableName);
expect(tableLabel).not.toHaveAttribute('tabindex', '0');
});
+
+ it('does not call onCreateBusinessGroup if newGroupName is empty on form submit', () => {
+ const onCreateBusinessGroup = vi.fn();
+
+ render(
+
+ );
+
+ const form = screen.getByRole('dialog').querySelector('form');
+ expect(form).not.toBeNull();
+ fireEvent.submit(form!);
+ expect(onCreateBusinessGroup).not.toHaveBeenCalled();
+ });
+
+ it('calls onCreateBusinessGroup if newGroupName is valid on form submit', () => {
+ const onCreateBusinessGroup = vi.fn();
+
+ render(
+
+ );
+
+ const form = screen.getByRole('dialog').querySelector('form');
+ expect(form).not.toBeNull();
+ fireEvent.submit(form!);
+ expect(onCreateBusinessGroup).toHaveBeenCalled();
+ });
});