Skip to content

[Unit Tests] Expand board DAO and ExpiredQuestScanTask test coverage - #347

Open
DiamondDagger590 wants to merge 2 commits into
recodefrom
claude/magical-cray-doe4w1
Open

[Unit Tests] Expand board DAO and ExpiredQuestScanTask test coverage#347
DiamondDagger590 wants to merge 2 commits into
recodefrom
claude/magical-cray-doe4w1

Conversation

@DiamondDagger590

@DiamondDagger590 DiamondDagger590 commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Summary

  • BoardCooldownDAOTest: Added 10 new tests covering listCooldowns (empty/populated results), deleteCooldowns (with/without category key filter, zero rows), isOnCooldown with questDefinitionKey filter, categoryKey filter, and both filters simultaneously, plus CooldownRecord record accessors and equality. Restructured existing tests into @Nested groups.
  • PlayerBoardStateDAOTest: Added 11 new tests covering deleteForPlayer (with/without rows), loadAcceptedForPlayer (empty, with quest UUID, null quest UUID, multiple entries), updateStateByQuestInstanceUUID (match/no match), bulkCancelExpiredBoardStates (with/without expired states), plus AcceptedBoardEntry record accessors and equality. Restructured existing tests into @Nested groups.
  • ExpiredQuestScanTaskTest: Restructured into @Nested grouping, consolidated standard cases into @ParameterizedTest with @CsvSource, added edge cases for sub-minute truncation, minute/hour boundaries, and 24+ hour non-wrapping behavior.

Test plan

  • All new tests pass (./gradlew test — BUILD SUCCESSFUL)
  • No compilation warnings
  • Full test suite passes with zero failures
  • Follows project naming conventions (action_outcome_whenCondition, descriptive @DisplayName)
  • DAO tests use mock JDBC pattern (no real database connections)

Generated by Claude Code

Summary by CodeRabbit

  • Tests
    • Expanded automated coverage for board cooldown and player board state operations, including empty results, nullable values, deletion behavior, and record comparisons.
    • Improved test organization and naming for clearer, more maintainable validation.
    • Added broader quest expiration time-formatting coverage, including boundary durations, negative values, very large durations, and sub-minute values.

- BoardCooldownDAOTest: add tests for listCooldowns, deleteCooldowns,
  isOnCooldown with key filters, CooldownRecord accessors/equality
- PlayerBoardStateDAOTest: add tests for deleteForPlayer,
  loadAcceptedForPlayer (with/without quest UUID, multiple entries),
  updateStateByQuestInstanceUUID, bulkCancelExpiredBoardStates,
  AcceptedBoardEntry accessors/equality
- ExpiredQuestScanTaskTest: restructure with @nested grouping,
  consolidate cases into @ParameterizedTest with @CsvSource,
  add minute/hour boundary and 24+ hour edge cases
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown

Claude finished @DiamondDagger590's task in 5m 16s —— View job


Review: 1 important, 2 nits

Test-only PR (703 additions / 195 deletions, no src/main/** changes) — expands mock-based JDBC coverage for BoardCooldownDAO, PlayerBoardStateDAO, and adds boundary cases for ExpiredQuestScanTask.formatTimeRemaining. I manually cross-checked every new verify(...).setString/setNull(eq(N), ...) parameter-index assertion against the actual conditional SQL-building logic in the two DAOs (in particular isOnCooldown's index-shifting when questDefinitionKey/categoryKey are null, and deleteCooldowns'/saveState's conditional branches) — all of them line up correctly with no off-by-one errors. Only the review-testing lens applies here since no src/main code changed.

Important

  • BoardCooldownDAOTest.java (isOnCooldown, listCooldowns, pruneExpiredCooldowns test groups, lines ~79-190, 199-244, 304-313): No test verifies that McRPG.getInstance().getTimeProvider().now() is actually bound as the expires_at comparison (setLong at the relevant index) for any of these three methods — only the return value / other string params are asserted. A regression that drops the time bind, shifts its index, or swaps in wall-clock time instead of the injected TimeProvider would pass every existing test unnoticed, since the mocked ResultSet/executeUpdate don't depend on which parameters were set. The codebase already has the fix pattern in PlayerLoginTimeDAOTest.saveLoggedOutInSafeZone_bindsAllParameters_whenTrue — stub when(mcRPG.getTimeProvider().now()).thenReturn(fixedInstant) (via the inherited mcRPG field from McRPGBaseTest) and add verify(mockStatement).setLong(eq(N), eq(fixedInstant.toEpochMilli())) to at least one test per method.
    Fix this →
Nits
  • BoardCooldownDAOTest.java (isOnCooldown_bindsQuestDefinitionKey/bindsCategoryKey/bindsBothKeys, deleteCooldowns_filtersByCategoryKey_whenProvided): these stub prepareStatement(anyString()) and only check parameter index, never the actual SQL text produced by the conditional StringBuilder clauses. A bug that appends the wrong clause while still binding at the expected index would go undetected. BoardOfferingDAOTest already has a precedent for this (verify(mockConnection).prepareStatement(contains("scope_target_id IS NULL"))) — worth adding verify(mockConnection).prepareStatement(contains("quest_definition_key = ?")) etc. alongside the index checks.
  • ExpiredQuestScanTaskTest.java:52-57 (formatTimeRemaining_minuteBoundary): re-asserts 59_999ms -> "0h 0m", which is already covered by both the CSV-sourced formatTimeRemaining_standardDurations case and the immediately preceding formatTimeRemaining_subMinute_truncatesToZero test. No added coverage — could drop the duplicate assertion and keep only the 60_000 boundary check.

@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: c3506362-854a-4ba7-82ee-7a0fee52f346

📥 Commits

Reviewing files that changed from the base of the PR and between cc181c0 and d2aaab2.

📒 Files selected for processing (3)
  • src/test/java/us/eunoians/mcrpg/database/table/board/BoardCooldownDAOTest.java
  • src/test/java/us/eunoians/mcrpg/database/table/board/PlayerBoardStateDAOTest.java
  • src/test/java/us/eunoians/mcrpg/task/quest/ExpiredQuestScanTaskTest.java

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The pull request reorganizes three test suites with JUnit nested groups. It expands board cooldown DAO coverage and adds parameterized and boundary cases for expired quest time formatting.

Changes

Board cooldown DAO tests

Layer / File(s) Summary
Cooldown DAO operation coverage
src/test/java/us/eunoians/mcrpg/database/table/board/BoardCooldownDAOTest.java
Tests cover save, lookup, listing, deletion, pruning, nullable keys, parameter binding, result mapping, and CooldownRecord equality behavior.

Player board state DAO tests

Layer / File(s) Summary
Player board state test organization
src/test/java/us/eunoians/mcrpg/database/table/board/PlayerBoardStateDAOTest.java
Nested groups organize save, count, delete, load, update, bulk cancellation, and AcceptedBoardEntry tests. Display names and method names describe the tested outcomes.

Expired quest time formatting tests

Layer / File(s) Summary
Time formatting cases and boundaries
src/test/java/us/eunoians/mcrpg/task/quest/ExpiredQuestScanTaskTest.java
Parameterized tests cover standard durations and negative values. Additional tests cover minute and hour boundaries, sub-minute truncation, and durations above 24 hours.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to d2aaa

This PR expands and reorganizes unit tests without changing product behavior or runtime code, so no actionable merge-blocking risk remains beyond normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 39 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: expanded unit test coverage for the board DAOs and ExpiredQuestScanTask.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/magical-cray-doe4w1

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

Comment @coderabbitai help to get the list of available commands.

Comment on lines +73 to 191
@Nested
@DisplayName("isOnCooldown")
class IsOnCooldownTests {

@DisplayName("Returns false when no matching rows exist")
@Test
void isOnCooldown_returnsFalse_whenNoResults() throws SQLException {
Connection mockConnection = mock(Connection.class);
PreparedStatement mockStatement = mock(PreparedStatement.class);
ResultSet mockResultSet = mock(ResultSet.class);
when(mockConnection.prepareStatement(anyString())).thenReturn(mockStatement);
when(mockStatement.executeQuery()).thenReturn(mockResultSet);
when(mockResultSet.next()).thenReturn(false);

boolean result = BoardCooldownDAO.isOnCooldown(
mockConnection,
"rotation",
"player",
UUID.randomUUID().toString(),
null,
null
);

assertFalse(result);
}

@DisplayName("Returns true when a matching row exists")
@Test
void isOnCooldown_returnsTrue_whenPresent() throws SQLException {
Connection mockConnection = mock(Connection.class);
PreparedStatement mockStatement = mock(PreparedStatement.class);
ResultSet mockResultSet = mock(ResultSet.class);
when(mockConnection.prepareStatement(anyString())).thenReturn(mockStatement);
when(mockStatement.executeQuery()).thenReturn(mockResultSet);
when(mockResultSet.next()).thenReturn(true);

boolean result = BoardCooldownDAO.isOnCooldown(
mockConnection,
"rotation",
"player",
UUID.randomUUID().toString(),
null,
null
);

assertTrue(result);
}

@DisplayName("Binds questDefinitionKey parameter when provided")
@Test
void isOnCooldown_bindsQuestDefinitionKey() throws SQLException {
Connection mockConnection = mock(Connection.class);
PreparedStatement mockStatement = mock(PreparedStatement.class);
ResultSet mockResultSet = mock(ResultSet.class);
when(mockConnection.prepareStatement(anyString())).thenReturn(mockStatement);
when(mockStatement.executeQuery()).thenReturn(mockResultSet);
when(mockResultSet.next()).thenReturn(false);

NamespacedKey questKey = new NamespacedKey("mcrpg", "mine_stone");
BoardCooldownDAO.isOnCooldown(
mockConnection,
"quest_repeat",
"player",
UUID.randomUUID().toString(),
questKey,
null
);

verify(mockStatement).setString(eq(5), eq(questKey.toString()));
}

@DisplayName("Binds categoryKey parameter when provided")
@Test
void isOnCooldown_bindsCategoryKey() throws SQLException {
Connection mockConnection = mock(Connection.class);
PreparedStatement mockStatement = mock(PreparedStatement.class);
ResultSet mockResultSet = mock(ResultSet.class);
when(mockConnection.prepareStatement(anyString())).thenReturn(mockStatement);
when(mockStatement.executeQuery()).thenReturn(mockResultSet);
when(mockResultSet.next()).thenReturn(false);

NamespacedKey categoryKey = new NamespacedKey("mcrpg", "daily_personal");
BoardCooldownDAO.isOnCooldown(
mockConnection,
"category_rotation",
"player",
UUID.randomUUID().toString(),
null,
categoryKey
);

verify(mockStatement).setString(eq(5), eq(categoryKey.toString()));
}

@DisplayName("Binds both keys at correct indices when both provided")
@Test
void isOnCooldown_bindsBothKeys() throws SQLException {
Connection mockConnection = mock(Connection.class);
PreparedStatement mockStatement = mock(PreparedStatement.class);
ResultSet mockResultSet = mock(ResultSet.class);
when(mockConnection.prepareStatement(anyString())).thenReturn(mockStatement);
when(mockStatement.executeQuery()).thenReturn(mockResultSet);
when(mockResultSet.next()).thenReturn(false);

NamespacedKey questKey = new NamespacedKey("mcrpg", "mine_stone");
NamespacedKey categoryKey = new NamespacedKey("mcrpg", "daily_personal");
BoardCooldownDAO.isOnCooldown(
mockConnection,
"quest_repeat",
"player",
UUID.randomUUID().toString(),
questKey,
categoryKey
);

verify(mockStatement).setString(eq(5), eq(questKey.toString()));
verify(mockStatement).setString(eq(6), eq(categoryKey.toString()));
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important (testing): None of the isOnCooldown tests (nor listCooldowns/pruneExpiredCooldowns below) verify that McRPG.getInstance().getTimeProvider().now() is actually bound as the expires_at comparison parameter (setLong at the relevant index) — only the return value / other string params are asserted. A regression that drops the time bind, shifts its index, or swaps in wall-clock time instead of the injected TimeProvider would pass every existing test unnoticed.

The codebase already has the fix pattern in PlayerLoginTimeDAOTest.saveLoggedOutInSafeZone_bindsAllParameters_whenTrue: stub when(mcRPG.getTimeProvider().now()).thenReturn(fixedInstant) (via the inherited mcRPG field from McRPGBaseTest) and add verify(mockStatement).setLong(eq(N), eq(fixedInstant.toEpochMilli())) to at least one test per method.

Fix this →

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.

2 participants