diff --git a/src/test/java/creator/loki/feature/economy/service/BlackjackServiceTest.java b/src/test/java/creator/loki/feature/economy/service/BlackjackServiceTest.java new file mode 100644 index 0000000..0d8631c --- /dev/null +++ b/src/test/java/creator/loki/feature/economy/service/BlackjackServiceTest.java @@ -0,0 +1,128 @@ +package creator.loki.feature.economy.service; + +import creator.loki.feature.economy.domain.EconomyAccount; +import creator.loki.feature.economy.repository.EconomyRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class BlackjackServiceTest { + + @Mock + private EconomyRepository repository; + + @Captor + private ArgumentCaptor accountCaptor; + + private BlackjackService service; + + private final EconomyAccount account = new EconomyAccount.Builder() + .userId("user1").cash(1000).build(); + + @BeforeEach + void setUp() { + service = new BlackjackService(repository); + } + + @Nested + class StartGame { + + @Test + void returnsNullWhenBetIsZeroOrNegative() { + assertNull(service.startGame("user1", 0)); + assertNull(service.startGame("user1", -50)); + } + + @Test + void returnsNullWhenInsufficientCash() { + when(repository.getAccount("user1")).thenReturn(account); + + assertNull(service.startGame("user1", 9999)); + } + + @Test + void deductsBetFromCash() { + when(repository.getAccount("user1")).thenReturn(account); + + service.startGame("user1", 200); + + verify(repository).save(accountCaptor.capture()); + assertEquals(800, accountCaptor.getValue().getCash()); + } + } + + @Nested + class Hit { + + @Test + void returnsNullWhenNoActiveGame() { + assertNull(service.hit("user1")); + } + } + + @Nested + class Stand { + + @Test + void returnsNullWhenNoActiveGame() { + assertNull(service.stand("user1")); + } + } + + @Nested + class GameFlow { + + @Test + void startGameThenStandCompletesGame() { + when(repository.getAccount("user1")).thenReturn(account); + + BlackjackService.BlackjackState state = service.startGame("user1", 100); + assertNotNull(state); + + state = service.stand("user1"); + assertNotNull(state); + assertTrue(state.isOver()); + assertTrue(state.payout() >= 0); + } + + @Test + void playerHitsUntilBust() { + when(repository.getAccount("user1")).thenReturn(account); + + BlackjackService.BlackjackState state = service.startGame("user1", 100); + assertNotNull(state); + + int maxHits = 20; + while (!state.isOver() && maxHits-- > 0) { + state = service.hit("user1"); + if (state == null) break; + } + + assertNotNull(state); + assertTrue(state.isOver()); + if (state.playerSum() > 21) { + assertEquals(0, state.payout()); + } + } + + @Test + void startGameThenStandSavesPayout() { + when(repository.getAccount("user1")).thenReturn(account); + + service.startGame("user1", 100); + service.stand("user1"); + + verify(repository, atLeast(2)).save(any()); + } + } +} diff --git a/src/test/java/creator/loki/feature/economy/service/EconomyServiceTest.java b/src/test/java/creator/loki/feature/economy/service/EconomyServiceTest.java new file mode 100644 index 0000000..498b131 --- /dev/null +++ b/src/test/java/creator/loki/feature/economy/service/EconomyServiceTest.java @@ -0,0 +1,252 @@ +package creator.loki.feature.economy.service; + +import creator.loki.core.security.CooldownManager; +import creator.loki.feature.economy.domain.EconomyAccount; +import creator.loki.feature.economy.repository.EconomyRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class EconomyServiceTest { + + @Mock + private EconomyRepository repository; + + @Mock + private CooldownManager cooldownManager; + + @Captor + private ArgumentCaptor accountCaptor; + + private EconomyService service; + + private final EconomyAccount baseAccount = new EconomyAccount.Builder() + .userId("user1").cash(1000).bank(500).totalEarned(200).totalSpent(100) + .lastDaily(LocalDate.now().minusDays(2).toString()) + .dailyStreak(0).efficiency(0).fishingLuck(0) + .build(); + + private final EconomyAccount targetAccount = new EconomyAccount.Builder() + .userId("user2").cash(500).bank(0).build(); + + @BeforeEach + void setUp() { + service = new EconomyService(repository, cooldownManager); + } + + @Nested + class Transfer { + + @Test + void returnsFalseWhenAmountIsZeroOrNegative() { + assertFalse(service.transfer("user1", "user2", 0)); + assertFalse(service.transfer("user1", "user2", -1)); + } + + @Test + void returnsFalseWhenSenderHasInsufficientCash() { + when(repository.getAccount("user1")).thenReturn(baseAccount); + + assertFalse(service.transfer("user1", "user2", 9999)); + verify(repository, never()).save(any()); + } + + @Test + void returnsTrueAndTransfersCashCorrectly() { + when(repository.getAccount("user1")).thenReturn(baseAccount); + when(repository.getAccount("user2")).thenReturn(targetAccount); + + assertTrue(service.transfer("user1", "user2", 200)); + + verify(repository, times(2)).save(accountCaptor.capture()); + assertEquals(800, accountCaptor.getAllValues().get(0).getCash()); + assertEquals(700, accountCaptor.getAllValues().get(1).getCash()); + } + + @Test + void callsRepositorySaveTwice() { + when(repository.getAccount("user1")).thenReturn(baseAccount); + when(repository.getAccount("user2")).thenReturn(targetAccount); + + service.transfer("user1", "user2", 100); + + verify(repository, times(2)).save(any()); + } + } + + @Nested + class Deposit { + + @Test + void returnsFalseWhenAmountIsZeroOrNegative() { + assertFalse(service.deposit("user1", 0)); + assertFalse(service.deposit("user1", -5)); + } + + @Test + void returnsFalseWhenInsufficientCash() { + when(repository.getAccount("user1")).thenReturn(baseAccount); + + assertFalse(service.deposit("user1", 9999)); + } + + @Test + void returnsTrueAndMovesCashToBank() { + when(repository.getAccount("user1")).thenReturn(baseAccount); + + assertTrue(service.deposit("user1", 300)); + + verify(repository).save(accountCaptor.capture()); + EconomyAccount saved = accountCaptor.getValue(); + assertEquals(700, saved.getCash()); + assertEquals(800, saved.getBank()); + } + } + + @Nested + class Withdraw { + + @Test + void returnsFalseWhenAmountIsZeroOrNegative() { + assertFalse(service.withdraw("user1", 0)); + assertFalse(service.withdraw("user1", -10)); + } + + @Test + void returnsFalseWhenInsufficientBank() { + when(repository.getAccount("user1")).thenReturn(baseAccount); + + assertFalse(service.withdraw("user1", 9999)); + } + + @Test + void returnsTrueAndMovesBankToCash() { + when(repository.getAccount("user1")).thenReturn(baseAccount); + + assertTrue(service.withdraw("user1", 200)); + + verify(repository).save(accountCaptor.capture()); + EconomyAccount saved = accountCaptor.getValue(); + assertEquals(1200, saved.getCash()); + assertEquals(300, saved.getBank()); + } + } + + @Nested + class EarnCash { + + @Test + void addsToCashAndTotalEarned() { + when(repository.getAccount("user1")).thenReturn(baseAccount); + + long newCash = service.earnCash("user1", 150); + + assertEquals(1150, newCash); + verify(repository).save(accountCaptor.capture()); + EconomyAccount saved = accountCaptor.getValue(); + assertEquals(1150, saved.getCash()); + assertEquals(350, saved.getTotalEarned()); + } + } + + @Nested + class SpendCash { + + @Test + void returnsFalseWhenAmountIsZeroOrNegative() { + assertFalse(service.spendCash("user1", 0)); + assertFalse(service.spendCash("user1", -50)); + } + + @Test + void returnsFalseWhenInsufficientCash() { + when(repository.getAccount("user1")).thenReturn(baseAccount); + + assertFalse(service.spendCash("user1", 9999)); + } + + @Test + void returnsTrueAndSubtractsCashAndAddsToTotalSpent() { + when(repository.getAccount("user1")).thenReturn(baseAccount); + + assertTrue(service.spendCash("user1", 400)); + + verify(repository).save(accountCaptor.capture()); + EconomyAccount saved = accountCaptor.getValue(); + assertEquals(600, saved.getCash()); + assertEquals(500, saved.getTotalSpent()); + } + } + + @Nested + class ClaimDaily { + + @Test + void returnsNullWhenAlreadyClaimedToday() { + EconomyAccount todayClaimed = new EconomyAccount.Builder(baseAccount) + .lastDaily(LocalDate.now().toString()) + .build(); + when(repository.getAccount("user1")).thenReturn(todayClaimed); + + assertNull(service.claimDaily("user1")); + } + + @Test + void returnsDailyResultWithStreakOneForNewUser() { + EconomyAccount newUser = new EconomyAccount.Builder() + .userId("user1").cash(100).lastDaily(null).dailyStreak(0).build(); + when(repository.getAccount("user1")).thenReturn(newUser); + + EconomyService.DailyResult result = service.claimDaily("user1"); + + assertNotNull(result); + assertEquals(1, result.streak()); + assertEquals(50, result.amount()); + assertEquals(150, result.newCash()); + } + + @Test + void returnsDailyResultWithStreakTwoForConsecutiveDays() { + EconomyAccount returning = new EconomyAccount.Builder(baseAccount) + .cash(200).totalEarned(50) + .lastDaily(LocalDate.now().minusDays(1).toString()) + .dailyStreak(1).build(); + when(repository.getAccount("user1")).thenReturn(returning); + + EconomyService.DailyResult result = service.claimDaily("user1"); + + assertNotNull(result); + assertEquals(2, result.streak()); + assertEquals(50, result.amount()); + assertEquals(250, result.newCash()); + } + + @Test + void returnsDailyResultWithAmountSeventyFiveForStreakAtLeastSeven() { + EconomyAccount veteran = new EconomyAccount.Builder(baseAccount) + .cash(500).totalEarned(300) + .lastDaily(LocalDate.now().minusDays(1).toString()) + .dailyStreak(6).build(); + when(repository.getAccount("user1")).thenReturn(veteran); + + EconomyService.DailyResult result = service.claimDaily("user1"); + + assertNotNull(result); + assertEquals(7, result.streak()); + assertEquals(75, result.amount()); + assertEquals(575, result.newCash()); + } + } +} diff --git a/src/test/java/creator/loki/feature/economy/service/JobServiceTest.java b/src/test/java/creator/loki/feature/economy/service/JobServiceTest.java new file mode 100644 index 0000000..923951c --- /dev/null +++ b/src/test/java/creator/loki/feature/economy/service/JobServiceTest.java @@ -0,0 +1,184 @@ +package creator.loki.feature.economy.service; + +import creator.loki.feature.economy.domain.EconomyAccount; +import creator.loki.feature.economy.domain.Job; +import creator.loki.feature.economy.repository.EconomyRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class JobServiceTest { + + @Mock + private EconomyRepository repository; + + @Captor + private ArgumentCaptor accountCaptor; + + private JobService service; + + private final EconomyAccount account = new EconomyAccount.Builder() + .userId("user1").cash(100).totalEarned(0).efficiency(0) + .build(); + + @BeforeEach + void setUp() { + service = new JobService(repository); + } + + @Nested + class ListJobs { + + @Test + void returnsSixJobs() { + List jobs = service.listJobs(); + + assertEquals(6, jobs.size()); + } + + @Test + void containsWoodcutter() { + List jobs = service.listJobs(); + + JobService.JobInfo woodcutter = jobs.stream() + .filter(j -> j.name().equals("WOODCUTTER")) + .findFirst().orElseThrow(); + assertEquals("Лесоруб", woodcutter.displayName()); + assertEquals(0, woodcutter.requiredLevel()); + assertEquals(15, woodcutter.minPayout()); + assertEquals(30, woodcutter.maxPayout()); + } + } + + @Nested + class SetJob { + + @Test + void savesJobNameToAccount() { + when(repository.getAccount("user1")).thenReturn(account); + + service.setJob("user1", "WOODCUTTER"); + + verify(repository).save(accountCaptor.capture()); + assertEquals("WOODCUTTER", accountCaptor.getValue().getJob()); + } + + @Test + void doesNothingForInvalidJobName() { + service.setJob("user1", "INVALID"); + + verify(repository, never()).save(any()); + } + } + + @Nested + class GetCurrentJob { + + @Test + void returnsDisplayNameWhenJobIsSet() { + EconomyAccount withJob = new EconomyAccount.Builder(account) + .job("MINER").build(); + when(repository.getAccount("user1")).thenReturn(withJob); + + String display = service.getCurrentJob("user1"); + + assertEquals("Шахтёр", display); + } + + @Test + void returnsNullWhenNoJob() { + when(repository.getAccount("user1")).thenReturn(account); + + assertNull(service.getCurrentJob("user1")); + } + } + + @Nested + class Work { + + @Test + void returnsNullWhenNoJobSet() { + when(repository.getAccount("user1")).thenReturn(account); + + assertNull(service.work("user1", 0)); + } + + @Test + void returnsNullWhenOnCooldown() { + EconomyAccount withJob = new EconomyAccount.Builder(account) + .job("WOODCUTTER").lastWork(String.valueOf(System.currentTimeMillis())) + .build(); + when(repository.getAccount("user1")).thenReturn(withJob); + + assertNull(service.work("user1", 0)); + } + + @Test + void returnsWorkResultWithPayoutInRange() { + EconomyAccount withJob = new EconomyAccount.Builder(account) + .job("WOODCUTTER").lastWork("0") + .build(); + when(repository.getAccount("user1")).thenReturn(withJob); + + JobService.WorkResult result = service.work("user1", 0); + + assertNotNull(result); + assertTrue(result.amount() >= 15); + assertTrue(result.amount() <= 30); + assertTrue(result.newCash() >= 115); + assertTrue(result.newCash() <= 130); + } + + @Test + void multipliesPayoutByEfficiency() { + EconomyAccount efficient = new EconomyAccount.Builder(account) + .job("WOODCUTTER").lastWork("0").efficiency(100) + .build(); + when(repository.getAccount("user1")).thenReturn(efficient); + + JobService.WorkResult result = service.work("user1", 0); + + assertNotNull(result); + assertTrue(result.amount() >= 30); + assertTrue(result.amount() <= 60); + } + + @Test + void returnsNullWhenLevelTooLow() { + EconomyAccount withJob = new EconomyAccount.Builder(account) + .job("PROGRAMMER").lastWork("0") + .build(); + when(repository.getAccount("user1")).thenReturn(withJob); + + assertNull(service.work("user1", 10)); + } + + @Test + void savesUpdatedAccount() { + EconomyAccount withJob = new EconomyAccount.Builder(account) + .job("WOODCUTTER").lastWork("0") + .build(); + when(repository.getAccount("user1")).thenReturn(withJob); + + service.work("user1", 0); + + verify(repository).save(accountCaptor.capture()); + EconomyAccount saved = accountCaptor.getValue(); + assertNotNull(saved.getLastWork()); + assertTrue(saved.getCash() >= 115); + assertTrue(saved.getTotalEarned() >= 15); + } + } +} diff --git a/src/test/java/creator/loki/feature/economy/service/SlotServiceTest.java b/src/test/java/creator/loki/feature/economy/service/SlotServiceTest.java new file mode 100644 index 0000000..805c1d8 --- /dev/null +++ b/src/test/java/creator/loki/feature/economy/service/SlotServiceTest.java @@ -0,0 +1,132 @@ +package creator.loki.feature.economy.service; + +import creator.loki.feature.economy.domain.EconomyAccount; +import creator.loki.feature.economy.domain.SlotCombination; +import creator.loki.feature.economy.repository.EconomyRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class SlotServiceTest { + + @Mock + private EconomyRepository repository; + + @Captor + private ArgumentCaptor accountCaptor; + + private SlotService service; + + private final EconomyAccount account = new EconomyAccount.Builder() + .userId("user1").cash(500).build(); + + @BeforeEach + void setUp() { + service = new SlotService(repository); + } + + @Nested + class Spin { + + @Test + void returnsNullWhenBetIsZeroOrNegative() { + assertNull(service.spin("user1", 0)); + assertNull(service.spin("user1", -10)); + } + + @Test + void returnsNullWhenInsufficientCash() { + when(repository.getAccount("user1")).thenReturn(account); + + assertNull(service.spin("user1", 9999)); + } + + @Test + void deductsBetFromCash() { + when(repository.getAccount("user1")).thenReturn(account); + + service.spin("user1", 100); + + verify(repository, atLeastOnce()).save(accountCaptor.capture()); + assertEquals(400, accountCaptor.getAllValues().get(0).getCash()); + } + + @Test + void returnsNonNullResultWithValidEmojis() { + when(repository.getAccount("user1")).thenReturn(account); + + SlotService.SlotResult result = service.spin("user1", 100); + + assertNotNull(result); + assertNotNull(result.emoji1()); + assertNotNull(result.emoji2()); + assertNotNull(result.emoji3()); + assertTrue(result.cashAfter() >= 0); + } + + @Test + void winAmountIsZeroAndComboNameIsNullWhenNoMatch() { + when(repository.getAccount("user1")).thenReturn(account); + + SlotService.SlotResult result = service.spin("user1", 100); + + assertNotNull(result); + assertEquals(result.cashAfter(), account.getCash() - 100 + result.winAmount()); + } + } + + @Nested + class SlotCombinationMatch { + + @Test + void returnsNullForNonMatchingEmojis() { + assertNull(SlotCombination.match("\uD83C\uDF52", "\uD83D\uDD14", "\uD83D\uDC8E")); + assertNull(SlotCombination.match("\uD83C\uDF52", "\uD83C\uDF52", "\uD83D\uDD14")); + } + + @Test + void returnsCherryForThreeCherries() { + SlotCombination result = SlotCombination.match("\uD83C\uDF52", "\uD83C\uDF52", "\uD83C\uDF52"); + assertEquals(SlotCombination.CHERRY, result); + assertEquals(3, result.getMultiplier()); + } + + @Test + void returnsBellForThreeBells() { + SlotCombination result = SlotCombination.match("\uD83D\uDD14", "\uD83D\uDD14", "\uD83D\uDD14"); + assertEquals(SlotCombination.BELL, result); + assertEquals(5, result.getMultiplier()); + } + + @Test + void returnsDiamondForThreeDiamonds() { + SlotCombination result = SlotCombination.match("\uD83D\uDC8E", "\uD83D\uDC8E", "\uD83D\uDC8E"); + assertEquals(SlotCombination.DIAMOND, result); + assertEquals(10, result.getMultiplier()); + } + + @Test + void returnsCrownForThreeCrowns() { + SlotCombination result = SlotCombination.match("\uD83D\uDC51", "\uD83D\uDC51", "\uD83D\uDC51"); + assertEquals(SlotCombination.CROWN, result); + assertEquals(15, result.getMultiplier()); + } + + @Test + void returnsJackpotForThreeLightningBolts() { + SlotCombination result = SlotCombination.match("\u26A1", "\u26A1", "\u26A1"); + assertEquals(SlotCombination.JACKPOT, result); + assertEquals(50, result.getMultiplier()); + } + } +}