From 4791bda66976cc40e11a6b0381791b6ac92e9570 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Mon, 9 Mar 2026 15:34:27 +0900 Subject: [PATCH] =?UTF-8?q?test(monetization):=20MonetizationState=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B3=B5=EA=B7=80=20=EB=B3=B4=EC=83=81=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MonetizationState 23개 테스트 (버프 활성/만료, 유료/무료 분기) - ReturnRewardsService 14개 테스트 (보상 계산, 2배 시간, 포맷팅) --- .../engine/return_rewards_service_test.dart | 192 +++++++++++++++ test/core/model/monetization_state_test.dart | 221 ++++++++++++++++++ 2 files changed, 413 insertions(+) create mode 100644 test/core/engine/return_rewards_service_test.dart create mode 100644 test/core/model/monetization_state_test.dart diff --git a/test/core/engine/return_rewards_service_test.dart b/test/core/engine/return_rewards_service_test.dart new file mode 100644 index 0000000..6d3f15f --- /dev/null +++ b/test/core/engine/return_rewards_service_test.dart @@ -0,0 +1,192 @@ +import 'package:asciineverdie/src/core/engine/return_rewards_service.dart'; +import 'package:asciineverdie/src/core/model/treasure_chest.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + // 싱글톤(singleton) 인스턴스 사용 + final service = ReturnRewardsService.instance; + + // 기준 시각(base time) 설정 + final baseTime = DateTime(2026, 3, 9, 12, 0, 0); + + group('ReturnRewardsService', () { + group('calculateReward', () { + test('lastPlayTime이 null이면 보상 없음', () { + final result = service.calculateReward( + lastPlayTime: null, + currentTime: baseTime, + isPaidUser: false, + ); + + expect(result.hasReward, isFalse); + expect(result.hoursAway, equals(0)); + expect(result.chestCount, equals(0)); + expect(result.bonusChestCount, equals(0)); + }); + + test('1시간 미만이면 보상 없음', () { + // 30분 전 플레이 + final lastPlay = baseTime.subtract(const Duration(minutes: 30)); + + final result = service.calculateReward( + lastPlayTime: lastPlay, + currentTime: baseTime, + isPaidUser: false, + ); + + expect(result.hasReward, isFalse); + expect(result.chestCount, equals(0)); + }); + + test('정확히 4시간이면 상자(chest) 1개', () { + final lastPlay = baseTime.subtract(const Duration(hours: 4)); + + final result = service.calculateReward( + lastPlayTime: lastPlay, + currentTime: baseTime, + isPaidUser: false, + ); + + expect(result.hasReward, isTrue); + expect(result.chestCount, equals(1)); + expect(result.hoursAway, equals(4)); + }); + + test('12시간이면 상자 3개', () { + final lastPlay = baseTime.subtract(const Duration(hours: 12)); + + final result = service.calculateReward( + lastPlayTime: lastPlay, + currentTime: baseTime, + isPaidUser: false, + ); + + expect(result.hasReward, isTrue); + expect(result.chestCount, equals(3)); + expect(result.hoursAway, equals(12)); + }); + + test('24시간 초과 시 maxHoursAway로 제한(cap)', () { + // 48시간 전 플레이 → effectiveHours = 24 + final lastPlay = baseTime.subtract(const Duration(hours: 48)); + + final result = service.calculateReward( + lastPlayTime: lastPlay, + currentTime: baseTime, + isPaidUser: false, + ); + + // 24 ~/ 4 = 6 → 무료 유저 최대(maxChestsFree) 5개 + expect(result.chestCount, equals(5)); + expect(result.hoursAway, equals(48)); + }); + + test('무료 유저(free user) 최대 상자 5개', () { + // 20시간 → 20 ~/ 4 = 5개 + final lastPlay = baseTime.subtract(const Duration(hours: 20)); + + final result = service.calculateReward( + lastPlayTime: lastPlay, + currentTime: baseTime, + isPaidUser: false, + ); + + expect(result.chestCount, equals(5)); + }); + + test('유료 유저(paid user) 최대 상자 10개', () { + // 유료 유저: 24시간 → creditedHours = 48 → cap 24 + // 24 ~/ 4 = 6개 (maxChestsPaid 10 이내) + final lastPlay = baseTime.subtract(const Duration(hours: 48)); + + final result = service.calculateReward( + lastPlayTime: lastPlay, + currentTime: baseTime, + isPaidUser: true, + ); + + // 48시간, 유료 2배 → 96 → cap 24 → 24 ~/ 4 = 6 + expect(result.chestCount, equals(6)); + expect(result.chestCount, lessThanOrEqualTo(10)); + }); + + test('유료 유저 오프라인 시간 2배 인정 (4시간 → 8시간 → 상자 2개)', () { + // 실제 4시간, 유료 2배 → creditedHours = 8 + // 8 ~/ 4 = 2개 + final lastPlay = baseTime.subtract(const Duration(hours: 4)); + + final result = service.calculateReward( + lastPlayTime: lastPlay, + currentTime: baseTime, + isPaidUser: true, + ); + + expect(result.chestCount, equals(2)); + }); + + test('유료 유저 2배 적용 후에도 24시간으로 제한', () { + // 14시간 → creditedHours = 28 → cap 24 + // 24 ~/ 4 = 6개 + final lastPlay = baseTime.subtract(const Duration(hours: 14)); + + final result = service.calculateReward( + lastPlayTime: lastPlay, + currentTime: baseTime, + isPaidUser: true, + ); + + // 14 * 2 = 28 → capped to 24 → 24 ~/ 4 = 6 + expect(result.chestCount, equals(6)); + }); + }); + + group('bonusChestCount', () { + test('bonusChestCount는 chestCount와 동일', () { + final lastPlay = baseTime.subtract(const Duration(hours: 8)); + + final result = service.calculateReward( + lastPlayTime: lastPlay, + currentTime: baseTime, + isPaidUser: false, + ); + + // 8 ~/ 4 = 2 + expect(result.chestCount, equals(2)); + expect(result.bonusChestCount, equals(result.chestCount)); + }); + }); + + group('claimBasicReward', () { + test('보상 없는 경우 빈 목록(empty list) 반환', () { + const reward = ReturnChestReward( + hoursAway: 0, + chestCount: 0, + bonusChestCount: 0, + ); + + final result = service.claimBasicReward(reward, 10); + + expect(result, isEmpty); + }); + }); + + group('formatHoursAway', () { + test('24시간 미만은 시간만 표시', () { + expect(service.formatHoursAway(5), equals('5h')); + expect(service.formatHoursAway(1), equals('1h')); + expect(service.formatHoursAway(23), equals('23h')); + }); + + test('정확히 24시간은 일(day) 단위 표시', () { + expect(service.formatHoursAway(24), equals('1d')); + expect(service.formatHoursAway(48), equals('2d')); + }); + + test('일 + 시간 혼합 표시', () { + expect(service.formatHoursAway(25), equals('1d 1h')); + expect(service.formatHoursAway(50), equals('2d 2h')); + expect(service.formatHoursAway(30), equals('1d 6h')); + }); + }); + }); +} diff --git a/test/core/model/monetization_state_test.dart b/test/core/model/monetization_state_test.dart new file mode 100644 index 0000000..37a368e --- /dev/null +++ b/test/core/model/monetization_state_test.dart @@ -0,0 +1,221 @@ +import 'package:asciineverdie/src/core/model/monetization_state.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('MonetizationState', () { + // ========================================================================= + // initial 팩토리(factory) 테스트 + // ========================================================================= + + group('initial', () { + test('무료 사용자(free user) 기본 상태', () { + final state = MonetizationState.initial(); + + expect(state.undoRemaining, equals(1)); + expect(state.adRemovalPurchased, isFalse); + expect(state.rollsRemaining, equals(5)); + expect(state.pendingChests, equals(0)); + expect(state.rollHistory, isNull); + }); + + test('유료 사용자(paid user) 기본 상태', () { + final state = MonetizationState.initial(isPaidUser: true); + + expect(state.undoRemaining, equals(3)); + expect(state.adRemovalPurchased, isTrue); + expect(state.rollsRemaining, equals(5)); + expect(state.pendingChests, equals(0)); + }); + }); + + // ========================================================================= + // isPaidUser / isFreeUser 게터(getter) 테스트 + // ========================================================================= + + group('isPaidUser / isFreeUser', () { + test('광고 제거 구매 시 isPaidUser true', () { + final state = MonetizationState.initial(isPaidUser: true); + + expect(state.isPaidUser, isTrue); + expect(state.isFreeUser, isFalse); + }); + + test('광고 제거 미구매 시 isFreeUser true', () { + final state = MonetizationState.initial(); + + expect(state.isPaidUser, isFalse); + expect(state.isFreeUser, isTrue); + }); + }); + + // ========================================================================= + // isAutoReviveActive 테스트 + // ========================================================================= + + group('isAutoReviveActive', () { + test('autoReviveEndMs가 null이면 false 반환', () { + final state = MonetizationState.initial(); + + expect(state.isAutoReviveActive(1000), isFalse); + }); + + test('elapsedMs가 종료 시점 이내면 true 반환', () { + final state = MonetizationState.initial().copyWith( + autoReviveEndMs: 5000, + ); + + expect(state.isAutoReviveActive(3000), isTrue); + }); + + test('elapsedMs가 종료 시점을 초과하면 false 반환', () { + final state = MonetizationState.initial().copyWith( + autoReviveEndMs: 5000, + ); + + expect(state.isAutoReviveActive(6000), isFalse); + }); + + test('elapsedMs가 종료 시점과 정확히 같으면 false 반환 (경계값)', () { + final state = MonetizationState.initial().copyWith( + autoReviveEndMs: 5000, + ); + + expect(state.isAutoReviveActive(5000), isFalse); + }); + }); + + // ========================================================================= + // isSpeedBoostActive 테스트 + // ========================================================================= + + group('isSpeedBoostActive', () { + test('유료 사용자는 항상 true 반환', () { + final state = MonetizationState.initial(isPaidUser: true); + + // speedBoostEndMs가 null이어도 유료 사용자는 항상 활성 + expect(state.isSpeedBoostActive(0), isTrue); + expect(state.isSpeedBoostActive(999999), isTrue); + }); + + test('무료 사용자 - 종료 시점 이내면 true 반환', () { + final state = MonetizationState.initial().copyWith( + speedBoostEndMs: 10000, + ); + + expect(state.isSpeedBoostActive(5000), isTrue); + }); + + test('무료 사용자 - 종료 시점 초과 시 false 반환', () { + final state = MonetizationState.initial().copyWith( + speedBoostEndMs: 10000, + ); + + expect(state.isSpeedBoostActive(15000), isFalse); + }); + + test('무료 사용자 - speedBoostEndMs가 null이면 false 반환', () { + final state = MonetizationState.initial(); + + expect(state.isSpeedBoostActive(1000), isFalse); + }); + }); + + // ========================================================================= + // isLuckyCharmActive 테스트 + // ========================================================================= + + group('isLuckyCharmActive', () { + test('종료 시점 이내면 true 반환', () { + final state = MonetizationState.initial().copyWith( + luckyCharmEndMs: 8000, + ); + + expect(state.isLuckyCharmActive(4000), isTrue); + }); + + test('종료 시점 초과 시 false 반환', () { + final state = MonetizationState.initial().copyWith( + luckyCharmEndMs: 8000, + ); + + expect(state.isLuckyCharmActive(9000), isFalse); + }); + + test('luckyCharmEndMs가 null이면 false 반환', () { + final state = MonetizationState.initial(); + + expect(state.isLuckyCharmActive(1000), isFalse); + }); + }); + + // ========================================================================= + // canRoll 테스트 + // ========================================================================= + + group('canRoll', () { + test('rollsRemaining > 0이면 true 반환', () { + final state = MonetizationState.initial(); // rollsRemaining=5 + + expect(state.canRoll, isTrue); + }); + + test('rollsRemaining == 0이면 false 반환', () { + final state = MonetizationState.initial().copyWith(rollsRemaining: 0); + + expect(state.canRoll, isFalse); + }); + }); + + // ========================================================================= + // maxChests 테스트 + // ========================================================================= + + group('maxChests', () { + test('유료 사용자는 최대 10개', () { + final state = MonetizationState.initial(isPaidUser: true); + + expect(state.maxChests, equals(10)); + }); + + test('무료 사용자는 최대 5개', () { + final state = MonetizationState.initial(); + + expect(state.maxChests, equals(5)); + }); + }); + + // ========================================================================= + // isChestsFull 테스트 + // ========================================================================= + + group('isChestsFull', () { + test('무료 사용자 - pendingChests가 maxChests 이상이면 true', () { + final state = MonetizationState.initial().copyWith(pendingChests: 5); + + expect(state.isChestsFull, isTrue); + }); + + test('무료 사용자 - pendingChests가 maxChests 미만이면 false', () { + final state = MonetizationState.initial().copyWith(pendingChests: 3); + + expect(state.isChestsFull, isFalse); + }); + + test('유료 사용자 - pendingChests가 maxChests 이상이면 true', () { + final state = MonetizationState.initial( + isPaidUser: true, + ).copyWith(pendingChests: 10); + + expect(state.isChestsFull, isTrue); + }); + + test('유료 사용자 - pendingChests가 maxChests 초과해도 true', () { + final state = MonetizationState.initial( + isPaidUser: true, + ).copyWith(pendingChests: 12); + + expect(state.isChestsFull, isTrue); + }); + }); + }); +}