refac: DELETE /v1/user/{id}?expected_role_id=N, user.remove audit event, 409 target_role_not_found, GetAll returns its error, Own-scope visibility filter moved into repos, JSON tags removed from use-case types
This commit is contained in:
1 parent
5d6f378fce
commit
b39a41be88
124 files changed
+8816
-523
No files matched your search
@@ -0,0 +1,157 @@
|
||||
Feature: Role 409 conflicts: stale save, change role, transfer ownership and a missing reassign target (group 15)
|
||||
|
||||
As a person editing a role, changing a member's role or handing over ownership
|
||||
I want a stale request to say who or what changed, to keep what I entered and to offer the next step
|
||||
So that nothing is overwritten and I never lose my edits
|
||||
|
||||
Background:
|
||||
Given the spec of record is docs/design/screens/roles.md groups 12, 13 and 15, the table "HTTP status to copy" rows role_conflict, and docs/design/states.md "Stale state"
|
||||
And the backend contract is, after this slice:
|
||||
| call | status | body |
|
||||
| PUT /v1/role/{id} with a stale version | 409 | role_conflict |
|
||||
| PUT /v1/user/{id}/role | 409 | user_role_changed |
|
||||
| POST /v1/site/transfer-ownership | 409 | user_role_changed |
|
||||
| DELETE /v1/user/{id} | 409 | user_role_changed |
|
||||
| DELETE /v1/role/{id} while it has members | 409 | role_has_members |
|
||||
| POST /v1/role/{id}/reassign, target missing | 409 | target_role_not_found |
|
||||
| POST /v1/role/{id}/reassign, same role, locked or uncovered target | 400 | invalid_reassign |
|
||||
| any action on a deleted role (the path role) | 404 | role_not_found |
|
||||
And a 409 body is plain text and names nobody: the SPA re-reads GET /v1/role/{id} (updated_by, updated_at) for a stale role save, and GET /v1/user for the member's current role
|
||||
And the removal route and its 409 are specified in features/roles_sync_remove_user.feature
|
||||
And the message appears where the person is working (banner in the editor, alert in a dialog) and never as a toast while that surface is open; a toast shows only when the surface is gone
|
||||
And a warning toast stays until dismissed or Reload is pressed; an info toast closes after 6 s
|
||||
And the SPA lives in web/src, Nuxt UI only, design tokens only, EN and FR in i18n/messages.ts, no comments in code
|
||||
|
||||
Rule: Backend: a missing reassign target is a distinct 409
|
||||
|
||||
Background for this Rule (house style of roles_permissions_roles_api.feature):
|
||||
Given the new domain sentinel role.ErrReassignTargetNotFound and the new apperr string ReassignTargetNotFoundErrorMessage = "target_role_not_found"
|
||||
And ReassignRoleMembersUseCase.checkTarget returns it when roles.Get(target) answers role.ErrRoleNotFound, instead of role.ErrInvalidReassign
|
||||
And api/handlers/role maps it to 409 with the plain-text body, in the existing refusals table
|
||||
And target 0, target equal to the source, a locked target and a target the caller cannot cover still answer 400 invalid_reassign
|
||||
And a missing source role still answers 404 role_not_found
|
||||
And nothing is moved and no event is recorded on this refusal (a refusal records nothing)
|
||||
And docs/api.md documents the 409 on POST /v1/role/{id}/reassign
|
||||
|
||||
Scenario Outline: Reassign target answers
|
||||
When the Owner sends POST /v1/role/<source>/reassign {"target_role_id": <target>}
|
||||
Then the answer is <status> <body>
|
||||
And no member moved
|
||||
Examples:
|
||||
| source | target | status | body |
|
||||
| Contributor | an id that no longer exists | 409 | target_role_not_found |
|
||||
| Contributor | Contributor (itself) | 400 | invalid_reassign |
|
||||
| Contributor | 0 | 400 | invalid_reassign |
|
||||
| Contributor | Owner (locked) | 400 | invalid_reassign |
|
||||
| Contributor | Admin, caller is a People manager | 400 | invalid_reassign |
|
||||
| 9999 | Writer | 404 | role_not_found |
|
||||
|
||||
Rule: Stale role save (editor)
|
||||
|
||||
Scenario: The banner
|
||||
Given Contributor was loaded at version 2 and the person made 2 edits
|
||||
When PUT /v1/role/4 answers 409 role_conflict and GET /v1/role/4 answers updated_by "Jonas Weber", updated_at 11:02
|
||||
Then a banner above the form reads "This role changed while you were editing · Jonas Weber saved changes to Contributor at 11:02. Reload to see them, or review how they differ from your edits." with "Reload role" and "Review differences"
|
||||
And the edits are kept, Save is off, and the bar reads "2 unsaved changes. Save is off until you reload"
|
||||
|
||||
Scenario: Review differences
|
||||
Given the banner is shown with edits Events Update Own to All and Events Delete None to Own against the saved role
|
||||
When "Review differences" is pressed
|
||||
Then a dialog "How the saved role differs from your edits · Jonas Weber saved changes to Contributor at 11:02." lists only the settings where the two disagree, as a table Setting, Saved, Yours: Events Update Own All; Events Delete None Own
|
||||
And it offers "Use the saved role" and "Keep my edits"
|
||||
And in FR the title is "Comment le rôle enregistré diffère de vos modifications" with "Réglage", "Enregistré", "Vous", "Utiliser le rôle enregistré", "Garder mes modifications"
|
||||
|
||||
Scenario Outline: Resolving
|
||||
When the person presses <action>
|
||||
Then <effect>
|
||||
Examples:
|
||||
| action | effect |
|
||||
| Keep my edits | GET /v1/role/4 replaces the saved role and its version, the edits are re-applied on top, changed cells carry the square marker, Save is on, the dialog and the banner close, an info toast reads "Role reloaded · Your 2 changes are still here. Review them, then save." and the bar reads "2 unsaved changes" |
|
||||
| Use the saved role | the edits are dropped, the form shows the saved role, Save is off, no toast about edits |
|
||||
| Reload role (banner) | the same as "Use the saved role" |
|
||||
| Escape or close | the dialog closes and the banner and kept edits stay |
|
||||
And "Keep my edits" followed by Save sends PUT /v1/role/4 with the reloaded version
|
||||
And in FR the toast reads "Rôle rechargé · Vos 2 modifications sont toujours là. Relisez-les, puis enregistrez."
|
||||
|
||||
Scenario: A second conflict
|
||||
Given edits were kept on top of version 3
|
||||
When the save answers 409 role_conflict again
|
||||
Then the banner returns with the newest updated_by and time, and the edits are still kept
|
||||
|
||||
Scenario: The name taken is not a conflict
|
||||
When the save answers 409 role_name_taken
|
||||
Then the existing field error shows and no banner
|
||||
|
||||
Scenario: The time is not clock-sensitive
|
||||
Given updated_at is a fixed string and the time zone is pinned in the test and in the visual setup
|
||||
Then "11:02" is the formatted time of that string, not the current time
|
||||
|
||||
Rule: Change role when the member's role changed first
|
||||
|
||||
Scenario: The alert in the dialog
|
||||
Given the Change role dialog for Ines Duarte (Writer) is open with Contributor chosen
|
||||
When PUT /v1/user/{id}/role answers 409 user_role_changed and GET /v1/user answers Ines Duarte with role Admin
|
||||
Then the dialog stays open with an alert "Role changed in the meantime · Ines Duarte is now Admin. Your choice is kept. Check it and try again."
|
||||
And Contributor stays chosen, the "Current" mark moves from Writer to Admin, and "Change role" stays enabled
|
||||
And pressing "Change role" again sends PUT /v1/user/{id}/role {"role_id": <Contributor id>}
|
||||
And in FR "Rôle modifié entre-temps · Ines Duarte a maintenant le rôle Admin. Votre choix est conservé. Vérifiez-le et réessayez."
|
||||
And no toast shows while the dialog is open
|
||||
|
||||
Scenario: The chosen role became the current one
|
||||
Given the re-read shows Ines Duarte now holds Contributor, the role the person chose
|
||||
Then the alert still shows and "Change role" is disabled because the chosen role is now the current one
|
||||
|
||||
Scenario: The member disappeared
|
||||
When GET /v1/user no longer lists Ines Duarte, or PUT answers 404 user_not_found
|
||||
Then the dialog closes, the list is refreshed and the existing "no longer exists" toast of slice 7 shows
|
||||
|
||||
Scenario: The dialog is gone when the answer arrives (component level)
|
||||
Given the dialog component was unmounted or its open state set to false before the 409 arrived
|
||||
Then a warning toast reads "Role not changed · Ines Duarte's role changed in the meantime, so nothing was saved. Reload and try again. · Reload"
|
||||
And "Reload" refetches GET /v1/user and dismisses the toast
|
||||
And in FR "Rôle non modifié · Le rôle d'Ines Duarte a changé entre-temps, donc rien n'a été enregistré. Rechargez et réessayez. · Recharger"
|
||||
And this is specified at component level only, no page-level trigger exists because the dialog cannot be dismissed while sending
|
||||
|
||||
Rule: Transfer ownership when the target's role changed first
|
||||
|
||||
Scenario: The alert in the dialog
|
||||
Given the Owner confirmed Jonas Weber and typed "jonas@renewbuild.example"
|
||||
When POST /v1/site/transfer-ownership answers 409 user_role_changed and GET /v1/user answers Jonas Weber with role Writer
|
||||
Then an alert in the dialog reads "Role changed in the meantime · Jonas Weber is now Writer. What you typed is kept. Check the change, then try again." with "Reload member"
|
||||
And the typed email is kept and "Transfer ownership" is disabled until "Reload member" refreshes the target
|
||||
And "Reload member" re-reads GET /v1/user, updates the shown role of the target and re-enables "Transfer ownership" with the typed email still valid
|
||||
And in FR "Rôle modifié entre-temps · Jonas Weber a maintenant le rôle Writer. Ce que vous avez saisi est conservé. Vérifiez le changement, puis réessayez. · Recharger le membre"
|
||||
|
||||
Scenario: The dialog is gone when the answer arrives (component level)
|
||||
Then a warning toast reads "Ownership not transferred · Jonas Weber's role changed in the meantime, so nothing was transferred. Check it and try again. · Reload"
|
||||
And in FR "Propriété non transférée · Le rôle de Jonas Weber a changé entre-temps, donc rien n'a été transféré. Vérifiez-le et réessayez. · Recharger"
|
||||
|
||||
Scenario Outline: Other transfer answers keep the slice 7 behaviour
|
||||
When the server answers <status> <code>
|
||||
Then the behaviour of features/roles_spa_users.feature "Transfer answers" holds
|
||||
Examples:
|
||||
| status | code |
|
||||
| 400 | invalid_transfer |
|
||||
| 400 | confirmation_mismatch |
|
||||
| 403 | missing_permission |
|
||||
| 500 | (network) |
|
||||
|
||||
Rule: Role list actions
|
||||
|
||||
Scenario: Role edited elsewhere (409 on a list action)
|
||||
Given the roles list was loaded and Contributor was edited by Jonas Weber afterwards
|
||||
When Duplicate, Delete or Move members answers 409 role_conflict
|
||||
Then a warning toast reads "Role changed elsewhere · Jonas Weber edited Contributor, so nothing was saved. Reload and try again. · Reload"
|
||||
And "Reload" refetches GET /v1/role
|
||||
And in FR "Rôle modifié ailleurs · Jonas Weber a modifié Contributor, donc rien n'a été enregistré. Rechargez et réessayez. · Recharger"
|
||||
|
||||
Scenario: Role deleted elsewhere (404)
|
||||
When an action answers 404 role_not_found
|
||||
Then the row is removed from the list and an info toast reads "That role no longer exists · It was deleted somewhere else, so we removed it from the list." and closes after 6 s
|
||||
And in FR "Ce rôle n'existe plus · Il a été supprimé ailleurs, nous l'avons donc retiré de la liste."
|
||||
|
||||
Scenario: Reassign target deleted (409 target_role_not_found)
|
||||
Given the Delete role dialog of Intern is open with Writer chosen
|
||||
When POST /v1/role/{id}/reassign answers 409 target_role_not_found
|
||||
Then the inline error "Writer no longer exists · It was deleted somewhere else. Choose another role to move the members to." of features/roles_sync_copy_activity.feature shows in the dialog, never a toast
|
||||
And GET /v1/role is re-read so the picker no longer offers Writer
|
||||
Reference in new issue
Block a user