Skip to content

Add the Plan 6 implementation plan for account settings - #62

Merged
malinfossum merged 2 commits into
mainfrom
docs/slice2a-plan6-implementation-plan
Sep 2, 2026
Merged

malinfossum merged 2 commits into
mainfrom
docs/slice2a-plan6-implementation-plan

Conversation

@malinfossum

Copy link
Copy Markdown
Member

Docs only — no production code, no tests, nothing to run. This is the implementation plan for
Plan 6 (account settings), written against the tree at e6bda61 now that the auth-input fix and
remember-me have both merged.

It follows the design merged in #48, and the reviewer's checklist that pairs with it is
docs/2026-08-13-wend-review-guide.md § Plan 6 review checklist.

What's in it

Eight tasks: the change-email token provider and the two new email-seam methods; three endpoints
(/change-password, /change-email, /confirm-email-change); a real-cookie test suite; the
Account screen; the /confirm-email-change screen; and the PR write-up. Every step carries the
actual code rather than a description of it.

The three findings worth your attention

The design closed by asking that its eight open items be verified against the .NET 10 source
before the plan was written rather than assumed. All eight are now answered against
dotnet/aspnetcore release/10.0. Three of the answers changed the plan:

  1. ChangeEmailAsync does distinguish DuplicateEmail from InvalidToken. ChangeEmailCoreAsync
    returns InvalidToken on a token failure and otherwise reaches UpdateUserAsync
    UserValidator, whose duplicate branches use Code = nameof(DuplicateEmail). So the 409-vs-400
    split works off the error code, and the pre-check does not have to move into the confirm handler
    as the design worried it might.

  2. SetUserNameAsync rotates the security stamp a second time. Harmless — every session is
    already gone from the first rotation — but it means two writes, and it is why the second call
    must use the same user instance rather than a reload.

  3. RefreshSignInAsync returns no result, and is a silent no-op under the Test auth scheme.
    It returns Task, not Task<IdentityResult>, so "if it fails" can only mean an exception. More
    importantly it calls Context.AuthenticateAsync(ApplicationScheme), finds no cookie under the
    test scheme, logs an error and returns. SecurityStampValidator hangs off the cookie too, so it
    never runs there either.

    This is the one that changed the shape of the plan. Two of the design's own required
    assertions — the acting session surviving and other sessions dying — would have passed vacuously
    where the design's task list put them. They move into their own real-cookie suite, which is the
    eighth task the design's seven-task breakdown does not have.

Verification built into the plan

Test totals are pinned at 257 → 291, per task. Two tasks end with a mutation check rather than a
green run:

  • comment out SetUserNameAsync → exactly one test must fail
    (Registering_the_old_address_afterwards_still_works);
  • comment out RefreshSignInAsync → exactly one test must fail
    (The_acting_session_survives_a_password_change_while_another_one_dies).

If either passes with the call removed, the guard is decorative and the task is not done.

Deviations

Nine are recorded up front for the implementation PR's body — four inherited from the design, five
introduced at plan time. The one most worth a second opinion is #6: the change-email success
message says "if that address is free, we've sent it a confirmation link" rather than naming the
address it went to. The endpoint answers 204 for both a free address and one somebody else holds,
so a screen claiming a link was sent would leak exactly what that 204 exists to protect.

Not touched

docs/backlog.md is unchanged. Its four Plan 6 entries were already extended on 2026-08-13, and
the stolen-session takeover stays open as a Plan 8 launch gate — the plan says so explicitly so
nobody closes it in passing.

Eight tasks against the signed-off design: the change-email token
provider and email seam, three endpoints (change-password,
change-email, confirm-email-change), a real-cookie suite, the Account
screen, the confirm-email-change screen, and the PR write-up.

All eight of the design's open items are resolved against
dotnet/aspnetcore release/10.0. Three answers changed the plan:

- ChangeEmailAsync distinguishes DuplicateEmail from InvalidToken, so
  the 409/400 split works off the error code and no pre-check moves
  into the confirm handler.
- SetUserNameAsync rotates the security stamp a second time, so the
  same-instance requirement is real rather than precautionary.
- RefreshSignInAsync returns no result and is a silent no-op under the
  Test auth scheme. Two of the design's required assertions would have
  passed vacuously where its task list put them, so they move to their
  own real-cookie suite - the eighth task the design does not have.

Test totals pinned 257 -> 291. Two tasks end with a mutation check
rather than a green run: removing SetUserNameAsync must fail exactly
one test, likewise RefreshSignInAsync.

Nine deviations recorded for the PR body, five of them introduced at
plan time. Nothing in docs/backlog.md is owed, and the stolen-session
takeover stays open as a Plan 8 launch gate.
@malinfossum
malinfossum merged commit d9af6b6 into main Sep 2, 2026
1 check passed
@malinfossum
malinfossum deleted the docs/slice2a-plan6-implementation-plan branch September 2, 2026 09:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants