/ docs / testing-guide.md
testing-guide.md
  1  # Testing Guide
  2  
  3  All tests run **inside Docker** via `docker-compose.test.yml` (root → drops to ag3ntum_api via `setpriv --init-groups`, `AG3NTUM_TEST_MODE=true`).
  4  
  5  **Test entrypoint** (`entrypoint-test.sh`): Installs test sudoers, syncs Linux users from DB, creates fully-equipped test users (`ag3ntum_tester_a` UID 59990, `ag3ntum_tester_b` UID 59991) with DB entries, venvs, persistent storage, and shared GID memberships, then drops privileges. Test users are at the high end of the isolated range to avoid conflicts with real users. Credentials: email `ag3ntum_tester_a@test.local`, password `TestPassword123!`.
  6  
  7  ---
  8  
  9  ## Writing Backend Tests
 10  
 11  ```python
 12  # tests/backend/test_<module>.py
 13  class TestFeature:
 14      @pytest.mark.unit
 15      async def test_behavior(self, test_app, auth_headers):
 16          response = await test_app.get("/endpoint", headers=auth_headers)
 17          assert response.status_code == 200
 18  
 19      @pytest.mark.e2e
 20      async def test_full_flow(self, test_app): ...
 21  ```
 22  
 23  **Backend fixtures** (`conftest.py`): `db_engine`/`db_session` (in-memory SQLite) | `test_app` (FastAPI client) | `auth_headers` (JWT) | `mock_agent_runner` | `temp_session_dir` | `test_user_manager`
 24  
 25  **Redis fixtures** (`redis/conftest.py`): `redis_connection` | `event_hub` | `tracer_factory`
 26  
 27  ---
 28  
 29  ## Writing E2E / Functional Tests (Real Users)
 30  
 31  Tests that need real Linux users (sandbox execution, filesystem permissions, mount access, user isolation) **must reuse pre-built test users**, not create them dynamically.
 32  
 33  **Why**: The API process gets its supplementary groups at startup via `setpriv --init-groups`. Dynamically-created users add `ag3ntum_api` to the new user's group, but the already-running API process doesn't pick up the change. This causes `Permission denied` on session workspace directories. Restarting the container mid-test is not viable.
 34  
 35  **Pre-built test users** (created by `entrypoint-test.sh`):
 36  
 37  | Field | tester_a | tester_b |
 38  |-------|----------|----------|
 39  | Username | `ag3ntum_tester_a` | `ag3ntum_tester_b` |
 40  | UID/GID | 59990 | 59991 |
 41  | Email | `ag3ntum_tester_a@test.local` | `ag3ntum_tester_b@test.local` |
 42  | Password | `TestPassword123!` | `TestPassword123!` |
 43  | Home | `/users/ag3ntum_tester_a` | `/users/ag3ntum_tester_b` |
 44  
 45  Both have: Linux accounts, DB entries, Python venvs, persistent storage, shared GID memberships, `.claude/skills/` dirs.
 46  
 47  **Pattern for E2E tests**:
 48  ```python
 49  from types import SimpleNamespace
 50  
 51  # Constants (reuse across test files)
 52  PREBUILT_USER_A_USERNAME = "ag3ntum_tester_a"
 53  PREBUILT_USER_A_UID = 59990
 54  
 55  def _prebuilt_user(username: str, uid: int) -> SimpleNamespace:
 56      return SimpleNamespace(username=username, linux_uid=uid)
 57  
 58  # Fixture — no DB session needed
 59  @pytest.fixture
 60  def test_user(self) -> SimpleNamespace:
 61      return _prebuilt_user(PREBUILT_USER_A_USERNAME, PREBUILT_USER_A_UID)
 62  
 63  # For API auth, login with known credentials
 64  response = await client.post("/auth/token", json={
 65      "email": "ag3ntum_tester_a@test.local",
 66      "password": "TestPassword123!",
 67  })
 68  ```
 69  
 70  **Rules**:
 71  - Prefix test artifacts with `_test_` or `_e2e_` for easy cleanup
 72  - Always clean up test files in fixture teardown (pre-built users persist across runs)
 73  - Use `try/finally` for cleanup in test bodies that create files
 74  - Only `TestRealUserCreation` in `test_real_user_integration.py` creates users dynamically (it tests the creation flow itself)
 75  - For two-user isolation tests, use both `ag3ntum_tester_a` and `ag3ntum_tester_b`
 76  
 77  ---
 78  
 79  ## Frontend Test Infrastructure
 80  
 81  Frontend tests (`./run.sh test --ui`) always run in **dev mode** regardless of the current deployment mode. The test runner:
 82  
 83  1. Starts the web container with `docker-compose.dev.yml` overlay (Vite dev server + node_modules)
 84  2. Runs `npm install` if needed (copies `package.json` to `/app/` as safety net)
 85  3. Executes `vitest run` inside the container
 86  4. Restores the previous deployment mode (prod/dev) after tests complete
 87  
 88  This means UI tests work correctly even after a `./run.sh build` (production mode) — the test infrastructure automatically switches the web container to dev mode.
 89  
 90  **If UI tests fail with `ENOENT /app/package.json`**: The web container may be running in prod mode without node_modules. Run `./run.sh test --ui` again — it will recreate the container in dev mode.
 91  
 92  ---
 93  
 94  ## Writing Frontend Tests
 95  
 96  vitest + React Testing Library + MSW:
 97  ```typescript
 98  // tests/web_terminal_console/unit/<Component>.test.tsx
 99  import { renderWithProviders } from '../utils/renderWithProviders';
100  test('renders', () => {
101    renderWithProviders(<MyComponent />);
102    expect(screen.getByText('expected')).toBeInTheDocument();
103  });
104  ```
105  Setup: `setup.ts` (MSW, jest-dom, window mocks). Mocks: `mocks/handlers.ts`.
106  
107  ---
108  
109  ## Reseller/Admin Integration Tests
110  
111  Backend test suites for reselling features follow a pattern of HTTP integration tests using the FastAPI test client with fixtures from `conftest.py`.
112  
113  **Key fixtures** (`tests/backend/conftest.py`): `client` (TestClient), `admin_auth_headers` (admin JWT), `reseller_auth_headers` (reseller JWT), `second_reseller_auth_headers` (for IDOR tests), `db` (async session).
114  
115  **Test files** (Phase 1 + Phase 2):
116  - `test_reseller.py` — Service-layer reseller tests (CRUD, quotas, flags, spending)
117  - `test_reseller_http.py` — HTTP integration tests for reseller endpoints
118  - `test_reseller_config_http.py` — HTTP tests for reseller config/security/ssh endpoints
119  - `test_reseller_metrics_http.py` — WHMCS metrics + usage export HTTP tests
120  - `test_admin.py` — Admin service-layer tests + platform config
121  - `test_admin_http.py` — Admin HTTP integration tests (reseller lifecycle, suspension)
122  - `test_admin_config_http.py` — Platform config GET/PUT HTTP tests
123  - `test_cidr_and_audit.py` — CIDR IP allowlisting + audit log tests
124  - `test_webhook_service.py` — Webhook service unit tests (HMAC, delivery, retry)
125  - `test_webhook_http.py` — Webhook CRUD HTTP integration tests + IDOR
126  - `test_processor_lifecycle.py` — WebhookProcessor + RetentionProcessor lifecycle
127  - `test_feature_flag_service.py` — FeatureFlagService unit tests (defaults, overrides, DB ops)
128  - `test_data_retention.py` — Data retention config + purge tests
129  - `test_simplify_fixes.py` — Regression tests for code quality fixes
130  
131  **QA playbooks** (manual testing): `docs/plans/enable-reselling/qa-test-playbook.md` (Phase 1, 96 tests), `docs/plans/enable-reselling/qa-test-stage2.md` (Phase 2/3, 81 tests).
132  
133  ---
134  
135  ## Test Output
136  
137  `logs/latest-test-results.log` (overwritten each run):
138  ```bash
139  grep -A 10 "FAILED\|ERROR" logs/latest-test-results.log
140  ```