test(monetization): MonetizationState 및 복귀 보상 테스트 추가
- MonetizationState 23개 테스트 (버프 활성/만료, 유료/무료 분기) - ReturnRewardsService 14개 테스트 (보상 계산, 2배 시간, 포맷팅)
This commit is contained in:
192
test/core/engine/return_rewards_service_test.dart
Normal file
192
test/core/engine/return_rewards_service_test.dart
Normal file
@@ -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'));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
221
test/core/model/monetization_state_test.dart
Normal file
221
test/core/model/monetization_state_test.dart
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user