diff --git a/.claude/commands/review-testing.md b/.claude/commands/review-testing.md index 5490027..21457fe 100644 --- a/.claude/commands/review-testing.md +++ b/.claude/commands/review-testing.md @@ -7,6 +7,7 @@ Adopt the Testing Auditor Persona for McCore. McCore is a framework library with **Coverage Completeness** - For every new public method with non-trivial logic (>3 lines), is there a corresponding unit or integration test? - Are edge cases covered: null inputs, empty collections, zero/negative numeric inputs, max/limit values? +- For config-driven values (`ReloadableContent` subclasses), is the code path tested with a value of `0` and at the maximum? - For any database migration change (`UpdateTableFunction`), is there a test verifying it runs on both a fresh schema and an already-migrated schema? - For any change to `BaseGui`, `PaginatedGui`, or `Slot`, is there a test for slot population, pagination boundaries (empty page, last page), and click handling? - If a bug was fixed, is there a regression test? @@ -21,16 +22,17 @@ Adopt the Testing Auditor Persona for McCore. McCore is a framework library with - Is MockBukkit set up and torn down correctly (`MockBukkit.mock()` / `MockBukkit.unmock()`) — not leaked across tests? - Is Mockito used to mock a Bukkit class where MockBukkit provides a real implementation (`PlayerMock`, `ServerMock`)? Use the real implementation. - Is `MockBukkit.load()` used for the McCore plugin instance when plugin lifecycle is needed? +- Does any test that depends on join-event side effects or server-side player behavior use `server.addPlayer()` rather than constructing `PlayerMock` directly? **Bukkit-Dependent vs. Pure-Java Separation** -- Does any class mix pure logic with Bukkit API calls where only the pure logic is tested? Extract and unit-test the pure logic separately. -- Does any test spin up MockBukkit but call zero Bukkit APIs? It should be a plain JUnit test instead. +- Does any class mix pure logic with Bukkit API calls where only the pure logic is tested? Extract the pure logic into a testable helper and unit-test it separately. +- Does any test spin up MockBukkit but use neither MockBukkit server interaction nor any Bukkit APIs? In that case, a plain JUnit test would suffice — but this check only applies if truly neither is needed. **Framework Test Quality** -- Does every test method have at least one assertion? A test with no assertion cannot fail. +- Does every test method have at least one assertion (`assertEquals`, `assertNotNull`, `assertTrue`, `assertThrows`, etc.)? A test with no assertion cannot fail. - Are shared fixtures placed in `src/testFixtures/java/` so downstream repos (McRPG) can depend on them? -- Does every test method follow the `givenContext_whenAction_thenOutcome` naming convention? -- Does every test method carry a `@DisplayName` annotation with a human-readable sentence describing the scenario? +- Does every test method follow the `methodUnderTest_expectedOutcome_whenCondition` naming convention (e.g., `register_throwsIllegalArgument_whenManagerAlreadyRegistered`)? The `_whenCondition` suffix is optional when the context is obvious from the action and outcome alone. +- Does every test method carry a `@DisplayName` annotation with a human-readable sentence in Given/When/Then format (e.g., `@DisplayName("Given a registered manager, when registering again, then throws IllegalArgumentException")`)? ## Instructions diff --git a/.cursor/rules/persona-testing.mdc b/.cursor/rules/persona-testing.mdc index 639a7a3..681bc5e 100644 --- a/.cursor/rules/persona-testing.mdc +++ b/.cursor/rules/persona-testing.mdc @@ -17,6 +17,7 @@ You are a test engineer reviewing whether this McCore framework change is adequa **Coverage Completeness** - [ ] For every new public method with non-trivial logic (>3 lines), is there a corresponding unit or integration test? - [ ] Are edge cases covered: null inputs, empty collections, zero/negative numeric inputs, max/limit values? +- [ ] For config-driven values (`ReloadableContent` subclasses), is the code path tested with a value of `0` and at the maximum? - [ ] For any change to database migration logic (`UpdateTableFunction`), is there a test that verifies the migration runs successfully on both a fresh schema and an already-migrated schema? - [ ] For any change to `BaseGui`, `PaginatedGui`, or `Slot`, is there a test verifying slot population, pagination boundary behavior (empty page, last page), and click handling? - [ ] If a bug was fixed, is there a regression test? @@ -31,16 +32,17 @@ You are a test engineer reviewing whether this McCore framework change is adequa - [ ] Is MockBukkit set up and torn down correctly (`MockBukkit.mock()` / `MockBukkit.unmock()`) — not leaked across tests? - [ ] Is Mockito used to mock a Bukkit class where MockBukkit already provides a real implementation (e.g., `PlayerMock`, `ServerMock`)? Use the real MockBukkit implementation. - [ ] Is `MockBukkit.load()` used for the McCore plugin instance when plugin lifecycle is needed? +- [ ] Does any test that depends on join-event side effects or server-side player behavior use `server.addPlayer()` rather than constructing `PlayerMock` directly? **Bukkit-Dependent vs. Pure-Java Separation** -- [ ] Does any class mix pure logic (math, data transformation, string processing) with Bukkit API calls, with only the pure logic tested? Extract and unit-test the pure logic separately. -- [ ] Does any test spin up MockBukkit but call zero Bukkit APIs? It should be a plain JUnit test instead to reduce overhead. +- [ ] Does any class mix pure logic (math, data transformation, string processing) with Bukkit API calls, with only the pure logic tested? Extract the pure logic into a testable helper and unit-test it separately. +- [ ] Does any test spin up MockBukkit but use neither MockBukkit server interaction nor any Bukkit APIs? In that case, a plain JUnit test would suffice — but this check only applies if truly neither is needed. **Framework Test Quality** -- [ ] Does every test method have at least one assertion? A test with no assertion cannot fail. +- [ ] Does every test method have at least one assertion (`assertEquals`, `assertNotNull`, `assertTrue`, `assertThrows`, etc.)? A test with no assertion cannot fail. - [ ] Are shared fixtures or plugin setup helpers placed in `src/testFixtures/java/` so downstream repos (`McRPG`) can depend on them without duplicating setup? -- [ ] Does every test method follow the `givenContext_whenAction_thenOutcome` naming convention (e.g., `givenEmptyPage_whenGetSlots_thenReturnsEmpty`)? -- [ ] Does every test method carry a `@DisplayName` annotation with a human-readable sentence describing the scenario? +- [ ] Does every test method follow the `methodUnderTest_expectedOutcome_whenCondition` naming convention (e.g., `register_throwsIllegalArgument_whenManagerAlreadyRegistered`)? The `_whenCondition` suffix is optional when the context is obvious from the action and outcome alone. +- [ ] Does every test method carry a `@DisplayName` annotation with a human-readable sentence in Given/When/Then format (e.g., `@DisplayName("Given a registered manager, when registering again, then throws IllegalArgumentException")`)? ## Output Format diff --git a/src/test/java/com/diamonddagger590/mccore/database/driver/DatabaseDriverTypeTest.java b/src/test/java/com/diamonddagger590/mccore/database/driver/DatabaseDriverTypeTest.java new file mode 100644 index 0000000..cb330eb --- /dev/null +++ b/src/test/java/com/diamonddagger590/mccore/database/driver/DatabaseDriverTypeTest.java @@ -0,0 +1,64 @@ +package com.diamonddagger590.mccore.database.driver; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DatabaseDriverTypeTest { + + @Test + @DisplayName("Given SQLITE driver type, when getting driver name, then returns sqlite") + void getDriverName_returnsSqlite_forSqliteType() { + assertEquals("sqlite", DatabaseDriverType.SQLITE.getDriverName()); + } + + @Test + @DisplayName("Given valid driver name string, when calling fromString, then returns matching type") + void fromString_returnsMatchingType_whenValidNameProvided() { + Optional result = DatabaseDriverType.fromString("sqlite"); + assertTrue(result.isPresent()); + assertEquals(DatabaseDriverType.SQLITE, result.get()); + } + + @Test + @DisplayName("Given valid driver name with different case, when calling fromString, then returns matching type") + void fromString_returnsMatchingType_whenCaseIsDifferent() { + Optional result = DatabaseDriverType.fromString("SQLITE"); + assertTrue(result.isPresent()); + assertEquals(DatabaseDriverType.SQLITE, result.get()); + } + + @Test + @DisplayName("Given mixed case driver name, when calling fromString, then returns matching type") + void fromString_returnsMatchingType_whenMixedCase() { + Optional result = DatabaseDriverType.fromString("SqLiTe"); + assertTrue(result.isPresent()); + assertEquals(DatabaseDriverType.SQLITE, result.get()); + } + + @Test + @DisplayName("Given invalid driver name, when calling fromString, then returns empty optional") + void fromString_returnsEmpty_whenInvalidNameProvided() { + Optional result = DatabaseDriverType.fromString("mysql"); + assertFalse(result.isPresent()); + } + + @Test + @DisplayName("Given empty string, when calling fromString, then returns empty optional") + void fromString_returnsEmpty_whenEmptyStringProvided() { + Optional result = DatabaseDriverType.fromString(""); + assertFalse(result.isPresent()); + } + + @Test + @DisplayName("Given values enumeration, when checking, then contains SQLITE") + void values_containsSqlite() { + assertTrue(Arrays.asList(DatabaseDriverType.values()).contains(DatabaseDriverType.SQLITE)); + } +} diff --git a/src/test/java/com/diamonddagger590/mccore/database/driver/DriverRegistryTest.java b/src/test/java/com/diamonddagger590/mccore/database/driver/DriverRegistryTest.java new file mode 100644 index 0000000..fba35ca --- /dev/null +++ b/src/test/java/com/diamonddagger590/mccore/database/driver/DriverRegistryTest.java @@ -0,0 +1,138 @@ +package com.diamonddagger590.mccore.database.driver; + +import com.diamonddagger590.mccore.database.Credentials; +import com.diamonddagger590.mccore.pair.Pair; +import com.zaxxer.hikari.HikariDataSource; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DriverRegistryTest { + + private static class TestDriver implements DatabaseDriver { + + private final boolean driverAvailable; + + TestDriver(boolean driverAvailable) { + this.driverAvailable = driverAvailable; + } + + @NotNull + @Override + public String getDatabaseDriverClass() { + return "org.sqlite.JDBC"; + } + + @NotNull + @Override + public String getConnectionUrl(@NotNull Credentials credentials) { + return "jdbc:sqlite:test.db"; + } + + @Override + public void populateDataSourceCredentials(@NotNull HikariDataSource dataSource, @NotNull Credentials credentials) { + } + + @NotNull + @Override + public DatabaseDriverType getDriverType() { + return DatabaseDriverType.SQLITE; + } + + @Override + public boolean tryDriver() { + return driverAvailable; + } + + @NotNull + @Override + public List> getDataSourceProperties() { + return List.of(); + } + } + + private DriverRegistry registry; + + @BeforeEach + void setUp() { + registry = new DriverRegistry(); + } + + @Test + @DisplayName("Given a valid driver, when registering, then registration succeeds") + void register_succeeds_whenDriverIsValid() { + TestDriver driver = new TestDriver(true); + registry.register(driver); + assertTrue(registry.registered(driver)); + } + + @Test + @DisplayName("Given a driver whose tryDriver fails, when registering, then throws RuntimeException") + void register_throwsRuntimeException_whenDriverClassMissing() { + TestDriver driver = new TestDriver(false); + assertThrows(RuntimeException.class, () -> registry.register(driver)); + } + + @Test + @DisplayName("Given a registered driver, when checking registered by instance, then returns true") + void registered_returnsTrue_whenDriverIsRegistered() { + TestDriver driver = new TestDriver(true); + registry.register(driver); + assertTrue(registry.registered(driver)); + } + + @Test + @DisplayName("Given no registered drivers, when checking registered by instance, then returns false") + void registered_returnsFalse_whenDriverIsNotRegistered() { + assertFalse(registry.registered(new TestDriver(true))); + } + + @Test + @DisplayName("Given a registered driver, when checking by driver type, then returns true") + void isDriverRegistered_returnsTrue_whenDriverTypeIsRegistered() { + registry.register(new TestDriver(true)); + assertTrue(registry.isDriverRegistered(DatabaseDriverType.SQLITE)); + } + + @Test + @DisplayName("Given no registered drivers, when checking by driver type, then returns false") + void isDriverRegistered_returnsFalse_whenDriverTypeIsNotRegistered() { + assertFalse(registry.isDriverRegistered(DatabaseDriverType.SQLITE)); + } + + @Test + @DisplayName("Given a registered driver, when getting by driver type, then returns Optional with driver") + void getDriver_returnsPresent_whenDriverTypeIsRegistered() { + TestDriver driver = new TestDriver(true); + registry.register(driver); + Optional result = registry.getDriver(DatabaseDriverType.SQLITE); + assertTrue(result.isPresent()); + assertEquals(driver, result.get()); + } + + @Test + @DisplayName("Given no registered drivers, when getting by driver type, then returns empty Optional") + void getDriver_returnsEmpty_whenDriverTypeIsNotRegistered() { + Optional result = registry.getDriver(DatabaseDriverType.SQLITE); + assertFalse(result.isPresent()); + } + + @Test + @DisplayName("Given a driver already registered for a type, when registering another driver for the same type, then overwrites the first") + void register_overwritesExistingDriver_whenSameDriverTypeRegisteredTwice() { + TestDriver first = new TestDriver(true); + TestDriver second = new TestDriver(true); + registry.register(first); + registry.register(second); + assertEquals(second, registry.getDriver(DatabaseDriverType.SQLITE).orElse(null)); + } +} diff --git a/src/test/java/com/diamonddagger590/mccore/parser/ParserTest.java b/src/test/java/com/diamonddagger590/mccore/parser/ParserTest.java index 5bd6507..a08fd05 100644 --- a/src/test/java/com/diamonddagger590/mccore/parser/ParserTest.java +++ b/src/test/java/com/diamonddagger590/mccore/parser/ParserTest.java @@ -360,7 +360,14 @@ void getValue_throwsParseError_whenOpenParenthesisUnmatched() { @Test @DisplayName("Given function without parenthesis, when evaluating, then throws ParseError") void getValue_throwsParseError_whenFunctionMissingParenthesis() { - assertThrows(ParseError.class, () -> new Parser("sin 5").getValue()); + assertThrows(ParseError.class, () -> new Parser("sin").getValue()); + } + + @Test + @DisplayName("Given function name with space-separated argument, when evaluating, then treats as variable due to space stripping") + void getValue_treatsAsVariable_whenFunctionNameSpaceSeparatedFromArgument() { + // "sin 5" has its space stripped → "sin5" → not a function name, treated as variable → default 0.0 + assertEquals(0.0, new Parser("sin 5").getValue(), 1e-10); } @Test diff --git a/src/test/java/com/diamonddagger590/mccore/registry/manager/ManagerRegistryTest.java b/src/test/java/com/diamonddagger590/mccore/registry/manager/ManagerRegistryTest.java new file mode 100644 index 0000000..1c78c43 --- /dev/null +++ b/src/test/java/com/diamonddagger590/mccore/registry/manager/ManagerRegistryTest.java @@ -0,0 +1,135 @@ +package com.diamonddagger590.mccore.registry.manager; + +import com.diamonddagger590.mccore.CorePlugin; +import com.diamonddagger590.mccore.registry.RegistryAccess; +import com.diamonddagger590.mccore.registry.RegistryKey; +import com.diamonddagger590.mccore.testing.RegistryResetExtension; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ManagerRegistryTest { + + private static class TestManagerA extends Manager { + TestManagerA() { + super(null); + } + } + + private static class TestManagerB extends Manager { + TestManagerB() { + super(null); + } + } + + private static class TestManagerASub extends TestManagerA { + } + + private static final ManagerKey KEY_A = ManagerKeyImpl.create(TestManagerA.class); + private static final ManagerKey KEY_B = ManagerKeyImpl.create(TestManagerB.class); + + @BeforeEach + void setUp() { + RegistryResetExtension.setupRegistry(); + } + + @AfterEach + void tearDown() { + RegistryResetExtension.resetRegistry(); + } + + private ManagerRegistry registry() { + return RegistryAccess.registryAccess().registry(RegistryKey.MANAGER); + } + + @Test + @DisplayName("Given a new manager, when registering, then registration succeeds") + void register_succeeds_whenManagerIsNew() { + TestManagerA manager = new TestManagerA(); + registry().register(manager); + assertTrue(registry().registered(manager)); + } + + @Test + @DisplayName("Given an already registered manager, when registering again, then throws IllegalArgumentException") + void register_throwsIllegalArgument_whenManagerAlreadyRegistered() { + TestManagerA manager = new TestManagerA(); + registry().register(manager); + assertThrows(IllegalArgumentException.class, () -> registry().register(new TestManagerA())); + } + + @Test + @DisplayName("Given a registered manager, when checking registered by instance, then returns true") + void registered_returnsTrue_whenManagerIsRegistered() { + TestManagerA manager = new TestManagerA(); + registry().register(manager); + assertTrue(registry().registered(manager)); + } + + @Test + @DisplayName("Given no registered managers, when checking registered by instance, then returns false") + void registered_returnsFalse_whenManagerIsNotRegistered() { + assertFalse(registry().registered(new TestManagerA())); + } + + @Test + @DisplayName("Given a registered manager, when checking registered by key, then returns true") + void registered_returnsTrue_whenCheckedByKey() { + registry().register(new TestManagerA()); + assertTrue(registry().registered(KEY_A)); + } + + @Test + @DisplayName("Given no registered managers, when checking registered by key, then returns false") + void registered_returnsFalse_whenCheckedByKeyAndNotRegistered() { + assertFalse(registry().registered(KEY_A)); + } + + @Test + @DisplayName("Given a registered manager, when retrieving by key, then returns the correct manager") + void manager_returnsCorrectInstance_whenRetrievedByKey() { + TestManagerA manager = new TestManagerA(); + registry().register(manager); + assertEquals(manager, registry().manager(KEY_A)); + } + + @Test + @DisplayName("Given multiple registered managers, when retrieving each by key, then returns correct instances") + void manager_returnsCorrectInstances_whenMultipleRegistered() { + TestManagerA managerA = new TestManagerA(); + TestManagerB managerB = new TestManagerB(); + registry().register(managerA); + registry().register(managerB); + assertEquals(managerA, registry().manager(KEY_A)); + assertEquals(managerB, registry().manager(KEY_B)); + } + + @Test + @DisplayName("Given a subclass manager registered, when retrieving by parent key, then returns subclass instance") + void manager_returnsSubclassInstance_whenRetrievedByParentKey() { + TestManagerASub subManager = new TestManagerASub(); + registry().register(subManager); + assertEquals(subManager, registry().manager(KEY_A)); + } + + @Test + @DisplayName("Given a subclass manager registered, when checking registered by parent key, then returns true") + void registered_returnsTrue_whenSubclassRegisteredAndCheckedByParentKey() { + TestManagerASub subManager = new TestManagerASub(); + registry().register(subManager); + assertTrue(registry().registered(KEY_A)); + } + + @Test + @DisplayName("Given no manager registered for key, when retrieving by key, then returns null") + void manager_returnsNull_whenNoManagerRegisteredForKey() { + assertNull(registry().manager(KEY_A)); + } +} diff --git a/src/test/java/com/diamonddagger590/mccore/registry/plugin/PluginHookRegistryTest.java b/src/test/java/com/diamonddagger590/mccore/registry/plugin/PluginHookRegistryTest.java new file mode 100644 index 0000000..6acc72e --- /dev/null +++ b/src/test/java/com/diamonddagger590/mccore/registry/plugin/PluginHookRegistryTest.java @@ -0,0 +1,157 @@ +package com.diamonddagger590.mccore.registry.plugin; + +import com.diamonddagger590.mccore.CorePlugin; +import com.diamonddagger590.mccore.registry.RegistryAccess; +import com.diamonddagger590.mccore.registry.RegistryKey; +import com.diamonddagger590.mccore.testing.RegistryResetExtension; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PluginHookRegistryTest { + + private interface MarkerHookInterface {} + + private static class TestHookA extends PluginHook { + TestHookA() { + super(null); + } + } + + private static class TestHookB extends PluginHook implements MarkerHookInterface { + TestHookB() { + super(null); + } + } + + private static class TestHookC extends PluginHook implements MarkerHookInterface { + TestHookC() { + super(null); + } + } + + private static final PluginHookKey KEY_A = PluginHookKeyImpl.create(TestHookA.class); + private static final PluginHookKey KEY_B = PluginHookKeyImpl.create(TestHookB.class); + private static final PluginHookKey KEY_C = PluginHookKeyImpl.create(TestHookC.class); + + @BeforeEach + void setUp() { + RegistryResetExtension.setupRegistry(); + } + + @AfterEach + void tearDown() { + RegistryResetExtension.resetRegistry(); + } + + private PluginHookRegistry registry() { + return RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK); + } + + @Test + @DisplayName("Given a new plugin hook, when registering, then registration succeeds") + void register_succeeds_whenHookIsNew() { + TestHookA hook = new TestHookA(); + registry().register(hook); + assertTrue(registry().registered(hook)); + } + + @Test + @DisplayName("Given an already registered plugin hook, when registering again, then throws IllegalArgumentException") + void register_throwsIllegalArgument_whenHookAlreadyRegistered() { + registry().register(new TestHookA()); + assertThrows(IllegalArgumentException.class, () -> registry().register(new TestHookA())); + } + + @Test + @DisplayName("Given a registered hook, when checking registered, then returns true") + void registered_returnsTrue_whenHookIsRegistered() { + TestHookA hook = new TestHookA(); + registry().register(hook); + assertTrue(registry().registered(hook)); + } + + @Test + @DisplayName("Given no registered hooks, when checking registered, then returns false") + void registered_returnsFalse_whenHookIsNotRegistered() { + assertFalse(registry().registered(new TestHookA())); + } + + @Test + @DisplayName("Given a registered hook, when retrieving by key, then returns Optional containing the hook") + void pluginHook_returnsPresent_whenHookIsRegistered() { + TestHookA hook = new TestHookA(); + registry().register(hook); + Optional result = registry().pluginHook(KEY_A); + assertTrue(result.isPresent()); + assertEquals(hook, result.get()); + } + + @Test + @DisplayName("Given no registered hook for key, when retrieving by key, then returns empty Optional") + void pluginHook_returnsEmpty_whenHookIsNotRegistered() { + Optional result = registry().pluginHook(KEY_A); + assertFalse(result.isPresent()); + } + + @Test + @DisplayName("Given multiple hooks implementing same interface, when querying by interface, then returns all matching") + void pluginHooks_returnsAllMatching_whenFilteredByInterface() { + TestHookB hookB = new TestHookB(); + TestHookC hookC = new TestHookC(); + registry().register(hookB); + registry().register(hookC); + + List hooks = registry().pluginHooks(MarkerHookInterface.class); + assertEquals(2, hooks.size()); + assertTrue(hooks.contains(hookB)); + assertTrue(hooks.contains(hookC)); + } + + @Test + @DisplayName("Given hooks that don't implement the interface, when querying by interface, then returns empty list") + void pluginHooks_returnsEmptyList_whenNoHooksMatchInterface() { + registry().register(new TestHookA()); + List hooks = registry().pluginHooks(MarkerHookInterface.class); + assertTrue(hooks.isEmpty()); + } + + @Test + @DisplayName("Given no registered hooks, when querying by interface, then returns empty list") + void pluginHooks_returnsEmptyList_whenNoHooksRegistered() { + List hooks = registry().pluginHooks(MarkerHookInterface.class); + assertTrue(hooks.isEmpty()); + } + + @Test + @DisplayName("Given mixed hooks, when querying by interface, then only returns matching hooks") + void pluginHooks_returnsOnlyMatching_whenMixedHooksRegistered() { + registry().register(new TestHookA()); + TestHookB hookB = new TestHookB(); + registry().register(hookB); + + List hooks = registry().pluginHooks(MarkerHookInterface.class); + assertEquals(1, hooks.size()); + assertEquals(hookB, hooks.get(0)); + } + + @Test + @DisplayName("Given multiple registered hooks, when retrieving each by key, then returns correct instances") + void pluginHook_returnsCorrectInstances_whenMultipleRegistered() { + TestHookA hookA = new TestHookA(); + TestHookB hookB = new TestHookB(); + registry().register(hookA); + registry().register(hookB); + assertEquals(hookA, registry().pluginHook(KEY_A).orElse(null)); + assertEquals(hookB, registry().pluginHook(KEY_B).orElse(null)); + } +} diff --git a/src/test/java/com/diamonddagger590/mccore/util/TimeProviderTest.java b/src/test/java/com/diamonddagger590/mccore/util/TimeProviderTest.java new file mode 100644 index 0000000..c77bf08 --- /dev/null +++ b/src/test/java/com/diamonddagger590/mccore/util/TimeProviderTest.java @@ -0,0 +1,83 @@ +package com.diamonddagger590.mccore.util; + +import com.diamonddagger590.mccore.testing.RegistryResetExtension; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TimeProviderTest { + + private static final Instant FIXED_INSTANT = Instant.parse("2025-06-15T12:30:00Z"); + private static final ZoneId UTC = ZoneOffset.UTC; + + @BeforeEach + void setUp() { + RegistryResetExtension.setupRegistry(); + } + + @AfterEach + void tearDown() { + RegistryResetExtension.resetRegistry(); + } + + private TimeProvider createFixedProvider() { + return new TimeProvider(Clock.fixed(FIXED_INSTANT, UTC)); + } + + @Test + @DisplayName("Given a fixed clock, when calling now, then returns the fixed instant") + void now_returnsFixedInstant_whenClockIsFixed() { + TimeProvider provider = createFixedProvider(); + assertEquals(FIXED_INSTANT, provider.now()); + } + + @Test + @DisplayName("Given a fixed clock, when calling now multiple times, then returns same instant") + void now_returnsSameInstant_whenCalledMultipleTimes() { + TimeProvider provider = createFixedProvider(); + assertEquals(provider.now(), provider.now()); + } + + @Test + @DisplayName("Given a fixed clock at UTC, when calling nowLocal with UTC, then returns correct local datetime") + void nowLocal_returnsCorrectDateTime_whenZonedToUtc() { + TimeProvider provider = createFixedProvider(); + LocalDateTime expected = LocalDateTime.of(2025, 6, 15, 12, 30, 0); + assertEquals(expected, provider.nowLocal(UTC)); + } + + @Test + @DisplayName("Given a fixed clock at UTC, when calling nowLocal with offset zone, then returns zone-adjusted datetime") + void nowLocal_returnsAdjustedDateTime_whenZonedToOffset() { + TimeProvider provider = createFixedProvider(); + ZoneId plus5 = ZoneOffset.ofHours(5); + LocalDateTime expected = LocalDateTime.of(2025, 6, 15, 17, 30, 0); + assertEquals(expected, provider.nowLocal(plus5)); + } + + @Test + @DisplayName("Given a fixed clock at UTC, when calling nowLocal with negative offset, then returns earlier datetime") + void nowLocal_returnsEarlierDateTime_whenZonedToNegativeOffset() { + TimeProvider provider = createFixedProvider(); + ZoneId minus3 = ZoneOffset.ofHours(-3); + LocalDateTime expected = LocalDateTime.of(2025, 6, 15, 9, 30, 0); + assertEquals(expected, provider.nowLocal(minus3)); + } + + @Test + @DisplayName("Given a time provider, when calling clock, then returns the underlying clock") + void clock_returnsUnderlyingClock_whenCalled() { + Clock fixedClock = Clock.fixed(FIXED_INSTANT, UTC); + TimeProvider provider = new TimeProvider(fixedClock); + assertEquals(fixedClock, provider.clock()); + } +} diff --git a/src/test/java/com/diamonddagger590/mccore/util/comparator/ChainComparatorTest.java b/src/test/java/com/diamonddagger590/mccore/util/comparator/ChainComparatorTest.java index 4d658ff..197aaef 100644 --- a/src/test/java/com/diamonddagger590/mccore/util/comparator/ChainComparatorTest.java +++ b/src/test/java/com/diamonddagger590/mccore/util/comparator/ChainComparatorTest.java @@ -15,7 +15,7 @@ class ChainComparatorTest { @Test @DisplayName("Given a single comparator, when comparing, then delegates to that comparator") void compare_delegatesToSingleComparator_whenOnlyOneProvided() { - ChainComparator chain = new ChainComparator<>(Comparator.naturalOrder()); + ChainComparator chain = new ChainComparator<>(Comparator.naturalOrder()); assertTrue(chain.compare(1, 2) < 0); assertTrue(chain.compare(2, 1) > 0); assertEquals(0, chain.compare(5, 5)); @@ -91,7 +91,7 @@ record Person(String first, String last, int age) {} @Test @DisplayName("Given a reverse-order comparator in the chain, when comparing, then orders descending") void compare_ordersDescending_whenReverseComparatorUsed() { - ChainComparator chain = new ChainComparator<>(Comparator.reverseOrder()); + ChainComparator chain = new ChainComparator<>(Comparator.reverseOrder()); assertTrue(chain.compare(1, 2) > 0); assertTrue(chain.compare(2, 1) < 0);