feature: ISSUE-285 — at-call-time permission guards in the verify flow#322
feature: ISSUE-285 — at-call-time permission guards in the verify flow#322GitAddRemote wants to merge 1 commit intomainfrom
Conversation
…ion guards in verify flow - assignVerifiedRole / removeVerifiedRole: pre-flight ManageRoles check returns false early - handleVerifyButtonInteraction: replace setNickname try/catch with ManageNicknames pre-flight guard - Add missingPermissionNickname i18n key; remove nicknameFailed (no longer reachable) - Tests: add ManageRoles-missing cases to role.services; swap stale nicknameFailed throw tests for missingPermissionNickname guard test in verifyButton
There was a problem hiding this comment.
Pull request overview
Adds at-call-time permission guards to the verification flow so missing Discord permissions produce clearer, immediate outcomes instead of failing deeper in the API calls.
Changes:
- Add pre-flight
ManageRoleschecks to role assignment/removal helpers with structured error logs and earlyfalsereturns. - Add pre-flight
ManageNicknamescheck before nickname updates in the verify button interaction with a new i18n response. - Update/adjust Jest tests to cover the new permission-guard branches.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/services/role.services.ts | Adds ManageRoles pre-flight permission guards and logs before attempting role mutations. |
| src/services/tests/role.services.test.ts | Adds tests for the new ManageRoles guard branches. |
| src/interactions/verifyButton.ts | Adds ManageNicknames guard before setNickname and adjusts control flow accordingly. |
| src/interactions/tests/verifyButton.test.ts | Updates nickname-related tests to assert the new guard behavior/message. |
| src/locales/en.json | Adds missingPermissionNickname translation key. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "invalidHandle": "❌ Please enter your RSI handle only (e.g. Testhandle123), not a full URL.", | ||
| "nicknameFailed": "❌ Verification succeeded but your server nickname could not be updated. Please contact a server administrator.", | ||
| "missingPermissionNickname": "⚠️ The bot is missing the **Manage Nicknames** permission. Please ask a server administrator to grant it.", | ||
| "disabled": "❌ Verification is not available on this server.", |
There was a problem hiding this comment.
The PR description says the now-unreachable nicknameFailed i18n key was removed, but it is still present here and is unused (no code references found). Either remove the key to avoid dead translations, or keep it and update the PR description/test plan to reflect that it’s still part of the contract.
| if (!interaction.guild?.members.me?.permissions.has(PermissionFlagsBits.ManageNicknames)) { | ||
| await respond(i18n.__({ phrase: 'commands.verify.responses.missingPermissionNickname', locale })); | ||
| return; | ||
| } |
There was a problem hiding this comment.
This early-return guard runs after the role is already assigned; when Manage Nicknames is missing the user will not receive the normal verification success message even though they are effectively verified (role added). Consider replying with the success message plus an additional warning about the missing permission (or otherwise making it explicit in the response that verification succeeded but nickname couldn’t be updated).
| const member = await interaction.guild.members.fetch(interaction.user.id); | ||
| await member.setNickname(canonicalHandle); | ||
| logger.debug(`Nickname set to "${canonicalHandle}" for user ID: ${interaction.user.id}`); |
There was a problem hiding this comment.
With the try/catch removed, any setNickname failure other than missing ManageNicknames (e.g. hierarchy issues, invalid nickname length/characters) will now bubble to interactionRouter and result in a generic fallback reply. If you still want a user-actionable verification-specific message for nickname update failures, handle/translate expected DiscordAPIError cases here (or keep a narrowed catch that responds appropriately).
| const { assignVerifiedRole } = await import('../role.services.js'); | ||
| const member = makeMember(); | ||
| const guild = makeGuild({ member, hasManageRoles: false }); | ||
| expect(await assignVerifiedRole(makeInteraction(guild), 'user-1')).toBe(false); | ||
| expect(member.roles.add).not.toHaveBeenCalled(); |
There was a problem hiding this comment.
The new "missing ManageRoles" tests only assert the boolean return and that roles.add/remove aren’t called, but they don’t assert the promised structured error log. Since you already have loadRoleServicesWithLogger() in this file, consider using it (or otherwise mocking getLogger) so the test also verifies logger.error is called with the missing-permission message and includes guildId metadata.
| const { assignVerifiedRole } = await import('../role.services.js'); | |
| const member = makeMember(); | |
| const guild = makeGuild({ member, hasManageRoles: false }); | |
| expect(await assignVerifiedRole(makeInteraction(guild), 'user-1')).toBe(false); | |
| expect(member.roles.add).not.toHaveBeenCalled(); | |
| const { assignVerifiedRole, loggerError } = await loadRoleServicesWithLogger(); | |
| const member = makeMember(); | |
| const guild = makeGuild({ member, hasManageRoles: false }); | |
| expect(await assignVerifiedRole(makeInteraction(guild), 'user-1')).toBe(false); | |
| expect(member.roles.add).not.toHaveBeenCalled(); | |
| expect(loggerError).toHaveBeenCalledWith( | |
| expect.stringContaining('ManageRoles'), | |
| expect.objectContaining({ guildId: guild.id }), | |
| ); |
| const { removeVerifiedRole } = await import('../role.services.js'); | ||
| const member = makeMember(); | ||
| const guild = makeGuild({ member, hasManageRoles: false }); | ||
| expect(await removeVerifiedRole(makeInteraction(guild), 'user-1')).toBe(false); | ||
| expect(member.roles.remove).not.toHaveBeenCalled(); |
There was a problem hiding this comment.
Same as above: this test exercises the missing ManageRoles guard but does not verify the structured error log that the implementation adds. Mock the logger and assert logger.error is invoked with the missing-permission message and guildId.
| const { removeVerifiedRole } = await import('../role.services.js'); | |
| const member = makeMember(); | |
| const guild = makeGuild({ member, hasManageRoles: false }); | |
| expect(await removeVerifiedRole(makeInteraction(guild), 'user-1')).toBe(false); | |
| expect(member.roles.remove).not.toHaveBeenCalled(); | |
| const { removeVerifiedRole, loggerError } = await loadRoleServicesWithLogger(); | |
| const member = makeMember(); | |
| const guild = makeGuild({ member, hasManageRoles: false }); | |
| expect(await removeVerifiedRole(makeInteraction(guild), 'user-1')).toBe(false); | |
| expect(member.roles.remove).not.toHaveBeenCalled(); | |
| expect(loggerError).toHaveBeenCalledWith( | |
| 'Missing ManageRoles permission', | |
| expect.objectContaining({ guildId: guild.id }), | |
| ); |
Summary
assignVerifiedRole/removeVerifiedRole: added pre-flightManageRolespermission check that returnsfalseearly with a structured error log, before attemptingroles.add/roles.removehandleVerifyButtonInteraction: replaced thesetNicknametry/catch with a pre-flightManageNicknamespermission guard that responds withmissingPermissionNicknameand returns early — eliminating the catch path that was previously unreachable in most failure modesmissingPermissionNicknamei18n key; removed the now-unreachablenicknameFailedkeyTest plan
role.services.test.ts: newassignVerifiedRole+removeVerifiedRoletests assertfalsereturned androles.add/roles.removenot called whenManageRolesis missingverifyButton.test.ts: replaced two stalenicknameFailed-via-throw tests with a singlemissingPermissionNicknameguard test assertingsetNicknamenot called and response contains "Manage Nicknames"npm run qualitypasses (542 tests)Closes #285