Skip to content

Mention each handler tests#96

Open
Devashish08 wants to merge 21 commits intoRealDevSquad:developfrom
Devashish08:mention-each-handler-tests
Open

Mention each handler tests#96
Devashish08 wants to merge 21 commits intoRealDevSquad:developfrom
Devashish08:mention-each-handler-tests

Conversation

@Devashish08
Copy link
Contributor

@Devashish08 Devashish08 commented Apr 29, 2025

Date: 29/04/2025

Developer Name: @Devashish08

Issue Ticket Number

closes #70

Description

Notes for Reviewers:

This PR introduces unit tests for the /mention-each command's background handler layer (commands/handlers/), covering the logic implemented in PR #95

Related PRs:

Builds Upon: Branch mention-each-handler


Documentation Updated?

  • Yes
  • No

Under Feature Flag

  • Yes
  • No

Database Changes

  • Yes
  • No

Breaking Changes

  • Yes
  • No

Development Tested?

  • Yes
  • No

Screenshots

Screenshot 2025-04-07 034029

Test Coverage

image

Additional Notes

@coderabbitai
Copy link

coderabbitai bot commented Apr 29, 2025

Summary by CodeRabbit

  • New Features
    • Introduced a new "Mention Each" command that allows mentioning all users with a specific role, with options for custom messages, individual mentions, and user list formatting.
    • Added support for advanced role-based user mentions in Discord, including batching and flexible messaging options.
  • Bug Fixes
    • Corrected typos in method names to ensure proper functionality and reliability.
  • Tests
    • Added comprehensive unit tests for the new "Mention Each" command, its handler, and related utility functions.
    • Introduced new mock implementations to facilitate robust testing of Discord session interactions.
  • Chores
    • Updated dependencies and improved code formatting for better maintainability.

Walkthrough

This update introduces the "MentionEach" command to the Discord bot, enabling users to mention all members of a specific role with flexible options. The implementation includes new command registration, request validation, and asynchronous processing via a message queue. The handler retrieves role members, formats mentions, and supports three modes: standard (all mentions in one message), dev (batch individual mentions), and dev-title (list users without pinging). The codebase is expanded with utility functions for member retrieval and formatting, comprehensive unit tests, and typo corrections in method names. All changes are modular, with dependency injection and robust error handling.

Changes

Files/Paths Change Summary
commands/handlers/mentionEachHandler.go, commands/handlers/mentionEachHandler_test.go Introduced the mention-each handler and comprehensive unit tests for all control flow branches and helper functions.
commands/handlers/main.go, commands/handlers/main_test.go Added dispatch and test case for the MentionEach command in the main handler.
commands/main.go Registered the new MentionEach command with required and optional parameters.
service/mentionEachService.go Added MentionEachService method for request validation, queuing, and response logic for the command.
service/main.go Routed MentionEach command to the new MentionEachService handler.
dtos/commands.go, utils/constants.go Added MentionEach field to command name types and constants.
utils/membersUtils.go, utils/membersUtils_test.go Added utility functions and tests for role-based member retrieval and mention formatting.
models/discord.go, models/discord_test.go Renamed GetUerId to GetUserId; added GuildMembers and ChannelMessageSend methods and corresponding tests.
commands/main/register.go, commands/register/register.go Fixed typo in method name GetUerId → GetUserId.
commands/main/resgister_test.go, commands/register/register_test.go Extended mocks for new session methods; fixed typo in mock method name.
tests/mocks/discordSessionMock.go Added a comprehensive Discord session mock for unit testing.
config/config.go Reordered import statements.
go.mod Added indirect dependency github.com/stretchr/objx.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant DiscordAPI
    participant Service
    participant Queue
    participant Handler
    participant Utils

    User->>DiscordAPI: Issues /mention-each command
    DiscordAPI->>Service: Sends interaction request
    Service->>Service: Validate input & feature flag
    Service->>Queue: Enqueue DataPacket with metadata
    Service->>DiscordAPI: Responds "Processing..."

    Queue->>Handler: Delivers DataPacket
    Handler->>Utils: Fetch members with role
    Utils-->>Handler: Returns member list
    Handler->>Utils: Format mentions/message
    Handler->>DiscordAPI: Sends mention(s) to channel
Loading

Assessment against linked issues

Objective (Issue #) Addressed Explanation
Migrate mention each command (#70)

Poem

A hop and a leap, the bunny’s at play,
With mentions for roles in a bright Discord way!
Batch or a list, or a ping for each friend,
This feature’s now live from tail-tip to end.
So gather your guild, let the mentions commence—
The code’s all in order, the logic immense!
(ʕ•ᴥ•ʔ)ノ゙✨

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🔭 Outside diff range comments (1)
commands/main/resgister_test.go (1)

98-104: ⚠️ Potential issue

Remove duplicated test case.

This test case is identical to the one on lines 91-97, which also tests that RegisterCommands panics when ApplicationCommandCreate returns an error.

-	t.Run("should panic when openSession.ApplicationCommandCreate() returns an error", func(t *testing.T) {
-		mockSess := &mockSession{openError: nil, commandError: assert.AnError}
-
-		assert.Panics(t, func() {
-			RegisterCommands(mockSess)
-		}, "RegisterCommands should panic when ApplicationCommandCreate returns an error")
-	})
📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ecf9b55 and 15c7ed6.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (20)
  • commands/handlers/main.go (1 hunks)
  • commands/handlers/main_test.go (1 hunks)
  • commands/handlers/mentionEachHandler.go (1 hunks)
  • commands/handlers/mentionEachHandler_test.go (1 hunks)
  • commands/main.go (1 hunks)
  • commands/main/register.go (1 hunks)
  • commands/main/resgister_test.go (2 hunks)
  • commands/register/register.go (1 hunks)
  • commands/register/register_test.go (2 hunks)
  • config/config.go (1 hunks)
  • dtos/commands.go (1 hunks)
  • go.mod (1 hunks)
  • models/discord.go (1 hunks)
  • models/discord_test.go (2 hunks)
  • service/main.go (1 hunks)
  • service/mentionEachService.go (1 hunks)
  • tests/mocks/discordSessionMock.go (1 hunks)
  • utils/constants.go (1 hunks)
  • utils/membersUtils.go (1 hunks)
  • utils/membersUtils_test.go (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
commands/main/resgister_test.go (1)
Learnt from: Devashish08
PR: Real-Dev-Squad/discord-service#80
File: utils/members_utils_test.go:28-37
Timestamp: 2025-04-11T16:51:24.677Z
Learning: The `ChannelMessageSend` mock implementation in `utils/members_utils_test.go` is necessary because it's part of the `DiscordSessionInterface` interface in the Discord service. In Go, all methods of an interface must be implemented for a mock to properly satisfy the interface, even if some methods aren't directly used in the current tests.
commands/register/register_test.go (1)
Learnt from: Devashish08
PR: Real-Dev-Squad/discord-service#80
File: utils/members_utils_test.go:28-37
Timestamp: 2025-04-11T16:51:24.677Z
Learning: The `ChannelMessageSend` mock implementation in `utils/members_utils_test.go` is necessary because it's part of the `DiscordSessionInterface` interface in the Discord service. In Go, all methods of an interface must be implemented for a mock to properly satisfy the interface, even if some methods aren't directly used in the current tests.
tests/mocks/discordSessionMock.go (1)
Learnt from: Devashish08
PR: Real-Dev-Squad/discord-service#80
File: utils/members_utils_test.go:28-37
Timestamp: 2025-04-11T16:51:24.677Z
Learning: The `ChannelMessageSend` mock implementation in `utils/members_utils_test.go` is necessary because it's part of the `DiscordSessionInterface` interface in the Discord service. In Go, all methods of an interface must be implemented for a mock to properly satisfy the interface, even if some methods aren't directly used in the current tests.
🧬 Code Graph Analysis (9)
commands/handlers/main_test.go (3)
dtos/queue.go (1)
  • DataPacket (9-13)
utils/constants.go (1)
  • CommandNames (11-16)
commands/handlers/main.go (1)
  • MainHandler (20-37)
service/main.go (2)
utils/constants.go (1)
  • CommandNames (11-16)
commands/handlers/main.go (1)
  • CS (18-18)
commands/handlers/main.go (2)
utils/constants.go (1)
  • CommandNames (11-16)
service/main.go (1)
  • CS (14-14)
commands/main.go (1)
utils/constants.go (1)
  • CommandNames (11-16)
utils/membersUtils_test.go (3)
tests/mocks/discordSessionMock.go (1)
  • DiscordSession (12-14)
utils/constants.go (1)
  • DISCORD_GUILD_MEMBER_API_LIMIT (8-8)
utils/membersUtils.go (4)
  • GetUsersWithRole (12-55)
  • FormatUserMentions (57-67)
  • FormatMentionResponse (69-77)
  • FormatUserListResponse (79-92)
utils/constants.go (1)
dtos/commands.go (1)
  • CommandNameTypes (3-8)
utils/membersUtils.go (2)
models/discord.go (1)
  • SessionInterface (33-40)
utils/constants.go (1)
  • DISCORD_GUILD_MEMBER_API_LIMIT (8-8)
commands/handlers/mentionEachHandler.go (3)
models/discord.go (2)
  • SessionInterface (33-40)
  • SessionWrapper (5-7)
utils/membersUtils.go (4)
  • GetUsersWithRole (12-55)
  • FormatUserListResponse (79-92)
  • FormatMentionResponse (69-77)
  • FormatUserMentions (57-67)
commands/handlers/main.go (2)
  • CommandHandler (14-16)
  • CreateSession (44-57)
service/mentionEachService.go (5)
service/main.go (1)
  • CommandService (10-12)
dtos/discord.go (1)
  • Data (5-8)
dtos/queue.go (1)
  • DataPacket (9-13)
utils/constants.go (1)
  • CommandNames (11-16)
queue/main.go (1)
  • SendMessage (90-114)
🔇 Additional comments (33)
config/config.go (1)

7-7: LGTM: Import reordering is a minor style change

The reordering of the "os" import to the last position is a simple style adjustment and doesn't affect functionality.

dtos/commands.go (1)

7-7: LGTM: New command type added correctly

The addition of the MentionEach field to the CommandNameTypes struct properly supports the new "mention-each" command functionality.

utils/constants.go (2)

5-9: LGTM: Well-structured constants block

Grouping related constants into a single block improves code organization. The new DISCORD_GUILD_MEMBER_API_LIMIT constant with a value of 1000 appropriately handles Discord API pagination limits.


12-15: LGTM: Command name constant added properly

The addition of the "mention-each" command name is correctly integrated into the CommandNames variable, maintaining consistency with the structure used for other commands.

go.mod (1)

19-19: LGTM: New indirect dependency

The addition of github.com/stretchr/objx v0.5.2 as an indirect dependency is likely introduced by test mock frameworks and is appropriate for the testing changes in this PR.

commands/register/register.go (1)

35-35: Method name correction implemented correctly.

The change from GetUerId() to GetUserId() fixes a typo in the method call name, ensuring consistency with the renamed method in the session interface.

commands/main/register.go (1)

33-33: Method name correction implemented correctly.

The change from GetUerId() to GetUserId() aligns with the renamed method in the session interface, ensuring consistency across the codebase.

service/main.go (1)

25-26: Command routing for "mention-each" added correctly.

The new case statement for handling the "mention-each" command follows the existing code pattern and properly routes to the MentionEachService handler function.

commands/handlers/main_test.go (1)

25-36: Test case for "mention-each" command handler is well-structured.

The test follows the established pattern and verifies that MainHandler correctly returns a non-nil handler when processing the "mention-each" command. The test includes appropriate assertions and error checking.

commands/handlers/main.go (1)

31-32: Integration of MentionEach command looks good.

The new case statement appropriately integrates the mentionEachHandler into the command dispatch flow, following the established pattern used for other commands.

models/discord_test.go (4)

10-11: Good improvement: Local constant instead of import dependency.

Using a local constant reduces unnecessary dependencies and improves the test file's maintainability.


41-42: Good fix: Method name typo correction.

Correcting GetUerId() to GetUserId() improves naming consistency throughout the codebase.


44-48: Appropriate test coverage for GuildMembers method.

This test properly verifies that the method is implemented and panics when invoked on the dummy session wrapper, following the pattern established for other methods.


50-54: Appropriate test coverage for ChannelMessageSend method.

This test correctly verifies that the method is implemented and panics when invoked on the dummy session wrapper, consistent with other interface method tests.

commands/register/register_test.go (3)

40-43: Good addition: Mock fields for new methods.

These fields support the mock implementations of the Discord session interface methods required for the MentionEach command.


62-65: Good fix: Method name typo correction.

Correcting GetUerId() to GetUserId() maintains consistency with the fixes in other files.


67-74: Appropriate mock implementations for interface completeness.

These implementations satisfy the Go interface requirement that all methods must be implemented, even if not directly used in the current tests.

commands/main/resgister_test.go (3)

40-43: Good addition: Mock fields for new methods.

These fields properly support the mock implementations of the Discord session interface methods required for the MentionEach command.


62-65: Good fix: Method name typo correction.

Correcting GetUerId() to GetUserId() maintains consistency with the fixes in other files.


67-75: Appropriate mock implementations for interface completeness.

These implementations satisfy the Go interface requirement that all methods must be implemented, even if not directly used in the current tests.

commands/main.go (1)

29-68: Well-structured command definition

The MentionEach command implementation is clean, well-documented with comments for each option, and follows the established pattern of other commands in the codebase. The options are logically organized with the required role parameter first, followed by optional parameters.

models/discord.go (3)

21-23: Typo correction improves code clarity

Correcting the method name from GetUerId to GetUserId improves readability and maintains consistent naming conventions.


25-31: Appropriate wrapper methods for Discord API

The new wrapper methods for GuildMembers and ChannelMessageSend provide a clean interface for the mention-each feature to interact with Discord. This follows the established pattern of wrapping Discord session methods.


37-39: Interface definition is consistent with implementation

The interface updates correctly reflect the method signatures added to the SessionWrapper struct, ensuring proper implementation compliance.

utils/membersUtils.go (3)

12-55: Robust implementation of member retrieval with pagination

The GetUsersWithRole function effectively handles Discord API pagination to fetch all guild members with a specific role. Good practices observed:

  • Proper error handling and wrapping
  • Comprehensive logging at debug and info levels
  • Defensive coding with nil checks for member.User
  • Efficient pagination by tracking the last member ID

57-67: Clean implementation of mention formatting with error handling

The FormatUserMentions function correctly converts member objects to Discord mention strings while handling nil User cases appropriately.


79-92: User-friendly response formatting with clear messaging

The FormatUserListResponse function uses a clean switch statement to handle different user count scenarios, providing appropriate messaging for each case.

utils/membersUtils_test.go (4)

14-145: Comprehensive tests for user role retrieval

The tests for GetUsersWithRole cover all important scenarios:

  • Single and multiple users with matching roles
  • Error handling from the Discord API
  • Empty result sets
  • Handling invalid member data
  • Pagination across multiple API calls

The test cases are well-structured with clear test names and thorough assertions.


147-178: Thorough tests for mention formatting

The tests for FormatUserMentions properly test the formatting logic, including handling:

  • Normal cases with valid users
  • Empty member lists
  • Nil member lists
  • Members with nil User fields

All edge cases are well-covered.


180-196: Good coverage of message formatting

The tests for FormatMentionResponse correctly verify both scenarios:

  • Formatting with a message prefix
  • Formatting with only mentions (empty message)

197-236: Complete tests for user list response formatting

The tests for FormatUserListResponse thoroughly cover all cases:

  • Zero users with the role
  • One user with the role
  • Multiple users with the role
  • Nil mentions list
  • Empty role ID

Each case has clear assertions to verify the correct output format.

service/mentionEachService.go (1)

83-91: Potentially long interaction response may exceed Discord’s 2 000-character limit

Although the response here only contains the role mention, later edits or localisation might accidentally push it over the limit.
Consider guarding with len(responseContent) <= 2000 or splitting the message.

commands/handlers/mentionEachHandler_test.go (1)

258-272: Test path may never hit handleStandardMode

fetchMembersWithRoleFunc returns zero members (l. 262-264).
In production code the handler short-circuits to sendNoMembersMessageFunc when the slice is empty.
Yet this case stubs handleStandardModeFunc to return mockErr (l. 265-267) and asserts that the error bubbles up.

Unless the handler purposefully calls handleStandardMode with an empty slice, the test is asserting an impossible path and will start failing once the real logic is corrected.

Please verify the intended behaviour and either:

  • return a non-empty slice here, or
  • assert on sendNoMembersMessageFunc instead.

Comment on lines +69 to +77
func FormatMentionResponse(mentions []string, message string) string {
mentionStrings := strings.Join(mentions, " ")
if message == "" {
return mentionStrings
}

return fmt.Sprintf("%s %s", message, mentionStrings)

}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Message formatting logic is clean and handles edge cases

The FormatMentionResponse function correctly handles both cases with and without a custom message.

Consider removing the unnecessary blank line on line 76 for cleaner code.

 	return fmt.Sprintf("%s %s", message, mentionStrings)
-
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func FormatMentionResponse(mentions []string, message string) string {
mentionStrings := strings.Join(mentions, " ")
if message == "" {
return mentionStrings
}
return fmt.Sprintf("%s %s", message, mentionStrings)
}
func FormatMentionResponse(mentions []string, message string) string {
mentionStrings := strings.Join(mentions, " ")
if message == "" {
return mentionStrings
}
return fmt.Sprintf("%s %s", message, mentionStrings)
}

Comment on lines +27 to +38
func (m *DiscordSession) ApplicationCommandCreate(appID, guildID string, cmd *discordgo.ApplicationCommand) (*discordgo.ApplicationCommand, error) {
args := m.Called(appID, guildID, cmd)
var retCmd *discordgo.ApplicationCommand
if args.Get(0) != nil {
var ok bool
retCmd, ok = args.Get(0).(*discordgo.ApplicationCommand)
if !ok {
panic(fmt.Sprintf("mock return value 0 for ApplicationCommandCreate is not *discordgo.ApplicationCommand: %T", args.Get(0)))
}
}
return retCmd, args.Error(1)
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Avoid panic by failing the test instead

panicing inside a mock unexpectedly aborts the entire test run and makes it harder to locate the failure.
Consider returning a descriptive error so that the caller’s require/assert captures it cleanly.

Comment on lines +12 to +15
type DiscordSession struct {
mock.Mock
}

Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Add a compile-time assertion to guarantee the mock stays in sync with the interface

Right now we only hope that DiscordSession fulfils models.SessionInterface.
A build-time check will fail fast whenever the interface is extended and the mock is not updated – this prevents subtle test breakages later on.

 type DiscordSession struct {
 	mock.Mock
 }
+
+// Ensure the mock implements all methods from models.SessionInterface
+var _ models.SessionInterface = (*DiscordSession)(nil)

You will have to import the models package at the top of the file.

Comment on lines +67 to +70
messageOption := findOption(s.discordMessage.Data.Options, "message")
if messageOption != nil && messageOption.Value != nil {
message, _ = messageOption.Value.(string)
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Nil-safe string cast for the optional message field

messageOption.Value.(string) will silently yield an empty string and a false flag when the value is not a string.
If a non-string sneaks through, we silently ignore the problem. Prefer validating the type and logging a warning, similar to how ff_enabled is handled.

Comment on lines +52 to +58
roleID, ok := roleOption.Value.(string)
if !ok {
errorMsg := "Invalid role format(not a String)"
logrus.Errorf("%s: Expected string, got %T", errorMsg, roleOption.Value)
sendErrorResponse(response, errorMsg)
return
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Shadowed ok variable hides earlier declaration

The ok declared here shadows the one defined on line 27, which can be confusing during debugging.

-	roleID, ok := roleOption.Value.(string)
+	roleID, ok := roleOption.Value.(string)

If you need a fresh boolean, give it a more expressive name (e.g. isString). Otherwise, reuse the existing variable.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +55 to +61
userMessage := metaData["message"]
if len(userMessage) > MaxUserMessageLength {
truncatedSuffix := "..."
logrus.Warnf("User provided message length (%d) exceeds limit (%d). Truncating.", len(userMessage), MaxUserMessageLength)
userMessage = userMessage[:MaxUserMessageLength] + truncatedSuffix
}
params.Message = userMessage
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Truncation logic can overflow the max length

userMessage[:MaxUserMessageLength] + "..." returns 1003 characters, not 1000.
Trim the suffix before appending to stay within Discord’s limit.

-	userMessage = userMessage[:MaxUserMessageLength] + truncatedSuffix
+	userMessage = userMessage[:MaxUserMessageLength-len(truncatedSuffix)] + truncatedSuffix
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
userMessage := metaData["message"]
if len(userMessage) > MaxUserMessageLength {
truncatedSuffix := "..."
logrus.Warnf("User provided message length (%d) exceeds limit (%d). Truncating.", len(userMessage), MaxUserMessageLength)
userMessage = userMessage[:MaxUserMessageLength] + truncatedSuffix
}
params.Message = userMessage
userMessage := metaData["message"]
if len(userMessage) > MaxUserMessageLength {
truncatedSuffix := "..."
logrus.Warnf("User provided message length (%d) exceeds limit (%d). Truncating.", len(userMessage), MaxUserMessageLength)
userMessage = userMessage[:MaxUserMessageLength-len(truncatedSuffix)] + truncatedSuffix
}
params.Message = userMessage

Comment on lines +346 to +387
func TestExtractCommandParamsLogic(t *testing.T) {

t.Run("Valid parameters all present", func(t *testing.T) {
metaData := map[string]string{
"role_id": "role1", "channel_id": "chan1", "guild_id": "guild1",
"message": "Hello", "dev": "true", "dev_title": "false",
}
params, err := extractCommandParamsFunc(metaData)
assert.NoError(t, err)
assert.Equal(t, "role1", params.RoleID)
assert.Equal(t, "chan1", params.ChannelID)
assert.Equal(t, "guild1", params.GuildID)
assert.Equal(t, "Hello", params.Message)
assert.True(t, params.Dev)
assert.False(t, params.DevTitle)
})

t.Run("Valid parameters optional missing", func(t *testing.T) {
metaData := map[string]string{
"role_id": "role1", "channel_id": "chan1", "guild_id": "guild1",
}
params, err := extractCommandParamsFunc(metaData)
assert.NoError(t, err)
assert.Equal(t, "role1", params.RoleID)
assert.Equal(t, "", params.Message)
assert.False(t, params.Dev)
assert.False(t, params.DevTitle)
})

t.Run("Valid parameters with long message (truncates)", func(t *testing.T) {
longMessage := strings.Repeat("a", MaxUserMessageLength+50)
truncatedSuffix := "..."
expectedMessage := strings.Repeat("a", MaxUserMessageLength) + truncatedSuffix
metaData := map[string]string{
"role_id": "role1", "channel_id": "chan1", "guild_id": "guild1", "message": longMessage,
}
params, err := extractCommandParamsFunc(metaData)
assert.NoError(t, err)
assert.Equal(t, expectedMessage, params.Message, "Message should be truncated")
assert.Len(t, params.Message, MaxUserMessageLength+len(truncatedSuffix))
})

Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

High duplication – table-driven tests would improve maintainability

TestExtractCommandParamsLogic manually repeats the same arrange-act-assert pattern for many permutations. A table-driven style can shrink ~120 LOC to ~30-40, making it easier to add new cases:

tests := []struct{
   name string
   meta map[string]string
   want CommandParams
   wantErr bool
}{ ... }

for _, tc := range tests {
   t.Run(tc.name, func(t *testing.T) {
        got, err := extractCommandParamsFunc(tc.meta)
        if tc.wantErr { assert.Error(t, err); return }
        assert.Equal(t, tc.want, got)
   })
}

Helps readability and reduces the risk of inconsistent setups.

Comment on lines +569 to +577
t.Run("Success with batching", func(t *testing.T) {
mockSession := new(mocks.DiscordSession)
for i := 0; i < 6; i++ {
expectedMsg := fmt.Sprintf("%s %s", params.Message, mentions[i])
mockSession.On("ChannelMessageSend", params.ChannelID, expectedMsg).Return(&discordgo.Message{}, nil).Once()
}

err := handleDevModeFunc(mockSession, mentions, params)

Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Potentially slow tests due to real time.Sleep in batching logic

handleDevModeFunc is invoked directly; if the production implementation sleeps between batches (e.g., time.Sleep(batchDelay)), these unit tests will incur that delay and slow down the test suite.

Consider injecting a sleepFn (defaulting to time.Sleep) into the handler and stub it with a no-op in tests:

-func handleDevMode(...) error {
-    ...
-    time.Sleep(batchDelay)
+func handleDevMode(..., sleep func(time.Duration)) error {
+    ...
+    sleep(batchDelay)
 }

Then pass func(_ time.Duration){} from tests.
This keeps production behaviour unchanged while making tests fast and deterministic.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +307 to +339
t.Run("Success Path with session Close Error", func(t *testing.T) {
extractCommandParamsFunc = func(map[string]string) (CommandParams, error) {
return CommandParams{RoleID: "testRole", ChannelID: "testChannel", GuildID: "testGuild", Message: "Hello", Dev: false, DevTitle: false}, nil
}

mockSessionInstance := new(mocks.DiscordSession)
closeErr := errors.New("failed t0 close session")
mockSessionInstance.On("Close").Return(closeErr).Once()

CreateSession = func() (*discordgo.Session, error) {
return &discordgo.Session{}, nil
}

fetchMembersWithRoleFunc = func(session models.SessionInterface, guildID, roleID, channelID string) ([]discordgo.Member, error) {
return membersList, nil
}

handleStandardModeFunc = func(session models.SessionInterface, mentions []string, params CommandParams) error {
return nil
}

handleDevModeFunc = func(session models.SessionInterface, mentions []string, params CommandParams) error {
return nil
}
handleDevTitleModeFunc = func(models.SessionInterface, []string, CommandParams) error {
t.Fatal("handleDevTitleMode called unexpectedly")
return nil
}
sendNoMembersMessageFunc = func(models.SessionInterface, string) error {
t.Fatal("sendNoMembersMessage called unexpectedly")
return nil
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Mock for Close() is never exercised – session leak assertion ineffective

You instantiate mockSessionInstance (l. 312-315) but CreateSession (l. 316-318) returns &discordgo.Session{}, not the mock.
Therefore:

  • session.Close() inside the handler is not routed to your mock, so the On("Close") expectation is never met.
  • The test passes even if Close() is never called.

Fix by returning the mock:

-mockSessionInstance := new(mocks.DiscordSession)
-...
-CreateSession = func() (*discordgo.Session, error) {
-    return &discordgo.Session{}, nil
-}
+mockSessionInstance := new(mocks.DiscordSession)
+CreateSession = func() (*discordgo.Session, error) {
+    return (*discordgo.Session)(mockSessionInstance) // ⚠️ adjust CreateSession signature to return the interface
+}

(or better: change CreateSession to return models.SessionInterface, eliminating the cast).

Without this, the test gives a false sense of coverage regarding resource cleanup.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +26 to +44
func TestMentionEachHandler(t *testing.T) {
originalCreateSession := CreateSession
originalExtractParams := extractCommandParamsFunc
originalFetchMembers := fetchMembersWithRoleFunc
originalSendNoMembers := sendNoMembersMessageFunc
originalHandleDevTitle := handleDevTitleModeFunc
originalHandleDev := handleDevModeFunc
originalHandleStandard := handleStandardModeFunc

defer func() {
CreateSession = originalCreateSession
extractCommandParamsFunc = originalExtractParams
fetchMembersWithRoleFunc = originalFetchMembers
sendNoMembersMessageFunc = originalSendNoMembers
handleDevTitleModeFunc = originalHandleDevTitle
handleDevModeFunc = originalHandleDev
handleStandardModeFunc = originalHandleStandard
}()

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Global monkey-patching makes the whole package non-parallel & brittle

All sub-tests change exported function variables (CreateSession, fetchMembersWithRoleFunc, …) that are shared across the package.
Because they are mutated in-place, these tests:

  1. Cannot safely run with t.Parallel() (inside this or other test files).
  2. May leak the last patched implementation to other packages’ tests executed after this one.

Prefer a safer pattern:

-func TestMentionEachHandler(t *testing.T) {
-   originalCreateSession := CreateSession
-   ...
-   defer func(){ ...restore... }()
+func TestMentionEachHandler(t *testing.T) {
+   t.Cleanup(func() {
+       // restore every patched fn so other tests see the real impl
+       CreateSession            = originalCreateSession
+       extractCommandParamsFunc = originalExtractParams
+       ...
+   })

…and wrap each t.Run in its own t.Cleanup or use local wrappers that return closures, so patches are isolated per case.
This keeps the tests deterministic and unlocks t.Parallel() when desired.

Committable suggestion skipped: line range outside the PR's diff.

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.

Migrate mention each command

1 participant