Skip to content

Commit 5fb3a12

Browse files
committed
docs: plan normalized database seed update
1 parent c20e9e3 commit 5fb3a12

1 file changed

Lines changed: 387 additions & 0 deletions

File tree

Lines changed: 387 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,387 @@
1+
# Normalized Database Seed Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Make configured user seeds compatible with the normalized user schema and document exactly which authorization and user records the seed creates.
6+
7+
**Architecture:** Keep orchestration in `src/core/seed/user.py` behind its repository and authorization protocols. Create identity data through the current `User` factory, then persist the legacy full-name setting as `UserProfile.display_name`; the existing SQLAlchemy repository remains responsible for default profile, settings, and security rows and the runner retains transaction ownership.
8+
9+
**Tech Stack:** Python 3.14, dataclasses, async repository protocols, SQLAlchemy async repository adapter, pytest, Ruff, Markdown.
10+
11+
---
12+
13+
## File Structure
14+
15+
- Create `tests/test_user_seed.py`: unit tests for normalized user/profile seeding, environment gating, idempotency, role assignment, and counters.
16+
- Modify `src/core/seed/user.py`: correct the `User.create()` call and persist seed names through `UserProfile`.
17+
- Modify `README.md`: document authorization records, normalized user records, configuration mapping, transaction behavior, and existing-user behavior.
18+
19+
### Task 1: Add normalized user seed contract tests
20+
21+
**Files:**
22+
- Create: `tests/test_user_seed.py`
23+
24+
- [ ] **Step 1: Write test fakes and a failing admin seed test**
25+
26+
Create `tests/test_user_seed.py` with:
27+
28+
```python
29+
import asyncio
30+
from uuid import uuid4
31+
32+
from src.core.seed.user import SeedUserConfig, seed_user
33+
from src.modules.authorization.domain.permissions import (
34+
ADMIN_ROLE,
35+
DEFAULT_USER_ROLE,
36+
MANAGER_ROLE,
37+
VIEWER_ROLE,
38+
)
39+
from src.modules.user.domain.entities.user import User, UserProfile
40+
41+
42+
class FakeUserRepository:
43+
def __init__(self, existing_users: tuple[User, ...] = ()) -> None:
44+
self.users = {user.email: user for user in existing_users}
45+
self.saved_users: list[User] = []
46+
self.saved_profiles: list[UserProfile] = []
47+
48+
async def get_by_email(self, email: str) -> User | None:
49+
return self.users.get(email)
50+
51+
async def save(self, user: User) -> User:
52+
self.users[user.email] = user
53+
self.saved_users.append(user)
54+
return user
55+
56+
async def save_profile(self, profile: UserProfile) -> UserProfile:
57+
self.saved_profiles.append(profile)
58+
return profile
59+
60+
61+
class FakeAuthorizationService:
62+
def __init__(self) -> None:
63+
self.assignments: list[tuple[str, str]] = []
64+
65+
async def assign_role(self, subject: str, role: str) -> None:
66+
self.assignments.append((subject, role))
67+
68+
69+
def test_seed_user_creates_admin_with_normalized_profile(monkeypatch):
70+
monkeypatch.setattr(
71+
"src.core.seed.user.PasswordSerrvice.hash",
72+
lambda password: f"hashed:{password}",
73+
)
74+
repository = FakeUserRepository()
75+
authorization = FakeAuthorizationService()
76+
77+
result = asyncio.run(
78+
seed_user(
79+
user_repository=repository,
80+
authorization_service=authorization,
81+
config=SeedUserConfig(
82+
app_env="production",
83+
admin_email="admin@example.com",
84+
admin_password="secret-password",
85+
admin_username="admin",
86+
admin_fullname="System Administrator",
87+
),
88+
)
89+
)
90+
91+
assert result.users_created == 1
92+
assert result.roles_assigned == 1
93+
assert len(repository.saved_users) == 1
94+
saved_user = repository.saved_users[0]
95+
assert saved_user.email == "admin@example.com"
96+
assert saved_user.username == "admin"
97+
assert saved_user.password_hash == "hashed:secret-password"
98+
assert repository.saved_profiles == [
99+
UserProfile(user_id=saved_user.id, display_name="System Administrator")
100+
]
101+
assert authorization.assignments == [(str(saved_user.id), ADMIN_ROLE)]
102+
```
103+
104+
- [ ] **Step 2: Run the admin test to verify the current factory mismatch fails**
105+
106+
Run: `.venv/bin/pytest tests/test_user_seed.py::test_seed_user_creates_admin_with_normalized_profile -v`
107+
108+
Expected: FAIL with `TypeError` because `User.create()` currently receives unsupported `password` and `fullname` keyword arguments.
109+
110+
- [ ] **Step 3: Add failing development, existing-user, and missing-credentials tests**
111+
112+
Append:
113+
114+
```python
115+
def test_seed_user_creates_development_users_and_profiles(monkeypatch):
116+
monkeypatch.setattr(
117+
"src.core.seed.user.PasswordSerrvice.hash",
118+
lambda password: f"hashed:{password}",
119+
)
120+
repository = FakeUserRepository()
121+
authorization = FakeAuthorizationService()
122+
123+
result = asyncio.run(
124+
seed_user(
125+
user_repository=repository,
126+
authorization_service=authorization,
127+
config=SeedUserConfig(
128+
app_env="development",
129+
admin_email="",
130+
admin_password="",
131+
development_users_password="demo-password",
132+
),
133+
)
134+
)
135+
136+
assert result.users_created == 3
137+
assert result.roles_assigned == 3
138+
assert [user.email for user in repository.saved_users] == [
139+
"user@example.com",
140+
"manager@example.com",
141+
"viewer@example.com",
142+
]
143+
assert [profile.display_name for profile in repository.saved_profiles] == [
144+
"Default User",
145+
"Todo Manager",
146+
"Todo Viewer",
147+
]
148+
assert [role for _, role in authorization.assignments] == [
149+
DEFAULT_USER_ROLE,
150+
MANAGER_ROLE,
151+
VIEWER_ROLE,
152+
]
153+
154+
155+
def test_seed_user_does_not_modify_an_existing_user(monkeypatch):
156+
monkeypatch.setattr(
157+
"src.core.seed.user.PasswordSerrvice.hash",
158+
lambda password: f"hashed:{password}",
159+
)
160+
existing_user = User(
161+
id=uuid4(),
162+
email="admin@example.com",
163+
password_hash="existing-hash",
164+
username="existing-admin",
165+
)
166+
repository = FakeUserRepository((existing_user,))
167+
authorization = FakeAuthorizationService()
168+
169+
result = asyncio.run(
170+
seed_user(
171+
user_repository=repository,
172+
authorization_service=authorization,
173+
config=SeedUserConfig(
174+
app_env="production",
175+
admin_email="admin@example.com",
176+
admin_password="new-password",
177+
admin_username="admin",
178+
admin_fullname="System Administrator",
179+
),
180+
)
181+
)
182+
183+
assert result.users_created == 0
184+
assert result.roles_assigned == 0
185+
assert repository.saved_users == []
186+
assert repository.saved_profiles == []
187+
assert authorization.assignments == []
188+
assert repository.users[existing_user.email] == existing_user
189+
190+
191+
def test_seed_user_skips_users_without_credentials():
192+
repository = FakeUserRepository()
193+
authorization = FakeAuthorizationService()
194+
195+
result = asyncio.run(
196+
seed_user(
197+
user_repository=repository,
198+
authorization_service=authorization,
199+
config=SeedUserConfig(
200+
app_env="production",
201+
admin_email="",
202+
admin_password="",
203+
),
204+
)
205+
)
206+
207+
assert result.users_created == 0
208+
assert result.roles_assigned == 0
209+
assert repository.saved_users == []
210+
assert repository.saved_profiles == []
211+
assert authorization.assignments == []
212+
```
213+
214+
- [ ] **Step 4: Run the test file**
215+
216+
Run: `.venv/bin/pytest tests/test_user_seed.py -v`
217+
218+
Expected: missing-credentials and existing-user tests PASS; admin and development creation tests FAIL at the outdated `User.create()` call.
219+
220+
- [ ] **Step 5: Commit the failing tests**
221+
222+
```bash
223+
git add tests/test_user_seed.py
224+
git commit -m "test: cover normalized user seeding"
225+
```
226+
227+
### Task 2: Persist seeded names through normalized profiles
228+
229+
**Files:**
230+
- Modify: `src/core/seed/user.py:1-161`
231+
- Test: `tests/test_user_seed.py`
232+
233+
- [ ] **Step 1: Import `UserProfile` and extend the seed repository contract**
234+
235+
```python
236+
from src.modules.user.domain.entities.user import User, UserProfile
237+
238+
239+
class SeedUserRepository(Protocol):
240+
async def get_by_email(self, email: str) -> User | None:
241+
raise NotImplementedError
242+
243+
async def save(self, user: User) -> User:
244+
raise NotImplementedError
245+
246+
async def save_profile(self, profile: UserProfile) -> UserProfile:
247+
raise NotImplementedError
248+
```
249+
250+
- [ ] **Step 2: Correct user creation and save the normalized profile before role assignment**
251+
252+
Replace the creation block in `_seed_one_user()` with:
253+
254+
```python
255+
user = User.create(
256+
email=email,
257+
password_hash=PasswordSerrvice.hash(password),
258+
username=username,
259+
)
260+
saved_user = await user_repository.save(user)
261+
await user_repository.save_profile(
262+
UserProfile(
263+
user_id=saved_user.id,
264+
display_name=fullname,
265+
)
266+
)
267+
await authorization_service.assign_role(str(saved_user.id), role)
268+
```
269+
270+
This preserves ordering inside the runner transaction: identity and defaults, profile value, then role assignment.
271+
272+
- [ ] **Step 3: Run seed tests**
273+
274+
Run: `.venv/bin/pytest tests/test_user_seed.py -v`
275+
276+
Expected: 4 tests PASS.
277+
278+
- [ ] **Step 4: Run all tests**
279+
280+
Run: `.venv/bin/pytest -q`
281+
282+
Expected: all tests PASS.
283+
284+
- [ ] **Step 5: Run focused lint**
285+
286+
Run: `.venv/bin/ruff check src/core/seed/user.py tests/test_user_seed.py`
287+
288+
Expected: `All checks passed!`
289+
290+
- [ ] **Step 6: Commit the implementation**
291+
292+
```bash
293+
git add src/core/seed/user.py
294+
git commit -m "fix: align user seed with normalized schema"
295+
```
296+
297+
### Task 3: Document normalized seed behavior
298+
299+
**Files:**
300+
- Modify: `README.md:431-462`
301+
302+
- [ ] **Step 1: Replace the database-seeding section with schema-accurate text**
303+
304+
The revised section must state:
305+
306+
````markdown
307+
Seed baseline records after applying migrations:
308+
309+
```bash
310+
make seed
311+
```
312+
313+
The seeder runs all changes in one transaction and is idempotent. It creates the default authorization resources, roles (`admin`, `user`, `manager`, and `viewer`), permissions, role-permission links, and matching Casbin policies without duplicating existing records.
314+
315+
For each new seeded user, the repository creates records that follow the normalized user schema:
316+
317+
- `users` stores email, username, password hash, authentication provider, and status.
318+
- `user_profiles` stores `SEED_ADMIN_FULLNAME` (or the demo name) as `display_name`.
319+
- `user_settings` stores default preferences.
320+
- `user_security` stores default security state.
321+
- `user_has_roles` associates the user with its seeded role.
322+
323+
`SEED_ADMIN_FULLNAME` is retained for configuration compatibility; there is no `users.fullname` column. Existing users are not modified.
324+
````
325+
326+
Keep the existing environment examples, add display names to the demo-account list, and explicitly describe transaction rollback and production/development gating.
327+
328+
- [ ] **Step 2: Search for obsolete seed claims**
329+
330+
Run: `rg -n "SEED_ADMIN_FULLNAME|fullname|default .*roles|user_profiles|user_settings|user_security" README.md src/core/seed`
331+
332+
Expected: README maps `SEED_ADMIN_FULLNAME` to `user_profiles.display_name`; source uses `fullname` only as seed input; README lists all four roles.
333+
334+
- [ ] **Step 3: Check whitespace**
335+
336+
Run: `git diff --check`
337+
338+
Expected: no output and exit status 0.
339+
340+
- [ ] **Step 4: Commit README changes**
341+
342+
```bash
343+
git add README.md
344+
git commit -m "docs: explain normalized database seeding"
345+
```
346+
347+
### Task 4: Verify the complete change
348+
349+
**Files:**
350+
- Verify: `src/core/seed/user.py`
351+
- Verify: `tests/test_user_seed.py`
352+
- Verify: `README.md`
353+
354+
- [ ] **Step 1: Run the full test suite**
355+
356+
Run: `.venv/bin/pytest -q`
357+
358+
Expected: all tests PASS.
359+
360+
- [ ] **Step 2: Run project lint**
361+
362+
Run: `.venv/bin/ruff check src tests scripts`
363+
364+
Expected: `All checks passed!`
365+
366+
- [ ] **Step 3: Run import-boundary checks**
367+
368+
Run: `.venv/bin/lint-imports`
369+
370+
Expected: all configured contracts are kept.
371+
372+
- [ ] **Step 4: Verify application imports**
373+
374+
Run: `PYTHONDONTWRITEBYTECODE=1 .venv/bin/python -c "import src.main; print('import ok')"`
375+
376+
Expected: `import ok`.
377+
378+
- [ ] **Step 5: Inspect the final scoped diff and worktree**
379+
380+
Run:
381+
382+
```bash
383+
git diff HEAD~3 -- src/core/seed/user.py tests/test_user_seed.py README.md
384+
git status --short
385+
```
386+
387+
Expected: only the planned seed, tests, and README changes appear; the pre-existing untracked `.vscode/PythonImportHelper-v2-Completion.json` remains untouched.

0 commit comments

Comments
 (0)