Commit cc3550cfbb

cc3550cfbbb0f7e58921f7da74052ce231d8f478

parent: 965735bfbc

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-05 03:17 UTC

Plan: confirm the user lookup, fix the index check

Ref #155

Layout: unified · split

docs/plans/2026-09-04-email-login.md +14 −19
@@ -169,24 +169,22 @@ Expected: PASS — the new migration must not break existing store tests.
169169
170170- [ ] **Step 6: Confirm the index is actually used**
171171
172Run:
173```bash
174cd /Users/cmc/git/krz/gitbay && cat > /tmp/plan_explain_test.go <<'EOF'
175EOF
176go test ./internal/store/ -run TestDashboardQueriesUseIndexes -v
177```
178Expected: PASS (unrelated, but proves the migration did not disturb existing
179plans). Then verify by hand that the count uses the index:
172The count runs on every anonymous request, so a sequential scan here would
173make the throttle its own denial-of-service vector. Verify the plan names the
174index:
180175
181176```bash
182sqlite3 "$(mktemp -d)/x.db" <<'EOF'
177sqlite3 "$SCRATCH/plan.db" <<'EOF'
183178CREATE TABLE login_tokens (token_hash TEXT PRIMARY KEY, user_id INTEGER NOT NULL,
184179 created_at TEXT NOT NULL, expires_at TEXT NOT NULL, used_at TEXT);
185180CREATE INDEX login_tokens_user_created ON login_tokens(user_id, created_at);
186181EXPLAIN QUERY PLAN SELECT count(*) FROM login_tokens WHERE user_id = 1 AND created_at > 'x';
187182EOF
188183```
189Expected: the plan names `login_tokens_user_created`, not `SCAN login_tokens`.
184
185Expected: the output names `login_tokens_user_created`. A line reading
186`SCAN login_tokens` means the index is not being used and the migration is
187wrong.
190188
191189- [ ] **Step 7: Commit**
192190
@@ -317,7 +315,7 @@ together because none of them is testable without the others.
317315- Create: `e2e/emaillogin_test.go`
318316
319317**Interfaces:**
320- Consumes: `store.UserIDByVerifiedEmail(address string) (int64, bool)` (`internal/store/activity.go:11`); `store.PrimaryVerifiedEmail(userID int64) (string, error)` (`internal/store/mrs.go:440`); `store.UserByName`; `store.CountLoginTokensSince` (Task 1); `store.NewToken`; `store.CreateLoginToken`; `mail.Send(cfg config.Config, to, subject, body string) error`; `Server.apiLimit.allow(key string, write bool) (bool, time.Duration)`; `Server.clientIP(r)`.
318- Consumes: `store.UserIDByVerifiedEmail(address string) (int64, bool)` (`internal/store/activity.go:11`); `store.PrimaryVerifiedEmail(userID int64) (string, error)` (`internal/store/mrs.go:440`); `store.UserByUsername`; `store.CountLoginTokensSince` (Task 1); `store.NewToken`; `store.CreateLoginToken`; `mail.Send(cfg config.Config, to, subject, body string) error`; `Server.apiLimit.allow(key string, write bool) (bool, time.Duration)`; `Server.clientIP(r)`.
321319- Produces: `func control.RequestLoginLink(cfg config.Config, st *store.Store, identifier string) error`
322320
323321- [ ] **Step 1: Write the failing e2e test**
@@ -511,7 +509,7 @@ func RequestLoginLink(cfg config.Config, st *store.Store, identifier string) err
511509 }
512510 userID, address = id, identifier
513511 } else {
514 u, err := st.UserByName(identifier)
512 u, err := st.UserByUsername(identifier)
515513 if err != nil {
516514 return nil
517515 }
@@ -547,9 +545,7 @@ func RequestLoginLink(cfg config.Config, st *store.Store, identifier string) err
547545}
548546```
549547
550Check `st.UserByName`'s real name and signature before writing this — if the
551store spells it differently, use the store's spelling rather than adding a
552wrapper. Run: `grep -n "func (s \*Store) UserByName" internal/store/users.go`
548`UserByUsername(name string) (User, error)` is at `internal/store/users.go:109`.
553549
554550- [ ] **Step 4: Write the handler**
555551
@@ -781,7 +777,6 @@ triple to a single `error`, recorded above under "Change from the spec".
781777Each is used with that signature everywhere it appears. `sessionSameSite` is
782778declared in Task 2 and used in Task 2 only.
783779
784**Unverified at plan time.** `store.UserByName` is used in Task 3 Step 3 but
785its exact name and signature were not confirmed; Step 3 carries an explicit
786instruction to check before writing. `Parity.org`'s table format is likewise
787read at execution rather than guessed.
780**Unverified at plan time.** `Parity.org`'s table format is read at execution
781rather than guessed, which is why Task 4 Step 2 says to read neighbouring rows
782first rather than giving the row verbatim.