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:
Florian Sylvain committed 2026-10-29 16:28:25 +01:00
1 parent 5d6f378fce
commit b39a41be88
124 files changed
+8816 -523

No files matched your search

+169
View File
@@ -0,0 +1,169 @@
Feature: Roles sync pass 6: final copy, neutral wording and role activity events
As a person using roles, users and activity in EN or FR
I want the final toast, error and activity wording of the roles-sync design
So that every message names what happened, says what was kept and says what to do next
Background:
Given the spec of record is docs/design/screens/roles.md groups 8, 12 and 13, the tables "Activity events: sentences", "Activity events: failed sentences", "Activity filter: category names" and "Final copy: toasts and errors, EN and FR", docs/design/states.md "Roles sync (pass 6)", docs/design/screens/activity.md "Roles sync (pass 6)" and docs/design/change-log-roles-sync.md section 1
And the stale-state toasts, banners and dialog alerts of group 15 are in features/roles_sync_conflicts.feature, not here
And the removal of a user (route, dialog, toast, user.remove event) is in features/roles_sync_remove_user.feature
And the audit actions are role.create, role.edit, role.delete, role.reassign, user.change_role and user.transfer_ownership (internal/domain/audit/auditDomain.go), the outcome is a field and a failed event keeps its name
And the SPA lives in web/src, Nuxt UI only, design tokens only, EN and FR in i18n/messages.ts, no comments in code
And a string is written "Title · Body": the first part is the toast or alert title, the second its description
And French is asserted only for the strings of the "Final copy" table and the activity tables; strings the design gives in EN only are listed in the qa as design gaps
And the "Why it failed" reasons of the design (role_conflict, role_locked, missing_permission) are out of this slice
Rule: Neutral wording
Scenario: No gendered pronoun in the role copy
Then the Change role dialog reads "Their API keys follow the new role."
And the Transfer ownership confirmation reads "You become an Admin. You can't take ownership back unless they transfer it."
And no EN string of the roles, users, profile, API keys and activity screens contains he, she, his, her or him as a word
Rule: Role changed (group 12)
Scenario: The success toast
Given Ines Duarte holds Writer and the Change role dialog is open
When the person picks Content lead and PUT /v1/user/{id}/role answers 200
Then a success toast reads "Role changed · Ines Duarte is now Content lead, and their API keys follow the new role."
And it closes after 6 s
And in FR it reads "Rôle modifié · Ines Duarte a maintenant le rôle Content lead, et ses clés API suivent le nouveau rôle."
Rule: Role editor inline errors (group 13)
Scenario Outline: Save failed
Given the person edited Contributor and presses Save changes
When the server answers <failure>
Then the editor shows "Couldn't save the role · Your changes are still here. Try again in a moment." with the action "Try again"
And every edit is kept, Save changes stays enabled and "Try again" sends the same save again
And in FR it reads "Impossible d'enregistrer le rôle · Vos modifications sont toujours là. Réessayez dans un instant." with "Réessayer"
And it applies to POST /v1/role (new role) and PUT /v1/role/{id}
Examples:
| failure |
| 500 |
| a network failure |
Scenario: Replaces the generic error
Then the placeholder wording of the previous slice for a failed save is gone from EN and FR
Rule: Change role inline error (group 13)
Scenario: Change role failed
Given the Change role dialog is open with Contributor chosen
When PUT /v1/user/{id}/role answers 500 or the network fails
Then an alert in the dialog reads "Couldn't change the role · Nothing was changed. Try again in a moment."
And the choice is kept and "Change role" stays enabled
And in FR it reads "Impossible de modifier le rôle · Rien n'a été modifié. Réessayez dans un instant."
Scenario: Role above the caller
Then a role above the caller's reads "Your role can't assign a role with more power than yours." in the picker and in the dialog
And in FR "Votre rôle ne peut pas attribuer un rôle plus puissant que le vôtre."
Rule: Delete role inline errors (group 13)
Scenario Outline: Delete answers inside the dialog
Given the Delete role dialog of <role> is open
When <trigger>
Then <effect>
Examples:
| role | trigger | effect |
| Intern | POST /v1/role/{id}/reassign answers 409 target_role_not_found for the chosen target Writer | alert "Writer no longer exists · It was deleted somewhere else. Choose another role to move the members to.", the picker is cleared and "Move 1 member and delete role" is disabled until a role is chosen |
| Intern | DELETE answers 409 role_has_members and the re-read of GET /v1/role shows 1 member | the dialog turns into the blocked state with "Role still has members · Intern now has 1 member. Move them to another role first." and "Move 1 member and delete role" disabled until a role is chosen |
| Intern | POST /v1/role/{id}/reassign answers 500 or the network fails with Writer chosen | alert "Couldn't move the members · Nothing was changed. Try again in a moment.", Writer stays chosen, the action stays enabled, DELETE is not sent |
| Intern | POST /v1/role/{id}/reassign answers 400 invalid_reassign (same role, locked or uncovered target) | the existing refusal copy of features/roles_spa_screens.feature, the picker is cleared |
And in FR the first three read "Writer n'existe plus · Il a été supprimé ailleurs. Choisissez un autre rôle pour y déplacer les membres.", "Le rôle a encore des membres · Intern a maintenant 1 membre. Déplacez-le d'abord vers un autre rôle." and "Impossible de déplacer les membres · Rien n'a été modifié. Réessayez dans un instant."
Rule: Invite inline errors (group 13)
Scenario Outline: Invite answers
Given the Invite user dialog holds "lea.martin@renew.example" and Writer
When <trigger>
Then <effect>
And the dialog stays open and keeps the email and the role
And in FR it reads <french>
Examples:
| trigger | effect | french |
| the client check or a 400 rejects the address | under Email: "Enter a valid email address." | "Saisissez une adresse e-mail valide." |
| POST /v1/user/invite answers 409 (existing account) | under Email: "This address already has an account." | "Cette adresse a déjà un compte." |
| POST /v1/user/invite answers 403 escalation | under Role: "Your role can't assign a role with more power than yours." | "Votre rôle ne peut pas attribuer un rôle plus puissant que le vôtre." |
| POST /v1/user/invite answers 500 or the network fails | alert "Couldn't send the invite · Try again in a moment." | "Impossible d'envoyer l'invitation · Réessayez dans un instant." |
And the existing 409 "pending invite" state of users.md is unchanged
Rule: Final copy parity
Scenario: Every string of the "Final copy" table exists in EN and FR
Then each key of the table exists in i18n/messages.ts with matching placeholders
And roleMessages.spec.ts lists them
Rule: Backend: the destination name of a reassignment
Background for this Rule (house style of roles_permissions_roles_api.feature):
Given the audit event role.reassign carries the params from (source role id), to (destination role id) and moved
And this slice adds one additive param, to_name: the destination role's name at move time, a string
And the event target keeps naming the source role, so the source name stays available after the source is deleted
And no new route, no new permission and no schema change: params are stored as the existing JSON map
And the layer touched is internal/infrastructure/useCases/ReassignRoleMembers.go and its audit param constant in internal/domain/audit; docs/api.md gains one line
Scenario: The param is recorded
When POST /v1/role/{id}/reassign moves 4 members from Contributor to Writer
Then the recorded role.reassign event has params from, to, moved 4 and to_name "Writer"
And a reassignment that moves 0 members still records nothing
And a refusal still records nothing
Scenario: Old events
Given a stored role.reassign event without to_name
Then GET /v1/activity returns it unchanged and the SPA uses the fallback below
Rule: Activity sentences for the six events
Scenario Outline: Success sentences
Given GET /v1/activity returns an event <action> with outcome success
Then the sentence reads <sentence> and in FR <french>
Examples:
| action | data | sentence | french |
| role.create | target role Contributor | Role Contributor created | Rôle Contributor créé |
| role.edit | target role Contributor | Role Contributor edited | Rôle Contributor modifié |
| role.delete | target role Intern | Role Intern deleted | Rôle Intern supprimé |
| role.reassign | target role Intern, params to_name Writer, moved 3 | Moved 3 members from Intern to Writer | 3 membres déplacés de Intern vers Writer |
| role.reassign | target role Intern, params to_name Writer, moved 1 | Moved 1 member from Intern to Writer | 1 membre déplacé de Intern vers Writer |
| user.change_role | target Ines Duarte, changes role before Writer after Content lead | Ines Duarte's role changed from Writer to Content lead | Le rôle d'Ines Duarte est passé de Writer à Content lead |
| user.transfer_ownership | actor Marta Alves, target Jonas Weber | Ownership transferred from Marta Alves to Jonas Weber | Propriété transférée de Marta Alves à Jonas Weber |
And the sentences "Moved the members of the role {0}" and "Changed the role of {0}" are no longer the output for these events
Scenario Outline: Failed sentences
Given the event <action> has the outcome failed
Then the sentence reads <sentence> followed by the Failed badge, and in FR <french>
Examples:
| action | sentence | french |
| role.create | Failed to create the role Contributor | Échec de la création du rôle Contributor |
| role.edit | Failed to edit the role Contributor | Échec de la modification du rôle Contributor |
| role.delete | Failed to delete the role Intern | Échec de la suppression du rôle Intern |
| role.reassign | Failed to move 3 members from Intern to Writer | Échec du déplacement de 3 membres de Intern vers Writer |
| user.change_role | Failed to change the role of Ines Duarte | Échec du changement de rôle d'Ines Duarte |
| user.transfer_ownership | Failed to transfer ownership to Jonas Weber | Échec du transfert de la propriété à Jonas Weber |
Scenario Outline: role.reassign without the destination name or the count (old events, failed events)
Given a role.reassign event with outcome <outcome> whose params lack <missing>
Then the sentence reads <sentence>
And it never contains "undefined" or an empty name
Examples:
| outcome | missing | sentence |
| success | to_name | Moved 3 members from Intern to another role |
| success | moved | Moved the members of the role Intern |
| failed | to_name, moved | Failed to move the members of the role Intern |
And these fallback wordings are a design gap (the design draws only the full sentences); the coder uses them and the qa lists them
Scenario: Unknown action
Given an event whose action the catalogue does not know
Then it renders as before and does not crash
Rule: Activity category names
Scenario: The category select
When the Category select is open
Then the options read All categories, Articles, Media, Users and invites, Roles, API keys, Authentication, System, in that order
And in FR Toutes les catégories, Articles, Médias, Utilisateurs et invitations, Rôles, Clés API, Authentification, Système
And "Users & invites" no longer appears in the Activity filter, the permission groups or the API catalogue
And choosing Roles requests category roles and shows the role events
And choosing Users and invites keeps requesting category users