Skip to content

NMS-20106: versioned user management API and PrimeVue Manage Users page - #8713

Open
joseanesONMS wants to merge 21 commits into
developfrom
jira/NMS-20106-manage-users-primevue
Open

NMS-20106: versioned user management API and PrimeVue Manage Users page#8713
joseanesONMS wants to merge 21 commits into
developfrom
jira/NMS-20106-manage-users-primevue

Conversation

@joseanesONMS

Copy link
Copy Markdown
Contributor

NMS-20106: a versioned user management API (/api/v2/users) so users can be provisioned by external tooling, and a PrimeVue Manage Users page that is a straight visualization of it. users.xml stays the system of record via the existing UserManager; fields the API does not expose (XMPP among them, deliberately hidden in the UI) and the password survive updates untouched.

  • Endpoints: list/get/create/update/set-password/rename/delete plus available-roles, admin-only via new Spring Security rules and in-code checks.
  • Responses never contain the password hash in any form (the v1 API returns it to admins).
  • The admin/rtc delete and rename protections and the admin ROLE_ADMIN-retention guard are enforced server-side; the legacy page only hid the buttons.
  • Requests are validated up front (ids, roles, time zones, duty schedule grammar, comments markup) and applied to a detached copy, so a rejected request leaves no partial state, while hand-edited values are grandfathered so files never become uneditable.
  • The page validates fields inline against the same rules and shows API rejections inside the dialogs.
  • 22 integration tests (mock managers, users.xml never touched) plus Vitest store/dialog tests; verified end to end against a local instance including byte-level users.xml restoration.

Adds /api/v2/users (interface + impl following the v2 conventions):
list/get/create/update, dedicated password and rename endpoints, delete,
and available-roles — all admin-only via new Spring Security rules plus
in-code checks. users.xml stays the system of record: updates apply only
the exposed fields, so contact types the API does not carry (XMPP among
them) and the password survive untouched, and passwords are stored
salted via the existing UserManager hashing. Unlike the legacy JSPs,
which only hid the buttons, the admin/rtc delete and rename protections
are enforced server-side; responses never include the password hash
(the v1 API returns it to admins).

The new Manage Users page (ui/#/admin/users) is a straight visualization
of that API: users table without the XMPP column, add/edit dialog with
role assignment, password and rename dialogs, and delete disabled for
the protected system accounts. The Manage Users menu entry now points at
the new page.
Fixes from self-review. The list fields of UserDto now default to null
so a request that omits them genuinely preserves roles and duty
schedules (empty-list defaults made the preservation guards dead code
and a partial update wiped both). Mutations validate the whole request
first and then apply it to a detached copy of the stored user, so a
rejected request can no longer leave partial changes in UserManager's
shared in-memory state; a failed create rolls the phantom map entry
back, and a service-level lock closes the check-then-act races. Duty
schedule validation accepts overnight ranges (legacy wrote MoTu2000-800
and hand-edited files contain them — rejecting those made such users
uneditable), user ids reject characters that cannot appear in a URL
path segment, a body/path user-id mismatch is rejected instead of
ignored, and HEAD joins the admin-only security rules. The UI keeps the
previous user list when a refresh fails instead of blanking the table.

The IT grows to 14 tests including regressions for omitted-field
preservation, rejected-update atomicity, and overnight schedules.
Follow-up from the group-management review, applying the same findings
here. Duty schedules with begin after end are rejected for new entries
(DutySchedule.isInSchedule compares within one calendar day, so an
overnight range never matches); strings already stored on the record
keep round-tripping so hand-edited files never make a user uneditable.
Dot-segment user ids are rejected. Emptied form fields now clear the
value instead of silently preserving it. The service serializes on the
same shared monitor as the groups API because user mutations cascade
into GroupManager. Adds a collapsed "?" help panel explaining what user
accounts are for and how to use the page.
…dialogs

The dialogs previously reported failures only through a toast that renders
behind the modal overlay, so a rejected save looked like nothing happened.
Mutating service calls now return the server's error message, which the
dialogs display inside the form while staying open; new client-side checks
(shared lib/adminValidation.ts) mirror the server rules for user-ids and
flag malformed email addresses before submitting. Users whose ids contain
/ \ or % (possible in hand-edited users.xml) get their row actions replaced
with a file-managed note, since the security filter rejects those characters
in URL paths. Error toasts now use the error styling.
The markup regexes used full-string matches that a newline anywhere
bypassed; they now scan for the characters directly. The admin account can
no longer have ROLE_ADMIN removed (it would lock every administrator out).
applyDto treats omitted scalar fields as preserve, matching the documented
contract and the sibling APIs, so a roles-only PUT no longer wipes names,
comments and contacts. User comments now reject markup like group comments
do, with unchanged hand-edited values grandfathered. Deleting a user who
supervises an on-call role is rejected, since GroupManager leaves the
supervisor attribute dangling. The client email check accepts display-name
and comma-separated recipient forms and, like the comments check, only
fires on changed input so hand-edited values never block the editor.
@joseanesONMS

Copy link
Copy Markdown
Contributor Author

@marshallmassengill could you take a review pass when you get a chance?

@joseanesONMS
joseanesONMS requested a review from synqotik July 30, 2026 20:51
…HeaderIT

The menu entry now lands on the Vue page, so the smoke test waits for its
page title instead of the legacy JSP breadcrumb.
Swap direct PrimeVue components for the Onms-XXX seam wrappers across the
users table and its dialogs: Button->OnmsButton (text/outlined mapped to
variant), Dialog->OnmsDialog, InputText->OnmsInputText,
Password->OnmsPassword, MultiSelect->OnmsMultiSelect, DataTable->OnmsTable,
Column->OnmsColumn, Tag->OnmsTag. Message and IftaLabel have no wrapper
yet and stay on PrimeVue. No behaviour change.
@joseanesONMS

Copy link
Copy Markdown
Contributor Author

@synqotik @marshallmassengill — ready for review. This page now uses the @opennms/onms-ui Onms-XXX wrappers instead of direct PrimeVue (Button, Dialog, InputText, Password, MultiSelect, DataTable, Column, Tag). Message and IftaLabel have no wrapper yet, so they stay on PrimeVue. pnpm lint is clean (ran lint:fix), and the vue-tsc build and the UI test suite pass. No behaviour change.

…g width prop

Replace :style="{ width, maxWidth }" on OnmsDialog (which rode $attrs into
PrimeVue) with the seam's declared width prop, using min(Npx, 95vw) to keep
the responsive cap.

@marshallmassengill marshallmassengill left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One blocker: Rename leaves on-call role supervisors dangling, which is the exact case delete guards against. deleteUser walks m_groupManager.getRoles() and returns 400 if the user supervises any role, with a comment noting that GroupManager.deleteUser "leaves role supervisors dangling". GroupManager.renameUser has the same hole: it rewrites group memberships and Schedule names but never touches role.getSupervisor() (GroupManager.java:498-528). So POST /api/v2/users/{id}/rename on a supervisor silently points the rota at a user id that no longer exists, killing the supervisor fallback with no error. Either apply the same 400 guard or fix up the supervisor as part of the rename.

GroupManager.renameUser rewrote group memberships and schedule names but
left role.getSupervisor() pointing at the old id, so renaming a supervisor
silently killed the rota's supervisor fallback -- the same dangling-reference
case the delete path guards against. Rewrite the supervisor as part of the
rename so every reference follows the user to the new id.
Drop the nested primevue IftaLabel from every field and use FormField's own
label/for/required/error props, and replace the direct primevue Message error
banners with a themed role=alert element, so the dialogs no longer import any
un-wrapped PrimeVue primitive. Field errors now render through FormField (the
two test selectors follow the FormField-generated error ids).
@joseanesONMS

Copy link
Copy Markdown
Contributor Author

@marshallmassengill Addressed the blocker:

Rename leaves on-call role supervisors dangling. Fixed at the root in GroupManager.renameUser — it now rewrites role.getSupervisor() alongside the group memberships and schedule names it already rewrote, so a renamed supervisor follows the rota to the new id instead of pointing at a user that no longer exists. I went with the fix-up rather than the 400 guard: a rename preserves the user (unlike delete, where blocking is the only option), so the reference should follow it. Fixing it in the manager also covers the legacy JSP rename path. New IT testRenameCarriesOnCallRoleSupervisor asserts the supervisor carries over (full UsersRestServiceIT green locally).

Also brought the dialogs onto the current @opennms/onms-ui seam while here: dropped the nested IftaLabel for FormField's own label/for/error, and replaced the primevue/message banners with a themed role=alert element — the ManageUsers dialogs now import no un-wrapped PrimeVue primitive. eslint clean, vitest + vue-tsc build pass.

Comment thread ui/src/types/userAdmin.ts Outdated
Comment thread opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/UserDto.java Outdated
Comment thread opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/UsersRestService.java Outdated
Comment thread opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/UsersRestService.java Outdated
Comment thread opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/UsersRestService.java Outdated
Comment thread opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/UsersRestService.java Outdated
Drop the JAXB annotations from the v2 user model DTOs so they serialize as
plain camelCase JSON (userId, fullName, pagerEmail, userComments, dutySchedules,
roles, readOnly, newUserId) instead of the hyphenated XML element names, and
carry that through the UI types, service, dialogs, and table. The XML mapping
to/from users.xml already lives in the REST service. Also swap the hand-rolled
isBlank/trimToNull helpers for org.apache.commons.lang3.StringUtils and match
the request-field names in the validation messages.
@joseanesONMS

Copy link
Copy Markdown
Contributor Author

@synqotik Addressed the API-shape feedback:

  • camelCase JSON, no XML — dropped the JAXB annotations from UserDto, UserWriteRequest, UserPasswordRequest, and UserRenameRequest, so they serialize as plain camelCase JSON (userId, fullName, pagerEmail, userComments, dutySchedules, roles, readOnly, newUserId) — matching the existing plain-POJO DTOs like Snmpv3UserDto. The users.xml mapping already lives in the REST service.
  • UI camelCaseui/src/types/userAdmin.ts and everything downstream (service, store, dialogs, table, tests) now use the camelCase fields.
  • userId not user-id in the REST messages, and the two Java nits: isBlank/trimToNull now go through org.apache.commons.lang3.StringUtils.

Verified: full UsersRestServiceIT (23) green with the new wire format, UI eslint clean, 24 vitest, vue-tsc build passes.

I'm applying the same camelCase-JSON convention to #8714 (groups) and auditing the notification PRs' v2 DTOs for consistency.

@joseanesONMS
joseanesONMS requested a review from synqotik August 13, 2026 16:50
Comment thread ui/src/containers/ManageUsers.vue Outdated
Comment thread ui/src/components/ManageUsers/UsersTable.vue Outdated
Comment thread ui/src/components/ManageUsers/UsersTable.vue Outdated
Comment thread ui/src/components/ManageUsers/UsersTable.vue
The Full Name and Pager Email columns still bound to the old hyphenated
field names after the camelCase migration, so they rendered blank; point
them at fullName/pagerEmail. Show '--' for an empty roles cell.
@joseanesONMS

Copy link
Copy Markdown
Contributor Author

Thanks @synqotik — went through all six:

Fixed now (pushed):

  • Column field props (UsersTable:25/41/51) — good catch. fullName and pagerEmail still bound to the old hyphenated names after the camelCase pass, so those columns rendered blank; fixed.
  • -- for empty roles (:56) — done.

Needs the seam, not on this branch:

  • v-onms-tooltip (:64) — the OnmsTooltip directive (NMS-20054) isn't present on develop yet; v-tooltip here resolves to PrimeVue's Tooltip, registered globally in theme/primevue-setup.ts. Switching to v-onms-tooltip now would be an unknown directive. Happy to flip it the moment that seam lands or this branch picks it up.

Design points — agree, and they're cross-cutting, so let's align before I change them:

  • Help panel → Info-icon + OnmsDialog (ManageUsers:9) — the same About TogglePanel pattern is on Groups and the notification config pages too, so I'd rather change it once, consistently. Happy to.
  • Row actions → Edit/Delete + ellipsis dropdown (:68) — same story; this is a shared admin-table pattern (users, groups). Let's settle the shape and I'll apply it across all of them.
  • Dialogs vs. a separate page (:112) — your call on direction; I can go either way.

I left these three as-is pending your steer rather than guessing. The camelCase-JSON convention is also applied to #8714 (groups) now.

Replace the inline About panel with an Info-icon button in the table header
that opens the help copy in an OnmsDialog (new shared AboutDialogButton), and
collapse the four per-row action buttons to inline Edit and Delete plus a
overflow OnmsMenu carrying Change Password and Rename. Protected-account
disabling and the file-managed note for unaddressable ids are unchanged.
@joseanesONMS

Copy link
Copy Markdown
Contributor Author

@synqotik Implemented the three UI patterns we discussed, on Manage Users as the template:

  • Help — the inline About panel is gone; there's now an Info-icon button in the table header that opens the help copy in an OnmsDialog. Extracted a shared Common/AboutDialogButton.vue (title + slotted content) so Groups and the notification About panels can reuse it.
  • Row actions — collapsed the four buttons to inline Edit and Delete plus a overflow OnmsMenu carrying Change Password and Rename. Protected-account disabling (admin/rtc) and the file-managed note for unaddressable ids are preserved.
  • Editor — kept the modal dialogs, per your read.

eslint clean, full vitest suite (2191) green, vue-tsc build passes.

If this shape looks right to you, I'll mirror the same AboutDialogButton + overflow-menu pattern onto #8714 (Groups) and reuse the About button for the notification config pages. Holding those until you confirm so I build it once.

@joseanesONMS
joseanesONMS requested a review from synqotik August 13, 2026 17:47
@synqotik

Copy link
Copy Markdown
Contributor

@joseanesONMS thanks changes look good!

One more thing, make same change to opennms-webapp-rest/src/main/webapp/WEB-INF/menu/menu-template-default.json as opennms-webapp-rest/src/main/webapp/WEB-INF/menu/menu-template.json, files should be identical.

…efault template too

menu-template-default.json must stay identical to menu-template.json.
@joseanesONMS

Copy link
Copy Markdown
Contributor Author

Done — menu-template-default.json now carries the same ui/index.html#/admin/users URL, and the two files are byte-identical again. Pushed.

Noted on the Drawers/Dialogs/page UX — happy to have that broader discussion separately; leaving the dialogs as-is for now. I'll mirror the Info-dialog + overflow-menu patterns onto #8714 (Groups) next, now that the shape's confirmed.

synqotik
synqotik previously approved these changes Aug 13, 2026

@synqotik synqotik left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Let's try this out and we can make adjustments as needed.

@marshallmassengill marshallmassengill left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Every write re-HTML-escapes full name and all contact addresses, cumulatively. copyOf (UsersRestService.java:325) rebuilds the user through setFullName / Contact.setInfo, and both
    run WebSecurityUtils.sanitizeString Encode.forHtml, which is not idempotent. Verified against the real config-model classes:

stored: O'Brien & Sons | bill&ted@example.com save 1: O'Brien & Sons | bill&ted@example.com save 2: O'Brien ...

So PUT /users/{id} and PUT /users/{id}/password mangle fullName, email, pagerEmail, and every unexposed contact (XMPP, phones, numeric/text pager PINs and service providers) one layer deeper each time, for any value containing & < > " '. O'Brien becomes O'Brien in the table after a single unrelated role edit. This is new: the legacy JSP is stable at one layer because the browser decodes the entities out of the value= attribute before POSTing them back; a JSON round-trip doesn't. It also contradicts "the password survives updates untouched"-style claims in the PR body and in UsersAbout.vue ("preserved untouched"). Fix by cloning without the sanitizing setters (JAXB marshal/unmarshal uses field access, @XmlAccessorType(NONE) + field annotations) and only writing fullName/contacts when the DTO value actually differs from stored, the way userComments is already grandfathered. testUpdatePreservesUnexposedContacts misses it only because xmpp@jabber.example.com has nothing escapable.

  1. Repointing the menu drops the only admin path to half the user record. modifyUser.jsp edits duty schedules, numeric/text paging service + PIN, work/mobile/home phone, telephone PIN,
    and time zone; the new page edits none of them and neither does the API surface them for editing. After this change, "User Management Manage Users" reaches the lesser editor, and the
    full one survives only under Admin "Configure Users, Groups and On-Call Roles" "Configure Users". Duty schedules and pager contacts are load-bearing for notifd. Either keep the menu
    on the JSP until parity, or add a link from the new page to the legacy editor for the uncovered fields.

… write

copyOf rebuilt the user through the config-model setters (setFullName,
Contact.setInfo), which run a non-idempotent Encode.forHtml, so every write —
a password change or any unrelated edit — re-escaped the stored value one
layer deeper (O'Brien -> O&#39;Brien -> O&amp;#39;Brien ...). Clone via a JAXB
round-trip (field access bypasses the sanitizing setters), and in applyDto only
write full name / email / pager-email through the setter when the value
actually changed. New IT stores escapable values and asserts a password change
and an unrelated edit leave them byte-for-byte unchanged.

Also fixes the build-ui lint failure (arrow-parens/quote style).
The new page (now the menu target) edited only name, email, pager-email,
comments and roles, dropping the legacy modifyUser.jsp fields that notifd
relies on. Surface the rest so the menu no longer routes to a lesser editor:

- API: UserDto gains work/mobile/home phone and numeric/text pager service +
  PIN; UsersRestService maps them to the workPhone/mobilePhone/homePhone and
  numericPage/textPage contacts (info + serviceProvider), writing through the
  sanitizing setters only when a value changed. tui-pin, time-zone-id and duty
  schedules were already on the DTO.
- UI: the editor adds phones, pager service/PIN, telephone PIN, a time-zone
  picker, and an add/remove duty-schedule list.

ITs cover create + partial-update round-trips of the new contacts; editor
tests cover load/save of the new fields and the duty-schedule list.
@joseanesONMS

Copy link
Copy Markdown
Contributor Author

@marshallmassengill — both blockers addressed, plus the red build-ui check.

Cumulative HTML-escaping of full name and contacts. Root-caused to copyOf rebuilding the user through the config-model setters (setFullName, Contact.setInfo), which run a non-idempotent Encode.forHtml — so every write escaped the stored value one layer deeper, exactly as you traced. copyOf now clones via a JAXB round-trip (User is @XmlAccessorType(NONE) with field annotations, Contact is FIELD, so marshal/unmarshal uses field access and bypasses the sanitizing setters), and applyDto only writes full name / email / pager-email / the new contacts through the setter when the value actually changed. New IT stores O'Brien & Sons / bill&ted@example.com and asserts a password change and an unrelated edit leave them byte-for-byte unchanged.

Menu routed to a lesser editor. Rather than repoint the menu, I brought the new page to parity so it's no longer lesser: UserDto + the service now expose work/mobile/home phone and numeric/text pager service + PIN (mapped to the workPhone/mobilePhone/homePhone and numericPage/textPage contacts, info + serviceProvider), and the editor adds those fields plus telephone PIN, a time-zone picker, and an add/remove duty-schedule list. tui-pin, time-zone-id and duty schedules were already on the DTO; they just weren't surfaced. ITs cover create + partial-update round-trips of the new contacts; editor tests cover load/save and the duty-schedule list.

build-ui was failing on lint (arrow-parens/quote style) — fixed, pnpm lint clean.

UsersRestServiceIT green for the new coverage; UI eslint + vitest + vue-tsc build pass.

Replace the raw MoWeFr800-1700 text inputs in the user editor with a
DutyScheduleEditor: each coverage window is a row of day toggles (Mo–Su)
plus native begin/end time pickers, parsed from and serialized back to the
canonical schedule string. A hand-edited entry the structured form can't
represent stays editable as raw text so it is never silently dropped.
@joseanesONMS

Copy link
Copy Markdown
Contributor Author

@marshallmassengill friendly ping — the cumulative HTML-escaping fix (JAXB copy + change-only writes), the field-parity work (phones, pager service/PIN, telephone PIN, time zone, and a day/time duty-schedule editor), and the build-ui lint failure are all pushed and green. Ready for another pass when you have a moment.

@marshallmassengill marshallmassengill left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers.

# Conflicts:
#	opennms-webapp/src/main/webapp/WEB-INF/applicationContext-spring-security.xml
#	ui/src/components/Common/AboutDialogButton.vue
#	ui/src/main/router/index.ts
#	ui/src/services/index.ts
@joseanesONMS

Copy link
Copy Markdown
Contributor Author

Merged current develop in to clear conflicts after #8714 landed — mechanical, no logic changes. Resolutions were all shared-wiring unions: both endpoints' rules in applicationContext-spring-security.xml, both routes in the Vue router, both service groups in services/index.ts, and the shared AboutDialogButton taking develop's copy. Also repointed UsersTable/DutyScheduleEditor icon imports to the @opennms/onms-ui/icons/* seam that develop migrated to. Lint, vue-tsc build, and the full user/group suite are green. The push dismissed the prior approval — @marshallmassengill could you re-approve when you have a moment? Conflicts are cleared and it's mergeable.

@marshallmassengill marshallmassengill left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple items:

  1. UsersRestService.java:403 (high) — tuiPin is the only field written with no validation, but users.xsd constrains tui-pin to [0-9]+. A PUT with 1234x gets into m_users via _writeUser, then _saveCurrent's schema-validating marshal throws: 500, users.xml unwritten, and since mtime/size are unchanged UserFactory.isUpdateNeeded() stays false, so the bad user is pinned in memory and every later user create/update/delete fails until restart.
  2. UsersRestService.java:213 (medium) — updateUser/setPassword have no rollbackPhantomUser equivalent (which createUser does have at :173). A failed setPassword returns 500 with the new hash live in memory, so the user logs in with it until a restart silently reverts. Same at :235.
  3. DutyScheduleEditor.vue:134 (medium) — serialize() canonicalizes day order and strips zero-padding, and the mount-time watch emits, so merely opening the dialog rewrites MoTu2000-0800 to MoTu2000-800. The server grandfathers only byte-identical strings, so validateDutySchedule then rejects it and the admin can't save even an email-only edit. The existing test passes only because its fixture is already canonical.

…tes !smoke

users.xsd constrains tui-pin to [0-9]+ but it was the only field written
unvalidated: a bad PIN entered the in-memory map, failed the schema-validating
marshal, and — with isUpdateNeeded() seeing an unchanged file — wedged every
later user mutation until restart. It is now rejected with a 400 before it
touches the map, and the IT proves a valid write still succeeds after the
rejected one.

updateUser and setPassword get the same rollback discipline createUser
already had: a failed save re-saves the previous state, so a 500 no longer
leaves a new password hash live in memory until a restart quietly reverts it.

The duty-schedule editor no longer rewrites schedules on open: parsed rows
keep their incoming string and serialize it byte-identically until the row
itself is edited, and the watcher only emits when the value actually changes.
The server grandfathers legacy strings byte-identically, so the old
mount-time canonicalization (day order, zero-padding) made such users
unsavable for unrelated edits. Tests cover the non-canonical shape from the
review: no emit on mount, untouched rows survive verbatim, canonicalization
only on a real edit.
@joseanesONMS

Copy link
Copy Markdown
Contributor Author

@marshallmassengill all three addressed in 39a756ddf7a:

  1. tuiPin — validated against the users.xsd [0-9]+ pattern with a 400 before it can reach the in-memory map. The new IT covers both halves of your scenario: 1234x is rejected AND a valid write still succeeds afterward, proving the map isn't poisoned.
  2. RollbackupdateUser and setPassword now mirror createUser's discipline: a failed save re-saves the previous state (restorePreviousUser), so memory and users.xml re-align and a "failed" password can no longer be used to log in.
  3. Duty-schedule rewrite-on-open — parsed rows keep the incoming string and serialize it byte-identically until that row is actually edited (day toggle or time input), and the watcher no longer emits when serialization equals the incoming value — opening the dialog provably changes nothing. Tests use your exact shape (TuMo2000-0800): no emit on mount, untouched rows survive a neighbor's edit verbatim, canonicalization only on a real edit. You were right that the old test passed only because its fixture was canonical — the new ones aren't.

UsersRestServiceIT 25/26 locally — the one failure is testRenameCarriesOnCallRoleSupervisor, the known local-env quirk that's green in CI. Push carries !smoke.

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.

3 participants