Skip to content

feature: ISSUE-285 — at-call-time permission guards in the verify flow#322

Open
GitAddRemote wants to merge 1 commit intomainfrom
feature/ISSUE-285
Open

feature: ISSUE-285 — at-call-time permission guards in the verify flow#322
GitAddRemote wants to merge 1 commit intomainfrom
feature/ISSUE-285

Conversation

@GitAddRemote
Copy link
Copy Markdown
Collaborator

Summary

  • assignVerifiedRole / removeVerifiedRole: added pre-flight ManageRoles permission check that returns false early with a structured error log, before attempting roles.add / roles.remove
  • handleVerifyButtonInteraction: replaced the setNickname try/catch with a pre-flight ManageNicknames permission guard that responds with missingPermissionNickname and returns early — eliminating the catch path that was previously unreachable in most failure modes
  • Added missingPermissionNickname i18n key; removed the now-unreachable nicknameFailed key

Test plan

  • role.services.test.ts: new assignVerifiedRole + removeVerifiedRole tests assert false returned and roles.add/roles.remove not called when ManageRoles is missing
  • verifyButton.test.ts: replaced two stale nicknameFailed-via-throw tests with a single missingPermissionNickname guard test asserting setNickname not called and response contains "Manage Nicknames"
  • npm run quality passes (542 tests)

Closes #285

…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
Copilot AI review requested due to automatic review settings April 5, 2026 08:11
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 ManageRoles checks to role assignment/removal helpers with structured error logs and early false returns.
  • Add pre-flight ManageNicknames check 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.

Comment on lines 33 to 36
"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.",
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +82 to 85
if (!interaction.guild?.members.me?.permissions.has(PermissionFlagsBits.ManageNicknames)) {
await respond(i18n.__({ phrase: 'commands.verify.responses.missingPermissionNickname', locale }));
return;
}
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
Comment on lines +87 to +89
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}`);
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
Comment on lines +140 to +144
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();
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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 }),
);

Copilot uses AI. Check for mistakes.
Comment on lines +215 to +219
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();
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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 }),
);

Copilot uses AI. Check for mistakes.
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.

feature: at-call-time permission guards in the verify flow

2 participants