draft-board 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/backend/.env.example +9 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_patch.txt +195 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_stat.txt +6 -0
- package/app/backend/CURL_EXAMPLES.md +335 -0
- package/app/backend/ENV_SETUP.md +65 -0
- package/app/backend/alembic/env.py +71 -0
- package/app/backend/alembic/script.py.mako +28 -0
- package/app/backend/alembic/versions/001_initial_schema.py +104 -0
- package/app/backend/alembic/versions/002_add_jobs_table.py +52 -0
- package/app/backend/alembic/versions/003_add_workspace_table.py +48 -0
- package/app/backend/alembic/versions/004_add_evidence_table.py +56 -0
- package/app/backend/alembic/versions/005_add_verification_commands.py +32 -0
- package/app/backend/alembic/versions/006_add_planner_lock_table.py +39 -0
- package/app/backend/alembic/versions/007_add_revision_review_tables.py +126 -0
- package/app/backend/alembic/versions/008_add_revision_idempotency_and_traceability.py +52 -0
- package/app/backend/alembic/versions/009_add_job_health_fields.py +46 -0
- package/app/backend/alembic/versions/010_add_review_comment_line_content.py +36 -0
- package/app/backend/alembic/versions/011_add_analysis_cache.py +47 -0
- package/app/backend/alembic/versions/012_add_boards_table.py +102 -0
- package/app/backend/alembic/versions/013_add_ticket_blocking.py +45 -0
- package/app/backend/alembic/versions/014_add_agent_sessions.py +220 -0
- package/app/backend/alembic/versions/015_add_ticket_sort_order.py +33 -0
- package/app/backend/alembic/versions/03220f0b93ae_add_pr_fields_to_ticket.py +49 -0
- package/app/backend/alembic/versions/0c2d89fff3b1_seed_board_configs_from_yaml.py +206 -0
- package/app/backend/alembic/versions/3348e5cf54c1_add_merge_checklist_table.py +67 -0
- package/app/backend/alembic/versions/357c780ee445_add_goal_status.py +34 -0
- package/app/backend/alembic/versions/553340b7e26c_add_autonomy_fields_to_goal.py +65 -0
- package/app/backend/alembic/versions/774dc335c679_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/7b307e847cbd_merge_heads.py +23 -0
- package/app/backend/alembic/versions/82ecd978cc70_add_missing_indexes.py +48 -0
- package/app/backend/alembic/versions/8ef5054dc280_add_normalized_log_entries.py +173 -0
- package/app/backend/alembic/versions/8f3e2bd8ea3b_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/9d17f0698d3b_add_config_column_to_boards_table.py +30 -0
- package/app/backend/alembic/versions/add_agent_conversation_history.py +72 -0
- package/app/backend/alembic/versions/add_job_variant.py +34 -0
- package/app/backend/alembic/versions/add_performance_indexes.py +95 -0
- package/app/backend/alembic/versions/add_repos_and_board_repos.py +174 -0
- package/app/backend/alembic/versions/add_session_id_to_jobs.py +27 -0
- package/app/backend/alembic/versions/add_sqlite_backend_tables.py +104 -0
- package/app/backend/alembic/versions/b10fb0b62240_add_diff_content_to_revisions.py +34 -0
- package/app/backend/alembic.ini +89 -0
- package/app/backend/app/__init__.py +3 -0
- package/app/backend/app/data_dir.py +85 -0
- package/app/backend/app/database.py +70 -0
- package/app/backend/app/database_sync.py +64 -0
- package/app/backend/app/dependencies/__init__.py +5 -0
- package/app/backend/app/dependencies/auth.py +80 -0
- package/app/backend/app/dependencies.py +43 -0
- package/app/backend/app/exceptions.py +178 -0
- package/app/backend/app/executors/__init__.py +1 -0
- package/app/backend/app/executors/adapters/__init__.py +1 -0
- package/app/backend/app/executors/adapters/aider.py +152 -0
- package/app/backend/app/executors/adapters/amazon_q.py +103 -0
- package/app/backend/app/executors/adapters/amp.py +123 -0
- package/app/backend/app/executors/adapters/claude.py +177 -0
- package/app/backend/app/executors/adapters/cline.py +127 -0
- package/app/backend/app/executors/adapters/codex.py +167 -0
- package/app/backend/app/executors/adapters/copilot.py +202 -0
- package/app/backend/app/executors/adapters/cursor.py +87 -0
- package/app/backend/app/executors/adapters/droid.py +123 -0
- package/app/backend/app/executors/adapters/gemini.py +132 -0
- package/app/backend/app/executors/adapters/goose.py +131 -0
- package/app/backend/app/executors/adapters/opencode.py +123 -0
- package/app/backend/app/executors/adapters/qwen.py +123 -0
- package/app/backend/app/executors/plugins/__init__.py +1 -0
- package/app/backend/app/executors/registry.py +202 -0
- package/app/backend/app/executors/spec.py +226 -0
- package/app/backend/app/main.py +486 -0
- package/app/backend/app/middleware/__init__.py +13 -0
- package/app/backend/app/middleware/idempotency.py +426 -0
- package/app/backend/app/middleware/rate_limit.py +312 -0
- package/app/backend/app/middleware/security_headers.py +43 -0
- package/app/backend/app/middleware/timeout.py +37 -0
- package/app/backend/app/models/__init__.py +56 -0
- package/app/backend/app/models/agent_conversation_history.py +56 -0
- package/app/backend/app/models/agent_session.py +127 -0
- package/app/backend/app/models/analysis_cache.py +49 -0
- package/app/backend/app/models/base.py +9 -0
- package/app/backend/app/models/board.py +79 -0
- package/app/backend/app/models/board_repo.py +68 -0
- package/app/backend/app/models/cost_budget.py +42 -0
- package/app/backend/app/models/enums.py +40 -0
- package/app/backend/app/models/evidence.py +132 -0
- package/app/backend/app/models/goal.py +102 -0
- package/app/backend/app/models/idempotency_entry.py +30 -0
- package/app/backend/app/models/job.py +163 -0
- package/app/backend/app/models/job_queue.py +39 -0
- package/app/backend/app/models/kv_store.py +28 -0
- package/app/backend/app/models/merge_checklist.py +87 -0
- package/app/backend/app/models/normalized_log.py +100 -0
- package/app/backend/app/models/planner_lock.py +43 -0
- package/app/backend/app/models/rate_limit_entry.py +25 -0
- package/app/backend/app/models/repo.py +66 -0
- package/app/backend/app/models/review_comment.py +91 -0
- package/app/backend/app/models/review_summary.py +69 -0
- package/app/backend/app/models/revision.py +130 -0
- package/app/backend/app/models/ticket.py +223 -0
- package/app/backend/app/models/ticket_event.py +83 -0
- package/app/backend/app/models/user.py +47 -0
- package/app/backend/app/models/workspace.py +71 -0
- package/app/backend/app/redis_client.py +119 -0
- package/app/backend/app/routers/__init__.py +29 -0
- package/app/backend/app/routers/agents.py +296 -0
- package/app/backend/app/routers/auth.py +94 -0
- package/app/backend/app/routers/board.py +885 -0
- package/app/backend/app/routers/dashboard.py +351 -0
- package/app/backend/app/routers/debug.py +528 -0
- package/app/backend/app/routers/evidence.py +96 -0
- package/app/backend/app/routers/executors.py +324 -0
- package/app/backend/app/routers/goals.py +574 -0
- package/app/backend/app/routers/jobs.py +448 -0
- package/app/backend/app/routers/maintenance.py +172 -0
- package/app/backend/app/routers/merge.py +360 -0
- package/app/backend/app/routers/planner.py +537 -0
- package/app/backend/app/routers/pull_requests.py +382 -0
- package/app/backend/app/routers/repos.py +263 -0
- package/app/backend/app/routers/revisions.py +939 -0
- package/app/backend/app/routers/settings.py +267 -0
- package/app/backend/app/routers/tickets.py +2003 -0
- package/app/backend/app/routers/webhooks.py +143 -0
- package/app/backend/app/routers/websocket.py +249 -0
- package/app/backend/app/schemas/__init__.py +109 -0
- package/app/backend/app/schemas/board.py +87 -0
- package/app/backend/app/schemas/common.py +33 -0
- package/app/backend/app/schemas/evidence.py +87 -0
- package/app/backend/app/schemas/goal.py +90 -0
- package/app/backend/app/schemas/job.py +97 -0
- package/app/backend/app/schemas/merge.py +139 -0
- package/app/backend/app/schemas/planner.py +500 -0
- package/app/backend/app/schemas/repo.py +187 -0
- package/app/backend/app/schemas/review.py +137 -0
- package/app/backend/app/schemas/revision.py +114 -0
- package/app/backend/app/schemas/ticket.py +238 -0
- package/app/backend/app/schemas/ticket_event.py +72 -0
- package/app/backend/app/schemas/workspace.py +19 -0
- package/app/backend/app/services/__init__.py +31 -0
- package/app/backend/app/services/agent_memory_service.py +223 -0
- package/app/backend/app/services/agent_registry.py +346 -0
- package/app/backend/app/services/agent_session_manager.py +318 -0
- package/app/backend/app/services/agent_session_service.py +219 -0
- package/app/backend/app/services/agent_tools.py +379 -0
- package/app/backend/app/services/auth_service.py +98 -0
- package/app/backend/app/services/autonomy_service.py +380 -0
- package/app/backend/app/services/board_repo_service.py +201 -0
- package/app/backend/app/services/board_service.py +326 -0
- package/app/backend/app/services/cleanup_service.py +1085 -0
- package/app/backend/app/services/config_service.py +908 -0
- package/app/backend/app/services/context_gatherer.py +557 -0
- package/app/backend/app/services/cost_tracking_service.py +293 -0
- package/app/backend/app/services/cursor_log_normalizer.py +536 -0
- package/app/backend/app/services/delivery_pipeline.py +440 -0
- package/app/backend/app/services/executor_service.py +634 -0
- package/app/backend/app/services/git_host/__init__.py +11 -0
- package/app/backend/app/services/git_host/factory.py +87 -0
- package/app/backend/app/services/git_host/github.py +270 -0
- package/app/backend/app/services/git_host/gitlab.py +194 -0
- package/app/backend/app/services/git_host/protocol.py +75 -0
- package/app/backend/app/services/git_merge_simple.py +346 -0
- package/app/backend/app/services/git_ops.py +384 -0
- package/app/backend/app/services/github_service.py +233 -0
- package/app/backend/app/services/goal_service.py +113 -0
- package/app/backend/app/services/job_service.py +423 -0
- package/app/backend/app/services/job_watchdog_service.py +424 -0
- package/app/backend/app/services/langchain_adapter.py +122 -0
- package/app/backend/app/services/llm_provider_clients.py +351 -0
- package/app/backend/app/services/llm_service.py +285 -0
- package/app/backend/app/services/log_normalizer.py +342 -0
- package/app/backend/app/services/log_stream_service.py +276 -0
- package/app/backend/app/services/merge_checklist_service.py +264 -0
- package/app/backend/app/services/merge_service.py +784 -0
- package/app/backend/app/services/orchestrator_log.py +84 -0
- package/app/backend/app/services/planner_service.py +1662 -0
- package/app/backend/app/services/planner_tick_sync.py +1040 -0
- package/app/backend/app/services/queued_message_service.py +156 -0
- package/app/backend/app/services/reliability_wrapper.py +389 -0
- package/app/backend/app/services/repo_discovery_service.py +318 -0
- package/app/backend/app/services/review_service.py +334 -0
- package/app/backend/app/services/revision_service.py +389 -0
- package/app/backend/app/services/safe_autopilot.py +510 -0
- package/app/backend/app/services/sqlite_worker.py +372 -0
- package/app/backend/app/services/task_dispatch.py +135 -0
- package/app/backend/app/services/ticket_generation_service.py +1781 -0
- package/app/backend/app/services/ticket_service.py +486 -0
- package/app/backend/app/services/udar_planner_service.py +1007 -0
- package/app/backend/app/services/webhook_service.py +126 -0
- package/app/backend/app/services/workspace_service.py +465 -0
- package/app/backend/app/services/worktree_file_service.py +92 -0
- package/app/backend/app/services/worktree_validator.py +213 -0
- package/app/backend/app/sqlite_kv.py +278 -0
- package/app/backend/app/state_machine.py +128 -0
- package/app/backend/app/templates/__init__.py +5 -0
- package/app/backend/app/templates/registry.py +243 -0
- package/app/backend/app/utils/__init__.py +5 -0
- package/app/backend/app/utils/artifact_reader.py +87 -0
- package/app/backend/app/utils/circuit_breaker.py +229 -0
- package/app/backend/app/utils/db_retry.py +136 -0
- package/app/backend/app/utils/ignored_fields.py +123 -0
- package/app/backend/app/utils/validators.py +54 -0
- package/app/backend/app/websocket/__init__.py +5 -0
- package/app/backend/app/websocket/manager.py +179 -0
- package/app/backend/app/websocket/state_tracker.py +113 -0
- package/app/backend/app/worker.py +3190 -0
- package/app/backend/calculator_tickets.json +40 -0
- package/app/backend/canary_tests.sh +591 -0
- package/app/backend/celerybeat-schedule +0 -0
- package/app/backend/celerybeat-schedule-shm +0 -0
- package/app/backend/celerybeat-schedule-wal +0 -0
- package/app/backend/logs/.gitkeep +3 -0
- package/app/backend/multiplication_division_implementation_tickets.json +55 -0
- package/app/backend/multiplication_division_tickets.json +42 -0
- package/app/backend/pyproject.toml +45 -0
- package/app/backend/requirements-dev.txt +8 -0
- package/app/backend/requirements.txt +20 -0
- package/app/backend/run.sh +30 -0
- package/app/backend/run_with_logs.sh +10 -0
- package/app/backend/scientific_calculator_tickets.json +40 -0
- package/app/backend/scripts/extract_openapi.py +21 -0
- package/app/backend/scripts/seed_demo.py +187 -0
- package/app/backend/setup_demo_review.py +302 -0
- package/app/backend/test_actual_parse.py +41 -0
- package/app/backend/test_agent_streaming.py +61 -0
- package/app/backend/test_parse.py +51 -0
- package/app/backend/test_streaming.py +51 -0
- package/app/backend/test_subprocess_streaming.py +50 -0
- package/app/backend/tests/__init__.py +1 -0
- package/app/backend/tests/conftest.py +46 -0
- package/app/backend/tests/test_auth.py +341 -0
- package/app/backend/tests/test_autonomy_service.py +391 -0
- package/app/backend/tests/test_cleanup_service_safety.py +417 -0
- package/app/backend/tests/test_middleware.py +279 -0
- package/app/backend/tests/test_planner_providers.py +290 -0
- package/app/backend/tests/test_planner_unblock.py +183 -0
- package/app/backend/tests/test_revision_invariants.py +618 -0
- package/app/backend/tests/test_sqlite_kv.py +290 -0
- package/app/backend/tests/test_sqlite_worker.py +353 -0
- package/app/backend/tests/test_task_dispatch.py +100 -0
- package/app/backend/tests/test_ticket_validation.py +304 -0
- package/app/backend/tests/test_udar_agent.py +693 -0
- package/app/backend/tests/test_webhook_service.py +184 -0
- package/app/backend/tickets_output.json +59 -0
- package/app/backend/user_management_tickets.json +50 -0
- package/app/backend/uvicorn.log +0 -0
- package/app/draft.yaml +313 -0
- package/app/frontend/dist/assets/index-LcjCczu5.js +155 -0
- package/app/frontend/dist/assets/index-_FP_279e.css +1 -0
- package/app/frontend/dist/index.html +14 -0
- package/app/frontend/dist/vite.svg +1 -0
- package/app/frontend/package.json +101 -0
- package/bin/cli.js +527 -0
- package/package.json +37 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Tests for multi-user authentication system."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from httpx import ASGITransport, AsyncClient
|
|
8
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
|
9
|
+
from sqlalchemy.pool import StaticPool
|
|
10
|
+
|
|
11
|
+
from app.models.base import Base
|
|
12
|
+
|
|
13
|
+
# Module-level engine (created once, reused across tests)
|
|
14
|
+
_test_engine = None
|
|
15
|
+
_test_session_maker = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _get_engine():
|
|
19
|
+
global _test_engine, _test_session_maker
|
|
20
|
+
if _test_engine is None:
|
|
21
|
+
_test_engine = create_async_engine(
|
|
22
|
+
"sqlite+aiosqlite:///:memory:",
|
|
23
|
+
echo=False,
|
|
24
|
+
connect_args={"check_same_thread": False},
|
|
25
|
+
poolclass=StaticPool,
|
|
26
|
+
)
|
|
27
|
+
_test_session_maker = async_sessionmaker(
|
|
28
|
+
_test_engine, class_=AsyncSession, expire_on_commit=False
|
|
29
|
+
)
|
|
30
|
+
return _test_engine, _test_session_maker
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
async def auth_client(monkeypatch):
|
|
35
|
+
"""Create a test client with auth enabled and fresh in-memory DB."""
|
|
36
|
+
import app.dependencies.auth as auth_mod
|
|
37
|
+
|
|
38
|
+
monkeypatch.setattr(auth_mod, "AUTH_ENABLED", True)
|
|
39
|
+
|
|
40
|
+
engine, session_maker = _get_engine()
|
|
41
|
+
|
|
42
|
+
async with engine.begin() as conn:
|
|
43
|
+
await conn.run_sync(Base.metadata.drop_all)
|
|
44
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
45
|
+
|
|
46
|
+
from app.database import get_db
|
|
47
|
+
from app.main import app
|
|
48
|
+
|
|
49
|
+
async def override_get_db():
|
|
50
|
+
async with session_maker() as session:
|
|
51
|
+
try:
|
|
52
|
+
yield session
|
|
53
|
+
await session.commit()
|
|
54
|
+
except Exception:
|
|
55
|
+
await session.rollback()
|
|
56
|
+
raise
|
|
57
|
+
|
|
58
|
+
app.dependency_overrides[get_db] = override_get_db
|
|
59
|
+
|
|
60
|
+
transport = ASGITransport(app=app)
|
|
61
|
+
async with AsyncClient(transport=transport, base_url="http://test") as c:
|
|
62
|
+
yield c
|
|
63
|
+
|
|
64
|
+
app.dependency_overrides.clear()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---- Auth endpoint tests ----
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@pytest.mark.asyncio
|
|
71
|
+
async def test_register_user(auth_client):
|
|
72
|
+
"""Test user registration."""
|
|
73
|
+
resp = await auth_client.post(
|
|
74
|
+
"/auth/register",
|
|
75
|
+
json={
|
|
76
|
+
"email": "alice@example.com",
|
|
77
|
+
"password": "securepass123",
|
|
78
|
+
"display_name": "Alice",
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
assert resp.status_code == 201
|
|
82
|
+
data = resp.json()
|
|
83
|
+
assert data["token_type"] == "bearer"
|
|
84
|
+
assert "access_token" in data
|
|
85
|
+
assert data["user"]["email"] == "alice@example.com"
|
|
86
|
+
assert data["user"]["display_name"] == "Alice"
|
|
87
|
+
assert data["user"]["is_active"] is True
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@pytest.mark.asyncio
|
|
91
|
+
async def test_register_duplicate_email(auth_client):
|
|
92
|
+
"""Duplicate email should be rejected."""
|
|
93
|
+
payload = {
|
|
94
|
+
"email": "bob@example.com",
|
|
95
|
+
"password": "securepass123",
|
|
96
|
+
"display_name": "Bob",
|
|
97
|
+
}
|
|
98
|
+
resp1 = await auth_client.post("/auth/register", json=payload)
|
|
99
|
+
assert resp1.status_code == 201
|
|
100
|
+
|
|
101
|
+
resp2 = await auth_client.post("/auth/register", json=payload)
|
|
102
|
+
assert resp2.status_code == 409
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@pytest.mark.asyncio
|
|
106
|
+
async def test_register_short_password(auth_client):
|
|
107
|
+
"""Password must be at least 8 characters."""
|
|
108
|
+
resp = await auth_client.post(
|
|
109
|
+
"/auth/register",
|
|
110
|
+
json={
|
|
111
|
+
"email": "short@example.com",
|
|
112
|
+
"password": "short",
|
|
113
|
+
"display_name": "Short",
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
assert resp.status_code == 422
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@pytest.mark.asyncio
|
|
120
|
+
async def test_login_success(auth_client):
|
|
121
|
+
"""Test successful login."""
|
|
122
|
+
await auth_client.post(
|
|
123
|
+
"/auth/register",
|
|
124
|
+
json={
|
|
125
|
+
"email": "carol@example.com",
|
|
126
|
+
"password": "securepass123",
|
|
127
|
+
"display_name": "Carol",
|
|
128
|
+
},
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
resp = await auth_client.post(
|
|
132
|
+
"/auth/login",
|
|
133
|
+
json={
|
|
134
|
+
"email": "carol@example.com",
|
|
135
|
+
"password": "securepass123",
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
assert resp.status_code == 200
|
|
139
|
+
data = resp.json()
|
|
140
|
+
assert "access_token" in data
|
|
141
|
+
assert data["user"]["email"] == "carol@example.com"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@pytest.mark.asyncio
|
|
145
|
+
async def test_login_wrong_password(auth_client):
|
|
146
|
+
"""Wrong password should be rejected."""
|
|
147
|
+
await auth_client.post(
|
|
148
|
+
"/auth/register",
|
|
149
|
+
json={
|
|
150
|
+
"email": "dave@example.com",
|
|
151
|
+
"password": "securepass123",
|
|
152
|
+
"display_name": "Dave",
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
resp = await auth_client.post(
|
|
157
|
+
"/auth/login",
|
|
158
|
+
json={
|
|
159
|
+
"email": "dave@example.com",
|
|
160
|
+
"password": "wrongpassword",
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
assert resp.status_code == 401
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@pytest.mark.asyncio
|
|
167
|
+
async def test_login_nonexistent_email(auth_client):
|
|
168
|
+
"""Login with non-existent email should fail."""
|
|
169
|
+
resp = await auth_client.post(
|
|
170
|
+
"/auth/login",
|
|
171
|
+
json={
|
|
172
|
+
"email": "nobody@example.com",
|
|
173
|
+
"password": "securepass123",
|
|
174
|
+
},
|
|
175
|
+
)
|
|
176
|
+
assert resp.status_code == 401
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@pytest.mark.asyncio
|
|
180
|
+
async def test_get_me_authenticated(auth_client):
|
|
181
|
+
"""GET /auth/me with valid token returns user profile."""
|
|
182
|
+
reg = await auth_client.post(
|
|
183
|
+
"/auth/register",
|
|
184
|
+
json={
|
|
185
|
+
"email": "eve@example.com",
|
|
186
|
+
"password": "securepass123",
|
|
187
|
+
"display_name": "Eve",
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
token = reg.json()["access_token"]
|
|
191
|
+
|
|
192
|
+
resp = await auth_client.get(
|
|
193
|
+
"/auth/me", headers={"Authorization": f"Bearer {token}"}
|
|
194
|
+
)
|
|
195
|
+
assert resp.status_code == 200
|
|
196
|
+
assert resp.json()["email"] == "eve@example.com"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@pytest.mark.asyncio
|
|
200
|
+
async def test_get_me_no_token(auth_client):
|
|
201
|
+
"""GET /auth/me without token should return 401 when auth enabled."""
|
|
202
|
+
resp = await auth_client.get("/auth/me")
|
|
203
|
+
assert resp.status_code == 401
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@pytest.mark.asyncio
|
|
207
|
+
async def test_get_me_invalid_token(auth_client):
|
|
208
|
+
"""GET /auth/me with invalid token should return 401."""
|
|
209
|
+
resp = await auth_client.get(
|
|
210
|
+
"/auth/me", headers={"Authorization": "Bearer invalid.token.here"}
|
|
211
|
+
)
|
|
212
|
+
assert resp.status_code == 401
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@pytest.mark.asyncio
|
|
216
|
+
async def test_board_owned_by_user(auth_client):
|
|
217
|
+
"""Boards created by a user should have owner_id set."""
|
|
218
|
+
# Register and get token
|
|
219
|
+
reg = await auth_client.post(
|
|
220
|
+
"/auth/register",
|
|
221
|
+
json={
|
|
222
|
+
"email": "frank@example.com",
|
|
223
|
+
"password": "securepass123",
|
|
224
|
+
"display_name": "Frank",
|
|
225
|
+
},
|
|
226
|
+
)
|
|
227
|
+
token = reg.json()["access_token"]
|
|
228
|
+
user_id = reg.json()["user"]["id"]
|
|
229
|
+
|
|
230
|
+
# Create a temp git repo
|
|
231
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
232
|
+
os.system(f"cd {tmpdir} && git init -q && git commit --allow-empty -m init -q")
|
|
233
|
+
|
|
234
|
+
resp = await auth_client.post(
|
|
235
|
+
"/boards",
|
|
236
|
+
json={
|
|
237
|
+
"name": "Frank's Board",
|
|
238
|
+
"repo_root": tmpdir,
|
|
239
|
+
},
|
|
240
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
assert resp.status_code == 201
|
|
244
|
+
data = resp.json()
|
|
245
|
+
assert data["owner_id"] == user_id
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@pytest.mark.asyncio
|
|
249
|
+
async def test_boards_scoped_by_user(auth_client):
|
|
250
|
+
"""Users should only see their own boards."""
|
|
251
|
+
# Register two users
|
|
252
|
+
reg1 = await auth_client.post(
|
|
253
|
+
"/auth/register",
|
|
254
|
+
json={
|
|
255
|
+
"email": "user1@example.com",
|
|
256
|
+
"password": "securepass123",
|
|
257
|
+
"display_name": "User1",
|
|
258
|
+
},
|
|
259
|
+
)
|
|
260
|
+
token1 = reg1.json()["access_token"]
|
|
261
|
+
|
|
262
|
+
reg2 = await auth_client.post(
|
|
263
|
+
"/auth/register",
|
|
264
|
+
json={
|
|
265
|
+
"email": "user2@example.com",
|
|
266
|
+
"password": "securepass123",
|
|
267
|
+
"display_name": "User2",
|
|
268
|
+
},
|
|
269
|
+
)
|
|
270
|
+
token2 = reg2.json()["access_token"]
|
|
271
|
+
|
|
272
|
+
# Each creates a board with a temp git repo
|
|
273
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
274
|
+
os.system(f"cd {tmpdir} && git init -q && git commit --allow-empty -m init -q")
|
|
275
|
+
|
|
276
|
+
await auth_client.post(
|
|
277
|
+
"/boards",
|
|
278
|
+
json={
|
|
279
|
+
"name": "User1 Board",
|
|
280
|
+
"repo_root": tmpdir,
|
|
281
|
+
},
|
|
282
|
+
headers={"Authorization": f"Bearer {token1}"},
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
await auth_client.post(
|
|
286
|
+
"/boards",
|
|
287
|
+
json={
|
|
288
|
+
"name": "User2 Board",
|
|
289
|
+
"repo_root": tmpdir,
|
|
290
|
+
},
|
|
291
|
+
headers={"Authorization": f"Bearer {token2}"},
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# User1 should only see their board
|
|
295
|
+
resp1 = await auth_client.get(
|
|
296
|
+
"/boards", headers={"Authorization": f"Bearer {token1}"}
|
|
297
|
+
)
|
|
298
|
+
assert resp1.status_code == 200
|
|
299
|
+
boards1 = resp1.json()["boards"]
|
|
300
|
+
assert len(boards1) == 1
|
|
301
|
+
assert boards1[0]["name"] == "User1 Board"
|
|
302
|
+
|
|
303
|
+
# User2 should only see their board
|
|
304
|
+
resp2 = await auth_client.get(
|
|
305
|
+
"/boards", headers={"Authorization": f"Bearer {token2}"}
|
|
306
|
+
)
|
|
307
|
+
boards2 = resp2.json()["boards"]
|
|
308
|
+
assert len(boards2) == 1
|
|
309
|
+
assert boards2[0]["name"] == "User2 Board"
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# ---- Unit tests (no HTTP, no DB) ----
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def test_password_hashing():
|
|
316
|
+
"""Test password hashing and verification."""
|
|
317
|
+
from app.services.auth_service import hash_password, verify_password
|
|
318
|
+
|
|
319
|
+
hashed = hash_password("mypassword")
|
|
320
|
+
assert hashed != "mypassword"
|
|
321
|
+
assert verify_password("mypassword", hashed) is True
|
|
322
|
+
assert verify_password("wrongpassword", hashed) is False
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def test_jwt_roundtrip():
|
|
326
|
+
"""Test JWT token creation and decoding."""
|
|
327
|
+
from app.services.auth_service import create_access_token, decode_access_token
|
|
328
|
+
|
|
329
|
+
token = create_access_token("user-123", "test@example.com")
|
|
330
|
+
payload = decode_access_token(token)
|
|
331
|
+
assert payload is not None
|
|
332
|
+
assert payload["sub"] == "user-123"
|
|
333
|
+
assert payload["email"] == "test@example.com"
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def test_jwt_invalid():
|
|
337
|
+
"""Invalid token should return None."""
|
|
338
|
+
from app.services.auth_service import decode_access_token
|
|
339
|
+
|
|
340
|
+
assert decode_access_token("bad.token.here") is None
|
|
341
|
+
assert decode_access_token("") is None
|