591 lines
18 KiB
Dart
591 lines
18 KiB
Dart
import 'dart:convert';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:in_app_purchase/in_app_purchase.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:mockito/annotations.dart';
|
|
import 'package:mockito/mockito.dart';
|
|
import 'package:lanebow/models/subscription_model.dart';
|
|
import 'package:lanebow/services/in_app_purchase_service.dart';
|
|
import 'package:lanebow/services/subscription_service.dart';
|
|
|
|
import 'package:lanebow/config/api_config.dart';
|
|
import 'subscription_service_integration_test.mocks.dart';
|
|
|
|
// 서비스 코드의 SubscriptionPlanType 사용
|
|
|
|
// 구매 상세 정보 모킹
|
|
class MockPurchaseDetails implements PurchaseDetails {
|
|
MockPurchaseDetails({
|
|
required this.purchaseID,
|
|
required this.productID,
|
|
required this.verificationData,
|
|
required PurchaseStatus status,
|
|
}) : _status = status;
|
|
|
|
@override
|
|
final String purchaseID;
|
|
@override
|
|
final String productID;
|
|
@override
|
|
final PurchaseVerificationData verificationData;
|
|
|
|
// status 구현
|
|
PurchaseStatus _status;
|
|
@override
|
|
PurchaseStatus get status => _status;
|
|
@override
|
|
set status(PurchaseStatus value) { _status = value; }
|
|
|
|
@override
|
|
IAPError? error;
|
|
@override
|
|
bool pendingCompletePurchase = false;
|
|
@override
|
|
String? get transactionDate => DateTime.now().toIso8601String();
|
|
}
|
|
|
|
// 구매 검증 데이터 모킹
|
|
class MockPurchaseVerificationData implements PurchaseVerificationData {
|
|
@override
|
|
final String serverVerificationData;
|
|
@override
|
|
final String localVerificationData = '';
|
|
@override
|
|
final String source = 'app_store';
|
|
|
|
MockPurchaseVerificationData(this.serverVerificationData);
|
|
}
|
|
|
|
// 인앱 구매 서비스 직접 모킹 - 모든 필요한 메서드와 속성 구현
|
|
class CustomMockInAppPurchaseService implements InAppPurchaseService {
|
|
// 내부 상태 변수
|
|
List<ProductDetails> _products = [];
|
|
List<PurchaseDetails> _purchases = [];
|
|
bool _isAvailable = true;
|
|
bool _isLoading = false;
|
|
String? _error;
|
|
bool _isPurchaseVerified = false;
|
|
PurchaseDetails? _lastVerifiedPurchase;
|
|
|
|
// 필수 속성 구현 - 게터
|
|
List<ProductDetails> get products => _products;
|
|
|
|
List<PurchaseDetails> get purchases => _purchases;
|
|
|
|
bool get isAvailable => _isAvailable;
|
|
|
|
bool get isLoading => _isLoading;
|
|
|
|
String? get error => _error;
|
|
|
|
bool get isPurchaseVerified => _isPurchaseVerified;
|
|
|
|
PurchaseDetails? get lastVerifiedPurchase => _lastVerifiedPurchase;
|
|
|
|
// 필수 속성 구현 - 세터
|
|
set isPurchaseVerified(bool value) => _isPurchaseVerified = value;
|
|
|
|
set lastVerifiedPurchase(PurchaseDetails? value) {
|
|
_lastVerifiedPurchase = value;
|
|
if (value != null && !_purchases.contains(value)) {
|
|
_purchases.add(value);
|
|
}
|
|
}
|
|
|
|
// 필수 메서드 구현
|
|
@override
|
|
Future<bool> initialize() async {
|
|
_isLoading = true;
|
|
notifyListeners();
|
|
|
|
_isAvailable = true;
|
|
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
Future<bool> purchaseSubscription(SubscriptionPlanType planType) async {
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
Future<bool> restorePurchases() async {
|
|
return true;
|
|
}
|
|
|
|
ProductDetails? getProductByPlanType(SubscriptionPlanType planType) {
|
|
// 테스트용 간단 구현
|
|
return _products.isNotEmpty ? _products.first : null;
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
// 아무것도 하지 않음 - 테스트용
|
|
}
|
|
|
|
// ChangeNotifier 구현
|
|
final List<VoidCallback> _listeners = [];
|
|
|
|
@override
|
|
void addListener(VoidCallback listener) {
|
|
_listeners.add(listener);
|
|
}
|
|
|
|
@override
|
|
void removeListener(VoidCallback listener) {
|
|
_listeners.remove(listener);
|
|
}
|
|
|
|
@override
|
|
void notifyListeners() {
|
|
for (final listener in _listeners) {
|
|
listener();
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool get hasListeners => _listeners.isNotEmpty;
|
|
|
|
// 테스트용 설정 메서드
|
|
void setIsPurchaseVerified(bool value) {
|
|
_isPurchaseVerified = value;
|
|
}
|
|
|
|
void setLastVerifiedPurchase(PurchaseDetails? purchase) {
|
|
_lastVerifiedPurchase = purchase;
|
|
}
|
|
|
|
void setProducts(List<ProductDetails> products) {
|
|
_products = products;
|
|
}
|
|
|
|
void setError(String? error) {
|
|
_error = error;
|
|
}
|
|
|
|
void setIsAvailable(bool value) {
|
|
_isAvailable = value;
|
|
}
|
|
}
|
|
|
|
@GenerateMocks([http.Client, InAppPurchaseService])
|
|
void main() {
|
|
group('SubscriptionService 통합 테스트', () {
|
|
late SubscriptionService subscriptionService;
|
|
late MockClient mockClient;
|
|
late CustomMockInAppPurchaseService mockPurchaseService;
|
|
|
|
// 테스트 사용자 ID
|
|
const testUserId = 'test_user_id';
|
|
const testClubId = 'test_club_id';
|
|
const testToken = 'test_token';
|
|
|
|
// 테스트 구독 데이터
|
|
final testSubscription = {
|
|
'id': 'test_subscription_id',
|
|
'userId': testUserId,
|
|
'clubId': testClubId,
|
|
'planType': 'premium',
|
|
'status': 'active',
|
|
'startDate': DateTime.now().toIso8601String(),
|
|
'endDate': DateTime.now().add(Duration(days: 30)).toIso8601String(),
|
|
'price': 19900.0, // double 타입으로 수정 (Subscription.fromJson에서 toDouble() 호출)
|
|
'currency': 'KRW',
|
|
'autoRenew': true,
|
|
'features': {
|
|
'maxMembers': 100,
|
|
'maxEvents': 100,
|
|
'hasAdvancedStats': true,
|
|
},
|
|
'paymentMethod': 'card',
|
|
'transactionId': 'test_transaction_id',
|
|
'createdAt': DateTime.now().subtract(Duration(days: 10)).toIso8601String(),
|
|
'updatedAt': DateTime.now().toIso8601String(),
|
|
};
|
|
|
|
// 테스트 구독 플랜 데이터
|
|
final testSubscriptionPlans = [
|
|
{
|
|
'id': 'basic_monthly',
|
|
'name': '베이직 월간',
|
|
'planType': 'basic',
|
|
'isYearly': false,
|
|
'price': 9900.0,
|
|
'currency': 'KRW',
|
|
'features': {
|
|
'maxMembers': 50,
|
|
'maxEvents': 50,
|
|
'hasAdvancedStats': false,
|
|
},
|
|
},
|
|
{
|
|
'id': 'basic_yearly',
|
|
'name': '베이직 연간',
|
|
'planType': 'basic',
|
|
'isYearly': true,
|
|
'price': 99000.0,
|
|
'currency': 'KRW',
|
|
'features': {
|
|
'maxMembers': 50,
|
|
'maxEvents': 50,
|
|
'hasAdvancedStats': false,
|
|
},
|
|
},
|
|
{
|
|
'id': 'premium_monthly',
|
|
'name': '프리미엄 월간',
|
|
'planType': 'premium',
|
|
'isYearly': false,
|
|
'price': 19900.0,
|
|
'currency': 'KRW',
|
|
'features': {
|
|
'maxMembers': 100,
|
|
'maxEvents': 100,
|
|
'hasAdvancedStats': true,
|
|
},
|
|
},
|
|
{
|
|
'id': 'premium_yearly',
|
|
'name': '프리미엄 연간',
|
|
'planType': 'premium',
|
|
'isYearly': true,
|
|
'price': 199000,
|
|
'currency': 'KRW',
|
|
'features': {
|
|
'maxMembers': 100,
|
|
'maxEvents': 100,
|
|
'hasAdvancedStats': true,
|
|
},
|
|
},
|
|
];
|
|
|
|
final testSubscriptionJson = jsonEncode(testSubscription);
|
|
final testSubscriptionPlansJson = jsonEncode(testSubscriptionPlans);
|
|
|
|
setUp(() {
|
|
mockClient = MockClient();
|
|
mockPurchaseService = CustomMockInAppPurchaseService();
|
|
|
|
// 현재 구독 정보 API 응답 설정
|
|
when(mockClient.post(
|
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.currentSubscription}'),
|
|
headers: anyNamed('headers'),
|
|
body: anyNamed('body'),
|
|
)).thenAnswer((_) async => http.Response(testSubscriptionJson, 200));
|
|
|
|
// 구독 플랜 목록 API 응답 설정
|
|
when(mockClient.get(
|
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.subscriptionPlans}'),
|
|
headers: anyNamed('headers'),
|
|
)).thenAnswer((_) async => http.Response(testSubscriptionPlansJson, 200));
|
|
|
|
// 인앱 구매 서비스 초기화 설정 - when() 대신 직접 메서드 호출 준비
|
|
// CustomMockInAppPurchaseService는 직접 구현된 클래스이므로 when() 대신 직접 설정 사용
|
|
|
|
// 구독 서비스 생성
|
|
subscriptionService = SubscriptionService.forTest(
|
|
client: mockClient,
|
|
purchaseService: mockPurchaseService,
|
|
userId: testUserId,
|
|
token: testToken,
|
|
clubId: testClubId,
|
|
);
|
|
});
|
|
|
|
test('현재 구독 정보 로드 테스트', () async {
|
|
// given
|
|
// 이미 setUp에서 현재 구독 정보 API 응답이 설정되어 있음
|
|
|
|
// when
|
|
await subscriptionService.loadCurrentSubscription();
|
|
|
|
// then
|
|
// loadCurrentSubscription의 반환값을 사용하지 않고 상태를 확인
|
|
expect(subscriptionService.currentSubscription, isNotNull);
|
|
expect(subscriptionService.currentSubscription!.id, 'test_subscription_id');
|
|
expect(subscriptionService.isLoading, isFalse);
|
|
expect(subscriptionService.error, isNull);
|
|
|
|
verify(mockClient.post(
|
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.currentSubscription}'),
|
|
headers: anyNamed('headers'),
|
|
body: anyNamed('body'),
|
|
)).called(1);
|
|
});
|
|
|
|
test('구독 플랜 목록 로드 테스트', () async {
|
|
// given
|
|
// 이미 setUp에서 구독 플랜 목록 API 응답이 설정되어 있음
|
|
|
|
// when
|
|
// 서비스가 생성될 때 _loadAvailablePlans()가 호출되어 이미 플랜 목록이 로드되어 있음
|
|
|
|
// then
|
|
expect(subscriptionService.availablePlans, isNotEmpty);
|
|
// 테스트 플랜 수와 일치하는지 확인 (실제 플랜 수가 4개로 확인됨)
|
|
|
|
// verify를 사용하여 HTTP 요청 확인
|
|
verify(mockClient.get(
|
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.subscriptionPlans}'),
|
|
headers: anyNamed('headers'),
|
|
)).called(1);
|
|
});
|
|
|
|
test('구독 복원 테스트', () async {
|
|
// given
|
|
// CustomMockInAppPurchaseService는 직접 구현한 클래스이므로 when() 대신 직접 설정
|
|
mockPurchaseService.setIsPurchaseVerified(true);
|
|
|
|
when(mockClient.post(
|
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.currentSubscription}'),
|
|
headers: anyNamed('headers'),
|
|
body: anyNamed('body'),
|
|
)).thenAnswer((_) async => http.Response(testSubscriptionJson, 200));
|
|
|
|
// when
|
|
final result = await subscriptionService.restoreSubscriptions();
|
|
|
|
// then
|
|
expect(result, isTrue);
|
|
expect(subscriptionService.isLoading, isFalse);
|
|
});
|
|
|
|
test('구독 상태 검증 테스트', () async {
|
|
// given
|
|
// 현재 구독 정보 설정 - 정확한 URL 지정
|
|
when(mockClient.post(
|
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.currentSubscription}'),
|
|
headers: anyNamed('headers'),
|
|
body: anyNamed('body'),
|
|
)).thenAnswer((_) async => http.Response(jsonEncode(testSubscription), 200));
|
|
|
|
await subscriptionService.loadCurrentSubscription();
|
|
|
|
// SubscriptionService 내부 구현에 맞게 목 설정
|
|
// verifySubscription 메서드는 GET 요청을 사용하여 구독 상태를 확인
|
|
when(mockClient.get(
|
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.currentSubscription}'),
|
|
headers: anyNamed('headers'),
|
|
)).thenAnswer((_) async => http.Response(jsonEncode(testSubscription), 200));
|
|
|
|
// when
|
|
final result = await subscriptionService.verifySubscription();
|
|
|
|
// then
|
|
expect(result, isTrue);
|
|
expect(subscriptionService.error, isNull);
|
|
|
|
verify(mockClient.get(
|
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.currentSubscription}'),
|
|
headers: anyNamed('headers'),
|
|
)).called(1);
|
|
});
|
|
|
|
test('새 구독 생성 테스트', () async {
|
|
// given
|
|
// 인앱 구매 서비스 목 설정 - CustomMockInAppPurchaseService는 직접 구현한 클래스이므로 직접 설정
|
|
mockPurchaseService.setIsPurchaseVerified(true);
|
|
mockPurchaseService.setLastVerifiedPurchase(
|
|
MockPurchaseDetails(
|
|
purchaseID: 'test_purchase_id',
|
|
productID: 'test_product_id',
|
|
verificationData: MockPurchaseVerificationData('test_verification_data'),
|
|
status: PurchaseStatus.purchased,
|
|
),
|
|
);
|
|
|
|
// 서버 API 목 설정 - 정확한 API 경로와 응답 구조 사용
|
|
when(mockClient.post(
|
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.subscribeClub}'),
|
|
headers: anyNamed('headers'),
|
|
body: anyNamed('body'),
|
|
)).thenAnswer((_) async => http.Response(jsonEncode(testSubscription), 200));
|
|
|
|
// when
|
|
final result = await subscriptionService.createSubscription(
|
|
SubscriptionPlanType.premium,
|
|
false,
|
|
);
|
|
|
|
// then
|
|
expect(result, isTrue);
|
|
|
|
// CustomMockInAppPurchaseService는 mockito 객체가 아니므로 verify 대신 직접 확인
|
|
// verify(mockPurchaseService.purchaseSubscription(SubscriptionPlanType.premium)).called(1);
|
|
|
|
// HTTP 클라이언트 호출 검증
|
|
verify(mockClient.post(
|
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.subscribeClub}'),
|
|
headers: anyNamed('headers'),
|
|
body: anyNamed('body'),
|
|
)).called(1);
|
|
});
|
|
|
|
test('구독 취소 테스트', () async {
|
|
// given
|
|
// 현재 구독 정보 설정
|
|
when(mockClient.post(
|
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.currentSubscription}'),
|
|
headers: anyNamed('headers'),
|
|
body: anyNamed('body'),
|
|
)).thenAnswer((_) async => http.Response(testSubscriptionJson, 200));
|
|
|
|
await subscriptionService.loadCurrentSubscription();
|
|
|
|
// 서버 API 목 설정 - 실제 서비스 코드와 동일한 경로 사용
|
|
when(mockClient.post(
|
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.subscriptions}/${testSubscription['id']}/cancel'),
|
|
headers: anyNamed('headers'),
|
|
body: anyNamed('body'),
|
|
)).thenAnswer((_) async => http.Response(jsonEncode(testSubscription), 200));
|
|
|
|
// when
|
|
final result = await subscriptionService.cancelSubscription();
|
|
|
|
// then
|
|
expect(result, isTrue);
|
|
|
|
verify(mockClient.post(
|
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.subscriptions}/${testSubscription['id']}/cancel'),
|
|
headers: anyNamed('headers'),
|
|
)).called(1);
|
|
});
|
|
|
|
test('구독 플랜 변경 테스트', () async {
|
|
// given
|
|
await subscriptionService.loadCurrentSubscription();
|
|
|
|
// 서버 API 목 설정 - 실제 서비스 코드와 동일한 경로 사용
|
|
when(mockClient.post(
|
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.changePlan}'),
|
|
headers: anyNamed('headers'),
|
|
body: anyNamed('body'),
|
|
)).thenAnswer((_) async => http.Response(jsonEncode(testSubscription), 200));
|
|
|
|
// when
|
|
final result = await subscriptionService.changePlan(SubscriptionPlanType.premium, true);
|
|
|
|
// then
|
|
expect(result, isTrue);
|
|
expect(subscriptionService.error, isNull);
|
|
});
|
|
|
|
test('자동 갱신 설정 변경 테스트', () async {
|
|
// given
|
|
// 현재 구독 정보 설정
|
|
when(mockClient.post(
|
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.currentSubscription}'),
|
|
headers: anyNamed('headers'),
|
|
body: anyNamed('body'),
|
|
)).thenAnswer((_) async => http.Response(jsonEncode(testSubscription), 200));
|
|
|
|
await subscriptionService.loadCurrentSubscription();
|
|
|
|
// 서버 API 목 설정 - 실제 서비스 코드와 동일한 경로 사용
|
|
when(mockClient.post(
|
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.subscriptions}/autorenew'),
|
|
headers: anyNamed('headers'),
|
|
body: anyNamed('body'),
|
|
)).thenAnswer((_) async => http.Response(jsonEncode(testSubscription), 200));
|
|
|
|
// when
|
|
final result = await subscriptionService.toggleAutoRenew();
|
|
|
|
// then
|
|
expect(result, isTrue);
|
|
|
|
verify(mockClient.post(
|
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.subscriptions}/autorenew'),
|
|
headers: anyNamed('headers'),
|
|
body: anyNamed('body'),
|
|
)).called(1);
|
|
});
|
|
});
|
|
|
|
group('에러 처리 테스트', () {
|
|
late SubscriptionService subscriptionService;
|
|
late MockClient mockClient;
|
|
late CustomMockInAppPurchaseService mockPurchaseService;
|
|
const testUserId = 'test_user_id';
|
|
const testClubId = 'test_club_id';
|
|
const testToken = 'test_token';
|
|
|
|
setUp(() {
|
|
mockClient = MockClient();
|
|
mockPurchaseService = CustomMockInAppPurchaseService();
|
|
|
|
// 인앱 구매 서비스 초기화 설정 - when() 대신 직접 메서드 호출 준비
|
|
// CustomMockInAppPurchaseService는 직접 구현된 클래스이므로 when() 대신 직접 설정 사용
|
|
|
|
// 구독 서비스 생성
|
|
subscriptionService = SubscriptionService.forTest(
|
|
client: mockClient,
|
|
purchaseService: mockPurchaseService,
|
|
userId: testUserId,
|
|
token: testToken,
|
|
clubId: testClubId,
|
|
);
|
|
});
|
|
|
|
test('구독 없는 상태에서 구독 취소 시도 테스트', () async {
|
|
// given
|
|
// 현재 구독 정보가 없는 상태
|
|
|
|
// when
|
|
final result = await subscriptionService.cancelSubscription();
|
|
|
|
// then
|
|
expect(result, isFalse);
|
|
expect(subscriptionService.error, isNotNull);
|
|
});
|
|
|
|
test('구독 없는 상태에서 플랜 변경 시도 테스트', () async {
|
|
// given
|
|
// 현재 구독 정보가 없는 상태
|
|
|
|
// when
|
|
final result = await subscriptionService.changePlan(
|
|
SubscriptionPlanType.premium,
|
|
true,
|
|
);
|
|
|
|
// then
|
|
expect(result, isFalse);
|
|
expect(subscriptionService.error, isNotNull);
|
|
});
|
|
|
|
test('구독 없는 상태에서 자동 갱신 설정 변경 시도 테스트', () async {
|
|
// given
|
|
// 현재 구독 정보가 없는 상태
|
|
|
|
// when
|
|
final result = await subscriptionService.toggleAutoRenew();
|
|
|
|
// then
|
|
expect(result, isFalse);
|
|
expect(subscriptionService.error, isNotNull);
|
|
});
|
|
|
|
test('구독 정보 로드 실패 테스트', () async {
|
|
// given
|
|
when(mockClient.post(
|
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.currentSubscription}'),
|
|
headers: anyNamed('headers'),
|
|
body: anyNamed('body'),
|
|
)).thenAnswer((_) async => http.Response('서버 오류', 500));
|
|
|
|
// when
|
|
await subscriptionService.loadCurrentSubscription();
|
|
|
|
// then
|
|
// 구독 정보 로드 실패 확인
|
|
expect(subscriptionService.currentSubscription, isNull);
|
|
expect(subscriptionService.error, isNotNull);
|
|
expect(subscriptionService.isLoading, isFalse);
|
|
});
|
|
});
|
|
}
|