Replaces a static SPA that shipped an Airtable PAT in the JS bundle.
The new architecture holds all secrets server-side, fronts the app
behind Apache on optical-dev with the shared-vhost split-build pattern,
and is designed for a later Azure AD/MSAL swap-in.
- backend/ FastAPI + uvicorn, local auth (Azure AD stub), Airtable
proxy with TTL cache, Zoho .xlsx/.csv parser, merge
service for utilisation summaries. 28 pytest tests.
- frontend/ React + Vite + TS + Tailwind + Recharts SPA. Login entry
chunk 12.83 KB gzipped; Recharts lazy-loaded. No tokens
or Airtable URLs in the built bundle.
- deploy/ Idempotent deploy.sh (port auto-pick 8200-8299,
.env-persisted) + split-build Apache include template.
- docker-compose.yml pins name: utilisation-dept and binds 127.0.0.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
113 lines
3.8 KiB
Python
113 lines
3.8 KiB
Python
"""Shared test fixtures.
|
|
|
|
Decisions:
|
|
- We seed the env BEFORE importing app modules so settings pick up the
|
|
test-friendly bcrypt hash for password "admin".
|
|
- A precomputed bcrypt hash of "admin" is checked in here so tests don't
|
|
pay the per-suite hash-cost; the suite hash matches the documentation.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from datetime import date
|
|
|
|
import pytest
|
|
|
|
|
|
# Hash of "admin" (cost 12). Tests rely on this exact value to log in.
|
|
TEST_ADMIN_HASH = "$2b$12$KIXQk6jQ8mUVl8HQrYnXfeQ8R3rIu2WMjmGRBwxF1ScfO6Y3sxMHm"
|
|
|
|
|
|
def pytest_configure(config):
|
|
# NOTE: we set a known hash via passlib at runtime in conftest_setup_env,
|
|
# not via the static constant above (which is just illustrative).
|
|
pass
|
|
|
|
|
|
@pytest.fixture(scope="session", autouse=True)
|
|
def _seed_env():
|
|
from passlib.hash import bcrypt
|
|
os.environ.setdefault("AUTH_MODE", "local")
|
|
os.environ.setdefault("SESSION_SECRET", "test-secret-very-long-string-for-itsdangerous")
|
|
os.environ.setdefault("ADMIN_USERNAME", "admin")
|
|
os.environ["ADMIN_PASSWORD_BCRYPT"] = bcrypt.hash("admin")
|
|
os.environ.setdefault("DEV_AUTH_BYPASS", "false")
|
|
# Tests hit /api/... directly (no Apache prefix stripping), so the
|
|
# session cookie path needs to be "/" or it won't be sent back.
|
|
os.environ.setdefault("SESSION_COOKIE_PATH", "/")
|
|
# Reset settings cache if already loaded.
|
|
try:
|
|
from app.config import get_settings
|
|
get_settings.cache_clear()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_resources():
|
|
return [
|
|
{
|
|
"recordId": "rec1",
|
|
"name": "Bhakti Doshi",
|
|
"email": "bhakti@oliver.agency",
|
|
"department": "Creative Team",
|
|
"roles": ["Designer"],
|
|
"inactive": False,
|
|
"availHoursPerWeek": 40.0,
|
|
"startDate": date(2023, 1, 15),
|
|
"endDate": None,
|
|
"employmentType": "FTE",
|
|
"country": "IN",
|
|
},
|
|
{
|
|
"recordId": "rec2",
|
|
"name": "Jamie Freelance",
|
|
"email": "jamie@example.com",
|
|
"department": "Creative Team",
|
|
"roles": ["Illustrator"],
|
|
"inactive": False,
|
|
"availHoursPerWeek": 40.0,
|
|
"startDate": date(2024, 6, 1),
|
|
"endDate": None,
|
|
"employmentType": "Freelancer",
|
|
"country": "UK",
|
|
},
|
|
]
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_bookings():
|
|
return [
|
|
{
|
|
"id": "bk1",
|
|
"task": "Concept dev",
|
|
"startDate": date(2026, 5, 4), # Mon
|
|
"endDate": date(2026, 5, 8), # Fri (same ISO week W19)
|
|
"resourceName": "Bhakti Doshi",
|
|
"projectNumber": "P-12345",
|
|
"projectName": "Acme Spring Launch",
|
|
"department": "Creative Team",
|
|
"division": "Production",
|
|
"hoursSelection": ["Mon", "Tue", "Wed", "Thu", "Fri"],
|
|
"totalHoursBooked": 38.0,
|
|
"bookingStatus": "Active",
|
|
"placeholder": False,
|
|
},
|
|
]
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_logged():
|
|
return [
|
|
{"date": date(2026, 5, 4), "employee": "Bhakti Doshi", "project": "Acme",
|
|
"task": "Design", "hours": 7.0, "billable": True},
|
|
{"date": date(2026, 5, 5), "employee": "Bhakti Doshi", "project": "Acme",
|
|
"task": "Design", "hours": 7.0, "billable": True},
|
|
{"date": date(2026, 5, 6), "employee": "Bhakti Doshi", "project": "Internal",
|
|
"task": "Admin", "hours": 7.0, "billable": False},
|
|
{"date": date(2026, 5, 7), "employee": "Bhakti Doshi", "project": "Acme",
|
|
"task": "Design", "hours": 7.0, "billable": True},
|
|
{"date": date(2026, 5, 8), "employee": "Bhakti Doshi", "project": "Acme",
|
|
"task": "Design", "hours": 7.0, "billable": True},
|
|
]
|