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.
Files changed (250) hide show
  1. package/app/backend/.env.example +9 -0
  2. package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_patch.txt +195 -0
  3. package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_stat.txt +6 -0
  4. package/app/backend/CURL_EXAMPLES.md +335 -0
  5. package/app/backend/ENV_SETUP.md +65 -0
  6. package/app/backend/alembic/env.py +71 -0
  7. package/app/backend/alembic/script.py.mako +28 -0
  8. package/app/backend/alembic/versions/001_initial_schema.py +104 -0
  9. package/app/backend/alembic/versions/002_add_jobs_table.py +52 -0
  10. package/app/backend/alembic/versions/003_add_workspace_table.py +48 -0
  11. package/app/backend/alembic/versions/004_add_evidence_table.py +56 -0
  12. package/app/backend/alembic/versions/005_add_verification_commands.py +32 -0
  13. package/app/backend/alembic/versions/006_add_planner_lock_table.py +39 -0
  14. package/app/backend/alembic/versions/007_add_revision_review_tables.py +126 -0
  15. package/app/backend/alembic/versions/008_add_revision_idempotency_and_traceability.py +52 -0
  16. package/app/backend/alembic/versions/009_add_job_health_fields.py +46 -0
  17. package/app/backend/alembic/versions/010_add_review_comment_line_content.py +36 -0
  18. package/app/backend/alembic/versions/011_add_analysis_cache.py +47 -0
  19. package/app/backend/alembic/versions/012_add_boards_table.py +102 -0
  20. package/app/backend/alembic/versions/013_add_ticket_blocking.py +45 -0
  21. package/app/backend/alembic/versions/014_add_agent_sessions.py +220 -0
  22. package/app/backend/alembic/versions/015_add_ticket_sort_order.py +33 -0
  23. package/app/backend/alembic/versions/03220f0b93ae_add_pr_fields_to_ticket.py +49 -0
  24. package/app/backend/alembic/versions/0c2d89fff3b1_seed_board_configs_from_yaml.py +206 -0
  25. package/app/backend/alembic/versions/3348e5cf54c1_add_merge_checklist_table.py +67 -0
  26. package/app/backend/alembic/versions/357c780ee445_add_goal_status.py +34 -0
  27. package/app/backend/alembic/versions/553340b7e26c_add_autonomy_fields_to_goal.py +65 -0
  28. package/app/backend/alembic/versions/774dc335c679_merge_migration_heads.py +23 -0
  29. package/app/backend/alembic/versions/7b307e847cbd_merge_heads.py +23 -0
  30. package/app/backend/alembic/versions/82ecd978cc70_add_missing_indexes.py +48 -0
  31. package/app/backend/alembic/versions/8ef5054dc280_add_normalized_log_entries.py +173 -0
  32. package/app/backend/alembic/versions/8f3e2bd8ea3b_merge_migration_heads.py +23 -0
  33. package/app/backend/alembic/versions/9d17f0698d3b_add_config_column_to_boards_table.py +30 -0
  34. package/app/backend/alembic/versions/add_agent_conversation_history.py +72 -0
  35. package/app/backend/alembic/versions/add_job_variant.py +34 -0
  36. package/app/backend/alembic/versions/add_performance_indexes.py +95 -0
  37. package/app/backend/alembic/versions/add_repos_and_board_repos.py +174 -0
  38. package/app/backend/alembic/versions/add_session_id_to_jobs.py +27 -0
  39. package/app/backend/alembic/versions/add_sqlite_backend_tables.py +104 -0
  40. package/app/backend/alembic/versions/b10fb0b62240_add_diff_content_to_revisions.py +34 -0
  41. package/app/backend/alembic.ini +89 -0
  42. package/app/backend/app/__init__.py +3 -0
  43. package/app/backend/app/data_dir.py +85 -0
  44. package/app/backend/app/database.py +70 -0
  45. package/app/backend/app/database_sync.py +64 -0
  46. package/app/backend/app/dependencies/__init__.py +5 -0
  47. package/app/backend/app/dependencies/auth.py +80 -0
  48. package/app/backend/app/dependencies.py +43 -0
  49. package/app/backend/app/exceptions.py +178 -0
  50. package/app/backend/app/executors/__init__.py +1 -0
  51. package/app/backend/app/executors/adapters/__init__.py +1 -0
  52. package/app/backend/app/executors/adapters/aider.py +152 -0
  53. package/app/backend/app/executors/adapters/amazon_q.py +103 -0
  54. package/app/backend/app/executors/adapters/amp.py +123 -0
  55. package/app/backend/app/executors/adapters/claude.py +177 -0
  56. package/app/backend/app/executors/adapters/cline.py +127 -0
  57. package/app/backend/app/executors/adapters/codex.py +167 -0
  58. package/app/backend/app/executors/adapters/copilot.py +202 -0
  59. package/app/backend/app/executors/adapters/cursor.py +87 -0
  60. package/app/backend/app/executors/adapters/droid.py +123 -0
  61. package/app/backend/app/executors/adapters/gemini.py +132 -0
  62. package/app/backend/app/executors/adapters/goose.py +131 -0
  63. package/app/backend/app/executors/adapters/opencode.py +123 -0
  64. package/app/backend/app/executors/adapters/qwen.py +123 -0
  65. package/app/backend/app/executors/plugins/__init__.py +1 -0
  66. package/app/backend/app/executors/registry.py +202 -0
  67. package/app/backend/app/executors/spec.py +226 -0
  68. package/app/backend/app/main.py +486 -0
  69. package/app/backend/app/middleware/__init__.py +13 -0
  70. package/app/backend/app/middleware/idempotency.py +426 -0
  71. package/app/backend/app/middleware/rate_limit.py +312 -0
  72. package/app/backend/app/middleware/security_headers.py +43 -0
  73. package/app/backend/app/middleware/timeout.py +37 -0
  74. package/app/backend/app/models/__init__.py +56 -0
  75. package/app/backend/app/models/agent_conversation_history.py +56 -0
  76. package/app/backend/app/models/agent_session.py +127 -0
  77. package/app/backend/app/models/analysis_cache.py +49 -0
  78. package/app/backend/app/models/base.py +9 -0
  79. package/app/backend/app/models/board.py +79 -0
  80. package/app/backend/app/models/board_repo.py +68 -0
  81. package/app/backend/app/models/cost_budget.py +42 -0
  82. package/app/backend/app/models/enums.py +40 -0
  83. package/app/backend/app/models/evidence.py +132 -0
  84. package/app/backend/app/models/goal.py +102 -0
  85. package/app/backend/app/models/idempotency_entry.py +30 -0
  86. package/app/backend/app/models/job.py +163 -0
  87. package/app/backend/app/models/job_queue.py +39 -0
  88. package/app/backend/app/models/kv_store.py +28 -0
  89. package/app/backend/app/models/merge_checklist.py +87 -0
  90. package/app/backend/app/models/normalized_log.py +100 -0
  91. package/app/backend/app/models/planner_lock.py +43 -0
  92. package/app/backend/app/models/rate_limit_entry.py +25 -0
  93. package/app/backend/app/models/repo.py +66 -0
  94. package/app/backend/app/models/review_comment.py +91 -0
  95. package/app/backend/app/models/review_summary.py +69 -0
  96. package/app/backend/app/models/revision.py +130 -0
  97. package/app/backend/app/models/ticket.py +223 -0
  98. package/app/backend/app/models/ticket_event.py +83 -0
  99. package/app/backend/app/models/user.py +47 -0
  100. package/app/backend/app/models/workspace.py +71 -0
  101. package/app/backend/app/redis_client.py +119 -0
  102. package/app/backend/app/routers/__init__.py +29 -0
  103. package/app/backend/app/routers/agents.py +296 -0
  104. package/app/backend/app/routers/auth.py +94 -0
  105. package/app/backend/app/routers/board.py +885 -0
  106. package/app/backend/app/routers/dashboard.py +351 -0
  107. package/app/backend/app/routers/debug.py +528 -0
  108. package/app/backend/app/routers/evidence.py +96 -0
  109. package/app/backend/app/routers/executors.py +324 -0
  110. package/app/backend/app/routers/goals.py +574 -0
  111. package/app/backend/app/routers/jobs.py +448 -0
  112. package/app/backend/app/routers/maintenance.py +172 -0
  113. package/app/backend/app/routers/merge.py +360 -0
  114. package/app/backend/app/routers/planner.py +537 -0
  115. package/app/backend/app/routers/pull_requests.py +382 -0
  116. package/app/backend/app/routers/repos.py +263 -0
  117. package/app/backend/app/routers/revisions.py +939 -0
  118. package/app/backend/app/routers/settings.py +267 -0
  119. package/app/backend/app/routers/tickets.py +2003 -0
  120. package/app/backend/app/routers/webhooks.py +143 -0
  121. package/app/backend/app/routers/websocket.py +249 -0
  122. package/app/backend/app/schemas/__init__.py +109 -0
  123. package/app/backend/app/schemas/board.py +87 -0
  124. package/app/backend/app/schemas/common.py +33 -0
  125. package/app/backend/app/schemas/evidence.py +87 -0
  126. package/app/backend/app/schemas/goal.py +90 -0
  127. package/app/backend/app/schemas/job.py +97 -0
  128. package/app/backend/app/schemas/merge.py +139 -0
  129. package/app/backend/app/schemas/planner.py +500 -0
  130. package/app/backend/app/schemas/repo.py +187 -0
  131. package/app/backend/app/schemas/review.py +137 -0
  132. package/app/backend/app/schemas/revision.py +114 -0
  133. package/app/backend/app/schemas/ticket.py +238 -0
  134. package/app/backend/app/schemas/ticket_event.py +72 -0
  135. package/app/backend/app/schemas/workspace.py +19 -0
  136. package/app/backend/app/services/__init__.py +31 -0
  137. package/app/backend/app/services/agent_memory_service.py +223 -0
  138. package/app/backend/app/services/agent_registry.py +346 -0
  139. package/app/backend/app/services/agent_session_manager.py +318 -0
  140. package/app/backend/app/services/agent_session_service.py +219 -0
  141. package/app/backend/app/services/agent_tools.py +379 -0
  142. package/app/backend/app/services/auth_service.py +98 -0
  143. package/app/backend/app/services/autonomy_service.py +380 -0
  144. package/app/backend/app/services/board_repo_service.py +201 -0
  145. package/app/backend/app/services/board_service.py +326 -0
  146. package/app/backend/app/services/cleanup_service.py +1085 -0
  147. package/app/backend/app/services/config_service.py +908 -0
  148. package/app/backend/app/services/context_gatherer.py +557 -0
  149. package/app/backend/app/services/cost_tracking_service.py +293 -0
  150. package/app/backend/app/services/cursor_log_normalizer.py +536 -0
  151. package/app/backend/app/services/delivery_pipeline.py +440 -0
  152. package/app/backend/app/services/executor_service.py +634 -0
  153. package/app/backend/app/services/git_host/__init__.py +11 -0
  154. package/app/backend/app/services/git_host/factory.py +87 -0
  155. package/app/backend/app/services/git_host/github.py +270 -0
  156. package/app/backend/app/services/git_host/gitlab.py +194 -0
  157. package/app/backend/app/services/git_host/protocol.py +75 -0
  158. package/app/backend/app/services/git_merge_simple.py +346 -0
  159. package/app/backend/app/services/git_ops.py +384 -0
  160. package/app/backend/app/services/github_service.py +233 -0
  161. package/app/backend/app/services/goal_service.py +113 -0
  162. package/app/backend/app/services/job_service.py +423 -0
  163. package/app/backend/app/services/job_watchdog_service.py +424 -0
  164. package/app/backend/app/services/langchain_adapter.py +122 -0
  165. package/app/backend/app/services/llm_provider_clients.py +351 -0
  166. package/app/backend/app/services/llm_service.py +285 -0
  167. package/app/backend/app/services/log_normalizer.py +342 -0
  168. package/app/backend/app/services/log_stream_service.py +276 -0
  169. package/app/backend/app/services/merge_checklist_service.py +264 -0
  170. package/app/backend/app/services/merge_service.py +784 -0
  171. package/app/backend/app/services/orchestrator_log.py +84 -0
  172. package/app/backend/app/services/planner_service.py +1662 -0
  173. package/app/backend/app/services/planner_tick_sync.py +1040 -0
  174. package/app/backend/app/services/queued_message_service.py +156 -0
  175. package/app/backend/app/services/reliability_wrapper.py +389 -0
  176. package/app/backend/app/services/repo_discovery_service.py +318 -0
  177. package/app/backend/app/services/review_service.py +334 -0
  178. package/app/backend/app/services/revision_service.py +389 -0
  179. package/app/backend/app/services/safe_autopilot.py +510 -0
  180. package/app/backend/app/services/sqlite_worker.py +372 -0
  181. package/app/backend/app/services/task_dispatch.py +135 -0
  182. package/app/backend/app/services/ticket_generation_service.py +1781 -0
  183. package/app/backend/app/services/ticket_service.py +486 -0
  184. package/app/backend/app/services/udar_planner_service.py +1007 -0
  185. package/app/backend/app/services/webhook_service.py +126 -0
  186. package/app/backend/app/services/workspace_service.py +465 -0
  187. package/app/backend/app/services/worktree_file_service.py +92 -0
  188. package/app/backend/app/services/worktree_validator.py +213 -0
  189. package/app/backend/app/sqlite_kv.py +278 -0
  190. package/app/backend/app/state_machine.py +128 -0
  191. package/app/backend/app/templates/__init__.py +5 -0
  192. package/app/backend/app/templates/registry.py +243 -0
  193. package/app/backend/app/utils/__init__.py +5 -0
  194. package/app/backend/app/utils/artifact_reader.py +87 -0
  195. package/app/backend/app/utils/circuit_breaker.py +229 -0
  196. package/app/backend/app/utils/db_retry.py +136 -0
  197. package/app/backend/app/utils/ignored_fields.py +123 -0
  198. package/app/backend/app/utils/validators.py +54 -0
  199. package/app/backend/app/websocket/__init__.py +5 -0
  200. package/app/backend/app/websocket/manager.py +179 -0
  201. package/app/backend/app/websocket/state_tracker.py +113 -0
  202. package/app/backend/app/worker.py +3190 -0
  203. package/app/backend/calculator_tickets.json +40 -0
  204. package/app/backend/canary_tests.sh +591 -0
  205. package/app/backend/celerybeat-schedule +0 -0
  206. package/app/backend/celerybeat-schedule-shm +0 -0
  207. package/app/backend/celerybeat-schedule-wal +0 -0
  208. package/app/backend/logs/.gitkeep +3 -0
  209. package/app/backend/multiplication_division_implementation_tickets.json +55 -0
  210. package/app/backend/multiplication_division_tickets.json +42 -0
  211. package/app/backend/pyproject.toml +45 -0
  212. package/app/backend/requirements-dev.txt +8 -0
  213. package/app/backend/requirements.txt +20 -0
  214. package/app/backend/run.sh +30 -0
  215. package/app/backend/run_with_logs.sh +10 -0
  216. package/app/backend/scientific_calculator_tickets.json +40 -0
  217. package/app/backend/scripts/extract_openapi.py +21 -0
  218. package/app/backend/scripts/seed_demo.py +187 -0
  219. package/app/backend/setup_demo_review.py +302 -0
  220. package/app/backend/test_actual_parse.py +41 -0
  221. package/app/backend/test_agent_streaming.py +61 -0
  222. package/app/backend/test_parse.py +51 -0
  223. package/app/backend/test_streaming.py +51 -0
  224. package/app/backend/test_subprocess_streaming.py +50 -0
  225. package/app/backend/tests/__init__.py +1 -0
  226. package/app/backend/tests/conftest.py +46 -0
  227. package/app/backend/tests/test_auth.py +341 -0
  228. package/app/backend/tests/test_autonomy_service.py +391 -0
  229. package/app/backend/tests/test_cleanup_service_safety.py +417 -0
  230. package/app/backend/tests/test_middleware.py +279 -0
  231. package/app/backend/tests/test_planner_providers.py +290 -0
  232. package/app/backend/tests/test_planner_unblock.py +183 -0
  233. package/app/backend/tests/test_revision_invariants.py +618 -0
  234. package/app/backend/tests/test_sqlite_kv.py +290 -0
  235. package/app/backend/tests/test_sqlite_worker.py +353 -0
  236. package/app/backend/tests/test_task_dispatch.py +100 -0
  237. package/app/backend/tests/test_ticket_validation.py +304 -0
  238. package/app/backend/tests/test_udar_agent.py +693 -0
  239. package/app/backend/tests/test_webhook_service.py +184 -0
  240. package/app/backend/tickets_output.json +59 -0
  241. package/app/backend/user_management_tickets.json +50 -0
  242. package/app/backend/uvicorn.log +0 -0
  243. package/app/draft.yaml +313 -0
  244. package/app/frontend/dist/assets/index-LcjCczu5.js +155 -0
  245. package/app/frontend/dist/assets/index-_FP_279e.css +1 -0
  246. package/app/frontend/dist/index.html +14 -0
  247. package/app/frontend/dist/vite.svg +1 -0
  248. package/app/frontend/package.json +101 -0
  249. package/bin/cli.js +527 -0
  250. 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