format and logs
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Web App CI / test (push) Waiting to run
Web App CI / build (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Web App CI / test (push) Waiting to run
Web App CI / build (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
This commit is contained in:
parent
feeced1543
commit
4ed1839893
54 changed files with 2055 additions and 1913 deletions
|
|
@ -965,3 +965,4 @@ void main() {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -281,3 +281,4 @@ Built with build_runner/jit in 1s; wrote 0 outputs.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -321,3 +321,4 @@ import 'package:mnemo_cards_backend/database/database.dart';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,3 +30,4 @@ GROUP BY is_blacklisted;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,9 @@ class PaymentManager {
|
||||||
/// Создать платеж в базе данных
|
/// Создать платеж в базе данных
|
||||||
Future<PaymentDto> createPayment(PaymentDto paymentDto, String userId) async {
|
Future<PaymentDto> createPayment(PaymentDto paymentDto, String userId) async {
|
||||||
print('🔍 PaymentManager.createPayment: Starting');
|
print('🔍 PaymentManager.createPayment: Starting');
|
||||||
print('🔍 PaymentManager.createPayment: userId=$userId, externalToken=${paymentDto.externalToken}');
|
print(
|
||||||
|
'🔍 PaymentManager.createPayment: userId=$userId, externalToken=${paymentDto.externalToken}',
|
||||||
|
);
|
||||||
final companion = paymentDto.toCompanion(userId);
|
final companion = paymentDto.toCompanion(userId);
|
||||||
print('🔍 PaymentManager.createPayment: Calling paymentDao.createPayment');
|
print('🔍 PaymentManager.createPayment: Calling paymentDao.createPayment');
|
||||||
final paymentId = await _db.paymentDao.createPayment(companion);
|
final paymentId = await _db.paymentDao.createPayment(companion);
|
||||||
|
|
@ -43,7 +45,9 @@ class PaymentManager {
|
||||||
print('🔍 PaymentManager.createPayment: Getting payment by id');
|
print('🔍 PaymentManager.createPayment: Getting payment by id');
|
||||||
final payment = await _db.paymentDao.getPaymentById(paymentId);
|
final payment = await _db.paymentDao.getPaymentById(paymentId);
|
||||||
if (payment == null) {
|
if (payment == null) {
|
||||||
print('❌ PaymentManager.createPayment: Payment not found after creation, id=$paymentId');
|
print(
|
||||||
|
'❌ PaymentManager.createPayment: Payment not found after creation, id=$paymentId',
|
||||||
|
);
|
||||||
throw Exception('Failed to create payment');
|
throw Exception('Failed to create payment');
|
||||||
}
|
}
|
||||||
print('✅ PaymentManager.createPayment: Payment retrieved successfully');
|
print('✅ PaymentManager.createPayment: Payment retrieved successfully');
|
||||||
|
|
@ -299,7 +303,7 @@ class PaymentManager {
|
||||||
try {
|
try {
|
||||||
final payment = await _db.paymentDao.getPaymentByExternalToken(token);
|
final payment = await _db.paymentDao.getPaymentByExternalToken(token);
|
||||||
if (payment == null) {
|
if (payment == null) {
|
||||||
log('Payment not found for token: $token');
|
print('Payment not found for token: $token');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -336,15 +340,21 @@ class PaymentManager {
|
||||||
List<MnemoCardsProductDto> products = const [],
|
List<MnemoCardsProductDto> products = const [],
|
||||||
}) async {
|
}) async {
|
||||||
print('🔍 PaymentManager.createYookassaUrl: Starting');
|
print('🔍 PaymentManager.createYookassaUrl: Starting');
|
||||||
print('🔍 PaymentManager.createYookassaUrl: amount=$amount, userId=$userId, products=${products.length}');
|
print(
|
||||||
|
'🔍 PaymentManager.createYookassaUrl: amount=$amount, userId=$userId, products=${products.length}',
|
||||||
|
);
|
||||||
|
|
||||||
print('🔍 PaymentManager.createYookassaUrl: Calling YooMoneyHandler.createPayment');
|
print(
|
||||||
|
'🔍 PaymentManager.createYookassaUrl: Calling YooMoneyHandler.createPayment',
|
||||||
|
);
|
||||||
final yookassaPayment = await _yooMoneyHandler.createPayment(
|
final yookassaPayment = await _yooMoneyHandler.createPayment(
|
||||||
amount: amount,
|
amount: amount,
|
||||||
description: description,
|
description: description,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
);
|
);
|
||||||
print('✅ PaymentManager.createYookassaUrl: YooKassa payment created, id=${yookassaPayment.id}');
|
print(
|
||||||
|
'✅ PaymentManager.createYookassaUrl: YooKassa payment created, id=${yookassaPayment.id}',
|
||||||
|
);
|
||||||
|
|
||||||
if (yookassaPayment.confirmationUrl == null) {
|
if (yookassaPayment.confirmationUrl == null) {
|
||||||
print('❌ PaymentManager.createYookassaUrl: confirmationUrl is null');
|
print('❌ PaymentManager.createYookassaUrl: confirmationUrl is null');
|
||||||
|
|
@ -368,7 +378,9 @@ class PaymentManager {
|
||||||
await createPayment(paymentDto, userId);
|
await createPayment(paymentDto, userId);
|
||||||
print('✅ PaymentManager.createYookassaUrl: Payment created in database');
|
print('✅ PaymentManager.createYookassaUrl: Payment created in database');
|
||||||
|
|
||||||
print('✅ PaymentManager.createYookassaUrl: Returning confirmationUrl and paymentId');
|
print(
|
||||||
|
'✅ PaymentManager.createYookassaUrl: Returning confirmationUrl and paymentId',
|
||||||
|
);
|
||||||
return YookassaPaymentResult(
|
return YookassaPaymentResult(
|
||||||
confirmationUrl: yookassaPayment.confirmationUrl!,
|
confirmationUrl: yookassaPayment.confirmationUrl!,
|
||||||
paymentId: yookassaPayment.id,
|
paymentId: yookassaPayment.id,
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,10 @@ class YooMoneyHandler {
|
||||||
final _uuid = const Uuid();
|
final _uuid = const Uuid();
|
||||||
static const String _baseUrl = 'https://api.yookassa.ru/v3';
|
static const String _baseUrl = 'https://api.yookassa.ru/v3';
|
||||||
|
|
||||||
YooMoneyHandler({
|
YooMoneyHandler({required String shopId, required String secretKey})
|
||||||
required String shopId,
|
: _shopId = shopId,
|
||||||
required String secretKey,
|
_secretKey = secretKey,
|
||||||
}) : _shopId = shopId,
|
_returnUrlBase = Platform.environment['YOOKASSA_RETURN_URL'] {
|
||||||
_secretKey = secretKey,
|
|
||||||
_returnUrlBase = Platform.environment['YOOKASSA_RETURN_URL'] {
|
|
||||||
// Validate credentials
|
// Validate credentials
|
||||||
if (_shopId.isEmpty || _secretKey.isEmpty) {
|
if (_shopId.isEmpty || _secretKey.isEmpty) {
|
||||||
log(
|
log(
|
||||||
|
|
@ -49,9 +47,7 @@ class YooMoneyHandler {
|
||||||
_dio = Dio(
|
_dio = Dio(
|
||||||
BaseOptions(
|
BaseOptions(
|
||||||
baseUrl: _baseUrl,
|
baseUrl: _baseUrl,
|
||||||
headers: {
|
headers: {'Content-Type': 'application/json'},
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -113,17 +109,11 @@ class YooMoneyHandler {
|
||||||
|
|
||||||
// Create payment request according to CreatePaymentRequest schema
|
// Create payment request according to CreatePaymentRequest schema
|
||||||
final requestBody = {
|
final requestBody = {
|
||||||
'amount': {
|
'amount': {'value': formattedAmount, 'currency': 'RUB'},
|
||||||
'value': formattedAmount,
|
|
||||||
'currency': 'RUB',
|
|
||||||
},
|
|
||||||
'description': description.length > 128
|
'description': description.length > 128
|
||||||
? description.substring(0, 128)
|
? description.substring(0, 128)
|
||||||
: description, // Max 128 chars per spec
|
: description, // Max 128 chars per spec
|
||||||
'confirmation': {
|
'confirmation': {'type': 'redirect', 'return_url': returnUrl},
|
||||||
'type': 'redirect',
|
|
||||||
'return_url': returnUrl,
|
|
||||||
},
|
|
||||||
'capture': true, // Auto-capture payment when succeeded
|
'capture': true, // Auto-capture payment when succeeded
|
||||||
'metadata': {
|
'metadata': {
|
||||||
'userId': userId,
|
'userId': userId,
|
||||||
|
|
@ -146,11 +136,7 @@ class YooMoneyHandler {
|
||||||
final response = await _dio!.post<Map<String, dynamic>>(
|
final response = await _dio!.post<Map<String, dynamic>>(
|
||||||
'/payments',
|
'/payments',
|
||||||
data: requestBody,
|
data: requestBody,
|
||||||
options: Options(
|
options: Options(headers: {'Idempotence-Key': idempotenceKey}),
|
||||||
headers: {
|
|
||||||
'Idempotence-Key': idempotenceKey,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final paymentData = response.data;
|
final paymentData = response.data;
|
||||||
|
|
@ -175,30 +161,15 @@ class YooMoneyHandler {
|
||||||
if (confirmationType == 'redirect') {
|
if (confirmationType == 'redirect') {
|
||||||
confirmationUrl = confirmation['confirmation_url'] as String?;
|
confirmationUrl = confirmation['confirmation_url'] as String?;
|
||||||
} else {
|
} else {
|
||||||
log(
|
print('Payment created with confirmation type: $confirmationType');
|
||||||
'Payment created with confirmation type: $confirmationType',
|
|
||||||
name: 'YooMoneyHandler',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (confirmationUrl == null) {
|
if (confirmationUrl == null) {
|
||||||
log(
|
print('Warning: No confirmation URL in payment response');
|
||||||
'Warning: No confirmation URL in payment response',
|
|
||||||
name: 'YooMoneyHandler',
|
|
||||||
error: jsonEncode(paymentData),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log(
|
print('YooKassa payment created successfully');
|
||||||
'YooKassa payment created successfully',
|
|
||||||
name: 'YooMoneyHandler',
|
|
||||||
error: {
|
|
||||||
'paymentId': paymentId,
|
|
||||||
'status': paymentStatus,
|
|
||||||
'hasConfirmationUrl': confirmationUrl != null,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return YookassaPayment(
|
return YookassaPayment(
|
||||||
id: paymentId,
|
id: paymentId,
|
||||||
|
|
@ -206,28 +177,15 @@ class YooMoneyHandler {
|
||||||
confirmationUrl: confirmationUrl,
|
confirmationUrl: confirmationUrl,
|
||||||
);
|
);
|
||||||
} on DioException catch (e, stackTrace) {
|
} on DioException catch (e, stackTrace) {
|
||||||
log(
|
print('YooKassa API error when creating payment $e $stackTrace');
|
||||||
'YooKassa API error when creating payment',
|
|
||||||
name: 'YooMoneyHandler',
|
|
||||||
error: e,
|
|
||||||
stackTrace: stackTrace,
|
|
||||||
);
|
|
||||||
if (e.response != null) {
|
if (e.response != null) {
|
||||||
log(
|
print('YooKassa error response: ${e.response?.data}');
|
||||||
'YooKassa error response: ${e.response?.data}',
|
|
||||||
name: 'YooMoneyHandler',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Failed to create YooKassa payment: ${e.message ?? 'Unknown error'}',
|
'Failed to create YooKassa payment: ${e.message ?? 'Unknown error'}',
|
||||||
);
|
);
|
||||||
} on Exception catch (e, stackTrace) {
|
} on Exception catch (e, stackTrace) {
|
||||||
log(
|
print('Unexpected error when creating YooKassa payment $e $stackTrace');
|
||||||
'Unexpected error when creating YooKassa payment',
|
|
||||||
name: 'YooMoneyHandler',
|
|
||||||
error: e,
|
|
||||||
stackTrace: stackTrace,
|
|
||||||
);
|
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -242,11 +200,7 @@ class YooMoneyHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log(
|
print('Checking YooKassa payment status $paymentId');
|
||||||
'Checking YooKassa payment status',
|
|
||||||
name: 'YooMoneyHandler',
|
|
||||||
error: {'paymentId': paymentId},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get payment info from YooKassa API
|
// Get payment info from YooKassa API
|
||||||
// According to spec: GET /v3/payments/{payment_id}
|
// According to spec: GET /v3/payments/{payment_id}
|
||||||
|
|
@ -274,15 +228,8 @@ class YooMoneyHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log(
|
print('''YooKassa payment status retrieved
|
||||||
'YooKassa payment status retrieved',
|
${{'paymentId': id, 'status': paymentStatus, 'paid': paid}}''');
|
||||||
name: 'YooMoneyHandler',
|
|
||||||
error: {
|
|
||||||
'paymentId': id,
|
|
||||||
'status': paymentStatus,
|
|
||||||
'paid': paid,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return YookassaPayment(
|
return YookassaPayment(
|
||||||
id: id,
|
id: id,
|
||||||
|
|
@ -290,28 +237,15 @@ class YooMoneyHandler {
|
||||||
confirmationUrl: confirmationUrl,
|
confirmationUrl: confirmationUrl,
|
||||||
);
|
);
|
||||||
} on DioException catch (e, stackTrace) {
|
} on DioException catch (e, stackTrace) {
|
||||||
log(
|
print('YooKassa API error when checking payment $e $stackTrace');
|
||||||
'YooKassa API error when checking payment',
|
|
||||||
name: 'YooMoneyHandler',
|
|
||||||
error: e,
|
|
||||||
stackTrace: stackTrace,
|
|
||||||
);
|
|
||||||
if (e.response != null) {
|
if (e.response != null) {
|
||||||
log(
|
print('YooKassa error response: ${e.response?.data}');
|
||||||
'YooKassa error response: ${e.response?.data}',
|
|
||||||
name: 'YooMoneyHandler',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Failed to check YooKassa payment: ${e.message ?? 'Unknown error'}',
|
'Failed to check YooKassa payment: ${e.message ?? 'Unknown error'}',
|
||||||
);
|
);
|
||||||
} on Exception catch (e, stackTrace) {
|
} on Exception catch (e, stackTrace) {
|
||||||
log(
|
print('Unexpected error when checking YooKassa payment $e $stackTrace');
|
||||||
'Unexpected error when checking YooKassa payment',
|
|
||||||
name: 'YooMoneyHandler',
|
|
||||||
error: e,
|
|
||||||
stackTrace: stackTrace,
|
|
||||||
);
|
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -602,10 +602,7 @@ class PacksApiV2 {
|
||||||
/// to allow image preview in public pack listings
|
/// to allow image preview in public pack listings
|
||||||
@Route.get('/packs/<packId>/cover')
|
@Route.get('/packs/<packId>/cover')
|
||||||
@OpenApiRouteHttp()
|
@OpenApiRouteHttp()
|
||||||
Future<Response> getPackCover(
|
Future<Response> getPackCover(Request request, String packId) async {
|
||||||
Request request,
|
|
||||||
String packId,
|
|
||||||
) async {
|
|
||||||
try {
|
try {
|
||||||
if (packId.isEmpty) {
|
if (packId.isEmpty) {
|
||||||
return _badRequest('Invalid pack ID');
|
return _badRequest('Invalid pack ID');
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,7 @@ class PurchasesApiV2 {
|
||||||
final PackManager _packManager;
|
final PackManager _packManager;
|
||||||
final AppDatabase _db;
|
final AppDatabase _db;
|
||||||
|
|
||||||
PurchasesApiV2(
|
PurchasesApiV2(this._paymentManager, this._packManager, this._db);
|
||||||
this._paymentManager,
|
|
||||||
this._packManager,
|
|
||||||
this._db,
|
|
||||||
);
|
|
||||||
|
|
||||||
Response _ok(Object? object, {Map<String, String> headers = const {}}) =>
|
Response _ok(Object? object, {Map<String, String> headers = const {}}) =>
|
||||||
Response.ok(
|
Response.ok(
|
||||||
|
|
@ -70,10 +66,7 @@ class PurchasesApiV2 {
|
||||||
/// Create purchase for a pack
|
/// Create purchase for a pack
|
||||||
@Route.post('/purchases/packs/<packId>')
|
@Route.post('/purchases/packs/<packId>')
|
||||||
@OpenApiRouteHttp()
|
@OpenApiRouteHttp()
|
||||||
Future<Response> createPackPurchase(
|
Future<Response> createPackPurchase(Request request, String packId) async {
|
||||||
Request request,
|
|
||||||
String packId,
|
|
||||||
) async {
|
|
||||||
try {
|
try {
|
||||||
print('🔍 createPackPurchase: Starting for packId=$packId');
|
print('🔍 createPackPurchase: Starting for packId=$packId');
|
||||||
final user = request.user;
|
final user = request.user;
|
||||||
|
|
@ -87,7 +80,9 @@ class PurchasesApiV2 {
|
||||||
return _unauthorized();
|
return _unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
print('🔍 createPackPurchase: Getting pack information for packId=$packId');
|
print(
|
||||||
|
'🔍 createPackPurchase: Getting pack information for packId=$packId',
|
||||||
|
);
|
||||||
// Get pack information
|
// Get pack information
|
||||||
final pack = await _packManager.getPack(packId);
|
final pack = await _packManager.getPack(packId);
|
||||||
if (pack == null) {
|
if (pack == null) {
|
||||||
|
|
@ -96,12 +91,11 @@ class PurchasesApiV2 {
|
||||||
}
|
}
|
||||||
print('✅ createPackPurchase: Pack found: ${pack.title}');
|
print('✅ createPackPurchase: Pack found: ${pack.title}');
|
||||||
|
|
||||||
print('🔍 createPackPurchase: Checking pack access for userId=${user.id}, packId=$packId');
|
print(
|
||||||
// Check if already purchased
|
'🔍 createPackPurchase: Checking pack access for userId=${user.id}, packId=$packId',
|
||||||
final hasAccess = await _db.userDao.hasPackAccess(
|
|
||||||
user.id!,
|
|
||||||
packId,
|
|
||||||
);
|
);
|
||||||
|
// Check if already purchased
|
||||||
|
final hasAccess = await _db.userDao.hasPackAccess(user.id!, packId);
|
||||||
if (hasAccess) {
|
if (hasAccess) {
|
||||||
print('❌ createPackPurchase: Pack already purchased');
|
print('❌ createPackPurchase: Pack already purchased');
|
||||||
return _badRequest('Pack is already purchased');
|
return _badRequest('Pack is already purchased');
|
||||||
|
|
@ -129,10 +123,7 @@ class PurchasesApiV2 {
|
||||||
|
|
||||||
// Create products list
|
// Create products list
|
||||||
final products = [
|
final products = [
|
||||||
MnemoCardsProductDto(
|
MnemoCardsProductDto(type: MnemoCardsProductType.pack, id: packId),
|
||||||
type: MnemoCardsProductType.pack,
|
|
||||||
id: packId,
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
print('🔍 createPackPurchase: Creating YooKassa payment URL');
|
print('🔍 createPackPurchase: Creating YooKassa payment URL');
|
||||||
|
|
@ -143,11 +134,14 @@ class PurchasesApiV2 {
|
||||||
userId: user.id!,
|
userId: user.id!,
|
||||||
products: products,
|
products: products,
|
||||||
);
|
);
|
||||||
print('✅ createPackPurchase: Payment URL created: ${paymentResult.confirmationUrl}, paymentId=${paymentResult.paymentId}');
|
print(
|
||||||
|
'✅ createPackPurchase: Payment URL created: ${paymentResult.confirmationUrl}, paymentId=${paymentResult.paymentId}',
|
||||||
|
);
|
||||||
|
|
||||||
// Build return URL for payment verification
|
// Build return URL for payment verification
|
||||||
final baseUri = request.requestedUri;
|
final baseUri = request.requestedUri;
|
||||||
final checkUrl = '${baseUri.scheme}://${baseUri.host}${baseUri.hasPort ? ':${baseUri.port}' : ''}/api/v2/purchases/payments/${paymentResult.paymentId}/verify?productId=$packId&productType=pack';
|
final checkUrl =
|
||||||
|
'${baseUri.scheme}://${baseUri.host}${baseUri.hasPort ? ':${baseUri.port}' : ''}/api/v2/purchases/payments/${paymentResult.paymentId}/verify?productId=$packId&productType=pack';
|
||||||
|
|
||||||
return _ok({
|
return _ok({
|
||||||
'purchaseUrl': paymentResult.confirmationUrl,
|
'purchaseUrl': paymentResult.confirmationUrl,
|
||||||
|
|
@ -207,10 +201,7 @@ class PurchasesApiV2 {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if already purchased
|
// Check if already purchased
|
||||||
final hasAccess = await _db.userDao.hasPackAccess(
|
final hasAccess = await _db.userDao.hasPackAccess(user.id!, productId);
|
||||||
user.id!,
|
|
||||||
productId,
|
|
||||||
);
|
|
||||||
if (hasAccess) {
|
if (hasAccess) {
|
||||||
return _badRequest('Pack is already purchased');
|
return _badRequest('Pack is already purchased');
|
||||||
}
|
}
|
||||||
|
|
@ -230,10 +221,7 @@ class PurchasesApiV2 {
|
||||||
|
|
||||||
description = 'Покупка пакета: ${pack.title}';
|
description = 'Покупка пакета: ${pack.title}';
|
||||||
products = [
|
products = [
|
||||||
MnemoCardsProductDto(
|
MnemoCardsProductDto(type: MnemoCardsProductType.pack, id: productId),
|
||||||
type: MnemoCardsProductType.pack,
|
|
||||||
id: productId,
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
} else if (productType == MnemoCardsProductType.subscription) {
|
} else if (productType == MnemoCardsProductType.subscription) {
|
||||||
// Get subscription plan
|
// Get subscription plan
|
||||||
|
|
@ -292,7 +280,8 @@ class PurchasesApiV2 {
|
||||||
|
|
||||||
// Build return URL for payment verification
|
// Build return URL for payment verification
|
||||||
final baseUri = request.requestedUri;
|
final baseUri = request.requestedUri;
|
||||||
final checkUrl = '${baseUri.scheme}://${baseUri.host}${baseUri.hasPort ? ':${baseUri.port}' : ''}/api/v2/purchases/payments/${paymentResult.paymentId}/verify?productId=$productId&productType=$productTypeStr';
|
final checkUrl =
|
||||||
|
'${baseUri.scheme}://${baseUri.host}${baseUri.hasPort ? ':${baseUri.port}' : ''}/api/v2/purchases/payments/${paymentResult.paymentId}/verify?productId=$productId&productType=$productTypeStr';
|
||||||
|
|
||||||
return _ok({
|
return _ok({
|
||||||
'purchaseUrl': paymentResult.confirmationUrl,
|
'purchaseUrl': paymentResult.confirmationUrl,
|
||||||
|
|
@ -309,10 +298,7 @@ class PurchasesApiV2 {
|
||||||
/// Verify payment status
|
/// Verify payment status
|
||||||
@Route.get('/purchases/payments/<paymentId>/verify')
|
@Route.get('/purchases/payments/<paymentId>/verify')
|
||||||
@OpenApiRouteHttp()
|
@OpenApiRouteHttp()
|
||||||
Future<Response> verifyPayment(
|
Future<Response> verifyPayment(Request request, String paymentId) async {
|
||||||
Request request,
|
|
||||||
String paymentId,
|
|
||||||
) async {
|
|
||||||
try {
|
try {
|
||||||
final user = request.user;
|
final user = request.user;
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
|
@ -360,10 +346,7 @@ class PurchasesApiV2 {
|
||||||
/// Check pack purchase status
|
/// Check pack purchase status
|
||||||
@Route.get('/purchases/packs/<packId>/status')
|
@Route.get('/purchases/packs/<packId>/status')
|
||||||
@OpenApiRouteHttp()
|
@OpenApiRouteHttp()
|
||||||
Future<Response> getPackPurchaseStatus(
|
Future<Response> getPackPurchaseStatus(Request request, String packId) async {
|
||||||
Request request,
|
|
||||||
String packId,
|
|
||||||
) async {
|
|
||||||
try {
|
try {
|
||||||
final user = request.user;
|
final user = request.user;
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
|
@ -384,13 +367,14 @@ class PurchasesApiV2 {
|
||||||
final isPurchased = await _db.userDao.hasPackAccess(user.id!, packId);
|
final isPurchased = await _db.userDao.hasPackAccess(user.id!, packId);
|
||||||
|
|
||||||
// Check subscription access
|
// Check subscription access
|
||||||
final activeSubscription = await _db.subscriptionDao.getActiveSubscription(
|
final activeSubscription = await _db.subscriptionDao
|
||||||
user.id!,
|
.getActiveSubscription(user.id!);
|
||||||
);
|
final hasSubscriptionAccess =
|
||||||
final hasSubscriptionAccess = activeSubscription != null &&
|
activeSubscription != null &&
|
||||||
(activeSubscription.features is List &&
|
(activeSubscription.features is List &&
|
||||||
(activeSubscription.features as List)
|
(activeSubscription.features as List).contains(
|
||||||
.contains(SubscriptionFeatureEnum.packs.name));
|
SubscriptionFeatureEnum.packs.name,
|
||||||
|
));
|
||||||
|
|
||||||
return _ok({
|
return _ok({
|
||||||
'packId': packId,
|
'packId': packId,
|
||||||
|
|
@ -399,11 +383,14 @@ class PurchasesApiV2 {
|
||||||
'hasSubscriptionAccess': hasSubscriptionAccess,
|
'hasSubscriptionAccess': hasSubscriptionAccess,
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
developer.log('Error in getPackPurchaseStatus: $e', error: e, stackTrace: s);
|
developer.log(
|
||||||
|
'Error in getPackPurchaseStatus: $e',
|
||||||
|
error: e,
|
||||||
|
stackTrace: s,
|
||||||
|
);
|
||||||
return _internalServerError(e.toString());
|
return _internalServerError(e.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Router get router => _$PurchasesApiV2Router(this);
|
Router get router => _$PurchasesApiV2Router(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,12 @@ mixin SoftDeleteMixin<T extends Table, D> on DatabaseAccessor<AppDatabase> {
|
||||||
///
|
///
|
||||||
/// Возвращает null если запись не найдена или удалена.
|
/// Возвращает null если запись не найдена или удалена.
|
||||||
Future<D?> getActiveById(String id) async {
|
Future<D?> getActiveById(String id) async {
|
||||||
print('🔍 SoftDeleteMixin.getActiveById: id=$id, table=${table.entityName}');
|
print(
|
||||||
print('🔍 SoftDeleteMixin.getActiveById: SQL: SELECT * FROM ${table.entityName} WHERE id = ? AND is_deleted = false LIMIT 1');
|
'🔍 SoftDeleteMixin.getActiveById: id=$id, table=${table.entityName}',
|
||||||
|
);
|
||||||
|
print(
|
||||||
|
'🔍 SoftDeleteMixin.getActiveById: SQL: SELECT * FROM ${table.entityName} WHERE id = ? AND is_deleted = false LIMIT 1',
|
||||||
|
);
|
||||||
final query = selectActive()
|
final query = selectActive()
|
||||||
..where((t) {
|
..where((t) {
|
||||||
final idColumn = (t as dynamic).id;
|
final idColumn = (t as dynamic).id;
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,9 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
|
||||||
/// Получить пак по ID
|
/// Получить пак по ID
|
||||||
Future<CardPack?> getPackById(String id) async {
|
Future<CardPack?> getPackById(String id) async {
|
||||||
print('🔍 PackDao.getPackById: packId=$id');
|
print('🔍 PackDao.getPackById: packId=$id');
|
||||||
print('🔍 PackDao.getPackById: SQL: SELECT * FROM card_packs WHERE id = ? LIMIT 1');
|
print(
|
||||||
|
'🔍 PackDao.getPackById: SQL: SELECT * FROM card_packs WHERE id = ? LIMIT 1',
|
||||||
|
);
|
||||||
final query = select(cardPacks)
|
final query = select(cardPacks)
|
||||||
..where((p) => p.id.equals(id))
|
..where((p) => p.id.equals(id))
|
||||||
..limit(1);
|
..limit(1);
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ class PaymentDao extends DatabaseAccessor<AppDatabase>
|
||||||
print('🔍 PaymentDao.getPaymentById: paymentId=$id');
|
print('🔍 PaymentDao.getPaymentById: paymentId=$id');
|
||||||
print('🔍 PaymentDao.getPaymentById: Calling getActiveById');
|
print('🔍 PaymentDao.getPaymentById: Calling getActiveById');
|
||||||
final result = await getActiveById(id);
|
final result = await getActiveById(id);
|
||||||
print('✅ PaymentDao.getPaymentById: Result=${result != null ? "found" : "not found"}');
|
print(
|
||||||
|
'✅ PaymentDao.getPaymentById: Result=${result != null ? "found" : "not found"}',
|
||||||
|
);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,48 +53,68 @@ class PaymentDao extends DatabaseAccessor<AppDatabase>
|
||||||
/// Создать платеж
|
/// Создать платеж
|
||||||
Future<String> createPayment(PaymentsCompanion payment) async {
|
Future<String> createPayment(PaymentsCompanion payment) async {
|
||||||
print('🔍 PaymentDao.createPayment: Starting payment creation');
|
print('🔍 PaymentDao.createPayment: Starting payment creation');
|
||||||
print('🔍 PaymentDao.createPayment: externalToken=${payment.externalToken.value}');
|
print(
|
||||||
|
'🔍 PaymentDao.createPayment: externalToken=${payment.externalToken.value}',
|
||||||
|
);
|
||||||
print('🔍 PaymentDao.createPayment: userId=${payment.userId.value}');
|
print('🔍 PaymentDao.createPayment: userId=${payment.userId.value}');
|
||||||
print('🔍 PaymentDao.createPayment: amount=${payment.amount.value}');
|
print('🔍 PaymentDao.createPayment: amount=${payment.amount.value}');
|
||||||
|
|
||||||
final inserted = await into(payments).insertReturning(payment);
|
final inserted = await into(payments).insertReturning(payment);
|
||||||
print('✅ PaymentDao.createPayment: Payment inserted, date=${inserted.date.dateTime}');
|
print(
|
||||||
|
'✅ PaymentDao.createPayment: Payment inserted, date=${inserted.date.dateTime}',
|
||||||
|
);
|
||||||
|
|
||||||
// Try to get id from inserted object first
|
// Try to get id from inserted object first
|
||||||
if (inserted.id.isNotEmpty) {
|
if (inserted.id.isNotEmpty) {
|
||||||
print('✅ PaymentDao.createPayment: Got id from inserted object, id=${inserted.id}');
|
print(
|
||||||
|
'✅ PaymentDao.createPayment: Got id from inserted object, id=${inserted.id}',
|
||||||
|
);
|
||||||
return inserted.id;
|
return inserted.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query back to get the id using type-safe queries
|
// Query back to get the id using type-safe queries
|
||||||
// Use externalToken if available, otherwise query by unique fields
|
// Use externalToken if available, otherwise query by unique fields
|
||||||
if (inserted.externalToken != null && inserted.externalToken!.isNotEmpty) {
|
if (inserted.externalToken != null && inserted.externalToken!.isNotEmpty) {
|
||||||
print('🔍 PaymentDao.createPayment: Querying by externalToken=${inserted.externalToken}');
|
print(
|
||||||
final foundPayment = await getPaymentByExternalToken(inserted.externalToken!);
|
'🔍 PaymentDao.createPayment: Querying by externalToken=${inserted.externalToken}',
|
||||||
|
);
|
||||||
|
final foundPayment = await getPaymentByExternalToken(
|
||||||
|
inserted.externalToken!,
|
||||||
|
);
|
||||||
if (foundPayment != null && foundPayment.id.isNotEmpty) {
|
if (foundPayment != null && foundPayment.id.isNotEmpty) {
|
||||||
print('✅ PaymentDao.createPayment: Found payment by externalToken, id=${foundPayment.id}');
|
print(
|
||||||
|
'✅ PaymentDao.createPayment: Found payment by externalToken, id=${foundPayment.id}',
|
||||||
|
);
|
||||||
return foundPayment.id;
|
return foundPayment.id;
|
||||||
}
|
}
|
||||||
print('⚠️ PaymentDao.createPayment: No payment found by externalToken');
|
print('⚠️ PaymentDao.createPayment: No payment found by externalToken');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: query by userId, date, and amount using type-safe queries
|
// Fallback: query by userId, date, and amount using type-safe queries
|
||||||
print('🔍 PaymentDao.createPayment: Querying by userId, date, amount (fallback)');
|
print(
|
||||||
|
'🔍 PaymentDao.createPayment: Querying by userId, date, amount (fallback)',
|
||||||
|
);
|
||||||
final query = select(payments)
|
final query = select(payments)
|
||||||
..where((p) =>
|
..where(
|
||||||
p.userId.equals(payment.userId.value) &
|
(p) =>
|
||||||
p.date.equals(inserted.date) &
|
p.userId.equals(payment.userId.value) &
|
||||||
p.amount.equals(inserted.amount))
|
p.date.equals(inserted.date) &
|
||||||
|
p.amount.equals(inserted.amount),
|
||||||
|
)
|
||||||
..orderBy([(p) => OrderingTerm.desc(p.createdAt)])
|
..orderBy([(p) => OrderingTerm.desc(p.createdAt)])
|
||||||
..limit(1);
|
..limit(1);
|
||||||
|
|
||||||
final results = await query.get();
|
final results = await query.get();
|
||||||
if (results.isNotEmpty && results.first.id.isNotEmpty) {
|
if (results.isNotEmpty && results.first.id.isNotEmpty) {
|
||||||
print('✅ PaymentDao.createPayment: Found payment by fallback query, id=${results.first.id}');
|
print(
|
||||||
|
'✅ PaymentDao.createPayment: Found payment by fallback query, id=${results.first.id}',
|
||||||
|
);
|
||||||
return results.first.id;
|
return results.first.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
print('❌ PaymentDao.createPayment: Failed to retrieve payment ID after creation');
|
print(
|
||||||
|
'❌ PaymentDao.createPayment: Failed to retrieve payment ID after creation',
|
||||||
|
);
|
||||||
throw Exception('Failed to retrieve payment ID after creation');
|
throw Exception('Failed to retrieve payment ID after creation');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,12 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
|
||||||
|
|
||||||
/// Получить пользователя с UserData
|
/// Получить пользователя с UserData
|
||||||
Future<UserWithData?> getUserWithDataById(String id) async {
|
Future<UserWithData?> getUserWithDataById(String id) async {
|
||||||
final query = select(users).join([
|
final query =
|
||||||
leftOuterJoin(userDatas, userDatas.userId.equalsExp(users.id)),
|
select(users).join([
|
||||||
])..where(users.id.equals(id))
|
leftOuterJoin(userDatas, userDatas.userId.equalsExp(users.id)),
|
||||||
..limit(1);
|
])
|
||||||
|
..where(users.id.equals(id))
|
||||||
|
..limit(1);
|
||||||
|
|
||||||
final results = await query.get();
|
final results = await query.get();
|
||||||
if (results.isEmpty) return null;
|
if (results.isEmpty) return null;
|
||||||
|
|
@ -146,7 +148,9 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
|
||||||
/// Получить UserData пользователя
|
/// Получить UserData пользователя
|
||||||
Future<UserData?> getUserData(String userId) async {
|
Future<UserData?> getUserData(String userId) async {
|
||||||
print('🔍 UserDao.getUserData: userId=$userId');
|
print('🔍 UserDao.getUserData: userId=$userId');
|
||||||
print('🔍 UserDao.getUserData: SQL: SELECT * FROM user_datas WHERE user_id = ? LIMIT 1');
|
print(
|
||||||
|
'🔍 UserDao.getUserData: SQL: SELECT * FROM user_datas WHERE user_id = ? LIMIT 1',
|
||||||
|
);
|
||||||
final query = select(userDatas)
|
final query = select(userDatas)
|
||||||
..where((ud) => ud.userId.equals(userId))
|
..where((ud) => ud.userId.equals(userId))
|
||||||
..limit(1);
|
..limit(1);
|
||||||
|
|
@ -202,8 +206,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
|
||||||
final query = select(tokens)
|
final query = select(tokens)
|
||||||
..where(
|
..where(
|
||||||
(t) =>
|
(t) =>
|
||||||
t.externalUserId.equals(externalUserId) &
|
t.externalUserId.equals(externalUserId) & t.isDeleted.equals(false),
|
||||||
t.isDeleted.equals(false),
|
|
||||||
)
|
)
|
||||||
..orderBy([(t) => OrderingTerm.desc(t.created)])
|
..orderBy([(t) => OrderingTerm.desc(t.created)])
|
||||||
..limit(1);
|
..limit(1);
|
||||||
|
|
@ -216,9 +219,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
|
||||||
Future<Token?> getTokenByUserId(String userId) async {
|
Future<Token?> getTokenByUserId(String userId) async {
|
||||||
final query = select(tokens)
|
final query = select(tokens)
|
||||||
..where((t) => t.userId.equals(userId) & t.isDeleted.equals(false))
|
..where((t) => t.userId.equals(userId) & t.isDeleted.equals(false))
|
||||||
..where(
|
..where((t) => t.expires.isBiggerThanValue(PgDateTime(DateTime.now())))
|
||||||
(t) => t.expires.isBiggerThanValue(PgDateTime(DateTime.now())),
|
|
||||||
)
|
|
||||||
..orderBy([(t) => OrderingTerm.desc(t.created)])
|
..orderBy([(t) => OrderingTerm.desc(t.created)])
|
||||||
..limit(1);
|
..limit(1);
|
||||||
|
|
||||||
|
|
@ -397,13 +398,17 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
|
||||||
/// Проверить, есть ли у пользователя доступ к паку
|
/// Проверить, есть ли у пользователя доступ к паку
|
||||||
Future<bool> hasPackAccess(String userId, String packId) async {
|
Future<bool> hasPackAccess(String userId, String packId) async {
|
||||||
print('🔍 UserDao.hasPackAccess: userId=$userId, packId=$packId');
|
print('🔍 UserDao.hasPackAccess: userId=$userId, packId=$packId');
|
||||||
print('🔍 UserDao.hasPackAccess: SQL: SELECT * FROM user_packs WHERE user_id = ? AND pack_id = ? LIMIT 1');
|
print(
|
||||||
|
'🔍 UserDao.hasPackAccess: SQL: SELECT * FROM user_packs WHERE user_id = ? AND pack_id = ? LIMIT 1',
|
||||||
|
);
|
||||||
final query = select(userPacks)
|
final query = select(userPacks)
|
||||||
..where((up) => up.userId.equals(userId) & up.packId.equals(packId))
|
..where((up) => up.userId.equals(userId) & up.packId.equals(packId))
|
||||||
..limit(1);
|
..limit(1);
|
||||||
|
|
||||||
final results = await query.get();
|
final results = await query.get();
|
||||||
print('✅ UserDao.hasPackAccess: Result count=${results.length}, hasAccess=${results.isNotEmpty}');
|
print(
|
||||||
|
'✅ UserDao.hasPackAccess: Result count=${results.length}, hasAccess=${results.isNotEmpty}',
|
||||||
|
);
|
||||||
return results.isNotEmpty;
|
return results.isNotEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -337,9 +337,9 @@ class AppDatabase extends _$AppDatabase {
|
||||||
// 2. Удаляем все старые вопросы (soft delete)
|
// 2. Удаляем все старые вопросы (soft delete)
|
||||||
print('Soft-deleting all existing test questions...');
|
print('Soft-deleting all existing test questions...');
|
||||||
final now = PgDateTime(DateTime.now());
|
final now = PgDateTime(DateTime.now());
|
||||||
await (update(testQuestions)
|
await (update(
|
||||||
..where((tq) => tq.isDeleted.equals(false)))
|
testQuestions,
|
||||||
.write(
|
)..where((tq) => tq.isDeleted.equals(false))).write(
|
||||||
TestQuestionsCompanion(
|
TestQuestionsCompanion(
|
||||||
isDeleted: const Value(true),
|
isDeleted: const Value(true),
|
||||||
deletedAt: Value(now),
|
deletedAt: Value(now),
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,8 @@ class PackDtoConverter {
|
||||||
final coverValue = model.cover!.trim();
|
final coverValue = model.cover!.trim();
|
||||||
|
|
||||||
// If it's already a remote URL, use it directly
|
// If it's already a remote URL, use it directly
|
||||||
if (coverValue.startsWith('http://') || coverValue.startsWith('https://')) {
|
if (coverValue.startsWith('http://') ||
|
||||||
|
coverValue.startsWith('https://')) {
|
||||||
coverUrl = coverValue;
|
coverUrl = coverValue;
|
||||||
}
|
}
|
||||||
// If it's a UUID or any other value, use API endpoint
|
// If it's a UUID or any other value, use API endpoint
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,11 @@ class InputButtonsQuestionGenerator implements QuestionGenerator {
|
||||||
final templateToAnswerIndex = <int, int>{};
|
final templateToAnswerIndex = <int, int>{};
|
||||||
int answerLetterIndex = 0;
|
int answerLetterIndex = 0;
|
||||||
|
|
||||||
for (int i = 0; i < template.length && answerLetterIndex < answerLetters.length; i++) {
|
for (
|
||||||
|
int i = 0;
|
||||||
|
i < template.length && answerLetterIndex < answerLetters.length;
|
||||||
|
i++
|
||||||
|
) {
|
||||||
if (template[i] == '_' || template[i] == '|') {
|
if (template[i] == '_' || template[i] == '|') {
|
||||||
templateToAnswerIndex[i] = answerLetterIndex;
|
templateToAnswerIndex[i] = answerLetterIndex;
|
||||||
answerLetterIndex++;
|
answerLetterIndex++;
|
||||||
|
|
@ -146,15 +150,21 @@ class InputButtonsQuestionGenerator implements QuestionGenerator {
|
||||||
// Skip other characters in template (visible letters, spaces)
|
// Skip other characters in template (visible letters, spaces)
|
||||||
}
|
}
|
||||||
|
|
||||||
int visibleLetters = (visibleButtonsPercent * slotPositions.length).floor();
|
int visibleLetters = (visibleButtonsPercent * slotPositions.length)
|
||||||
|
.floor();
|
||||||
final shuffledPositions = List<int>.from(slotPositions)..shuffle(random);
|
final shuffledPositions = List<int>.from(slotPositions)..shuffle(random);
|
||||||
final positionsToReveal = shuffledPositions.take(visibleLetters).toList();
|
final positionsToReveal = shuffledPositions.take(visibleLetters).toList();
|
||||||
|
|
||||||
// Replace slots with actual letters from answer
|
// Replace slots with actual letters from answer
|
||||||
for (final pos in positionsToReveal) {
|
for (final pos in positionsToReveal) {
|
||||||
final answerLetterIndex = templateToAnswerIndex[pos];
|
final answerLetterIndex = templateToAnswerIndex[pos];
|
||||||
if (answerLetterIndex != null && answerLetterIndex < answerLetters.length) {
|
if (answerLetterIndex != null &&
|
||||||
template = template.replaceRange(pos, pos + 1, answerLetters[answerLetterIndex]);
|
answerLetterIndex < answerLetters.length) {
|
||||||
|
template = template.replaceRange(
|
||||||
|
pos,
|
||||||
|
pos + 1,
|
||||||
|
answerLetters[answerLetterIndex],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,3 +50,4 @@ VACUUM FULL ANALYZE payments;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -123,3 +123,4 @@ DROP TYPE IF EXISTS grant_type CASCADE;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -195,3 +195,4 @@ PRINT 'Migration 003 completed successfully!';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,33 +29,25 @@ void main() {
|
||||||
late AccessService accessService;
|
late AccessService accessService;
|
||||||
late UserModel adminUser;
|
late UserModel adminUser;
|
||||||
|
|
||||||
Request buildAdminRequest(
|
Request buildAdminRequest(String method, String url, {Object? body}) {
|
||||||
String method,
|
|
||||||
String url, {
|
|
||||||
Object? body,
|
|
||||||
}) {
|
|
||||||
return Request(
|
return Request(
|
||||||
method,
|
method,
|
||||||
Uri.parse(url),
|
Uri.parse(url),
|
||||||
body: body == null ? null : jsonEncode(body),
|
body: body == null ? null : jsonEncode(body),
|
||||||
).change(
|
).change(context: {'user': adminUser, 'accessService': accessService});
|
||||||
context: {
|
|
||||||
'user': adminUser,
|
|
||||||
'accessService': accessService,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setUpAll(() async {
|
setUpAll(() async {
|
||||||
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
|
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
|
||||||
final port =
|
final port =
|
||||||
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
|
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
|
||||||
final database =
|
final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
||||||
Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
final username =
|
||||||
final username = Platform.environment['TEST_DB_USER'] ??
|
Platform.environment['TEST_DB_USER'] ??
|
||||||
Platform.environment['DB_USER'] ??
|
Platform.environment['DB_USER'] ??
|
||||||
'mnemo_user';
|
'mnemo_user';
|
||||||
final password = Platform.environment['TEST_DB_PASSWORD'] ??
|
final password =
|
||||||
|
Platform.environment['TEST_DB_PASSWORD'] ??
|
||||||
Platform.environment['DB_PASSWORD'] ??
|
Platform.environment['DB_PASSWORD'] ??
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
|
@ -78,7 +70,10 @@ void main() {
|
||||||
final discountsManager = DiscountsManager(db);
|
final discountsManager = DiscountsManager(db);
|
||||||
final productsPriceResolver = ProductsPriceResolver(discountsManager, db);
|
final productsPriceResolver = ProductsPriceResolver(discountsManager, db);
|
||||||
final adsManager = AdsManager();
|
final adsManager = AdsManager();
|
||||||
final packDtoConverter = PackDtoConverter(productsPriceResolver, adsManager);
|
final packDtoConverter = PackDtoConverter(
|
||||||
|
productsPriceResolver,
|
||||||
|
adsManager,
|
||||||
|
);
|
||||||
final packManager = PackManager(db, packDtoConverter);
|
final packManager = PackManager(db, packDtoConverter);
|
||||||
|
|
||||||
final resourceLoader = ResourceLoader(packManager);
|
final resourceLoader = ResourceLoader(packManager);
|
||||||
|
|
@ -107,23 +102,25 @@ void main() {
|
||||||
Future<void> cleanup() async {
|
Future<void> cleanup() async {
|
||||||
// Delete relations first.
|
// Delete relations first.
|
||||||
if (testId != null) {
|
if (testId != null) {
|
||||||
await (db.delete(db.testPackRelations)
|
await (db.delete(
|
||||||
..where((r) => r.testId.equals(testId!)))
|
db.testPackRelations,
|
||||||
.go();
|
)..where((r) => r.testId.equals(testId!))).go();
|
||||||
|
|
||||||
await (db.delete(db.testQuestions)
|
await (db.delete(
|
||||||
..where((q) => q.testId.equals(testId!)))
|
db.testQuestions,
|
||||||
.go();
|
)..where((q) => q.testId.equals(testId!))).go();
|
||||||
|
|
||||||
await (db.delete(db.tests)..where((t) => t.id.equals(testId!))).go();
|
await (db.delete(db.tests)..where((t) => t.id.equals(testId!))).go();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (packId != null) {
|
if (packId != null) {
|
||||||
await (db.delete(db.cardPackCards)
|
await (db.delete(
|
||||||
..where((c) => c.packId.equals(packId!)))
|
db.cardPackCards,
|
||||||
.go();
|
)..where((c) => c.packId.equals(packId!))).go();
|
||||||
|
|
||||||
await (db.delete(db.cardPacks)..where((p) => p.id.equals(packId!))).go();
|
await (db.delete(
|
||||||
|
db.cardPacks,
|
||||||
|
)..where((p) => p.id.equals(packId!))).go();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final cardId in createdCardIds) {
|
for (final cardId in createdCardIds) {
|
||||||
|
|
@ -131,7 +128,9 @@ void main() {
|
||||||
await (db.delete(db.gameCards)..where((c) => c.id.equals(cardId))).go();
|
await (db.delete(db.gameCards)..where((c) => c.id.equals(cardId))).go();
|
||||||
|
|
||||||
// Remove possible image files in data/cards
|
// Remove possible image files in data/cards
|
||||||
final cardsDir = Directory('${PackManagerUtils.assetsDirectory.path}/cards');
|
final cardsDir = Directory(
|
||||||
|
'${PackManagerUtils.assetsDirectory.path}/cards',
|
||||||
|
);
|
||||||
final candidates = [
|
final candidates = [
|
||||||
File('${cardsDir.path}/$cardId.png'),
|
File('${cardsDir.path}/$cardId.png'),
|
||||||
File('${cardsDir.path}/$cardId.webp'),
|
File('${cardsDir.path}/$cardId.webp'),
|
||||||
|
|
@ -159,144 +158,162 @@ void main() {
|
||||||
await cleanup();
|
await cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getTest returns image URLs (no base64) and cards are linked to pack',
|
test(
|
||||||
() async {
|
'getTest returns image URLs (no base64) and cards are linked to pack',
|
||||||
// Create pack
|
() async {
|
||||||
packId = await db.packDao.createPack(
|
// Create pack
|
||||||
CardPacksCompanion.insert(
|
packId = await db.packDao.createPack(
|
||||||
title: 'pack for admin tests images',
|
CardPacksCompanion.insert(
|
||||||
subtitle: 'subtitle',
|
title: 'pack for admin tests images',
|
||||||
size: 0,
|
subtitle: 'subtitle',
|
||||||
),
|
size: 0,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Create test with base64 cover + base64 question/button images
|
// Create test with base64 cover + base64 question/button images
|
||||||
final createRequest = buildAdminRequest(
|
final createRequest = buildAdminRequest(
|
||||||
'POST',
|
'POST',
|
||||||
'http://localhost/api/v2/admin/tests',
|
'http://localhost/api/v2/admin/tests',
|
||||||
body: {
|
body: {
|
||||||
'name': 'test with images',
|
'name': 'test with images',
|
||||||
'cover': oneByOnePngBase64,
|
'cover': oneByOnePngBase64,
|
||||||
'questions': [
|
'questions': [
|
||||||
{
|
{
|
||||||
'questionType': 'simple',
|
'questionType': 'simple',
|
||||||
'word': 'word',
|
'word': 'word',
|
||||||
'answer': 'btn1',
|
'answer': 'btn1',
|
||||||
'image': oneByOnePngBase64,
|
'image': oneByOnePngBase64,
|
||||||
'buttons': [
|
'buttons': [
|
||||||
{
|
{'id': 'btn1', 'text': 'ok', 'image': oneByOnePngBase64},
|
||||||
'id': 'btn1',
|
],
|
||||||
'text': 'ok',
|
},
|
||||||
'image': oneByOnePngBase64,
|
],
|
||||||
},
|
},
|
||||||
],
|
);
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
final createResp = await adminTestsApi.upsertTest(createRequest);
|
final createResp = await adminTestsApi.upsertTest(createRequest);
|
||||||
expect(createResp.statusCode, anyOf(equals(201), equals(200)));
|
expect(createResp.statusCode, anyOf(equals(201), equals(200)));
|
||||||
final createBody = jsonDecode(await createResp.readAsString())
|
final createBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await createResp.readAsString()) as Map<String, dynamic>;
|
||||||
testId = (createBody['test'] as Map<String, dynamic>)['id'] as String;
|
testId = (createBody['test'] as Map<String, dynamic>)['id'] as String;
|
||||||
expect(testId, isNotEmpty);
|
expect(testId, isNotEmpty);
|
||||||
|
|
||||||
// Link test to pack via admin packs API (this must also link referenced cards)
|
// Link test to pack via admin packs API (this must also link referenced cards)
|
||||||
final linkRequest = buildAdminRequest(
|
final linkRequest = buildAdminRequest(
|
||||||
'POST',
|
'POST',
|
||||||
'http://localhost/api/v2/admin/packs',
|
'http://localhost/api/v2/admin/packs',
|
||||||
body: {
|
body: {
|
||||||
'id': packId,
|
'id': packId,
|
||||||
'title': 'pack for admin tests images',
|
'title': 'pack for admin tests images',
|
||||||
'addTestIds': [testId],
|
'addTestIds': [testId],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
final linkResp = await adminPacksApi.upsertPack(linkRequest);
|
final linkResp = await adminPacksApi.upsertPack(linkRequest);
|
||||||
expect(linkResp.statusCode, equals(200));
|
expect(linkResp.statusCode, equals(200));
|
||||||
|
|
||||||
// Fetch test and verify URLs
|
// Fetch test and verify URLs
|
||||||
final getReq = buildAdminRequest(
|
final getReq = buildAdminRequest(
|
||||||
'GET',
|
'GET',
|
||||||
'http://localhost/api/v2/admin/tests/$testId',
|
'http://localhost/api/v2/admin/tests/$testId',
|
||||||
);
|
);
|
||||||
final getResp = await adminTestsApi.getTest(getReq, testId!);
|
final getResp = await adminTestsApi.getTest(getReq, testId!);
|
||||||
expect(getResp.statusCode, equals(200));
|
expect(getResp.statusCode, equals(200));
|
||||||
|
|
||||||
final body = jsonDecode(await getResp.readAsString())
|
final body =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await getResp.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
final cover = body['cover'] as String?;
|
final cover = body['cover'] as String?;
|
||||||
expect(cover, isNotNull);
|
expect(cover, isNotNull);
|
||||||
expect(cover, isNot(contains(oneByOnePngBase64)));
|
expect(cover, isNot(contains(oneByOnePngBase64)));
|
||||||
expect(cover, startsWith('/api/v2/packs/$packId/cards/'));
|
expect(cover, startsWith('/api/v2/packs/$packId/cards/'));
|
||||||
expect(cover, endsWith('/image'));
|
expect(cover, endsWith('/image'));
|
||||||
|
|
||||||
final questions = body['questions'] as List<dynamic>;
|
final questions = body['questions'] as List<dynamic>;
|
||||||
expect(questions, hasLength(1));
|
expect(questions, hasLength(1));
|
||||||
final q0 = questions.first as Map<String, dynamic>;
|
final q0 = questions.first as Map<String, dynamic>;
|
||||||
|
|
||||||
final qImage = q0['image'] as String?;
|
final qImage = q0['image'] as String?;
|
||||||
expect(qImage, isNotNull);
|
expect(qImage, isNotNull);
|
||||||
expect(qImage, isNot(contains(oneByOnePngBase64)));
|
expect(qImage, isNot(contains(oneByOnePngBase64)));
|
||||||
expect(qImage, startsWith('/api/v2/packs/$packId/cards/'));
|
expect(qImage, startsWith('/api/v2/packs/$packId/cards/'));
|
||||||
expect(qImage, endsWith('/image'));
|
expect(qImage, endsWith('/image'));
|
||||||
|
|
||||||
final buttons = q0['buttons'] as List<dynamic>;
|
final buttons = q0['buttons'] as List<dynamic>;
|
||||||
expect(buttons, hasLength(1));
|
expect(buttons, hasLength(1));
|
||||||
final b0 = buttons.first as Map<String, dynamic>;
|
final b0 = buttons.first as Map<String, dynamic>;
|
||||||
final bImage = b0['image'] as String?;
|
final bImage = b0['image'] as String?;
|
||||||
expect(bImage, isNotNull);
|
expect(bImage, isNotNull);
|
||||||
expect(bImage, isNot(contains(oneByOnePngBase64)));
|
expect(bImage, isNot(contains(oneByOnePngBase64)));
|
||||||
expect(bImage, startsWith('/api/v2/packs/$packId/cards/'));
|
expect(bImage, startsWith('/api/v2/packs/$packId/cards/'));
|
||||||
expect(bImage, endsWith('/image'));
|
expect(bImage, endsWith('/image'));
|
||||||
|
|
||||||
// Extract card ids from URLs and ensure they are linked to the pack.
|
// Extract card ids from URLs and ensure they are linked to the pack.
|
||||||
final extractCardId = (String url) {
|
final extractCardId = (String url) {
|
||||||
final match = RegExp(r'/cards/([^/]+)/image$').firstMatch(url);
|
final match = RegExp(r'/cards/([^/]+)/image$').firstMatch(url);
|
||||||
return match?.group(1);
|
return match?.group(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
final coverCardId = extractCardId(cover!);
|
final coverCardId = extractCardId(cover!);
|
||||||
final qCardId = extractCardId(qImage!);
|
final qCardId = extractCardId(qImage!);
|
||||||
final bCardId = extractCardId(bImage!);
|
final bCardId = extractCardId(bImage!);
|
||||||
|
|
||||||
expect(coverCardId, isNotNull);
|
expect(coverCardId, isNotNull);
|
||||||
expect(qCardId, isNotNull);
|
expect(qCardId, isNotNull);
|
||||||
expect(bCardId, isNotNull);
|
expect(bCardId, isNotNull);
|
||||||
|
|
||||||
createdCardIds.addAll([coverCardId!, qCardId!, bCardId!]);
|
createdCardIds.addAll([coverCardId!, qCardId!, bCardId!]);
|
||||||
|
|
||||||
final linked = await (db.select(db.cardPackCards)
|
final linked =
|
||||||
..where((c) => c.packId.equals(packId!) &
|
await (db.select(db.cardPackCards)..where(
|
||||||
c.cardId.isIn([coverCardId, qCardId, bCardId])))
|
(c) =>
|
||||||
.get();
|
c.packId.equals(packId!) &
|
||||||
expect(linked.map((e) => e.cardId).toSet(),
|
c.cardId.isIn([coverCardId, qCardId, bCardId]),
|
||||||
containsAll([coverCardId, qCardId, bCardId]));
|
))
|
||||||
|
.get();
|
||||||
|
expect(
|
||||||
|
linked.map((e) => e.cardId).toSet(),
|
||||||
|
containsAll([coverCardId, qCardId, bCardId]),
|
||||||
|
);
|
||||||
|
|
||||||
// Also ensure DB stores cardIds, not API URLs/base64.
|
// Also ensure DB stores cardIds, not API URLs/base64.
|
||||||
final dbQuestions = await db.testDao.getTestQuestions(testId!);
|
final dbQuestions = await db.testDao.getTestQuestions(testId!);
|
||||||
expect(dbQuestions, hasLength(1));
|
expect(dbQuestions, hasLength(1));
|
||||||
final qDb = dbQuestions.single;
|
final qDb = dbQuestions.single;
|
||||||
final options = jsonDecode(qDb.options) as List<dynamic>;
|
final options = jsonDecode(qDb.options) as List<dynamic>;
|
||||||
final uiData = jsonDecode(qDb.uiData) as Map<String, dynamic>;
|
final uiData = jsonDecode(qDb.uiData) as Map<String, dynamic>;
|
||||||
|
|
||||||
final storedUiImage = uiData['image']?.toString();
|
final storedUiImage = uiData['image']?.toString();
|
||||||
expect(storedUiImage, isNotNull);
|
expect(storedUiImage, isNotNull);
|
||||||
expect(RegExp(r'^[0-9a-f\-]{36}$', caseSensitive: false)
|
expect(
|
||||||
.hasMatch(storedUiImage!), isTrue);
|
RegExp(
|
||||||
|
r'^[0-9a-f\-]{36}$',
|
||||||
|
caseSensitive: false,
|
||||||
|
).hasMatch(storedUiImage!),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
|
||||||
final storedBtnImage =
|
final storedBtnImage = (options.first as Map<String, dynamic>)['image']
|
||||||
(options.first as Map<String, dynamic>)['image']?.toString();
|
?.toString();
|
||||||
expect(storedBtnImage, isNotNull);
|
expect(storedBtnImage, isNotNull);
|
||||||
expect(RegExp(r'^[0-9a-f\-]{36}$', caseSensitive: false)
|
expect(
|
||||||
.hasMatch(storedBtnImage!), isTrue);
|
RegExp(
|
||||||
|
r'^[0-9a-f\-]{36}$',
|
||||||
|
caseSensitive: false,
|
||||||
|
).hasMatch(storedBtnImage!),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
|
||||||
final storedCover = (await db.testDao.getTestById(testId!))!.cover;
|
final storedCover = (await db.testDao.getTestById(testId!))!.cover;
|
||||||
expect(storedCover, isNotNull);
|
expect(storedCover, isNotNull);
|
||||||
expect(RegExp(r'^[0-9a-f\-]{36}$', caseSensitive: false)
|
expect(
|
||||||
.hasMatch(storedCover!), isTrue);
|
RegExp(
|
||||||
});
|
r'^[0-9a-f\-]{36}$',
|
||||||
|
caseSensitive: false,
|
||||||
|
).hasMatch(storedCover!),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,11 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
authApiV2 = AuthApiV2(
|
authApiV2 = AuthApiV2(
|
||||||
userManager, mockGoogleApi, jwtService, telegramAuthCodeService);
|
userManager,
|
||||||
|
mockGoogleApi,
|
||||||
|
jwtService,
|
||||||
|
telegramAuthCodeService,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
|
|
@ -101,8 +105,9 @@ void main() {
|
||||||
const email = 'user@example.com';
|
const email = 'user@example.com';
|
||||||
const name = 'Test User';
|
const name = 'Test User';
|
||||||
|
|
||||||
when(mockGoogleApi.getGoogleUserId(googleIdToken))
|
when(
|
||||||
.thenAnswer((_) async => googleUserId);
|
mockGoogleApi.getGoogleUserId(googleIdToken),
|
||||||
|
).thenAnswer((_) async => googleUserId);
|
||||||
|
|
||||||
final request = Request(
|
final request = Request(
|
||||||
'POST',
|
'POST',
|
||||||
|
|
@ -151,8 +156,9 @@ void main() {
|
||||||
test('should return 401 when Google token is invalid', () async {
|
test('should return 401 when Google token is invalid', () async {
|
||||||
const googleIdToken = 'invalid_google_token';
|
const googleIdToken = 'invalid_google_token';
|
||||||
|
|
||||||
when(mockGoogleApi.getGoogleUserId(googleIdToken))
|
when(
|
||||||
.thenAnswer((_) async => null);
|
mockGoogleApi.getGoogleUserId(googleIdToken),
|
||||||
|
).thenAnswer((_) async => null);
|
||||||
|
|
||||||
final request = Request(
|
final request = Request(
|
||||||
'POST',
|
'POST',
|
||||||
|
|
@ -267,9 +273,7 @@ void main() {
|
||||||
final request = Request(
|
final request = Request(
|
||||||
'GET',
|
'GET',
|
||||||
Uri.parse('http://localhost/api/v2/auth/me'),
|
Uri.parse('http://localhost/api/v2/auth/me'),
|
||||||
headers: {
|
headers: {'Authorization': 'Bearer ${tokens.accessToken}'},
|
||||||
'Authorization': 'Bearer ${tokens.accessToken}',
|
|
||||||
},
|
|
||||||
).change(context: {'user': testUser});
|
).change(context: {'user': testUser});
|
||||||
|
|
||||||
final response = await authApiV2.getCurrentUser(request);
|
final response = await authApiV2.getCurrentUser(request);
|
||||||
|
|
@ -340,9 +344,7 @@ void main() {
|
||||||
'POST',
|
'POST',
|
||||||
Uri.parse('http://localhost/api/v2/auth/logout'),
|
Uri.parse('http://localhost/api/v2/auth/logout'),
|
||||||
body: '',
|
body: '',
|
||||||
headers: {
|
headers: {'Authorization': 'Bearer ${tokens.accessToken}'},
|
||||||
'Authorization': 'Bearer ${tokens.accessToken}',
|
|
||||||
},
|
|
||||||
).change(context: {'user': testUser});
|
).change(context: {'user': testUser});
|
||||||
|
|
||||||
final response = await authApiV2.logout(request);
|
final response = await authApiV2.logout(request);
|
||||||
|
|
@ -369,117 +371,132 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
group('AuthApiV2 - Telegram Web Code Flow', () {
|
group('AuthApiV2 - Telegram Web Code Flow', () {
|
||||||
test('should create, claim, and authenticate using web-generated code',
|
test(
|
||||||
() async {
|
'should create, claim, and authenticate using web-generated code',
|
||||||
// Step 1: create code from web
|
() async {
|
||||||
final createRequest = Request(
|
// Step 1: create code from web
|
||||||
'POST',
|
final createRequest = Request(
|
||||||
Uri.parse('http://localhost/api/v2/auth/telegram/web-code'),
|
'POST',
|
||||||
body: '',
|
Uri.parse('http://localhost/api/v2/auth/telegram/web-code'),
|
||||||
);
|
body: '',
|
||||||
|
);
|
||||||
|
|
||||||
final createResponse =
|
final createResponse = await authApiV2.createWebTelegramCode(
|
||||||
await authApiV2.createWebTelegramCode(createRequest);
|
createRequest,
|
||||||
final createBody = jsonDecode(await createResponse.readAsString())
|
);
|
||||||
as Map<String, dynamic>;
|
final createBody =
|
||||||
|
jsonDecode(await createResponse.readAsString())
|
||||||
|
as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(createResponse.statusCode, equals(200));
|
expect(createResponse.statusCode, equals(200));
|
||||||
final code = createBody['code'] as String?;
|
final code = createBody['code'] as String?;
|
||||||
expect(code, isNotNull);
|
expect(code, isNotNull);
|
||||||
expect(createBody['status'], equals('pending'));
|
expect(createBody['status'], equals('pending'));
|
||||||
expect(createBody['remainingSeconds'], greaterThan(0));
|
expect(createBody['remainingSeconds'], greaterThan(0));
|
||||||
expect(createBody['remainingSeconds'], lessThanOrEqualTo(300));
|
expect(createBody['remainingSeconds'], lessThanOrEqualTo(300));
|
||||||
|
|
||||||
// Step 2: status should be pending before claim
|
// Step 2: status should be pending before claim
|
||||||
final statusRequest = Request(
|
final statusRequest = Request(
|
||||||
'GET',
|
'GET',
|
||||||
Uri.parse('http://localhost/api/v2/auth/telegram/code-status/$code'),
|
Uri.parse('http://localhost/api/v2/auth/telegram/code-status/$code'),
|
||||||
);
|
);
|
||||||
|
|
||||||
final statusResponse =
|
final statusResponse = await authApiV2.getTelegramCodeStatus(
|
||||||
await authApiV2.getTelegramCodeStatus(statusRequest, code!);
|
statusRequest,
|
||||||
final statusBody = jsonDecode(await statusResponse.readAsString())
|
code!,
|
||||||
as Map<String, dynamic>;
|
);
|
||||||
|
final statusBody =
|
||||||
|
jsonDecode(await statusResponse.readAsString())
|
||||||
|
as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(statusResponse.statusCode, equals(200));
|
expect(statusResponse.statusCode, equals(200));
|
||||||
expect(statusBody['status'], equals('pending'));
|
expect(statusBody['status'], equals('pending'));
|
||||||
expect(statusBody['isClaimed'], isFalse);
|
expect(statusBody['isClaimed'], isFalse);
|
||||||
expect(statusBody['isUsed'], isFalse);
|
expect(statusBody['isUsed'], isFalse);
|
||||||
|
|
||||||
// Step 3: authentication before claim should fail
|
// Step 3: authentication before claim should fail
|
||||||
final preAuthRequest = Request(
|
final preAuthRequest = Request(
|
||||||
'POST',
|
'POST',
|
||||||
Uri.parse('http://localhost/api/v2/auth/oauth/telegram'),
|
Uri.parse('http://localhost/api/v2/auth/oauth/telegram'),
|
||||||
body: jsonEncode({'code': code}),
|
body: jsonEncode({'code': code}),
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
);
|
);
|
||||||
|
|
||||||
final preAuthResponse =
|
final preAuthResponse = await authApiV2.authenticateTelegram(
|
||||||
await authApiV2.authenticateTelegram(preAuthRequest);
|
preAuthRequest,
|
||||||
expect(preAuthResponse.statusCode, equals(401));
|
);
|
||||||
|
expect(preAuthResponse.statusCode, equals(401));
|
||||||
|
|
||||||
// Step 4: claim code via bot
|
// Step 4: claim code via bot
|
||||||
final claimRequest = Request(
|
final claimRequest = Request(
|
||||||
'POST',
|
'POST',
|
||||||
Uri.parse('http://localhost/api/v2/auth/telegram/claim-code'),
|
Uri.parse('http://localhost/api/v2/auth/telegram/claim-code'),
|
||||||
body: jsonEncode({
|
body: jsonEncode({
|
||||||
'code': code,
|
'code': code,
|
||||||
'telegramUserId': 'telegram_user_1',
|
'telegramUserId': 'telegram_user_1',
|
||||||
'telegramUsername': 'webuser',
|
'telegramUsername': 'webuser',
|
||||||
'firstName': 'Web',
|
'firstName': 'Web',
|
||||||
'lastName': 'User',
|
'lastName': 'User',
|
||||||
}),
|
}),
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
);
|
);
|
||||||
|
|
||||||
final claimResponse = await authApiV2.claimTelegramCode(claimRequest);
|
final claimResponse = await authApiV2.claimTelegramCode(claimRequest);
|
||||||
final claimBody = jsonDecode(await claimResponse.readAsString())
|
final claimBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await claimResponse.readAsString())
|
||||||
|
as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(claimResponse.statusCode, equals(200));
|
expect(claimResponse.statusCode, equals(200));
|
||||||
expect(claimBody['status'], equals('claimed'));
|
expect(claimBody['status'], equals('claimed'));
|
||||||
expect(claimBody['remainingSeconds'], greaterThan(0));
|
expect(claimBody['remainingSeconds'], greaterThan(0));
|
||||||
|
|
||||||
// Step 5: status should be claimed
|
// Step 5: status should be claimed
|
||||||
final claimedStatusResponse =
|
final claimedStatusResponse = await authApiV2.getTelegramCodeStatus(
|
||||||
await authApiV2.getTelegramCodeStatus(statusRequest, code);
|
statusRequest,
|
||||||
final claimedStatusBody =
|
code,
|
||||||
jsonDecode(await claimedStatusResponse.readAsString())
|
);
|
||||||
as Map<String, dynamic>;
|
final claimedStatusBody =
|
||||||
|
jsonDecode(await claimedStatusResponse.readAsString())
|
||||||
|
as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(claimedStatusResponse.statusCode, equals(200));
|
expect(claimedStatusResponse.statusCode, equals(200));
|
||||||
expect(claimedStatusBody['status'], equals('claimed'));
|
expect(claimedStatusBody['status'], equals('claimed'));
|
||||||
expect(claimedStatusBody['isClaimed'], isTrue);
|
expect(claimedStatusBody['isClaimed'], isTrue);
|
||||||
expect(claimedStatusBody['isUsed'], isFalse);
|
expect(claimedStatusBody['isUsed'], isFalse);
|
||||||
|
|
||||||
// Step 6: authentication after claim should succeed
|
// Step 6: authentication after claim should succeed
|
||||||
final authRequest = Request(
|
final authRequest = Request(
|
||||||
'POST',
|
'POST',
|
||||||
Uri.parse('http://localhost/api/v2/auth/oauth/telegram'),
|
Uri.parse('http://localhost/api/v2/auth/oauth/telegram'),
|
||||||
body: jsonEncode({'code': code}),
|
body: jsonEncode({'code': code}),
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
);
|
);
|
||||||
|
|
||||||
final authResponse = await authApiV2.authenticateTelegram(authRequest);
|
final authResponse = await authApiV2.authenticateTelegram(authRequest);
|
||||||
final authBody =
|
final authBody =
|
||||||
jsonDecode(await authResponse.readAsString()) as Map<String, dynamic>;
|
jsonDecode(await authResponse.readAsString())
|
||||||
|
as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(authResponse.statusCode, equals(200));
|
expect(authResponse.statusCode, equals(200));
|
||||||
expect(authBody['user'], isNotNull);
|
expect(authBody['user'], isNotNull);
|
||||||
expect(authBody['accessToken'], isNotEmpty);
|
expect(authBody['accessToken'], isNotEmpty);
|
||||||
expect(authBody['refreshToken'], isNotEmpty);
|
expect(authBody['refreshToken'], isNotEmpty);
|
||||||
|
|
||||||
// Step 7: status reflects used code
|
// Step 7: status reflects used code
|
||||||
final usedStatusResponse =
|
final usedStatusResponse = await authApiV2.getTelegramCodeStatus(
|
||||||
await authApiV2.getTelegramCodeStatus(statusRequest, code);
|
statusRequest,
|
||||||
final usedStatusBody = jsonDecode(await usedStatusResponse.readAsString())
|
code,
|
||||||
as Map<String, dynamic>;
|
);
|
||||||
|
final usedStatusBody =
|
||||||
|
jsonDecode(await usedStatusResponse.readAsString())
|
||||||
|
as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(usedStatusResponse.statusCode, equals(200));
|
expect(usedStatusResponse.statusCode, equals(200));
|
||||||
expect(usedStatusBody['status'], equals('used'));
|
expect(usedStatusBody['status'], equals('used'));
|
||||||
expect(usedStatusBody['isClaimed'], isTrue);
|
expect(usedStatusBody['isClaimed'], isTrue);
|
||||||
expect(usedStatusBody['isUsed'], isTrue);
|
expect(usedStatusBody['isUsed'], isTrue);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('should return error when claiming unknown code', () async {
|
test('should return error when claiming unknown code', () async {
|
||||||
final claimRequest = Request(
|
final claimRequest = Request(
|
||||||
|
|
|
||||||
|
|
@ -74,10 +74,10 @@ void main() {
|
||||||
handler = const Pipeline()
|
handler = const Pipeline()
|
||||||
.addMiddleware(authorizeV2(userManager, jwtService))
|
.addMiddleware(authorizeV2(userManager, jwtService))
|
||||||
.addHandler((request) {
|
.addHandler((request) {
|
||||||
final user = request.context['user'] as UserModel?;
|
final user = request.context['user'] as UserModel?;
|
||||||
final body = user == null ? 'no-user' : 'user-${user.id}';
|
final body = user == null ? 'no-user' : 'user-${user.id}';
|
||||||
return Response.ok(body);
|
return Response.ok(body);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDownAll(() async {
|
tearDownAll(() async {
|
||||||
|
|
@ -93,8 +93,11 @@ void main() {
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
headers['authorization'] = 'Bearer $token';
|
headers['authorization'] = 'Bearer $token';
|
||||||
}
|
}
|
||||||
final request =
|
final request = Request(
|
||||||
Request(method, Uri.parse('http://localhost$path'), headers: headers);
|
method,
|
||||||
|
Uri.parse('http://localhost$path'),
|
||||||
|
headers: headers,
|
||||||
|
);
|
||||||
return Future.sync(() => handler(request));
|
return Future.sync(() => handler(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,8 +109,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('attaches user context when token present on GET /packs/<id>', () async {
|
test('attaches user context when token present on GET /packs/<id>', () async {
|
||||||
final response =
|
final response = await _makeRequest('/packs/10', token: accessToken);
|
||||||
await _makeRequest('/packs/10', token: accessToken);
|
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(await response.readAsString(), equals('user-${testUser.id}'));
|
expect(await response.readAsString(), equals('user-${testUser.id}'));
|
||||||
|
|
@ -117,13 +119,11 @@ void main() {
|
||||||
final response = await _makeRequest('/packs/10/tests');
|
final response = await _makeRequest('/packs/10/tests');
|
||||||
|
|
||||||
expect(response.statusCode, equals(401));
|
expect(response.statusCode, equals(401));
|
||||||
expect(await response.readAsString(),
|
expect(await response.readAsString(), contains('"error":"Unauthorized"'));
|
||||||
contains('"error":"Unauthorized"'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('allows GET /packs/<id>/tests when token provided', () async {
|
test('allows GET /packs/<id>/tests when token provided', () async {
|
||||||
final response =
|
final response = await _makeRequest('/packs/10/tests', token: accessToken);
|
||||||
await _makeRequest('/packs/10/tests', token: accessToken);
|
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(await response.readAsString(), equals('user-${testUser.id}'));
|
expect(await response.readAsString(), equals('user-${testUser.id}'));
|
||||||
|
|
@ -133,8 +133,6 @@ void main() {
|
||||||
final response = await _makeRequest('/packs/10', token: 'invalid');
|
final response = await _makeRequest('/packs/10', token: 'invalid');
|
||||||
|
|
||||||
expect(response.statusCode, equals(401));
|
expect(response.statusCode, equals(401));
|
||||||
expect(await response.readAsString(),
|
expect(await response.readAsString(), contains('"error":"Unauthorized"'));
|
||||||
contains('"error":"Unauthorized"'));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,9 +64,12 @@ void main() {
|
||||||
Uri.parse('http://localhost/api/v2/games/nonexistent_game/assets'),
|
Uri.parse('http://localhost/api/v2/games/nonexistent_game/assets'),
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await gamesApiV2.getGameAssets(request, 'nonexistent_game');
|
final response = await gamesApiV2.getGameAssets(
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
request,
|
||||||
as Map<String, dynamic>;
|
'nonexistent_game',
|
||||||
|
);
|
||||||
|
final responseBody =
|
||||||
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(404));
|
expect(response.statusCode, equals(404));
|
||||||
expect(responseBody['error'], equals('Not Found'));
|
expect(responseBody['error'], equals('Not Found'));
|
||||||
|
|
@ -87,8 +90,8 @@ void main() {
|
||||||
expect(response.statusCode, isIn([200, 404]));
|
expect(response.statusCode, isIn([200, 404]));
|
||||||
|
|
||||||
if (response.statusCode == 404) {
|
if (response.statusCode == 404) {
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
expect(responseBody['error'], equals('Not Found'));
|
expect(responseBody['error'], equals('Not Found'));
|
||||||
} else {
|
} else {
|
||||||
// If file exists, verify it returns binary data
|
// If file exists, verify it returns binary data
|
||||||
|
|
@ -108,14 +111,8 @@ void main() {
|
||||||
// Only check headers if the file exists (status 200)
|
// Only check headers if the file exists (status 200)
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
expect(response.headers['content-type'], equals('application/zip'));
|
expect(response.headers['content-type'], equals('application/zip'));
|
||||||
expect(
|
expect(response.headers['content-disposition'], contains('attachment'));
|
||||||
response.headers['content-disposition'],
|
expect(response.headers['cache-control'], contains('max-age=86400'));
|
||||||
contains('attachment'),
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
response.headers['cache-control'],
|
|
||||||
contains('max-age=86400'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -131,12 +128,11 @@ void main() {
|
||||||
expect(response.statusCode, isIn([200, 404]));
|
expect(response.statusCode, isIn([200, 404]));
|
||||||
|
|
||||||
if (response.statusCode == 404) {
|
if (response.statusCode == 404) {
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
// Should say "assets not found" not "game not found"
|
// Should say "assets not found" not "game not found"
|
||||||
expect(responseBody['message'], isNot(contains('Game not found')));
|
expect(responseBody['message'], isNot(contains('Game not found')));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -234,7 +234,9 @@ void main() {
|
||||||
.findFirst();
|
.findFirst();
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
await testIsar.refreshTokenModels.put(
|
await testIsar.refreshTokenModels.put(
|
||||||
token.copyWith(expiresAt: DateTime.now().subtract(Duration(hours: 1))),
|
token.copyWith(
|
||||||
|
expiresAt: DateTime.now().subtract(Duration(hours: 1)),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -274,4 +276,3 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,7 @@ void main() {
|
||||||
AdminAccessPolicy(),
|
AdminAccessPolicy(),
|
||||||
);
|
);
|
||||||
|
|
||||||
final context = <String, Object?>{
|
final context = <String, Object?>{'accessService': accessService};
|
||||||
'accessService': accessService,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
context['user'] = user;
|
context['user'] = user;
|
||||||
|
|
@ -109,11 +107,13 @@ void main() {
|
||||||
final response = await mediaApiV2.uploadCardImage(request);
|
final response = await mediaApiV2.uploadCardImage(request);
|
||||||
|
|
||||||
expect(response.statusCode, equals(403));
|
expect(response.statusCode, equals(403));
|
||||||
verifyNever(mockMinioService.uploadFile(
|
verifyNever(
|
||||||
bucket: anyNamed('bucket'),
|
mockMinioService.uploadFile(
|
||||||
bytes: anyNamed('bytes'),
|
bucket: anyNamed('bucket'),
|
||||||
contentType: anyNamed('contentType'),
|
bytes: anyNamed('bytes'),
|
||||||
));
|
contentType: anyNamed('contentType'),
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return 400 when file size exceeds limit', () async {
|
test('should return 400 when file size exceeds limit', () async {
|
||||||
|
|
@ -142,16 +142,20 @@ void main() {
|
||||||
const testObjectId = 'test-uuid-456';
|
const testObjectId = 'test-uuid-456';
|
||||||
const testPresignedUrl = 'https://minio.example.com/presigned-url-2';
|
const testPresignedUrl = 'https://minio.example.com/presigned-url-2';
|
||||||
|
|
||||||
when(mockMinioService.uploadFile(
|
when(
|
||||||
bucket: MinioConfig.cardImagesBucket,
|
mockMinioService.uploadFile(
|
||||||
bytes: anyNamed('bytes'),
|
bucket: MinioConfig.cardImagesBucket,
|
||||||
contentType: 'image/jpeg',
|
bytes: anyNamed('bytes'),
|
||||||
)).thenAnswer((_) async => testObjectId);
|
contentType: 'image/jpeg',
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => testObjectId);
|
||||||
|
|
||||||
when(mockMinioService.getPresignedUrl(
|
when(
|
||||||
bucket: MinioConfig.cardImagesBucket,
|
mockMinioService.getPresignedUrl(
|
||||||
objectId: testObjectId,
|
bucket: MinioConfig.cardImagesBucket,
|
||||||
)).thenAnswer((_) async => testPresignedUrl);
|
objectId: testObjectId,
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => testPresignedUrl);
|
||||||
|
|
||||||
// Test images now use the same bucket as card images
|
// Test images now use the same bucket as card images
|
||||||
expect(testObjectId, isNotEmpty);
|
expect(testObjectId, isNotEmpty);
|
||||||
|
|
@ -164,16 +168,20 @@ void main() {
|
||||||
const testObjectId = 'test-uuid-789';
|
const testObjectId = 'test-uuid-789';
|
||||||
const testPresignedUrl = 'https://minio.example.com/presigned-url-3';
|
const testPresignedUrl = 'https://minio.example.com/presigned-url-3';
|
||||||
|
|
||||||
when(mockMinioService.uploadFile(
|
when(
|
||||||
bucket: MinioConfig.voiceAudioBucket,
|
mockMinioService.uploadFile(
|
||||||
bytes: anyNamed('bytes'),
|
bucket: MinioConfig.voiceAudioBucket,
|
||||||
contentType: 'audio/mpeg',
|
bytes: anyNamed('bytes'),
|
||||||
)).thenAnswer((_) async => testObjectId);
|
contentType: 'audio/mpeg',
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => testObjectId);
|
||||||
|
|
||||||
when(mockMinioService.getPresignedUrl(
|
when(
|
||||||
bucket: MinioConfig.voiceAudioBucket,
|
mockMinioService.getPresignedUrl(
|
||||||
objectId: testObjectId,
|
bucket: MinioConfig.voiceAudioBucket,
|
||||||
)).thenAnswer((_) async => testPresignedUrl);
|
objectId: testObjectId,
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => testPresignedUrl);
|
||||||
|
|
||||||
// Similar to uploadCardImage test
|
// Similar to uploadCardImage test
|
||||||
expect(testObjectId, isNotEmpty);
|
expect(testObjectId, isNotEmpty);
|
||||||
|
|
@ -205,11 +213,13 @@ void main() {
|
||||||
const testPresignedUrl = 'https://minio.example.com/presigned-url-4';
|
const testPresignedUrl = 'https://minio.example.com/presigned-url-4';
|
||||||
const testBucket = MinioConfig.cardImagesBucket;
|
const testBucket = MinioConfig.cardImagesBucket;
|
||||||
|
|
||||||
when(mockMinioService.getPresignedUrl(
|
when(
|
||||||
bucket: testBucket,
|
mockMinioService.getPresignedUrl(
|
||||||
objectId: testObjectId,
|
bucket: testBucket,
|
||||||
expirySeconds: anyNamed('expirySeconds'),
|
objectId: testObjectId,
|
||||||
)).thenAnswer((_) async => testPresignedUrl);
|
expirySeconds: anyNamed('expirySeconds'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => testPresignedUrl);
|
||||||
|
|
||||||
final request = buildRequest(
|
final request = buildRequest(
|
||||||
'GET',
|
'GET',
|
||||||
|
|
@ -221,29 +231,33 @@ void main() {
|
||||||
testBucket,
|
testBucket,
|
||||||
testObjectId,
|
testObjectId,
|
||||||
);
|
);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['url'], equals(testPresignedUrl));
|
expect(responseBody['url'], equals(testPresignedUrl));
|
||||||
expect(responseBody['expiresAt'], isNotNull);
|
expect(responseBody['expiresAt'], isNotNull);
|
||||||
|
|
||||||
verify(mockMinioService.getPresignedUrl(
|
verify(
|
||||||
bucket: testBucket,
|
mockMinioService.getPresignedUrl(
|
||||||
objectId: testObjectId,
|
bucket: testBucket,
|
||||||
expirySeconds: anyNamed('expirySeconds'),
|
objectId: testObjectId,
|
||||||
)).called(1);
|
expirySeconds: anyNamed('expirySeconds'),
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return 404 when object does not exist', () async {
|
test('should return 404 when object does not exist', () async {
|
||||||
const testObjectId = 'non-existent-id';
|
const testObjectId = 'non-existent-id';
|
||||||
const testBucket = MinioConfig.cardImagesBucket;
|
const testBucket = MinioConfig.cardImagesBucket;
|
||||||
|
|
||||||
when(mockMinioService.getPresignedUrl(
|
when(
|
||||||
bucket: testBucket,
|
mockMinioService.getPresignedUrl(
|
||||||
objectId: testObjectId,
|
bucket: testBucket,
|
||||||
expirySeconds: anyNamed('expirySeconds'),
|
objectId: testObjectId,
|
||||||
)).thenAnswer((_) async => null);
|
expirySeconds: anyNamed('expirySeconds'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => null);
|
||||||
|
|
||||||
final request = buildRequest(
|
final request = buildRequest(
|
||||||
'GET',
|
'GET',
|
||||||
|
|
@ -275,11 +289,13 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
verifyNever(mockMinioService.getPresignedUrl(
|
verifyNever(
|
||||||
bucket: anyNamed('bucket'),
|
mockMinioService.getPresignedUrl(
|
||||||
objectId: anyNamed('objectId'),
|
bucket: anyNamed('bucket'),
|
||||||
expirySeconds: anyNamed('expirySeconds'),
|
objectId: anyNamed('objectId'),
|
||||||
));
|
expirySeconds: anyNamed('expirySeconds'),
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -288,10 +304,9 @@ void main() {
|
||||||
const testObjectId = 'test-uuid-delete';
|
const testObjectId = 'test-uuid-delete';
|
||||||
const testBucket = MinioConfig.cardImagesBucket;
|
const testBucket = MinioConfig.cardImagesBucket;
|
||||||
|
|
||||||
when(mockMinioService.deleteFile(
|
when(
|
||||||
bucket: testBucket,
|
mockMinioService.deleteFile(bucket: testBucket, objectId: testObjectId),
|
||||||
objectId: testObjectId,
|
).thenAnswer((_) async => Future.value());
|
||||||
)).thenAnswer((_) async => Future.value());
|
|
||||||
|
|
||||||
final request = buildRequest(
|
final request = buildRequest(
|
||||||
'DELETE',
|
'DELETE',
|
||||||
|
|
@ -304,16 +319,15 @@ void main() {
|
||||||
testBucket,
|
testBucket,
|
||||||
testObjectId,
|
testObjectId,
|
||||||
);
|
);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['success'], isTrue);
|
expect(responseBody['success'], isTrue);
|
||||||
|
|
||||||
verify(mockMinioService.deleteFile(
|
verify(
|
||||||
bucket: testBucket,
|
mockMinioService.deleteFile(bucket: testBucket, objectId: testObjectId),
|
||||||
objectId: testObjectId,
|
).called(1);
|
||||||
)).called(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return 403 when user is not admin', () async {
|
test('should return 403 when user is not admin', () async {
|
||||||
|
|
@ -340,10 +354,12 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.statusCode, equals(403));
|
expect(response.statusCode, equals(403));
|
||||||
verifyNever(mockMinioService.deleteFile(
|
verifyNever(
|
||||||
bucket: anyNamed('bucket'),
|
mockMinioService.deleteFile(
|
||||||
objectId: anyNamed('objectId'),
|
bucket: anyNamed('bucket'),
|
||||||
));
|
objectId: anyNamed('objectId'),
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return 400 for invalid bucket name', () async {
|
test('should return 400 for invalid bucket name', () async {
|
||||||
|
|
@ -363,10 +379,12 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
verifyNever(mockMinioService.deleteFile(
|
verifyNever(
|
||||||
bucket: anyNamed('bucket'),
|
mockMinioService.deleteFile(
|
||||||
objectId: anyNamed('objectId'),
|
bucket: anyNamed('bucket'),
|
||||||
));
|
objectId: anyNamed('objectId'),
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,7 @@ void main() {
|
||||||
late PacksApiV2 packsApiV2;
|
late PacksApiV2 packsApiV2;
|
||||||
late UserModel testUser;
|
late UserModel testUser;
|
||||||
|
|
||||||
Request buildRequest(
|
Request buildRequest(String method, String url, {UserModel? user}) {
|
||||||
String method,
|
|
||||||
String url, {
|
|
||||||
UserModel? user,
|
|
||||||
}) {
|
|
||||||
final resourceLoader = ResourceLoader(packManager);
|
final resourceLoader = ResourceLoader(packManager);
|
||||||
final accessService = AccessService(
|
final accessService = AccessService(
|
||||||
PackAccessPolicy(resourceLoader),
|
PackAccessPolicy(resourceLoader),
|
||||||
|
|
@ -87,10 +83,7 @@ void main() {
|
||||||
final discountsManager = const DiscountsManager();
|
final discountsManager = const DiscountsManager();
|
||||||
final productsPriceResolver = ProductsPriceResolver(discountsManager);
|
final productsPriceResolver = ProductsPriceResolver(discountsManager);
|
||||||
final adsManager = AdsManager();
|
final adsManager = AdsManager();
|
||||||
packDtoConverter = PackDtoConverter(
|
packDtoConverter = PackDtoConverter(productsPriceResolver, adsManager);
|
||||||
productsPriceResolver,
|
|
||||||
adsManager,
|
|
||||||
);
|
|
||||||
|
|
||||||
packManager = PackManager(packDtoConverter);
|
packManager = PackManager(packDtoConverter);
|
||||||
testManager = TestManager(packDtoConverter);
|
testManager = TestManager(packDtoConverter);
|
||||||
|
|
@ -148,10 +141,7 @@ void main() {
|
||||||
|
|
||||||
// Update cardsOrder
|
// Update cardsOrder
|
||||||
await testIsar.cardPackModels.put(
|
await testIsar.cardPackModels.put(
|
||||||
savedPack.copyWith(
|
savedPack.copyWith(cardsOrder: [card1.id!, card2.id!], size: 2),
|
||||||
cardsOrder: [card1.id!, card2.id!],
|
|
||||||
size: 2,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final privatePack = CardPackModel(
|
final privatePack = CardPackModel(
|
||||||
|
|
@ -183,10 +173,7 @@ void main() {
|
||||||
await savedPrivatePack.cards.save();
|
await savedPrivatePack.cards.save();
|
||||||
|
|
||||||
await testIsar.cardPackModels.put(
|
await testIsar.cardPackModels.put(
|
||||||
savedPrivatePack.copyWith(
|
savedPrivatePack.copyWith(cardsOrder: [privateCard.id!], size: 1),
|
||||||
cardsOrder: [privateCard.id!],
|
|
||||||
size: 1,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -213,8 +200,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getPacks(request);
|
final response = await packsApiV2.getPacks(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['items'], isA<List>());
|
expect(responseBody['items'], isA<List>());
|
||||||
|
|
@ -225,14 +212,11 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return packs with default pagination', () async {
|
test('should return packs with default pagination', () async {
|
||||||
final request = buildRequest(
|
final request = buildRequest('GET', 'http://localhost/api/v2/packs');
|
||||||
'GET',
|
|
||||||
'http://localhost/api/v2/packs',
|
|
||||||
);
|
|
||||||
|
|
||||||
final response = await packsApiV2.getPacks(request);
|
final response = await packsApiV2.getPacks(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['page'], equals(1));
|
expect(responseBody['page'], equals(1));
|
||||||
|
|
@ -246,14 +230,15 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getPacks(request);
|
final response = await packsApiV2.getPacks(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
final items = responseBody['items'] as List;
|
final items = responseBody['items'] as List;
|
||||||
// Should find testPack with "Test" in title
|
// Should find testPack with "Test" in title
|
||||||
final found = items.any((pack) =>
|
final found = items.any(
|
||||||
(pack['title'] as String).contains('Test'));
|
(pack) => (pack['title'] as String).contains('Test'),
|
||||||
|
);
|
||||||
expect(found, isTrue);
|
expect(found, isTrue);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -264,8 +249,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getPacks(request);
|
final response = await packsApiV2.getPacks(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('Bad Request'));
|
expect(responseBody['error'], equals('Bad Request'));
|
||||||
|
|
@ -279,64 +264,67 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getPacks(request);
|
final response = await packsApiV2.getPacks(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('Bad Request'));
|
expect(responseBody['error'], equals('Bad Request'));
|
||||||
expect(responseBody['message'], contains('Limit must be between 1 and 100'));
|
expect(
|
||||||
|
responseBody['message'],
|
||||||
|
contains('Limit must be between 1 and 100'),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('PacksApiV2 - Get Pack', () {
|
group('PacksApiV2 - Get Pack', () {
|
||||||
test('should return pack details for public pack (id=10)', () async {
|
test('should return pack details for public pack (id=10)', () async {
|
||||||
final request = buildRequest(
|
final request = buildRequest('GET', 'http://localhost/api/v2/packs/10');
|
||||||
'GET',
|
|
||||||
'http://localhost/api/v2/packs/10',
|
|
||||||
);
|
|
||||||
|
|
||||||
final response = await packsApiV2.getPack(request, '10');
|
final response = await packsApiV2.getPack(request, '10');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['id'], equals('10'));
|
expect(responseBody['id'], equals('10'));
|
||||||
expect(responseBody['title'], equals('Test Pack'));
|
expect(responseBody['title'], equals('Test Pack'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return pack with purchase status for authenticated user', () async {
|
test(
|
||||||
// First, add pack to user
|
'should return pack with purchase status for authenticated user',
|
||||||
await testIsar.writeTxn(() async {
|
() async {
|
||||||
final user = await testIsar.userModels.get(1);
|
// First, add pack to user
|
||||||
if (user != null) {
|
await testIsar.writeTxn(() async {
|
||||||
await user.packs.load();
|
final user = await testIsar.userModels.get(1);
|
||||||
final pack = await testIsar.cardPackModels.get(10);
|
if (user != null) {
|
||||||
if (pack != null) {
|
await user.packs.load();
|
||||||
user.packs.add(pack);
|
final pack = await testIsar.cardPackModels.get(10);
|
||||||
await user.packs.save();
|
if (pack != null) {
|
||||||
|
user.packs.add(pack);
|
||||||
|
await user.packs.save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Reload user to get updated packs
|
// Reload user to get updated packs
|
||||||
final updatedUser = await testIsar.userModels.get(1);
|
final updatedUser = await testIsar.userModels.get(1);
|
||||||
expect(updatedUser, isNotNull);
|
expect(updatedUser, isNotNull);
|
||||||
|
|
||||||
final request = buildRequest(
|
final request = buildRequest(
|
||||||
'GET',
|
'GET',
|
||||||
'http://localhost/api/v2/packs/10',
|
'http://localhost/api/v2/packs/10',
|
||||||
user: updatedUser,
|
user: updatedUser,
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getPack(request, '10');
|
final response = await packsApiV2.getPack(request, '10');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['id'], equals('10'));
|
expect(responseBody['id'], equals('10'));
|
||||||
// isPurchased may be false since pack id=10 is public, so just check it exists
|
// isPurchased may be false since pack id=10 is public, so just check it exists
|
||||||
expect(responseBody.containsKey('isPurchased'), isTrue);
|
expect(responseBody.containsKey('isPurchased'), isTrue);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('should return 404 for non-existent pack', () async {
|
test('should return 404 for non-existent pack', () async {
|
||||||
final request = buildRequest(
|
final request = buildRequest(
|
||||||
|
|
@ -346,45 +334,48 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getPack(request, '999');
|
final response = await packsApiV2.getPack(request, '999');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(404));
|
expect(response.statusCode, equals(404));
|
||||||
expect(responseBody['error'], equals('Not Found'));
|
expect(responseBody['error'], equals('Not Found'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return buy page for unauthenticated private pack access', () async {
|
test(
|
||||||
final request = buildRequest(
|
'should return buy page for unauthenticated private pack access',
|
||||||
'GET',
|
() async {
|
||||||
'http://localhost/api/v2/packs/11',
|
final request = buildRequest('GET', 'http://localhost/api/v2/packs/11');
|
||||||
);
|
|
||||||
|
|
||||||
final response = await packsApiV2.getPack(request, '11');
|
final response = await packsApiV2.getPack(request, '11');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['id'], equals('11'));
|
expect(responseBody['id'], equals('11'));
|
||||||
expect(responseBody['price'], equals('199'));
|
expect(responseBody['price'], equals('199'));
|
||||||
expect(responseBody.containsKey('cards'), isTrue);
|
expect(responseBody.containsKey('cards'), isTrue);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('should return buy page for authenticated user without pack access', () async {
|
test(
|
||||||
final request = buildRequest(
|
'should return buy page for authenticated user without pack access',
|
||||||
'GET',
|
() async {
|
||||||
'http://localhost/api/v2/packs/11',
|
final request = buildRequest(
|
||||||
user: testUser,
|
'GET',
|
||||||
);
|
'http://localhost/api/v2/packs/11',
|
||||||
|
user: testUser,
|
||||||
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getPack(request, '11');
|
final response = await packsApiV2.getPack(request, '11');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['id'], equals('11'));
|
expect(responseBody['id'], equals('11'));
|
||||||
expect(responseBody['price'], equals('199'));
|
expect(responseBody['price'], equals('199'));
|
||||||
expect(responseBody.containsKey('cards'), isTrue);
|
expect(responseBody.containsKey('cards'), isTrue);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
group('PacksApiV2 - Get Pack Cards', () {
|
group('PacksApiV2 - Get Pack Cards', () {
|
||||||
|
|
@ -396,8 +387,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getPackCards(request, '10');
|
final response = await packsApiV2.getPackCards(request, '10');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['items'], isA<List>());
|
expect(responseBody['items'], isA<List>());
|
||||||
|
|
@ -409,7 +400,6 @@ void main() {
|
||||||
expect(items.length, greaterThanOrEqualTo(0));
|
expect(items.length, greaterThanOrEqualTo(0));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
test('should return 404 for non-existent pack', () async {
|
test('should return 404 for non-existent pack', () async {
|
||||||
final request = buildRequest(
|
final request = buildRequest(
|
||||||
'GET',
|
'GET',
|
||||||
|
|
@ -418,8 +408,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getPackCards(request, '999');
|
final response = await packsApiV2.getPackCards(request, '999');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(404));
|
expect(response.statusCode, equals(404));
|
||||||
expect(responseBody['error'], equals('Not Found'));
|
expect(responseBody['error'], equals('Not Found'));
|
||||||
|
|
@ -433,8 +423,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getPackCards(request, '10');
|
final response = await packsApiV2.getPackCards(request, '10');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('Bad Request'));
|
expect(responseBody['error'], equals('Bad Request'));
|
||||||
|
|
@ -450,8 +440,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getCardImage(request, '10', '999');
|
final response = await packsApiV2.getCardImage(request, '10', '999');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(404));
|
expect(response.statusCode, equals(404));
|
||||||
expect(responseBody['error'], equals('Not Found'));
|
expect(responseBody['error'], equals('Not Found'));
|
||||||
|
|
@ -465,8 +455,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getCardImage(request, '999', '1');
|
final response = await packsApiV2.getCardImage(request, '999', '1');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(404));
|
expect(response.statusCode, equals(404));
|
||||||
expect(responseBody['error'], equals('Not Found'));
|
expect(responseBody['error'], equals('Not Found'));
|
||||||
|
|
@ -480,8 +470,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getCardImage(request, '10', 'invalid');
|
final response = await packsApiV2.getCardImage(request, '10', 'invalid');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('Bad Request'));
|
expect(responseBody['error'], equals('Bad Request'));
|
||||||
|
|
@ -495,8 +485,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getCardImage(request, 'invalid', '1');
|
final response = await packsApiV2.getCardImage(request, 'invalid', '1');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('Bad Request'));
|
expect(responseBody['error'], equals('Bad Request'));
|
||||||
|
|
@ -524,28 +514,31 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getCardImage(request, '99', '1');
|
final response = await packsApiV2.getCardImage(request, '99', '1');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(404));
|
expect(response.statusCode, equals(404));
|
||||||
expect(responseBody['error'], equals('Not Found'));
|
expect(responseBody['error'], equals('Not Found'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should allow access without authentication for enabled packs', () async {
|
test(
|
||||||
// Request without user context (public access)
|
'should allow access without authentication for enabled packs',
|
||||||
final request = buildRequest(
|
() async {
|
||||||
'GET',
|
// Request without user context (public access)
|
||||||
'http://localhost/api/v2/packs/10/cards/1/image',
|
final request = buildRequest(
|
||||||
);
|
'GET',
|
||||||
|
'http://localhost/api/v2/packs/10/cards/1/image',
|
||||||
|
);
|
||||||
|
|
||||||
// This should work even without auth because pack 10 is enabled
|
// This should work even without auth because pack 10 is enabled
|
||||||
// and images are now accessible for enabled packs
|
// and images are now accessible for enabled packs
|
||||||
final response = await packsApiV2.getCardImage(request, '10', '1');
|
final response = await packsApiV2.getCardImage(request, '10', '1');
|
||||||
|
|
||||||
// Note: This might return 404 if image file doesn't exist,
|
// Note: This might return 404 if image file doesn't exist,
|
||||||
// but it should not return 401 Unauthorized
|
// but it should not return 401 Unauthorized
|
||||||
expect(response.statusCode, isNot(equals(401)));
|
expect(response.statusCode, isNot(equals(401)));
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('should return 404 if card does not belong to pack', () async {
|
test('should return 404 if card does not belong to pack', () async {
|
||||||
// Create another pack and card
|
// Create another pack and card
|
||||||
|
|
@ -580,8 +573,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getCardImage(request, '10', '3');
|
final response = await packsApiV2.getCardImage(request, '10', '3');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(404));
|
expect(response.statusCode, equals(404));
|
||||||
expect(responseBody['error'], equals('Not Found'));
|
expect(responseBody['error'], equals('Not Found'));
|
||||||
|
|
@ -597,8 +590,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getCardImageBack(request, '10', '1');
|
final response = await packsApiV2.getCardImageBack(request, '10', '1');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(404));
|
expect(response.statusCode, equals(404));
|
||||||
expect(responseBody['error'], equals('Not Found'));
|
expect(responseBody['error'], equals('Not Found'));
|
||||||
|
|
@ -612,8 +605,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getCardImageBack(request, '10', '999');
|
final response = await packsApiV2.getCardImageBack(request, '10', '999');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(404));
|
expect(response.statusCode, equals(404));
|
||||||
expect(responseBody['error'], equals('Not Found'));
|
expect(responseBody['error'], equals('Not Found'));
|
||||||
|
|
@ -627,8 +620,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getCardImageBack(request, '999', '1');
|
final response = await packsApiV2.getCardImageBack(request, '999', '1');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(404));
|
expect(response.statusCode, equals(404));
|
||||||
expect(responseBody['error'], equals('Not Found'));
|
expect(responseBody['error'], equals('Not Found'));
|
||||||
|
|
@ -646,8 +639,8 @@ void main() {
|
||||||
'10',
|
'10',
|
||||||
'invalid',
|
'invalid',
|
||||||
);
|
);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('Bad Request'));
|
expect(responseBody['error'], equals('Bad Request'));
|
||||||
|
|
@ -665,8 +658,8 @@ void main() {
|
||||||
'invalid',
|
'invalid',
|
||||||
'1',
|
'1',
|
||||||
);
|
);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('Bad Request'));
|
expect(responseBody['error'], equals('Bad Request'));
|
||||||
|
|
@ -680,28 +673,31 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getCardImageBack(request, '99', '1');
|
final response = await packsApiV2.getCardImageBack(request, '99', '1');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(404));
|
expect(response.statusCode, equals(404));
|
||||||
expect(responseBody['error'], equals('Not Found'));
|
expect(responseBody['error'], equals('Not Found'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should allow access without authentication for enabled packs', () async {
|
test(
|
||||||
// Request without user context (public access)
|
'should allow access without authentication for enabled packs',
|
||||||
final request = buildRequest(
|
() async {
|
||||||
'GET',
|
// Request without user context (public access)
|
||||||
'http://localhost/api/v2/packs/10/cards/1/imageBack',
|
final request = buildRequest(
|
||||||
);
|
'GET',
|
||||||
|
'http://localhost/api/v2/packs/10/cards/1/imageBack',
|
||||||
|
);
|
||||||
|
|
||||||
// This should work even without auth because pack 10 is enabled
|
// This should work even without auth because pack 10 is enabled
|
||||||
// and images are now accessible for enabled packs
|
// and images are now accessible for enabled packs
|
||||||
final response = await packsApiV2.getCardImageBack(request, '10', '1');
|
final response = await packsApiV2.getCardImageBack(request, '10', '1');
|
||||||
|
|
||||||
// Note: This might return 404 if image file doesn't exist,
|
// Note: This might return 404 if image file doesn't exist,
|
||||||
// but it should not return 401 Unauthorized
|
// but it should not return 401 Unauthorized
|
||||||
expect(response.statusCode, isNot(equals(401)));
|
expect(response.statusCode, isNot(equals(401)));
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('should return 404 if card does not belong to pack', () async {
|
test('should return 404 if card does not belong to pack', () async {
|
||||||
// Try to get card 3 from pack 10 (card 3 belongs to pack 20)
|
// Try to get card 3 from pack 10 (card 3 belongs to pack 20)
|
||||||
|
|
@ -711,8 +707,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getCardImageBack(request, '10', '3');
|
final response = await packsApiV2.getCardImageBack(request, '10', '3');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(404));
|
expect(response.statusCode, equals(404));
|
||||||
expect(responseBody['error'], equals('Not Found'));
|
expect(responseBody['error'], equals('Not Found'));
|
||||||
|
|
@ -727,43 +723,46 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getPackTests(request, '10');
|
final response = await packsApiV2.getPackTests(request, '10');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(401));
|
expect(response.statusCode, equals(401));
|
||||||
expect(responseBody['error'], equals('Unauthorized'));
|
expect(responseBody['error'], equals('Unauthorized'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return tests list for authenticated user with pack access', () async {
|
test(
|
||||||
// First, add pack to user so they have access
|
'should return tests list for authenticated user with pack access',
|
||||||
await testIsar.writeTxn(() async {
|
() async {
|
||||||
final user = await testIsar.userModels.get(1);
|
// First, add pack to user so they have access
|
||||||
if (user != null) {
|
await testIsar.writeTxn(() async {
|
||||||
await user.packs.load();
|
final user = await testIsar.userModels.get(1);
|
||||||
final pack = await testIsar.cardPackModels.get(10);
|
if (user != null) {
|
||||||
if (pack != null) {
|
await user.packs.load();
|
||||||
user.packs.add(pack);
|
final pack = await testIsar.cardPackModels.get(10);
|
||||||
await user.packs.save();
|
if (pack != null) {
|
||||||
|
user.packs.add(pack);
|
||||||
|
await user.packs.save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Reload user to get updated packs
|
// Reload user to get updated packs
|
||||||
final updatedUser = await testIsar.userModels.get(1);
|
final updatedUser = await testIsar.userModels.get(1);
|
||||||
expect(updatedUser, isNotNull);
|
expect(updatedUser, isNotNull);
|
||||||
|
|
||||||
final request = buildRequest(
|
final request = buildRequest(
|
||||||
'GET',
|
'GET',
|
||||||
'http://localhost/api/v2/packs/10/tests',
|
'http://localhost/api/v2/packs/10/tests',
|
||||||
user: updatedUser,
|
user: updatedUser,
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getPackTests(request, '10');
|
final response = await packsApiV2.getPackTests(request, '10');
|
||||||
final responseBody = jsonDecode(await response.readAsString());
|
final responseBody = jsonDecode(await response.readAsString());
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody, isA<List>());
|
expect(responseBody, isA<List>());
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('should return 404 for non-existent pack', () async {
|
test('should return 404 for non-existent pack', () async {
|
||||||
final request = buildRequest(
|
final request = buildRequest(
|
||||||
|
|
@ -773,8 +772,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getPackTests(request, '999');
|
final response = await packsApiV2.getPackTests(request, '999');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(404));
|
expect(response.statusCode, equals(404));
|
||||||
expect(responseBody['error'], equals('Not Found'));
|
expect(responseBody['error'], equals('Not Found'));
|
||||||
|
|
@ -789,8 +788,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getPackBuyPage(request, '11');
|
final response = await packsApiV2.getPackBuyPage(request, '11');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['id'], equals('11'));
|
expect(responseBody['id'], equals('11'));
|
||||||
|
|
@ -798,55 +797,61 @@ void main() {
|
||||||
expect(responseBody.containsKey('cards'), isTrue);
|
expect(responseBody.containsKey('cards'), isTrue);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return buy page for authenticated user without pack', () async {
|
test(
|
||||||
final request = buildRequest(
|
'should return buy page for authenticated user without pack',
|
||||||
'GET',
|
() async {
|
||||||
'http://localhost/api/v2/packs/11/buy',
|
final request = buildRequest(
|
||||||
user: testUser,
|
'GET',
|
||||||
);
|
'http://localhost/api/v2/packs/11/buy',
|
||||||
|
user: testUser,
|
||||||
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getPackBuyPage(request, '11');
|
final response = await packsApiV2.getPackBuyPage(request, '11');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['id'], equals('11'));
|
expect(responseBody['id'], equals('11'));
|
||||||
expect(responseBody['price'], equals('199'));
|
expect(responseBody['price'], equals('199'));
|
||||||
expect(responseBody.containsKey('cards'), isTrue);
|
expect(responseBody.containsKey('cards'), isTrue);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('should return pack info with isPurchased=true for authenticated user who already owns pack', () async {
|
test(
|
||||||
// First, add pack to user
|
'should return pack info with isPurchased=true for authenticated user who already owns pack',
|
||||||
await testIsar.writeTxn(() async {
|
() async {
|
||||||
final user = await testIsar.userModels.get(1);
|
// First, add pack to user
|
||||||
if (user != null) {
|
await testIsar.writeTxn(() async {
|
||||||
await user.packs.load();
|
final user = await testIsar.userModels.get(1);
|
||||||
final pack = await testIsar.cardPackModels.get(11);
|
if (user != null) {
|
||||||
if (pack != null) {
|
await user.packs.load();
|
||||||
user.packs.add(pack);
|
final pack = await testIsar.cardPackModels.get(11);
|
||||||
await user.packs.save();
|
if (pack != null) {
|
||||||
|
user.packs.add(pack);
|
||||||
|
await user.packs.save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Reload user to get updated packs
|
// Reload user to get updated packs
|
||||||
final updatedUser = await testIsar.userModels.get(1);
|
final updatedUser = await testIsar.userModels.get(1);
|
||||||
expect(updatedUser, isNotNull);
|
expect(updatedUser, isNotNull);
|
||||||
|
|
||||||
final request = buildRequest(
|
final request = buildRequest(
|
||||||
'GET',
|
'GET',
|
||||||
'http://localhost/api/v2/packs/11/buy',
|
'http://localhost/api/v2/packs/11/buy',
|
||||||
user: updatedUser,
|
user: updatedUser,
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getPackBuyPage(request, '11');
|
final response = await packsApiV2.getPackBuyPage(request, '11');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['id'], equals('11'));
|
expect(responseBody['id'], equals('11'));
|
||||||
expect(responseBody['isPurchased'], equals(true));
|
expect(responseBody['isPurchased'], equals(true));
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('should return 404 for non-existent pack (unauthenticated)', () async {
|
test('should return 404 for non-existent pack (unauthenticated)', () async {
|
||||||
final request = buildRequest(
|
final request = buildRequest(
|
||||||
|
|
@ -855,8 +860,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getPackBuyPage(request, '999');
|
final response = await packsApiV2.getPackBuyPage(request, '999');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(404));
|
expect(response.statusCode, equals(404));
|
||||||
expect(responseBody['error'], equals('Not Found'));
|
expect(responseBody['error'], equals('Not Found'));
|
||||||
|
|
@ -870,8 +875,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getPackBuyPage(request, '999');
|
final response = await packsApiV2.getPackBuyPage(request, '999');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(404));
|
expect(response.statusCode, equals(404));
|
||||||
expect(responseBody['error'], equals('Not Found'));
|
expect(responseBody['error'], equals('Not Found'));
|
||||||
|
|
@ -884,12 +889,11 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await packsApiV2.getPackBuyPage(request, 'invalid');
|
final response = await packsApiV2.getPackBuyPage(request, 'invalid');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('Bad Request'));
|
expect(responseBody['error'], equals('Bad Request'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,18 +26,16 @@ void main() {
|
||||||
late UserModel testUser;
|
late UserModel testUser;
|
||||||
late UserDataModel testUserData;
|
late UserDataModel testUserData;
|
||||||
|
|
||||||
Request buildRequest(
|
Request buildRequest(String method, String url, {UserModel? user}) {
|
||||||
String method,
|
|
||||||
String url, {
|
|
||||||
UserModel? user,
|
|
||||||
}) {
|
|
||||||
final context = <String, Object?>{};
|
final context = <String, Object?>{};
|
||||||
|
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
context['user'] = user;
|
context['user'] = user;
|
||||||
}
|
}
|
||||||
|
|
||||||
final uri = url.startsWith('http') ? Uri.parse(url) : Uri.parse('http://localhost$url');
|
final uri = url.startsWith('http')
|
||||||
|
? Uri.parse(url)
|
||||||
|
: Uri.parse('http://localhost$url');
|
||||||
return Request(method, uri).change(context: context);
|
return Request(method, uri).change(context: context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,10 +105,7 @@ void main() {
|
||||||
await testIsar.userModels.put(user);
|
await testIsar.userModels.put(user);
|
||||||
testUser = user;
|
testUser = user;
|
||||||
|
|
||||||
final userData = UserDataModel(
|
final userData = UserDataModel(tags: ['premium', 'beta'], words: []);
|
||||||
tags: ['premium', 'beta'],
|
|
||||||
words: [],
|
|
||||||
);
|
|
||||||
await testIsar.userDataModels.put(userData);
|
await testIsar.userDataModels.put(userData);
|
||||||
userData.user.value = user;
|
userData.user.value = user;
|
||||||
await userData.user.save();
|
await userData.user.save();
|
||||||
|
|
@ -137,7 +132,8 @@ void main() {
|
||||||
final response = await promocodesApiV2.listPromocodes(request);
|
final response = await promocodesApiV2.listPromocodes(request);
|
||||||
|
|
||||||
expect(response.statusCode, equals(401));
|
expect(response.statusCode, equals(401));
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
final body =
|
||||||
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
expect(body['error'], equals('unauthorized'));
|
expect(body['error'], equals('unauthorized'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -146,7 +142,8 @@ void main() {
|
||||||
final response = await promocodesApiV2.listPromocodes(request);
|
final response = await promocodesApiV2.listPromocodes(request);
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
final body =
|
||||||
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
expect(body['campaigns'], isA<List>());
|
expect(body['campaigns'], isA<List>());
|
||||||
expect((body['campaigns'] as List).isEmpty, isTrue);
|
expect((body['campaigns'] as List).isEmpty, isTrue);
|
||||||
});
|
});
|
||||||
|
|
@ -184,11 +181,13 @@ void main() {
|
||||||
final response = await promocodesApiV2.listPromocodes(request);
|
final response = await promocodesApiV2.listPromocodes(request);
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
final body =
|
||||||
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
expect(body['campaigns'], isA<List>());
|
expect(body['campaigns'], isA<List>());
|
||||||
expect((body['campaigns'] as List).length, equals(1));
|
expect((body['campaigns'] as List).length, equals(1));
|
||||||
|
|
||||||
final campaign = (body['campaigns'] as List).first as Map<String, dynamic>;
|
final campaign =
|
||||||
|
(body['campaigns'] as List).first as Map<String, dynamic>;
|
||||||
expect(campaign['status'], equals('active'));
|
expect(campaign['status'], equals('active'));
|
||||||
expect(campaign['promoCodes'], isA<List>());
|
expect(campaign['promoCodes'], isA<List>());
|
||||||
expect((campaign['promoCodes'] as List).length, equals(2));
|
expect((campaign['promoCodes'] as List).length, equals(2));
|
||||||
|
|
@ -238,10 +237,14 @@ void main() {
|
||||||
final response = await promocodesApiV2.listPromocodes(request);
|
final response = await promocodesApiV2.listPromocodes(request);
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
final body =
|
||||||
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
final campaigns = body['campaigns'] as List;
|
final campaigns = body['campaigns'] as List;
|
||||||
expect(campaigns.length, equals(1));
|
expect(campaigns.length, equals(1));
|
||||||
expect((campaigns.first as Map<String, dynamic>)['template'], equals('ACTIVE'));
|
expect(
|
||||||
|
(campaigns.first as Map<String, dynamic>)['template'],
|
||||||
|
equals('ACTIVE'),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('filters out campaigns outside date range', () async {
|
test('filters out campaigns outside date range', () async {
|
||||||
|
|
@ -300,10 +303,14 @@ void main() {
|
||||||
final response = await promocodesApiV2.listPromocodes(request);
|
final response = await promocodesApiV2.listPromocodes(request);
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
final body =
|
||||||
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
final campaigns = body['campaigns'] as List;
|
final campaigns = body['campaigns'] as List;
|
||||||
expect(campaigns.length, equals(1));
|
expect(campaigns.length, equals(1));
|
||||||
expect((campaigns.first as Map<String, dynamic>)['template'], equals('ACTIVE'));
|
expect(
|
||||||
|
(campaigns.first as Map<String, dynamic>)['template'],
|
||||||
|
equals('ACTIVE'),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('filters campaigns by user tags', () async {
|
test('filters campaigns by user tags', () async {
|
||||||
|
|
@ -368,10 +375,13 @@ void main() {
|
||||||
final response = await promocodesApiV2.listPromocodes(request);
|
final response = await promocodesApiV2.listPromocodes(request);
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
final body =
|
||||||
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
final campaigns = body['campaigns'] as List;
|
final campaigns = body['campaigns'] as List;
|
||||||
expect(campaigns.length, equals(2));
|
expect(campaigns.length, equals(2));
|
||||||
final templates = campaigns.map((c) => (c as Map<String, dynamic>)['template']).toList();
|
final templates = campaigns
|
||||||
|
.map((c) => (c as Map<String, dynamic>)['template'])
|
||||||
|
.toList();
|
||||||
expect(templates, contains('MATCHING'));
|
expect(templates, contains('MATCHING'));
|
||||||
expect(templates, contains('NOTAGS'));
|
expect(templates, contains('NOTAGS'));
|
||||||
expect(templates, isNot(contains('NONMATCHING')));
|
expect(templates, isNot(contains('NONMATCHING')));
|
||||||
|
|
@ -411,7 +421,8 @@ void main() {
|
||||||
final response = await promocodesApiV2.listPromocodes(request);
|
final response = await promocodesApiV2.listPromocodes(request);
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
final body =
|
||||||
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
final campaigns = body['campaigns'] as List;
|
final campaigns = body['campaigns'] as List;
|
||||||
expect(campaigns.isEmpty, isTrue);
|
expect(campaigns.isEmpty, isTrue);
|
||||||
});
|
});
|
||||||
|
|
@ -435,8 +446,14 @@ void main() {
|
||||||
);
|
);
|
||||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||||
|
|
||||||
final promoCode1 = PromoCodeModel(code: 'LIMITED1', activations: 1); // Already used
|
final promoCode1 = PromoCodeModel(
|
||||||
final promoCode2 = PromoCodeModel(code: 'LIMITED2', activations: 0); // Available
|
code: 'LIMITED1',
|
||||||
|
activations: 1,
|
||||||
|
); // Already used
|
||||||
|
final promoCode2 = PromoCodeModel(
|
||||||
|
code: 'LIMITED2',
|
||||||
|
activations: 0,
|
||||||
|
); // Available
|
||||||
await testIsar.promoCodeModels.putAll([promoCode1, promoCode2]);
|
await testIsar.promoCodeModels.putAll([promoCode1, promoCode2]);
|
||||||
|
|
||||||
campaign.promoCodes.addAll([promoCode1, promoCode2]);
|
campaign.promoCodes.addAll([promoCode1, promoCode2]);
|
||||||
|
|
@ -447,7 +464,8 @@ void main() {
|
||||||
final response = await promocodesApiV2.listPromocodes(request);
|
final response = await promocodesApiV2.listPromocodes(request);
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
final body =
|
||||||
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
final campaigns = body['campaigns'] as List;
|
final campaigns = body['campaigns'] as List;
|
||||||
expect(campaigns.length, equals(1));
|
expect(campaigns.length, equals(1));
|
||||||
|
|
||||||
|
|
@ -493,7 +511,8 @@ void main() {
|
||||||
final response = await promocodesApiV2.listPromocodes(request);
|
final response = await promocodesApiV2.listPromocodes(request);
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
final body =
|
||||||
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
final campaigns = body['campaigns'] as List;
|
final campaigns = body['campaigns'] as List;
|
||||||
expect(campaigns.length, equals(1));
|
expect(campaigns.length, equals(1));
|
||||||
|
|
||||||
|
|
@ -503,61 +522,76 @@ void main() {
|
||||||
expect(promoCodes.first, equals('AVAILABLE1'));
|
expect(promoCodes.first, equals('AVAILABLE1'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns proper JSON format with campaign and code information', () async {
|
test(
|
||||||
final now = DateTime.now();
|
'returns proper JSON format with campaign and code information',
|
||||||
final start = now.subtract(const Duration(days: 1));
|
() async {
|
||||||
final finish = now.add(const Duration(days: 1));
|
final now = DateTime.now();
|
||||||
|
final start = now.subtract(const Duration(days: 1));
|
||||||
|
final finish = now.add(const Duration(days: 1));
|
||||||
|
|
||||||
await testIsar.writeTxn(() async {
|
await testIsar.writeTxn(() async {
|
||||||
final campaign = PromoCodesCampaignModel(
|
final campaign = PromoCodesCampaignModel(
|
||||||
template: 'FORMAT',
|
template: 'FORMAT',
|
||||||
products: [],
|
products: [],
|
||||||
activationsPerCode: 10,
|
activationsPerCode: 10,
|
||||||
activationsPerUser: 1,
|
activationsPerUser: 1,
|
||||||
generationSize: 100,
|
generationSize: 100,
|
||||||
start: start,
|
start: start,
|
||||||
finish: finish,
|
finish: finish,
|
||||||
status: PromoCodeCampaignModelStatus.active,
|
status: PromoCodeCampaignModelStatus.active,
|
||||||
tags: [],
|
tags: [],
|
||||||
name: 'Test Campaign',
|
name: 'Test Campaign',
|
||||||
|
);
|
||||||
|
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||||
|
|
||||||
|
final promoCode = PromoCodeModel(code: 'FORMAT123');
|
||||||
|
await testIsar.promoCodeModels.put(promoCode);
|
||||||
|
campaign.promoCodes.add(promoCode);
|
||||||
|
await campaign.promoCodes.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
final request = buildRequest(
|
||||||
|
'GET',
|
||||||
|
'/api/v2/promocodes',
|
||||||
|
user: testUser,
|
||||||
);
|
);
|
||||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
final response = await promocodesApiV2.listPromocodes(request);
|
||||||
|
|
||||||
final promoCode = PromoCodeModel(code: 'FORMAT123');
|
expect(response.statusCode, equals(200));
|
||||||
await testIsar.promoCodeModels.put(promoCode);
|
final body =
|
||||||
campaign.promoCodes.add(promoCode);
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
await campaign.promoCodes.save();
|
expect(body.containsKey('campaigns'), isTrue);
|
||||||
});
|
|
||||||
|
|
||||||
final request = buildRequest('GET', '/api/v2/promocodes', user: testUser);
|
final campaigns = body['campaigns'] as List;
|
||||||
final response = await promocodesApiV2.listPromocodes(request);
|
expect(campaigns.length, equals(1));
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
final campaign = campaigns.first as Map<String, dynamic>;
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
expect(campaign.containsKey('id'), isTrue);
|
||||||
expect(body.containsKey('campaigns'), isTrue);
|
expect(campaign.containsKey('name'), isTrue);
|
||||||
|
expect(campaign.containsKey('status'), isTrue);
|
||||||
final campaigns = body['campaigns'] as List;
|
expect(campaign.containsKey('start'), isTrue);
|
||||||
expect(campaigns.length, equals(1));
|
expect(campaign.containsKey('finish'), isTrue);
|
||||||
|
expect(campaign.containsKey('promoCodes'), isTrue);
|
||||||
final campaign = campaigns.first as Map<String, dynamic>;
|
expect(campaign['status'], equals('active'));
|
||||||
expect(campaign.containsKey('id'), isTrue);
|
expect(campaign['promoCodes'], isA<List>());
|
||||||
expect(campaign.containsKey('name'), isTrue);
|
},
|
||||||
expect(campaign.containsKey('status'), isTrue);
|
);
|
||||||
expect(campaign.containsKey('start'), isTrue);
|
|
||||||
expect(campaign.containsKey('finish'), isTrue);
|
|
||||||
expect(campaign.containsKey('promoCodes'), isTrue);
|
|
||||||
expect(campaign['status'], equals('active'));
|
|
||||||
expect(campaign['promoCodes'], isA<List>());
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
group('GET /api/v2/promocodes/{code}/validate', () {
|
group('GET /api/v2/promocodes/{code}/validate', () {
|
||||||
test('returns 401 for unauthenticated requests', () async {
|
test('returns 401 for unauthenticated requests', () async {
|
||||||
final request = buildRequest('GET', '/api/v2/promocodes/TEST123/validate');
|
final request = buildRequest(
|
||||||
final response = await promocodesApiV2.validatePromocode(request, 'TEST123');
|
'GET',
|
||||||
|
'/api/v2/promocodes/TEST123/validate',
|
||||||
|
);
|
||||||
|
final response = await promocodesApiV2.validatePromocode(
|
||||||
|
request,
|
||||||
|
'TEST123',
|
||||||
|
);
|
||||||
|
|
||||||
expect(response.statusCode, equals(401));
|
expect(response.statusCode, equals(401));
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
final body =
|
||||||
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
expect(body['error'], equals('unauthorized'));
|
expect(body['error'], equals('unauthorized'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -567,10 +601,14 @@ void main() {
|
||||||
'/api/v2/promocodes/NONEXISTENT/validate',
|
'/api/v2/promocodes/NONEXISTENT/validate',
|
||||||
user: testUser,
|
user: testUser,
|
||||||
);
|
);
|
||||||
final response = await promocodesApiV2.validatePromocode(request, 'NONEXISTENT');
|
final response = await promocodesApiV2.validatePromocode(
|
||||||
|
request,
|
||||||
|
'NONEXISTENT',
|
||||||
|
);
|
||||||
|
|
||||||
expect(response.statusCode, equals(404));
|
expect(response.statusCode, equals(404));
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
final body =
|
||||||
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
expect(body['valid'], equals(false));
|
expect(body['valid'], equals(false));
|
||||||
expect(body['message'], equals('Промокод не найден'));
|
expect(body['message'], equals('Промокод не найден'));
|
||||||
});
|
});
|
||||||
|
|
@ -584,7 +622,8 @@ void main() {
|
||||||
final response = await promocodesApiV2.validatePromocode(request, '');
|
final response = await promocodesApiV2.validatePromocode(request, '');
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
final body =
|
||||||
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
expect(body['error'], equals('bad_request'));
|
expect(body['error'], equals('bad_request'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -618,10 +657,14 @@ void main() {
|
||||||
'/api/v2/promocodes/VALID123/validate',
|
'/api/v2/promocodes/VALID123/validate',
|
||||||
user: testUser,
|
user: testUser,
|
||||||
);
|
);
|
||||||
final response = await promocodesApiV2.validatePromocode(request, 'VALID123');
|
final response = await promocodesApiV2.validatePromocode(
|
||||||
|
request,
|
||||||
|
'VALID123',
|
||||||
|
);
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
final body =
|
||||||
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
expect(body['valid'], equals(true));
|
expect(body['valid'], equals(true));
|
||||||
expect(body['message'], equals('Промокод действителен'));
|
expect(body['message'], equals('Промокод действителен'));
|
||||||
});
|
});
|
||||||
|
|
@ -656,10 +699,14 @@ void main() {
|
||||||
'/api/v2/promocodes/INACTIVE123/validate',
|
'/api/v2/promocodes/INACTIVE123/validate',
|
||||||
user: testUser,
|
user: testUser,
|
||||||
);
|
);
|
||||||
final response = await promocodesApiV2.validatePromocode(request, 'INACTIVE123');
|
final response = await promocodesApiV2.validatePromocode(
|
||||||
|
request,
|
||||||
|
'INACTIVE123',
|
||||||
|
);
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
final body =
|
||||||
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
expect(body['valid'], equals(false));
|
expect(body['valid'], equals(false));
|
||||||
expect(body['message'], equals('Промокод недействителен'));
|
expect(body['message'], equals('Промокод недействителен'));
|
||||||
});
|
});
|
||||||
|
|
@ -694,10 +741,14 @@ void main() {
|
||||||
'/api/v2/promocodes/FUTURE123/validate',
|
'/api/v2/promocodes/FUTURE123/validate',
|
||||||
user: testUser,
|
user: testUser,
|
||||||
);
|
);
|
||||||
final response = await promocodesApiV2.validatePromocode(request, 'FUTURE123');
|
final response = await promocodesApiV2.validatePromocode(
|
||||||
|
request,
|
||||||
|
'FUTURE123',
|
||||||
|
);
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
final body =
|
||||||
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
expect(body['valid'], equals(false));
|
expect(body['valid'], equals(false));
|
||||||
expect(body['message'], equals('Промокод еще не активен'));
|
expect(body['message'], equals('Промокод еще не активен'));
|
||||||
});
|
});
|
||||||
|
|
@ -732,176 +783,211 @@ void main() {
|
||||||
'/api/v2/promocodes/EXPIRED123/validate',
|
'/api/v2/promocodes/EXPIRED123/validate',
|
||||||
user: testUser,
|
user: testUser,
|
||||||
);
|
);
|
||||||
final response = await promocodesApiV2.validatePromocode(request, 'EXPIRED123');
|
final response = await promocodesApiV2.validatePromocode(
|
||||||
|
request,
|
||||||
|
'EXPIRED123',
|
||||||
|
);
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
final body =
|
||||||
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
expect(body['valid'], equals(false));
|
expect(body['valid'], equals(false));
|
||||||
expect(body['message'], equals('Промокод истек'));
|
expect(body['message'], equals('Промокод истек'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns valid: false for promocode that reached activation limit', () async {
|
test(
|
||||||
final now = DateTime.now();
|
'returns valid: false for promocode that reached activation limit',
|
||||||
final start = now.subtract(const Duration(days: 1));
|
() async {
|
||||||
final finish = now.add(const Duration(days: 1));
|
final now = DateTime.now();
|
||||||
|
final start = now.subtract(const Duration(days: 1));
|
||||||
|
final finish = now.add(const Duration(days: 1));
|
||||||
|
|
||||||
await testIsar.writeTxn(() async {
|
await testIsar.writeTxn(() async {
|
||||||
final campaign = PromoCodesCampaignModel(
|
final campaign = PromoCodesCampaignModel(
|
||||||
template: 'EXHAUSTED',
|
template: 'EXHAUSTED',
|
||||||
products: [],
|
products: [],
|
||||||
activationsPerCode: 1,
|
activationsPerCode: 1,
|
||||||
activationsPerUser: 10,
|
activationsPerUser: 10,
|
||||||
generationSize: 100,
|
generationSize: 100,
|
||||||
start: start,
|
start: start,
|
||||||
finish: finish,
|
finish: finish,
|
||||||
status: PromoCodeCampaignModelStatus.active,
|
status: PromoCodeCampaignModelStatus.active,
|
||||||
tags: [],
|
tags: [],
|
||||||
|
);
|
||||||
|
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||||
|
|
||||||
|
final promoCode = PromoCodeModel(
|
||||||
|
code: 'EXHAUSTED123',
|
||||||
|
activations: 1,
|
||||||
|
);
|
||||||
|
await testIsar.promoCodeModels.put(promoCode);
|
||||||
|
campaign.promoCodes.add(promoCode);
|
||||||
|
await campaign.promoCodes.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
final request = buildRequest(
|
||||||
|
'GET',
|
||||||
|
'/api/v2/promocodes/EXHAUSTED123/validate',
|
||||||
|
user: testUser,
|
||||||
);
|
);
|
||||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
final response = await promocodesApiV2.validatePromocode(
|
||||||
|
request,
|
||||||
final promoCode = PromoCodeModel(code: 'EXHAUSTED123', activations: 1);
|
'EXHAUSTED123',
|
||||||
await testIsar.promoCodeModels.put(promoCode);
|
|
||||||
campaign.promoCodes.add(promoCode);
|
|
||||||
await campaign.promoCodes.save();
|
|
||||||
});
|
|
||||||
|
|
||||||
final request = buildRequest(
|
|
||||||
'GET',
|
|
||||||
'/api/v2/promocodes/EXHAUSTED123/validate',
|
|
||||||
user: testUser,
|
|
||||||
);
|
|
||||||
final response = await promocodesApiV2.validatePromocode(request, 'EXHAUSTED123');
|
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
|
||||||
expect(body['valid'], equals(false));
|
|
||||||
expect(body['message'], equals('Промокод исчерпан'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns valid: false for promocode already activated by user', () async {
|
|
||||||
final now = DateTime.now();
|
|
||||||
final start = now.subtract(const Duration(days: 1));
|
|
||||||
final finish = now.add(const Duration(days: 1));
|
|
||||||
|
|
||||||
await testIsar.writeTxn(() async {
|
|
||||||
final campaign = PromoCodesCampaignModel(
|
|
||||||
template: 'USED',
|
|
||||||
products: [],
|
|
||||||
activationsPerCode: 10,
|
|
||||||
activationsPerUser: 10,
|
|
||||||
generationSize: 100,
|
|
||||||
start: start,
|
|
||||||
finish: finish,
|
|
||||||
status: PromoCodeCampaignModelStatus.active,
|
|
||||||
tags: [],
|
|
||||||
);
|
);
|
||||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
|
||||||
|
|
||||||
final promoCode = PromoCodeModel(code: 'USED123', activations: 0);
|
expect(response.statusCode, equals(200));
|
||||||
await testIsar.promoCodeModels.put(promoCode);
|
final body =
|
||||||
campaign.promoCodes.add(promoCode);
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
await campaign.promoCodes.save();
|
expect(body['valid'], equals(false));
|
||||||
|
expect(body['message'], equals('Промокод исчерпан'));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// User has already activated this code
|
test(
|
||||||
testUserData.activatedPromoCodes.add(promoCode);
|
'returns valid: false for promocode already activated by user',
|
||||||
await testUserData.activatedPromoCodes.save();
|
() async {
|
||||||
await testIsar.userDataModels.put(testUserData);
|
final now = DateTime.now();
|
||||||
});
|
final start = now.subtract(const Duration(days: 1));
|
||||||
|
final finish = now.add(const Duration(days: 1));
|
||||||
|
|
||||||
final request = buildRequest(
|
await testIsar.writeTxn(() async {
|
||||||
'GET',
|
final campaign = PromoCodesCampaignModel(
|
||||||
'/api/v2/promocodes/USED123/validate',
|
template: 'USED',
|
||||||
user: testUser,
|
products: [],
|
||||||
);
|
activationsPerCode: 10,
|
||||||
final response = await promocodesApiV2.validatePromocode(request, 'USED123');
|
activationsPerUser: 10,
|
||||||
|
generationSize: 100,
|
||||||
|
start: start,
|
||||||
|
finish: finish,
|
||||||
|
status: PromoCodeCampaignModelStatus.active,
|
||||||
|
tags: [],
|
||||||
|
);
|
||||||
|
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
final promoCode = PromoCodeModel(code: 'USED123', activations: 0);
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
await testIsar.promoCodeModels.put(promoCode);
|
||||||
expect(body['valid'], equals(false));
|
campaign.promoCodes.add(promoCode);
|
||||||
expect(body['message'], equals('Промокод уже был активирован'));
|
await campaign.promoCodes.save();
|
||||||
});
|
|
||||||
|
|
||||||
test('returns valid: false when user reached campaign activation limit', () async {
|
// User has already activated this code
|
||||||
final now = DateTime.now();
|
testUserData.activatedPromoCodes.add(promoCode);
|
||||||
final start = now.subtract(const Duration(days: 1));
|
await testUserData.activatedPromoCodes.save();
|
||||||
final finish = now.add(const Duration(days: 1));
|
await testIsar.userDataModels.put(testUserData);
|
||||||
|
});
|
||||||
|
|
||||||
await testIsar.writeTxn(() async {
|
final request = buildRequest(
|
||||||
final campaign = PromoCodesCampaignModel(
|
'GET',
|
||||||
template: 'LIMITED',
|
'/api/v2/promocodes/USED123/validate',
|
||||||
products: [],
|
user: testUser,
|
||||||
activationsPerCode: 10,
|
|
||||||
activationsPerUser: 1, // User can only activate once
|
|
||||||
generationSize: 100,
|
|
||||||
start: start,
|
|
||||||
finish: finish,
|
|
||||||
status: PromoCodeCampaignModelStatus.active,
|
|
||||||
tags: [],
|
|
||||||
);
|
);
|
||||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
final response = await promocodesApiV2.validatePromocode(
|
||||||
|
request,
|
||||||
final promoCode1 = PromoCodeModel(code: 'LIMITED1', activations: 0);
|
'USED123',
|
||||||
final promoCode2 = PromoCodeModel(code: 'LIMITED2', activations: 0);
|
|
||||||
await testIsar.promoCodeModels.putAll([promoCode1, promoCode2]);
|
|
||||||
campaign.promoCodes.addAll([promoCode1, promoCode2]);
|
|
||||||
await campaign.promoCodes.save();
|
|
||||||
|
|
||||||
// User has already activated one code from this campaign
|
|
||||||
testUserData.activatedPromoCodes.add(promoCode1);
|
|
||||||
await testUserData.activatedPromoCodes.save();
|
|
||||||
await testIsar.userDataModels.put(testUserData);
|
|
||||||
});
|
|
||||||
|
|
||||||
final request = buildRequest(
|
|
||||||
'GET',
|
|
||||||
'/api/v2/promocodes/LIMITED2/validate',
|
|
||||||
user: testUser,
|
|
||||||
);
|
|
||||||
final response = await promocodesApiV2.validatePromocode(request, 'LIMITED2');
|
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
|
||||||
expect(body['valid'], equals(false));
|
|
||||||
expect(body['message'], equals('Вы уже участвовали в этой акции'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns valid: false when user tags do not match campaign tags', () async {
|
|
||||||
final now = DateTime.now();
|
|
||||||
final start = now.subtract(const Duration(days: 1));
|
|
||||||
final finish = now.add(const Duration(days: 1));
|
|
||||||
|
|
||||||
await testIsar.writeTxn(() async {
|
|
||||||
final campaign = PromoCodesCampaignModel(
|
|
||||||
template: 'TAGGED',
|
|
||||||
products: [],
|
|
||||||
activationsPerCode: 10,
|
|
||||||
activationsPerUser: 1,
|
|
||||||
generationSize: 100,
|
|
||||||
start: start,
|
|
||||||
finish: finish,
|
|
||||||
status: PromoCodeCampaignModelStatus.active,
|
|
||||||
tags: ['vip'], // User has ['premium', 'beta'], no 'vip'
|
|
||||||
);
|
);
|
||||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
|
||||||
|
|
||||||
final promoCode = PromoCodeModel(code: 'TAGGED123', activations: 0);
|
expect(response.statusCode, equals(200));
|
||||||
await testIsar.promoCodeModels.put(promoCode);
|
final body =
|
||||||
campaign.promoCodes.add(promoCode);
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
await campaign.promoCodes.save();
|
expect(body['valid'], equals(false));
|
||||||
});
|
expect(body['message'], equals('Промокод уже был активирован'));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
final request = buildRequest(
|
test(
|
||||||
'GET',
|
'returns valid: false when user reached campaign activation limit',
|
||||||
'/api/v2/promocodes/TAGGED123/validate',
|
() async {
|
||||||
user: testUser,
|
final now = DateTime.now();
|
||||||
);
|
final start = now.subtract(const Duration(days: 1));
|
||||||
final response = await promocodesApiV2.validatePromocode(request, 'TAGGED123');
|
final finish = now.add(const Duration(days: 1));
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
await testIsar.writeTxn(() async {
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
final campaign = PromoCodesCampaignModel(
|
||||||
expect(body['valid'], equals(false));
|
template: 'LIMITED',
|
||||||
expect(body['message'], equals('Промокод недействителен'));
|
products: [],
|
||||||
});
|
activationsPerCode: 10,
|
||||||
|
activationsPerUser: 1, // User can only activate once
|
||||||
|
generationSize: 100,
|
||||||
|
start: start,
|
||||||
|
finish: finish,
|
||||||
|
status: PromoCodeCampaignModelStatus.active,
|
||||||
|
tags: [],
|
||||||
|
);
|
||||||
|
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||||
|
|
||||||
|
final promoCode1 = PromoCodeModel(code: 'LIMITED1', activations: 0);
|
||||||
|
final promoCode2 = PromoCodeModel(code: 'LIMITED2', activations: 0);
|
||||||
|
await testIsar.promoCodeModels.putAll([promoCode1, promoCode2]);
|
||||||
|
campaign.promoCodes.addAll([promoCode1, promoCode2]);
|
||||||
|
await campaign.promoCodes.save();
|
||||||
|
|
||||||
|
// User has already activated one code from this campaign
|
||||||
|
testUserData.activatedPromoCodes.add(promoCode1);
|
||||||
|
await testUserData.activatedPromoCodes.save();
|
||||||
|
await testIsar.userDataModels.put(testUserData);
|
||||||
|
});
|
||||||
|
|
||||||
|
final request = buildRequest(
|
||||||
|
'GET',
|
||||||
|
'/api/v2/promocodes/LIMITED2/validate',
|
||||||
|
user: testUser,
|
||||||
|
);
|
||||||
|
final response = await promocodesApiV2.validatePromocode(
|
||||||
|
request,
|
||||||
|
'LIMITED2',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.statusCode, equals(200));
|
||||||
|
final body =
|
||||||
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
expect(body['valid'], equals(false));
|
||||||
|
expect(body['message'], equals('Вы уже участвовали в этой акции'));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'returns valid: false when user tags do not match campaign tags',
|
||||||
|
() async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final start = now.subtract(const Duration(days: 1));
|
||||||
|
final finish = now.add(const Duration(days: 1));
|
||||||
|
|
||||||
|
await testIsar.writeTxn(() async {
|
||||||
|
final campaign = PromoCodesCampaignModel(
|
||||||
|
template: 'TAGGED',
|
||||||
|
products: [],
|
||||||
|
activationsPerCode: 10,
|
||||||
|
activationsPerUser: 1,
|
||||||
|
generationSize: 100,
|
||||||
|
start: start,
|
||||||
|
finish: finish,
|
||||||
|
status: PromoCodeCampaignModelStatus.active,
|
||||||
|
tags: ['vip'], // User has ['premium', 'beta'], no 'vip'
|
||||||
|
);
|
||||||
|
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||||
|
|
||||||
|
final promoCode = PromoCodeModel(code: 'TAGGED123', activations: 0);
|
||||||
|
await testIsar.promoCodeModels.put(promoCode);
|
||||||
|
campaign.promoCodes.add(promoCode);
|
||||||
|
await campaign.promoCodes.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
final request = buildRequest(
|
||||||
|
'GET',
|
||||||
|
'/api/v2/promocodes/TAGGED123/validate',
|
||||||
|
user: testUser,
|
||||||
|
);
|
||||||
|
final response = await promocodesApiV2.validatePromocode(
|
||||||
|
request,
|
||||||
|
'TAGGED123',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.statusCode, equals(200));
|
||||||
|
final body =
|
||||||
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
expect(body['valid'], equals(false));
|
||||||
|
expect(body['message'], equals('Промокод недействителен'));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('returns valid: true when user tags match campaign tags', () async {
|
test('returns valid: true when user tags match campaign tags', () async {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
|
@ -933,118 +1019,139 @@ void main() {
|
||||||
'/api/v2/promocodes/MATCHING123/validate',
|
'/api/v2/promocodes/MATCHING123/validate',
|
||||||
user: testUser,
|
user: testUser,
|
||||||
);
|
);
|
||||||
final response = await promocodesApiV2.validatePromocode(request, 'MATCHING123');
|
final response = await promocodesApiV2.validatePromocode(
|
||||||
|
request,
|
||||||
|
'MATCHING123',
|
||||||
|
);
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
final body =
|
||||||
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
expect(body['valid'], equals(true));
|
expect(body['valid'], equals(true));
|
||||||
expect(body['message'], equals('Промокод действителен'));
|
expect(body['message'], equals('Промокод действителен'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns valid: false for individual promocode for another user', () async {
|
test(
|
||||||
final now = DateTime.now();
|
'returns valid: false for individual promocode for another user',
|
||||||
final start = now.subtract(const Duration(days: 1));
|
() async {
|
||||||
final finish = now.add(const Duration(days: 1));
|
final now = DateTime.now();
|
||||||
|
final start = now.subtract(const Duration(days: 1));
|
||||||
|
final finish = now.add(const Duration(days: 1));
|
||||||
|
|
||||||
late UserModel otherUser;
|
late UserModel otherUser;
|
||||||
late UserDataModel otherUserData;
|
late UserDataModel otherUserData;
|
||||||
|
|
||||||
await testIsar.writeTxn(() async {
|
await testIsar.writeTxn(() async {
|
||||||
// Create another user
|
// Create another user
|
||||||
otherUser = UserModel.empty.copyWith(
|
otherUser = UserModel.empty.copyWith(
|
||||||
id: 2,
|
id: 2,
|
||||||
name: 'Other User',
|
name: 'Other User',
|
||||||
email: 'other@example.com',
|
email: 'other@example.com',
|
||||||
|
);
|
||||||
|
await testIsar.userModels.put(otherUser);
|
||||||
|
|
||||||
|
otherUserData = UserDataModel(tags: [], words: []);
|
||||||
|
await testIsar.userDataModels.put(otherUserData);
|
||||||
|
otherUserData.user.value = otherUser;
|
||||||
|
await otherUserData.user.save();
|
||||||
|
otherUser.userData.value = otherUserData;
|
||||||
|
await otherUser.userData.save();
|
||||||
|
|
||||||
|
final campaign = PromoCodesCampaignModel(
|
||||||
|
template: 'INDIVIDUAL',
|
||||||
|
products: [],
|
||||||
|
activationsPerCode: 10,
|
||||||
|
activationsPerUser: 1,
|
||||||
|
generationSize: 100,
|
||||||
|
start: start,
|
||||||
|
finish: finish,
|
||||||
|
status: PromoCodeCampaignModelStatus.active,
|
||||||
|
tags: [],
|
||||||
|
);
|
||||||
|
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||||
|
|
||||||
|
final promoCode = PromoCodeModel(
|
||||||
|
code: 'INDIVIDUAL123',
|
||||||
|
activations: 0,
|
||||||
|
);
|
||||||
|
await testIsar.promoCodeModels.put(promoCode);
|
||||||
|
campaign.promoCodes.add(promoCode);
|
||||||
|
await campaign.promoCodes.save();
|
||||||
|
|
||||||
|
// Assign promocode to other user
|
||||||
|
promoCode.userData.value = otherUserData;
|
||||||
|
await promoCode.userData.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
final request = buildRequest(
|
||||||
|
'GET',
|
||||||
|
'/api/v2/promocodes/INDIVIDUAL123/validate',
|
||||||
|
user: testUser,
|
||||||
);
|
);
|
||||||
await testIsar.userModels.put(otherUser);
|
final response = await promocodesApiV2.validatePromocode(
|
||||||
|
request,
|
||||||
otherUserData = UserDataModel(
|
'INDIVIDUAL123',
|
||||||
tags: [],
|
|
||||||
words: [],
|
|
||||||
);
|
);
|
||||||
await testIsar.userDataModels.put(otherUserData);
|
|
||||||
otherUserData.user.value = otherUser;
|
|
||||||
await otherUserData.user.save();
|
|
||||||
otherUser.userData.value = otherUserData;
|
|
||||||
await otherUser.userData.save();
|
|
||||||
|
|
||||||
final campaign = PromoCodesCampaignModel(
|
expect(response.statusCode, equals(200));
|
||||||
template: 'INDIVIDUAL',
|
final body =
|
||||||
products: [],
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
activationsPerCode: 10,
|
expect(body['valid'], equals(false));
|
||||||
activationsPerUser: 1,
|
expect(
|
||||||
generationSize: 100,
|
body['message'],
|
||||||
start: start,
|
equals('Это промокод для другого пользователя'),
|
||||||
finish: finish,
|
|
||||||
status: PromoCodeCampaignModelStatus.active,
|
|
||||||
tags: [],
|
|
||||||
);
|
);
|
||||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
},
|
||||||
|
);
|
||||||
|
|
||||||
final promoCode = PromoCodeModel(code: 'INDIVIDUAL123', activations: 0);
|
test(
|
||||||
await testIsar.promoCodeModels.put(promoCode);
|
'returns valid: true for individual promocode for current user',
|
||||||
campaign.promoCodes.add(promoCode);
|
() async {
|
||||||
await campaign.promoCodes.save();
|
final now = DateTime.now();
|
||||||
|
final start = now.subtract(const Duration(days: 1));
|
||||||
|
final finish = now.add(const Duration(days: 1));
|
||||||
|
|
||||||
// Assign promocode to other user
|
await testIsar.writeTxn(() async {
|
||||||
promoCode.userData.value = otherUserData;
|
final campaign = PromoCodesCampaignModel(
|
||||||
await promoCode.userData.save();
|
template: 'MYCODE',
|
||||||
});
|
products: [],
|
||||||
|
activationsPerCode: 10,
|
||||||
|
activationsPerUser: 1,
|
||||||
|
generationSize: 100,
|
||||||
|
start: start,
|
||||||
|
finish: finish,
|
||||||
|
status: PromoCodeCampaignModelStatus.active,
|
||||||
|
tags: [],
|
||||||
|
);
|
||||||
|
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||||
|
|
||||||
final request = buildRequest(
|
final promoCode = PromoCodeModel(code: 'MYCODE123', activations: 0);
|
||||||
'GET',
|
await testIsar.promoCodeModels.put(promoCode);
|
||||||
'/api/v2/promocodes/INDIVIDUAL123/validate',
|
campaign.promoCodes.add(promoCode);
|
||||||
user: testUser,
|
await campaign.promoCodes.save();
|
||||||
);
|
|
||||||
final response = await promocodesApiV2.validatePromocode(request, 'INDIVIDUAL123');
|
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
// Assign promocode to current user
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
promoCode.userData.value = testUserData;
|
||||||
expect(body['valid'], equals(false));
|
await promoCode.userData.save();
|
||||||
expect(body['message'], equals('Это промокод для другого пользователя'));
|
});
|
||||||
});
|
|
||||||
|
|
||||||
test('returns valid: true for individual promocode for current user', () async {
|
final request = buildRequest(
|
||||||
final now = DateTime.now();
|
'GET',
|
||||||
final start = now.subtract(const Duration(days: 1));
|
'/api/v2/promocodes/MYCODE123/validate',
|
||||||
final finish = now.add(const Duration(days: 1));
|
user: testUser,
|
||||||
|
);
|
||||||
await testIsar.writeTxn(() async {
|
final response = await promocodesApiV2.validatePromocode(
|
||||||
final campaign = PromoCodesCampaignModel(
|
request,
|
||||||
template: 'MYCODE',
|
'MYCODE123',
|
||||||
products: [],
|
|
||||||
activationsPerCode: 10,
|
|
||||||
activationsPerUser: 1,
|
|
||||||
generationSize: 100,
|
|
||||||
start: start,
|
|
||||||
finish: finish,
|
|
||||||
status: PromoCodeCampaignModelStatus.active,
|
|
||||||
tags: [],
|
|
||||||
);
|
);
|
||||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
|
||||||
|
|
||||||
final promoCode = PromoCodeModel(code: 'MYCODE123', activations: 0);
|
expect(response.statusCode, equals(200));
|
||||||
await testIsar.promoCodeModels.put(promoCode);
|
final body =
|
||||||
campaign.promoCodes.add(promoCode);
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
await campaign.promoCodes.save();
|
expect(body['valid'], equals(true));
|
||||||
|
expect(body['message'], equals('Промокод действителен'));
|
||||||
// Assign promocode to current user
|
},
|
||||||
promoCode.userData.value = testUserData;
|
);
|
||||||
await promoCode.userData.save();
|
|
||||||
});
|
|
||||||
|
|
||||||
final request = buildRequest(
|
|
||||||
'GET',
|
|
||||||
'/api/v2/promocodes/MYCODE123/validate',
|
|
||||||
user: testUser,
|
|
||||||
);
|
|
||||||
final response = await promocodesApiV2.validatePromocode(request, 'MYCODE123');
|
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
|
||||||
expect(body['valid'], equals(true));
|
|
||||||
expect(body['message'], equals('Промокод действителен'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('handles case-insensitive promocode', () async {
|
test('handles case-insensitive promocode', () async {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
|
@ -1077,10 +1184,14 @@ void main() {
|
||||||
'/api/v2/promocodes/case123/validate',
|
'/api/v2/promocodes/case123/validate',
|
||||||
user: testUser,
|
user: testUser,
|
||||||
);
|
);
|
||||||
final response = await promocodesApiV2.validatePromocode(request, 'case123');
|
final response = await promocodesApiV2.validatePromocode(
|
||||||
|
request,
|
||||||
|
'case123',
|
||||||
|
);
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
final body =
|
||||||
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
expect(body['valid'], equals(true));
|
expect(body['valid'], equals(true));
|
||||||
expect(body['message'], equals('Промокод действителен'));
|
expect(body['message'], equals('Промокод действителен'));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -64,10 +64,7 @@ void main() {
|
||||||
final discountsManager = const DiscountsManager();
|
final discountsManager = const DiscountsManager();
|
||||||
final productsPriceResolver = ProductsPriceResolver(discountsManager);
|
final productsPriceResolver = ProductsPriceResolver(discountsManager);
|
||||||
final adsManager = AdsManager();
|
final adsManager = AdsManager();
|
||||||
packDtoConverter = PackDtoConverter(
|
packDtoConverter = PackDtoConverter(productsPriceResolver, adsManager);
|
||||||
productsPriceResolver,
|
|
||||||
adsManager,
|
|
||||||
);
|
|
||||||
|
|
||||||
packManager = PackManager(packDtoConverter);
|
packManager = PackManager(packDtoConverter);
|
||||||
|
|
||||||
|
|
@ -141,8 +138,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await purchasesApiV2.createPackPurchase(request, '20');
|
final response = await purchasesApiV2.createPackPurchase(request, '20');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(401));
|
expect(response.statusCode, equals(401));
|
||||||
expect(responseBody['error'], equals('Unauthorized'));
|
expect(responseBody['error'], equals('Unauthorized'));
|
||||||
|
|
@ -155,8 +152,8 @@ void main() {
|
||||||
).change(context: {'user': testUser});
|
).change(context: {'user': testUser});
|
||||||
|
|
||||||
final response = await purchasesApiV2.createPackPurchase(request, '999');
|
final response = await purchasesApiV2.createPackPurchase(request, '999');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(404));
|
expect(response.statusCode, equals(404));
|
||||||
expect(responseBody['error'], equals('Not Found'));
|
expect(responseBody['error'], equals('Not Found'));
|
||||||
|
|
@ -185,8 +182,8 @@ void main() {
|
||||||
).change(context: {'user': updatedUser!});
|
).change(context: {'user': updatedUser!});
|
||||||
|
|
||||||
final response = await purchasesApiV2.createPackPurchase(request, '20');
|
final response = await purchasesApiV2.createPackPurchase(request, '20');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('Bad Request'));
|
expect(responseBody['error'], equals('Bad Request'));
|
||||||
|
|
@ -205,8 +202,8 @@ void main() {
|
||||||
request,
|
request,
|
||||||
'20',
|
'20',
|
||||||
);
|
);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['packId'], equals('20'));
|
expect(responseBody['packId'], equals('20'));
|
||||||
|
|
@ -241,8 +238,8 @@ void main() {
|
||||||
request,
|
request,
|
||||||
'20',
|
'20',
|
||||||
);
|
);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['isPurchased'], isTrue);
|
expect(responseBody['isPurchased'], isTrue);
|
||||||
|
|
@ -259,8 +256,8 @@ void main() {
|
||||||
request,
|
request,
|
||||||
'20',
|
'20',
|
||||||
);
|
);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(401));
|
expect(response.statusCode, equals(401));
|
||||||
expect(responseBody['error'], equals('Unauthorized'));
|
expect(responseBody['error'], equals('Unauthorized'));
|
||||||
|
|
@ -276,8 +273,8 @@ void main() {
|
||||||
request,
|
request,
|
||||||
'invalid',
|
'invalid',
|
||||||
);
|
);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('Bad Request'));
|
expect(responseBody['error'], equals('Bad Request'));
|
||||||
|
|
@ -289,15 +286,12 @@ void main() {
|
||||||
final request = Request(
|
final request = Request(
|
||||||
'POST',
|
'POST',
|
||||||
Uri.parse('http://localhost/api/v2/purchases/payments'),
|
Uri.parse('http://localhost/api/v2/purchases/payments'),
|
||||||
body: jsonEncode({
|
body: jsonEncode({'productId': '20', 'productType': 'pack'}),
|
||||||
'productId': '20',
|
|
||||||
'productType': 'pack',
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await purchasesApiV2.createPayment(request);
|
final response = await purchasesApiV2.createPayment(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(401));
|
expect(response.statusCode, equals(401));
|
||||||
expect(responseBody['error'], equals('Unauthorized'));
|
expect(responseBody['error'], equals('Unauthorized'));
|
||||||
|
|
@ -307,14 +301,12 @@ void main() {
|
||||||
final request = Request(
|
final request = Request(
|
||||||
'POST',
|
'POST',
|
||||||
Uri.parse('http://localhost/api/v2/purchases/payments'),
|
Uri.parse('http://localhost/api/v2/purchases/payments'),
|
||||||
body: jsonEncode({
|
body: jsonEncode({'productType': 'pack'}),
|
||||||
'productType': 'pack',
|
|
||||||
}),
|
|
||||||
).change(context: {'user': testUser});
|
).change(context: {'user': testUser});
|
||||||
|
|
||||||
final response = await purchasesApiV2.createPayment(request);
|
final response = await purchasesApiV2.createPayment(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('Bad Request'));
|
expect(responseBody['error'], equals('Bad Request'));
|
||||||
|
|
@ -329,8 +321,8 @@ void main() {
|
||||||
).change(context: {'user': testUser});
|
).change(context: {'user': testUser});
|
||||||
|
|
||||||
final response = await purchasesApiV2.createPayment(request);
|
final response = await purchasesApiV2.createPayment(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('Bad Request'));
|
expect(responseBody['error'], equals('Bad Request'));
|
||||||
|
|
@ -351,8 +343,8 @@ void main() {
|
||||||
request,
|
request,
|
||||||
'payment123',
|
'payment123',
|
||||||
);
|
);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(401));
|
expect(response.statusCode, equals(401));
|
||||||
expect(responseBody['error'], equals('Unauthorized'));
|
expect(responseBody['error'], equals('Unauthorized'));
|
||||||
|
|
@ -370,8 +362,8 @@ void main() {
|
||||||
request,
|
request,
|
||||||
'payment123',
|
'payment123',
|
||||||
);
|
);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('Bad Request'));
|
expect(responseBody['error'], equals('Bad Request'));
|
||||||
|
|
@ -397,8 +389,8 @@ void main() {
|
||||||
expect(response.statusCode, isIn([200, 500]));
|
expect(response.statusCode, isIn([200, 500]));
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
expect(responseBody['paymentId'], equals('payment123'));
|
expect(responseBody['paymentId'], equals('payment123'));
|
||||||
expect(responseBody['status'], isA<String>());
|
expect(responseBody['status'], isA<String>());
|
||||||
expect(responseBody['result'], isA<bool>());
|
expect(responseBody['result'], isA<bool>());
|
||||||
|
|
@ -406,4 +398,3 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,20 +87,23 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
group('SubscriptionsApiV2 - Get Plans', () {
|
group('SubscriptionsApiV2 - Get Plans', () {
|
||||||
test('should return 200 with empty array when no plans available', () async {
|
test(
|
||||||
final request = Request(
|
'should return 200 with empty array when no plans available',
|
||||||
'GET',
|
() async {
|
||||||
Uri.parse('http://localhost/api/v2/subscriptions/plans'),
|
final request = Request(
|
||||||
);
|
'GET',
|
||||||
|
Uri.parse('http://localhost/api/v2/subscriptions/plans'),
|
||||||
|
);
|
||||||
|
|
||||||
final response = await subscriptionsApiV2.getPlans(request);
|
final response = await subscriptionsApiV2.getPlans(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['plans'], isA<List>());
|
expect(responseBody['plans'], isA<List>());
|
||||||
expect(responseBody['plans'], isEmpty);
|
expect(responseBody['plans'], isEmpty);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('should return 200 with list of plans when plans exist', () async {
|
test('should return 200 with list of plans when plans exist', () async {
|
||||||
// Create test subscription plans
|
// Create test subscription plans
|
||||||
|
|
@ -147,8 +150,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await subscriptionsApiV2.getPlans(request);
|
final response = await subscriptionsApiV2.getPlans(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['plans'], isA<List>());
|
expect(responseBody['plans'], isA<List>());
|
||||||
|
|
@ -194,8 +197,8 @@ void main() {
|
||||||
).change(context: {'user': testUser});
|
).change(context: {'user': testUser});
|
||||||
|
|
||||||
final response = await subscriptionsApiV2.getPlans(request);
|
final response = await subscriptionsApiV2.getPlans(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['plans'], isA<List>());
|
expect(responseBody['plans'], isA<List>());
|
||||||
|
|
@ -228,8 +231,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await subscriptionsApiV2.getPlans(request);
|
final response = await subscriptionsApiV2.getPlans(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(response.headers['content-type'], contains('application/json'));
|
expect(response.headers['content-type'], contains('application/json'));
|
||||||
|
|
@ -249,57 +252,56 @@ void main() {
|
||||||
expect(plan.containsKey('ui'), isTrue);
|
expect(plan.containsKey('ui'), isTrue);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle multiple plans with different payment systems', () async {
|
test(
|
||||||
// Create test subscription plans with different payment systems
|
'should handle multiple plans with different payment systems',
|
||||||
await testIsar.writeTxn(() async {
|
() async {
|
||||||
final plan1 = SubscriptionPlanModel(
|
// Create test subscription plans with different payment systems
|
||||||
id: 1,
|
await testIsar.writeTxn(() async {
|
||||||
price: '299',
|
final plan1 = SubscriptionPlanModel(
|
||||||
currency: 'RUB',
|
id: 1,
|
||||||
durationDays: 30,
|
price: '299',
|
||||||
features: [SubscriptionFeatureEnum.packs],
|
currency: 'RUB',
|
||||||
paymentSystem: PaymentSystem.yookassa,
|
durationDays: 30,
|
||||||
paymentId: 'plan_1',
|
features: [SubscriptionFeatureEnum.packs],
|
||||||
ui: SubscriptionPlanUI(
|
paymentSystem: PaymentSystem.yookassa,
|
||||||
title: 'YooKassa Plan',
|
paymentId: 'plan_1',
|
||||||
),
|
ui: SubscriptionPlanUI(title: 'YooKassa Plan'),
|
||||||
|
);
|
||||||
|
await testIsar.subscriptionPlanModels.put(plan1);
|
||||||
|
|
||||||
|
final plan2 = SubscriptionPlanModel(
|
||||||
|
id: 2,
|
||||||
|
price: '399',
|
||||||
|
currency: 'RUB',
|
||||||
|
durationDays: 30,
|
||||||
|
features: [SubscriptionFeatureEnum.packs],
|
||||||
|
paymentSystem: PaymentSystem.google,
|
||||||
|
paymentId: 'plan_2',
|
||||||
|
ui: SubscriptionPlanUI(title: 'Google Play Plan'),
|
||||||
|
);
|
||||||
|
await testIsar.subscriptionPlanModels.put(plan2);
|
||||||
|
});
|
||||||
|
|
||||||
|
final request = Request(
|
||||||
|
'GET',
|
||||||
|
Uri.parse('http://localhost/api/v2/subscriptions/plans'),
|
||||||
);
|
);
|
||||||
await testIsar.subscriptionPlanModels.put(plan1);
|
|
||||||
|
|
||||||
final plan2 = SubscriptionPlanModel(
|
final response = await subscriptionsApiV2.getPlans(request);
|
||||||
id: 2,
|
final responseBody =
|
||||||
price: '399',
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
currency: 'RUB',
|
|
||||||
durationDays: 30,
|
|
||||||
features: [SubscriptionFeatureEnum.packs],
|
|
||||||
paymentSystem: PaymentSystem.google,
|
|
||||||
paymentId: 'plan_2',
|
|
||||||
ui: SubscriptionPlanUI(
|
|
||||||
title: 'Google Play Plan',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await testIsar.subscriptionPlanModels.put(plan2);
|
|
||||||
});
|
|
||||||
|
|
||||||
final request = Request(
|
expect(response.statusCode, equals(200));
|
||||||
'GET',
|
expect(responseBody['plans'], hasLength(2));
|
||||||
Uri.parse('http://localhost/api/v2/subscriptions/plans'),
|
|
||||||
);
|
|
||||||
|
|
||||||
final response = await subscriptionsApiV2.getPlans(request);
|
final plans = responseBody['plans'] as List;
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final paymentSystems = plans
|
||||||
as Map<String, dynamic>;
|
.map((p) => (p as Map<String, dynamic>)['paymentSystem'])
|
||||||
|
.toList();
|
||||||
expect(response.statusCode, equals(200));
|
expect(paymentSystems, contains('yookassa'));
|
||||||
expect(responseBody['plans'], hasLength(2));
|
expect(paymentSystems, contains('google'));
|
||||||
|
},
|
||||||
final plans = responseBody['plans'] as List;
|
);
|
||||||
final paymentSystems = plans
|
|
||||||
.map((p) => (p as Map<String, dynamic>)['paymentSystem'])
|
|
||||||
.toList();
|
|
||||||
expect(paymentSystems, contains('yookassa'));
|
|
||||||
expect(paymentSystems, contains('google'));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
group('SubscriptionsApiV2 - Get Status', () {
|
group('SubscriptionsApiV2 - Get Status', () {
|
||||||
|
|
@ -310,8 +312,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await subscriptionsApiV2.getStatus(request);
|
final response = await subscriptionsApiV2.getStatus(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(401));
|
expect(response.statusCode, equals(401));
|
||||||
expect(responseBody['error'], equals('unauthorized'));
|
expect(responseBody['error'], equals('unauthorized'));
|
||||||
|
|
@ -325,8 +327,8 @@ void main() {
|
||||||
).change(context: {'user': testUser});
|
).change(context: {'user': testUser});
|
||||||
|
|
||||||
final response = await subscriptionsApiV2.getStatus(request);
|
final response = await subscriptionsApiV2.getStatus(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['active'], isFalse);
|
expect(responseBody['active'], isFalse);
|
||||||
|
|
@ -334,52 +336,55 @@ void main() {
|
||||||
expect(responseBody.containsKey('finish'), isFalse);
|
expect(responseBody.containsKey('finish'), isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return active: true with dates when subscription is active', () async {
|
test(
|
||||||
final now = DateTime.now();
|
'should return active: true with dates when subscription is active',
|
||||||
final startDate = now.subtract(const Duration(days: 5));
|
() async {
|
||||||
final finishDate = now.add(const Duration(days: 25));
|
final now = DateTime.now();
|
||||||
|
final startDate = now.subtract(const Duration(days: 5));
|
||||||
|
final finishDate = now.add(const Duration(days: 25));
|
||||||
|
|
||||||
// Create active subscription for test user
|
// Create active subscription for test user
|
||||||
await testIsar.writeTxn(() async {
|
await testIsar.writeTxn(() async {
|
||||||
final subscription = UserSubscriptionModel(
|
final subscription = UserSubscriptionModel(
|
||||||
start: startDate,
|
start: startDate,
|
||||||
finish: finishDate,
|
finish: finishDate,
|
||||||
features: [SubscriptionFeatureEnum.packs],
|
features: [SubscriptionFeatureEnum.packs],
|
||||||
);
|
);
|
||||||
await testIsar.userSubscriptionModels.put(subscription);
|
await testIsar.userSubscriptionModels.put(subscription);
|
||||||
|
|
||||||
final user = await testIsar.userModels.get(1);
|
final user = await testIsar.userModels.get(1);
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
user.subscriptionModel.value = subscription;
|
user.subscriptionModel.value = subscription;
|
||||||
await user.subscriptionModel.save();
|
await user.subscriptionModel.save();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reload user with subscription
|
// Reload user with subscription
|
||||||
final userWithSubscription = await testIsar.userModels.get(1);
|
final userWithSubscription = await testIsar.userModels.get(1);
|
||||||
expect(userWithSubscription, isNotNull);
|
expect(userWithSubscription, isNotNull);
|
||||||
await userWithSubscription!.subscriptionModel.load();
|
await userWithSubscription!.subscriptionModel.load();
|
||||||
|
|
||||||
final request = Request(
|
final request = Request(
|
||||||
'GET',
|
'GET',
|
||||||
Uri.parse('http://localhost/api/v2/subscriptions/status'),
|
Uri.parse('http://localhost/api/v2/subscriptions/status'),
|
||||||
).change(context: {'user': userWithSubscription});
|
).change(context: {'user': userWithSubscription});
|
||||||
|
|
||||||
final response = await subscriptionsApiV2.getStatus(request);
|
final response = await subscriptionsApiV2.getStatus(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['active'], isTrue);
|
expect(responseBody['active'], isTrue);
|
||||||
expect(responseBody.containsKey('start'), isTrue);
|
expect(responseBody.containsKey('start'), isTrue);
|
||||||
expect(responseBody.containsKey('finish'), isTrue);
|
expect(responseBody.containsKey('finish'), isTrue);
|
||||||
|
|
||||||
final start = DateTime.parse(responseBody['start'] as String);
|
final start = DateTime.parse(responseBody['start'] as String);
|
||||||
final finish = DateTime.parse(responseBody['finish'] as String);
|
final finish = DateTime.parse(responseBody['finish'] as String);
|
||||||
|
|
||||||
expect(start, equals(startDate));
|
expect(start, equals(startDate));
|
||||||
expect(finish, equals(finishDate));
|
expect(finish, equals(finishDate));
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('should return active: false when subscription is expired', () async {
|
test('should return active: false when subscription is expired', () async {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
|
@ -413,8 +418,8 @@ void main() {
|
||||||
).change(context: {'user': userWithSubscription});
|
).change(context: {'user': userWithSubscription});
|
||||||
|
|
||||||
final response = await subscriptionsApiV2.getStatus(request);
|
final response = await subscriptionsApiV2.getStatus(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['active'], isFalse);
|
expect(responseBody['active'], isFalse);
|
||||||
|
|
@ -430,7 +435,10 @@ void main() {
|
||||||
final subscription = UserSubscriptionModel(
|
final subscription = UserSubscriptionModel(
|
||||||
start: startDate,
|
start: startDate,
|
||||||
finish: finishDate,
|
finish: finishDate,
|
||||||
features: [SubscriptionFeatureEnum.packs, SubscriptionFeatureEnum.ads],
|
features: [
|
||||||
|
SubscriptionFeatureEnum.packs,
|
||||||
|
SubscriptionFeatureEnum.ads,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
await testIsar.userSubscriptionModels.put(subscription);
|
await testIsar.userSubscriptionModels.put(subscription);
|
||||||
|
|
||||||
|
|
@ -451,8 +459,8 @@ void main() {
|
||||||
).change(context: {'user': userWithSubscription});
|
).change(context: {'user': userWithSubscription});
|
||||||
|
|
||||||
final response = await subscriptionsApiV2.getStatus(request);
|
final response = await subscriptionsApiV2.getStatus(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(response.headers['content-type'], contains('application/json'));
|
expect(response.headers['content-type'], contains('application/json'));
|
||||||
|
|
|
||||||
|
|
@ -62,10 +62,7 @@ void main() {
|
||||||
final discountsManager = const DiscountsManager();
|
final discountsManager = const DiscountsManager();
|
||||||
final productsPriceResolver = ProductsPriceResolver(discountsManager);
|
final productsPriceResolver = ProductsPriceResolver(discountsManager);
|
||||||
final adsManager = AdsManager();
|
final adsManager = AdsManager();
|
||||||
packDtoConverter = PackDtoConverter(
|
packDtoConverter = PackDtoConverter(productsPriceResolver, adsManager);
|
||||||
productsPriceResolver,
|
|
||||||
adsManager,
|
|
||||||
);
|
|
||||||
|
|
||||||
testManager = TestManager(packDtoConverter);
|
testManager = TestManager(packDtoConverter);
|
||||||
|
|
||||||
|
|
@ -93,11 +90,7 @@ void main() {
|
||||||
|
|
||||||
// Create test model
|
// Create test model
|
||||||
await testIsar.writeTxn(() async {
|
await testIsar.writeTxn(() async {
|
||||||
final test = TestModel(
|
final test = TestModel(id: 100, name: 'Test Test', version: '1.0');
|
||||||
id: 100,
|
|
||||||
name: 'Test Test',
|
|
||||||
version: '1.0',
|
|
||||||
);
|
|
||||||
await testIsar.testModels.put(test);
|
await testIsar.testModels.put(test);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -125,8 +118,8 @@ void main() {
|
||||||
).change(context: {'user': testUser});
|
).change(context: {'user': testUser});
|
||||||
|
|
||||||
final response = await testsApiV2.getTest(request, '100');
|
final response = await testsApiV2.getTest(request, '100');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['id'], equals('100'));
|
expect(responseBody['id'], equals('100'));
|
||||||
|
|
@ -140,8 +133,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await testsApiV2.getTest(request, '100');
|
final response = await testsApiV2.getTest(request, '100');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(401));
|
expect(response.statusCode, equals(401));
|
||||||
expect(responseBody['error'], equals('Unauthorized'));
|
expect(responseBody['error'], equals('Unauthorized'));
|
||||||
|
|
@ -154,8 +147,8 @@ void main() {
|
||||||
).change(context: {'user': testUser});
|
).change(context: {'user': testUser});
|
||||||
|
|
||||||
final response = await testsApiV2.getTest(request, '999');
|
final response = await testsApiV2.getTest(request, '999');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(404));
|
expect(response.statusCode, equals(404));
|
||||||
expect(responseBody['error'], equals('Not Found'));
|
expect(responseBody['error'], equals('Not Found'));
|
||||||
|
|
@ -168,8 +161,8 @@ void main() {
|
||||||
).change(context: {'user': testUser});
|
).change(context: {'user': testUser});
|
||||||
|
|
||||||
final response = await testsApiV2.getTest(request, 'invalid');
|
final response = await testsApiV2.getTest(request, 'invalid');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('Bad Request'));
|
expect(responseBody['error'], equals('Bad Request'));
|
||||||
|
|
@ -200,8 +193,8 @@ void main() {
|
||||||
).change(context: {'user': testUser});
|
).change(context: {'user': testUser});
|
||||||
|
|
||||||
final response = await testsApiV2.submitTestResults(request, '100');
|
final response = await testsApiV2.submitTestResults(request, '100');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['success'], equals(true));
|
expect(responseBody['success'], equals(true));
|
||||||
|
|
@ -222,8 +215,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await testsApiV2.submitTestResults(request, '100');
|
final response = await testsApiV2.submitTestResults(request, '100');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(401));
|
expect(response.statusCode, equals(401));
|
||||||
expect(responseBody['error'], equals('Unauthorized'));
|
expect(responseBody['error'], equals('Unauthorized'));
|
||||||
|
|
@ -243,8 +236,8 @@ void main() {
|
||||||
).change(context: {'user': testUser});
|
).change(context: {'user': testUser});
|
||||||
|
|
||||||
final response = await testsApiV2.submitTestResults(request, '100');
|
final response = await testsApiV2.submitTestResults(request, '100');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('Bad Request'));
|
expect(responseBody['error'], equals('Bad Request'));
|
||||||
|
|
@ -259,12 +252,15 @@ void main() {
|
||||||
).change(context: {'user': testUser});
|
).change(context: {'user': testUser});
|
||||||
|
|
||||||
final response = await testsApiV2.submitTestResults(request, '100');
|
final response = await testsApiV2.submitTestResults(request, '100');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('Bad Request'));
|
expect(responseBody['error'], equals('Bad Request'));
|
||||||
expect(responseBody['message'], contains('Invalid test statistics format'));
|
expect(
|
||||||
|
responseBody['message'],
|
||||||
|
contains('Invalid test statistics format'),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return 400 for empty request body', () async {
|
test('should return 400 for empty request body', () async {
|
||||||
|
|
@ -275,8 +271,8 @@ void main() {
|
||||||
).change(context: {'user': testUser});
|
).change(context: {'user': testUser});
|
||||||
|
|
||||||
final response = await testsApiV2.submitTestResults(request, '100');
|
final response = await testsApiV2.submitTestResults(request, '100');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('Bad Request'));
|
expect(responseBody['error'], equals('Bad Request'));
|
||||||
|
|
@ -297,8 +293,8 @@ void main() {
|
||||||
).change(context: {'user': testUser});
|
).change(context: {'user': testUser});
|
||||||
|
|
||||||
final response = await testsApiV2.submitTestResults(request, 'invalid');
|
final response = await testsApiV2.submitTestResults(request, 'invalid');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('Bad Request'));
|
expect(responseBody['error'], equals('Bad Request'));
|
||||||
|
|
@ -313,8 +309,8 @@ void main() {
|
||||||
).change(context: {'user': testUser});
|
).change(context: {'user': testUser});
|
||||||
|
|
||||||
final response = await testsApiV2.getTestHistory(request, '100');
|
final response = await testsApiV2.getTestHistory(request, '100');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['items'], isA<List>());
|
expect(responseBody['items'], isA<List>());
|
||||||
|
|
@ -370,8 +366,8 @@ void main() {
|
||||||
).change(context: {'user': updatedUser!});
|
).change(context: {'user': updatedUser!});
|
||||||
|
|
||||||
final response = await testsApiV2.getTestHistory(request, '100');
|
final response = await testsApiV2.getTestHistory(request, '100');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['items'], isA<List>());
|
expect(responseBody['items'], isA<List>());
|
||||||
|
|
@ -411,8 +407,8 @@ void main() {
|
||||||
).change(context: {'user': updatedUser!});
|
).change(context: {'user': updatedUser!});
|
||||||
|
|
||||||
final response = await testsApiV2.getTestHistory(request, '100');
|
final response = await testsApiV2.getTestHistory(request, '100');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['items'].length, lessThanOrEqualTo(2));
|
expect(responseBody['items'].length, lessThanOrEqualTo(2));
|
||||||
|
|
@ -428,8 +424,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await testsApiV2.getTestHistory(request, '100');
|
final response = await testsApiV2.getTestHistory(request, '100');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(401));
|
expect(response.statusCode, equals(401));
|
||||||
expect(responseBody['error'], equals('Unauthorized'));
|
expect(responseBody['error'], equals('Unauthorized'));
|
||||||
|
|
@ -442,8 +438,8 @@ void main() {
|
||||||
).change(context: {'user': testUser});
|
).change(context: {'user': testUser});
|
||||||
|
|
||||||
final response = await testsApiV2.getTestHistory(request, 'invalid');
|
final response = await testsApiV2.getTestHistory(request, 'invalid');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('Bad Request'));
|
expect(responseBody['error'], equals('Bad Request'));
|
||||||
|
|
@ -456,8 +452,8 @@ void main() {
|
||||||
).change(context: {'user': testUser});
|
).change(context: {'user': testUser});
|
||||||
|
|
||||||
final response = await testsApiV2.getTestHistory(request, '100');
|
final response = await testsApiV2.getTestHistory(request, '100');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('Bad Request'));
|
expect(responseBody['error'], equals('Bad Request'));
|
||||||
|
|
@ -471,13 +467,15 @@ void main() {
|
||||||
).change(context: {'user': testUser});
|
).change(context: {'user': testUser});
|
||||||
|
|
||||||
final response = await testsApiV2.getTestHistory(request, '100');
|
final response = await testsApiV2.getTestHistory(request, '100');
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('Bad Request'));
|
expect(responseBody['error'], equals('Bad Request'));
|
||||||
expect(responseBody['message'], contains('Limit must be between 1 and 100'));
|
expect(
|
||||||
|
responseBody['message'],
|
||||||
|
contains('Limit must be between 1 and 100'),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,15 +40,9 @@ void main() {
|
||||||
UserModel? user,
|
UserModel? user,
|
||||||
String? body,
|
String? body,
|
||||||
}) {
|
}) {
|
||||||
var request = Request(
|
var request = Request(method, Uri.parse(url), body: body);
|
||||||
method,
|
|
||||||
Uri.parse(url),
|
|
||||||
body: body,
|
|
||||||
);
|
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
request = request.change(
|
request = request.change(context: {'user': user});
|
||||||
context: {'user': user},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
@ -115,11 +109,7 @@ void main() {
|
||||||
statisticsCalculator,
|
statisticsCalculator,
|
||||||
AchievementManager(testIsar),
|
AchievementManager(testIsar),
|
||||||
);
|
);
|
||||||
usersApiV2 = UsersApiV2(
|
usersApiV2 = UsersApiV2(userManager, paymentManager, statisticsCalculator);
|
||||||
userManager,
|
|
||||||
paymentManager,
|
|
||||||
statisticsCalculator,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create test user with statistics data
|
// Create test user with statistics data
|
||||||
await testIsar.writeTxn(() async {
|
await testIsar.writeTxn(() async {
|
||||||
|
|
@ -243,8 +233,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.getDetailedStatistics(request);
|
final response = await usersApiV2.getDetailedStatistics(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
// The endpoint should return 200 with UserDataDto or 404 if userData not found
|
// The endpoint should return 200 with UserDataDto or 404 if userData not found
|
||||||
if (response.statusCode == 404) {
|
if (response.statusCode == 404) {
|
||||||
|
|
@ -300,8 +290,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.getDetailedStatistics(request);
|
final response = await usersApiV2.getDetailedStatistics(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(401));
|
expect(response.statusCode, equals(401));
|
||||||
expect(responseBody['error'], equals('unauthorized'));
|
expect(responseBody['error'], equals('unauthorized'));
|
||||||
|
|
@ -315,8 +305,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.getDetailedStatistics(request);
|
final response = await usersApiV2.getDetailedStatistics(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(404));
|
expect(response.statusCode, equals(404));
|
||||||
expect(responseBody['error'], equals('user_data_not_found'));
|
expect(responseBody['error'], equals('user_data_not_found'));
|
||||||
|
|
@ -394,8 +384,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.getPacksStatistics(request);
|
final response = await usersApiV2.getPacksStatistics(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(401));
|
expect(response.statusCode, equals(401));
|
||||||
expect(responseBody['error'], equals('unauthorized'));
|
expect(responseBody['error'], equals('unauthorized'));
|
||||||
|
|
@ -411,8 +401,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.getWordsStatistics(request);
|
final response = await usersApiV2.getWordsStatistics(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['words'], isA<List>());
|
expect(responseBody['words'], isA<List>());
|
||||||
|
|
@ -431,8 +421,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.getWordsStatistics(request);
|
final response = await usersApiV2.getWordsStatistics(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect((responseBody['words'] as List).length, equals(2));
|
expect((responseBody['words'] as List).length, equals(2));
|
||||||
|
|
@ -446,8 +436,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response2 = await usersApiV2.getWordsStatistics(request2);
|
final response2 = await usersApiV2.getWordsStatistics(request2);
|
||||||
final responseBody2 = jsonDecode(await response2.readAsString())
|
final responseBody2 =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response2.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(responseBody2['totalCount'], equals(3));
|
expect(responseBody2['totalCount'], equals(3));
|
||||||
expect((responseBody2['words'] as List).length, equals(1));
|
expect((responseBody2['words'] as List).length, equals(1));
|
||||||
|
|
@ -462,8 +452,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.getWordsStatistics(request);
|
final response = await usersApiV2.getWordsStatistics(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
final words = responseBody['words'] as List;
|
final words = responseBody['words'] as List;
|
||||||
|
|
@ -476,10 +466,15 @@ void main() {
|
||||||
expect(word['incorrect'], isA<double>());
|
expect(word['incorrect'], isA<double>());
|
||||||
}
|
}
|
||||||
// Verify sorting: check that difficulty scores are in descending order
|
// Verify sorting: check that difficulty scores are in descending order
|
||||||
final difficulties = words.map((w) => w['difficultyScore'] as double).toList();
|
final difficulties = words
|
||||||
|
.map((w) => w['difficultyScore'] as double)
|
||||||
|
.toList();
|
||||||
for (var i = 0; i < difficulties.length - 1; i++) {
|
for (var i = 0; i < difficulties.length - 1; i++) {
|
||||||
expect(difficulties[i], greaterThanOrEqualTo(difficulties[i + 1]),
|
expect(
|
||||||
reason: 'Words should be sorted by difficulty descending');
|
difficulties[i],
|
||||||
|
greaterThanOrEqualTo(difficulties[i + 1]),
|
||||||
|
reason: 'Words should be sorted by difficulty descending',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -491,8 +486,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.getWordsStatistics(request);
|
final response = await usersApiV2.getWordsStatistics(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
final words = responseBody['words'] as List;
|
final words = responseBody['words'] as List;
|
||||||
|
|
@ -508,8 +503,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.getWordsStatistics(request);
|
final response = await usersApiV2.getWordsStatistics(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['words'], isA<List>());
|
expect(responseBody['words'], isA<List>());
|
||||||
|
|
@ -523,8 +518,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.getWordsStatistics(request);
|
final response = await usersApiV2.getWordsStatistics(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
final words = responseBody['words'] as List;
|
final words = responseBody['words'] as List;
|
||||||
|
|
@ -541,8 +536,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.getWordsStatistics(request);
|
final response = await usersApiV2.getWordsStatistics(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['pageSize'], equals(50)); // Default limit
|
expect(responseBody['pageSize'], equals(50)); // Default limit
|
||||||
|
|
@ -557,8 +552,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.getWordsStatistics(request);
|
final response = await usersApiV2.getWordsStatistics(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['pageSize'], lessThanOrEqualTo(100));
|
expect(responseBody['pageSize'], lessThanOrEqualTo(100));
|
||||||
|
|
@ -572,8 +567,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.getWordsStatistics(request);
|
final response = await usersApiV2.getWordsStatistics(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['page'], equals(0)); // Should clamp to 0
|
expect(responseBody['page'], equals(0)); // Should clamp to 0
|
||||||
|
|
@ -587,8 +582,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.getWordsStatistics(request);
|
final response = await usersApiV2.getWordsStatistics(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['words'], isA<List>());
|
expect(responseBody['words'], isA<List>());
|
||||||
|
|
@ -604,8 +599,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.getWordsStatistics(request);
|
final response = await usersApiV2.getWordsStatistics(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(401));
|
expect(response.statusCode, equals(401));
|
||||||
expect(responseBody['error'], equals('unauthorized'));
|
expect(responseBody['error'], equals('unauthorized'));
|
||||||
|
|
@ -621,8 +616,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.getTimelineStatistics(request);
|
final response = await usersApiV2.getTimelineStatistics(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['period'], equals('month'));
|
expect(responseBody['period'], equals('month'));
|
||||||
|
|
@ -643,8 +638,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.getTimelineStatistics(request);
|
final response = await usersApiV2.getTimelineStatistics(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['period'], equals('week'));
|
expect(responseBody['period'], equals('week'));
|
||||||
|
|
@ -660,8 +655,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.getTimelineStatistics(request);
|
final response = await usersApiV2.getTimelineStatistics(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['period'], isA<String>());
|
expect(responseBody['period'], isA<String>());
|
||||||
|
|
@ -675,8 +670,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.getTimelineStatistics(request);
|
final response = await usersApiV2.getTimelineStatistics(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
// Should still return valid response, ignoring invalid date
|
// Should still return valid response, ignoring invalid date
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
|
|
@ -691,8 +686,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.getTimelineStatistics(request);
|
final response = await usersApiV2.getTimelineStatistics(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['totalDays'], equals(0));
|
expect(responseBody['totalDays'], equals(0));
|
||||||
|
|
@ -713,8 +708,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.getTimelineStatistics(request);
|
final response = await usersApiV2.getTimelineStatistics(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(401));
|
expect(response.statusCode, equals(401));
|
||||||
expect(responseBody['error'], equals('unauthorized'));
|
expect(responseBody['error'], equals('unauthorized'));
|
||||||
|
|
@ -735,8 +730,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.recordStudySession(request);
|
final response = await usersApiV2.recordStudySession(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(responseBody['result'], equals(true));
|
expect(responseBody['result'], equals(true));
|
||||||
|
|
@ -752,8 +747,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.recordStudySession(request);
|
final response = await usersApiV2.recordStudySession(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('bad_request'));
|
expect(responseBody['error'], equals('bad_request'));
|
||||||
|
|
@ -769,8 +764,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.recordStudySession(request);
|
final response = await usersApiV2.recordStudySession(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('bad_request'));
|
expect(responseBody['error'], equals('bad_request'));
|
||||||
|
|
@ -786,8 +781,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.recordStudySession(request);
|
final response = await usersApiV2.recordStudySession(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(400));
|
expect(response.statusCode, equals(400));
|
||||||
expect(responseBody['error'], equals('bad_request'));
|
expect(responseBody['error'], equals('bad_request'));
|
||||||
|
|
@ -805,8 +800,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.recordStudySession(request);
|
final response = await usersApiV2.recordStudySession(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(401));
|
expect(response.statusCode, equals(401));
|
||||||
expect(responseBody['error'], equals('unauthorized'));
|
expect(responseBody['error'], equals('unauthorized'));
|
||||||
|
|
@ -854,8 +849,8 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await usersApiV2.getAchievements(request);
|
final response = await usersApiV2.getAchievements(request);
|
||||||
final responseBody = jsonDecode(await response.readAsString())
|
final responseBody =
|
||||||
as Map<String, dynamic>;
|
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
expect(response.statusCode, equals(401));
|
expect(response.statusCode, equals(401));
|
||||||
expect(responseBody['error'], equals('unauthorized'));
|
expect(responseBody['error'], equals('unauthorized'));
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ void main() {
|
||||||
setUpAll(() async {
|
setUpAll(() async {
|
||||||
// Подключение к тестовой БД
|
// Подключение к тестовой БД
|
||||||
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
|
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
|
||||||
final port = int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
|
final port =
|
||||||
|
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
|
||||||
final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
||||||
final username = Platform.environment['TEST_DB_USER'] ?? 'postgres';
|
final username = Platform.environment['TEST_DB_USER'] ?? 'postgres';
|
||||||
final password = Platform.environment['TEST_DB_PASSWORD'] ?? 'postgres';
|
final password = Platform.environment['TEST_DB_PASSWORD'] ?? 'postgres';
|
||||||
|
|
@ -114,14 +115,14 @@ void main() {
|
||||||
expect(activeStats.length, equals(3));
|
expect(activeStats.length, equals(3));
|
||||||
|
|
||||||
// Удалить одну запись (soft delete)
|
// Удалить одну запись (soft delete)
|
||||||
await (db.update(db.wordStatistics)
|
await (db.update(
|
||||||
..where((w) => w.id.equals(stats2.id)))
|
db.wordStatistics,
|
||||||
.write(
|
)..where((w) => w.id.equals(stats2.id))).write(
|
||||||
WordStatisticsCompanion(
|
WordStatisticsCompanion(
|
||||||
isDeleted: const Value(true),
|
isDeleted: const Value(true),
|
||||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// selectActive должен вернуть только активные
|
// selectActive должен вернуть только активные
|
||||||
activeStats = await db.wordStatisticsDao.selectActive().get();
|
activeStats = await db.wordStatisticsDao.selectActive().get();
|
||||||
|
|
@ -145,14 +146,14 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Удалить запись
|
// Удалить запись
|
||||||
await (db.update(db.wordStatistics)
|
await (db.update(
|
||||||
..where((w) => w.id.equals(stats.id)))
|
db.wordStatistics,
|
||||||
.write(
|
)..where((w) => w.id.equals(stats.id))).write(
|
||||||
WordStatisticsCompanion(
|
WordStatisticsCompanion(
|
||||||
isDeleted: const Value(true),
|
isDeleted: const Value(true),
|
||||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final activeStats = await db.wordStatisticsDao.selectActive().get();
|
final activeStats = await db.wordStatisticsDao.selectActive().get();
|
||||||
expect(activeStats, isEmpty);
|
expect(activeStats, isEmpty);
|
||||||
|
|
@ -162,7 +163,8 @@ void main() {
|
||||||
// Создать записи для разных пользователей
|
// Создать записи для разных пользователей
|
||||||
final user2 = await db.userDao.createUser(
|
final user2 = await db.userDao.createUser(
|
||||||
UsersCompanion.insert(
|
UsersCompanion.insert(
|
||||||
externalUserId: 'test_user_2_${DateTime.now().millisecondsSinceEpoch}',
|
externalUserId:
|
||||||
|
'test_user_2_${DateTime.now().millisecondsSinceEpoch}',
|
||||||
name: Value('Test User 2'),
|
name: Value('Test User 2'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -185,32 +187,35 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// selectActive с where должен фильтровать и по isDeleted, и по условию
|
// selectActive с where должен фильтровать и по isDeleted, и по условию
|
||||||
final user1Stats = await (db.wordStatisticsDao.selectActive()
|
final user1Stats =
|
||||||
..where((w) => w.userId.equals(testUserId)))
|
await (db.wordStatisticsDao.selectActive()
|
||||||
.get();
|
..where((w) => w.userId.equals(testUserId)))
|
||||||
|
.get();
|
||||||
expect(user1Stats.length, equals(1));
|
expect(user1Stats.length, equals(1));
|
||||||
expect(user1Stats.first.id, equals(stats1.id));
|
expect(user1Stats.first.id, equals(stats1.id));
|
||||||
|
|
||||||
// Удалить запись пользователя 1
|
// Удалить запись пользователя 1
|
||||||
await (db.update(db.wordStatistics)
|
await (db.update(
|
||||||
..where((w) => w.id.equals(stats1.id)))
|
db.wordStatistics,
|
||||||
.write(
|
)..where((w) => w.id.equals(stats1.id))).write(
|
||||||
WordStatisticsCompanion(
|
WordStatisticsCompanion(
|
||||||
isDeleted: const Value(true),
|
isDeleted: const Value(true),
|
||||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Теперь selectActive для пользователя 1 должен вернуть пустой список
|
// Теперь selectActive для пользователя 1 должен вернуть пустой список
|
||||||
final user1StatsAfterDelete = await (db.wordStatisticsDao.selectActive()
|
final user1StatsAfterDelete =
|
||||||
..where((w) => w.userId.equals(testUserId)))
|
await (db.wordStatisticsDao.selectActive()
|
||||||
.get();
|
..where((w) => w.userId.equals(testUserId)))
|
||||||
|
.get();
|
||||||
expect(user1StatsAfterDelete, isEmpty);
|
expect(user1StatsAfterDelete, isEmpty);
|
||||||
|
|
||||||
// Но запись пользователя 2 все еще активна
|
// Но запись пользователя 2 все еще активна
|
||||||
final user2Stats = await (db.wordStatisticsDao.selectActive()
|
final user2Stats =
|
||||||
..where((w) => w.userId.equals(user2.id)))
|
await (db.wordStatisticsDao.selectActive()
|
||||||
.get();
|
..where((w) => w.userId.equals(user2.id)))
|
||||||
|
.get();
|
||||||
expect(user2Stats.length, equals(1));
|
expect(user2Stats.length, equals(1));
|
||||||
expect(user2Stats.first.id, equals(stats2.id));
|
expect(user2Stats.first.id, equals(stats2.id));
|
||||||
});
|
});
|
||||||
|
|
@ -233,7 +238,9 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('возвращает null если запись не существует', () async {
|
test('возвращает null если запись не существует', () async {
|
||||||
final result = await db.wordStatisticsDao.getActiveById('non_existent_id');
|
final result = await db.wordStatisticsDao.getActiveById(
|
||||||
|
'non_existent_id',
|
||||||
|
);
|
||||||
expect(result, isNull);
|
expect(result, isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -246,14 +253,14 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Удалить запись
|
// Удалить запись
|
||||||
await (db.update(db.wordStatistics)
|
await (db.update(
|
||||||
..where((w) => w.id.equals(stats.id)))
|
db.wordStatistics,
|
||||||
.write(
|
)..where((w) => w.id.equals(stats.id))).write(
|
||||||
WordStatisticsCompanion(
|
WordStatisticsCompanion(
|
||||||
isDeleted: const Value(true),
|
isDeleted: const Value(true),
|
||||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final result = await db.wordStatisticsDao.getActiveById(stats.id);
|
final result = await db.wordStatisticsDao.getActiveById(stats.id);
|
||||||
expect(result, isNull);
|
expect(result, isNull);
|
||||||
|
|
@ -268,14 +275,14 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Удалить запись
|
// Удалить запись
|
||||||
await (db.update(db.wordStatistics)
|
await (db.update(
|
||||||
..where((w) => w.id.equals(stats.id)))
|
db.wordStatistics,
|
||||||
.write(
|
)..where((w) => w.id.equals(stats.id))).write(
|
||||||
WordStatisticsCompanion(
|
WordStatisticsCompanion(
|
||||||
isDeleted: const Value(true),
|
isDeleted: const Value(true),
|
||||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Проверить что запись физически существует в БД, но getActiveById не возвращает её
|
// Проверить что запись физически существует в БД, но getActiveById не возвращает её
|
||||||
final allRecords = await db.select(db.wordStatistics).get();
|
final allRecords = await db.select(db.wordStatistics).get();
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,13 @@ void main() {
|
||||||
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
|
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
|
||||||
final port =
|
final port =
|
||||||
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
|
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
|
||||||
final database =
|
final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
||||||
Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
final username =
|
||||||
final username = Platform.environment['TEST_DB_USER'] ??
|
Platform.environment['TEST_DB_USER'] ??
|
||||||
Platform.environment['DB_USER'] ??
|
Platform.environment['DB_USER'] ??
|
||||||
'mnemo_user';
|
'mnemo_user';
|
||||||
final password = Platform.environment['TEST_DB_PASSWORD'] ??
|
final password =
|
||||||
|
Platform.environment['TEST_DB_PASSWORD'] ??
|
||||||
Platform.environment['DB_PASSWORD'] ??
|
Platform.environment['DB_PASSWORD'] ??
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
|
@ -44,9 +45,9 @@ void main() {
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
// Keep cleanup scoped to generated tests only to avoid touching other data
|
// Keep cleanup scoped to generated tests only to avoid touching other data
|
||||||
// that might exist in the shared test DB.
|
// that might exist in the shared test DB.
|
||||||
final allGenerated = await (db.select(db.tests)
|
final allGenerated = await (db.select(
|
||||||
..where((t) => t.version.equals('generated')))
|
db.tests,
|
||||||
.get();
|
)..where((t) => t.version.equals('generated'))).get();
|
||||||
|
|
||||||
for (final t in allGenerated) {
|
for (final t in allGenerated) {
|
||||||
await (db.delete(db.tests)..where((x) => x.id.equals(t.id))).go();
|
await (db.delete(db.tests)..where((x) => x.id.equals(t.id))).go();
|
||||||
|
|
@ -84,53 +85,58 @@ void main() {
|
||||||
expect(stillThere, isNull);
|
expect(stillThere, isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('hardDeleteOldSoftDeletedGeneratedTests removes old soft-deleted tests',
|
test(
|
||||||
() async {
|
'hardDeleteOldSoftDeletedGeneratedTests removes old soft-deleted tests',
|
||||||
final now = DateTime.now();
|
() async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
|
||||||
// Create a pack so we have a normal relation entry.
|
// Create a pack so we have a normal relation entry.
|
||||||
final packId = await db.packDao.createPack(
|
final packId = await db.packDao.createPack(
|
||||||
CardPacksCompanion.insert(
|
CardPacksCompanion.insert(
|
||||||
title: 'pack for generated test cleanup',
|
title: 'pack for generated test cleanup',
|
||||||
subtitle: 'subtitle',
|
subtitle: 'subtitle',
|
||||||
size: 1,
|
size: 1,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create a generated test and link it to the pack.
|
// Create a generated test and link it to the pack.
|
||||||
final testId = await db.testDao.createTest(
|
final testId = await db.testDao.createTest(
|
||||||
TestsCompanion.insert(
|
TestsCompanion.insert(
|
||||||
name: 'linked generated test',
|
name: 'linked generated test',
|
||||||
version: const Value('generated'),
|
version: const Value('generated'),
|
||||||
createdAt: Value(PgDateTime(now.subtract(const Duration(days: 10)))),
|
createdAt: Value(
|
||||||
updatedAt: Value(PgDateTime(now.subtract(const Duration(days: 10)))),
|
PgDateTime(now.subtract(const Duration(days: 10))),
|
||||||
),
|
),
|
||||||
);
|
updatedAt: Value(
|
||||||
await db.testDao.linkTestToPack(testId, packId);
|
PgDateTime(now.subtract(const Duration(days: 10))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await db.testDao.linkTestToPack(testId, packId);
|
||||||
|
|
||||||
// Soft delete it long ago so it's eligible for TTL purge.
|
// Soft delete it long ago so it's eligible for TTL purge.
|
||||||
await (db.update(db.tests)..where((t) => t.id.equals(testId))).write(
|
await (db.update(db.tests)..where((t) => t.id.equals(testId))).write(
|
||||||
TestsCompanion(
|
TestsCompanion(
|
||||||
isDeleted: const Value(true),
|
isDeleted: const Value(true),
|
||||||
deletedAt: Value(PgDateTime(now.subtract(const Duration(days: 9)))),
|
deletedAt: Value(PgDateTime(now.subtract(const Duration(days: 9)))),
|
||||||
updatedAt: Value(PgDateTime(now.subtract(const Duration(days: 9)))),
|
updatedAt: Value(PgDateTime(now.subtract(const Duration(days: 9)))),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final deleted = await db.testDao.hardDeleteOldSoftDeletedGeneratedTests(
|
final deleted = await db.testDao.hardDeleteOldSoftDeletedGeneratedTests(
|
||||||
olderThan: const Duration(days: 7),
|
olderThan: const Duration(days: 7),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(deleted, equals(1));
|
expect(deleted, equals(1));
|
||||||
final stillThere = await db.testDao.getTestById(testId);
|
final stillThere = await db.testDao.getTestById(testId);
|
||||||
expect(stillThere, isNull);
|
expect(stillThere, isNull);
|
||||||
|
|
||||||
// Cleanup the pack relation/pack.
|
// Cleanup the pack relation/pack.
|
||||||
await (db.delete(db.testPackRelations)
|
await (db.delete(
|
||||||
..where((r) => r.packId.equals(packId)))
|
db.testPackRelations,
|
||||||
.go();
|
)..where((r) => r.packId.equals(packId))).go();
|
||||||
await (db.delete(db.cardPacks)..where((p) => p.id.equals(packId))).go();
|
await (db.delete(db.cardPacks)..where((p) => p.id.equals(packId))).go();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,13 @@ void main() {
|
||||||
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
|
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
|
||||||
final port =
|
final port =
|
||||||
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
|
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
|
||||||
final database =
|
final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
||||||
Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
final username =
|
||||||
final username = Platform.environment['TEST_DB_USER'] ??
|
Platform.environment['TEST_DB_USER'] ??
|
||||||
Platform.environment['DB_USER'] ??
|
Platform.environment['DB_USER'] ??
|
||||||
'mnemo_user';
|
'mnemo_user';
|
||||||
final password = Platform.environment['TEST_DB_PASSWORD'] ??
|
final password =
|
||||||
|
Platform.environment['TEST_DB_PASSWORD'] ??
|
||||||
Platform.environment['DB_PASSWORD'] ??
|
Platform.environment['DB_PASSWORD'] ??
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
|
@ -40,12 +41,12 @@ void main() {
|
||||||
await Migrator(db).createAll();
|
await Migrator(db).createAll();
|
||||||
|
|
||||||
// Initialize MinioService
|
// Initialize MinioService
|
||||||
final minioEndpoint = Platform.environment['MINIO_ENDPOINT'] ??
|
final minioEndpoint =
|
||||||
'localhost:9000';
|
Platform.environment['MINIO_ENDPOINT'] ?? 'localhost:9000';
|
||||||
final minioAccessKey = Platform.environment['MINIO_ACCESS_KEY'] ??
|
final minioAccessKey =
|
||||||
'minioadmin';
|
Platform.environment['MINIO_ACCESS_KEY'] ?? 'minioadmin';
|
||||||
final minioSecretKey = Platform.environment['MINIO_SECRET_KEY'] ??
|
final minioSecretKey =
|
||||||
'minioadmin';
|
Platform.environment['MINIO_SECRET_KEY'] ?? 'minioadmin';
|
||||||
|
|
||||||
minioService = MinioService(
|
minioService = MinioService(
|
||||||
endpoint: minioEndpoint,
|
endpoint: minioEndpoint,
|
||||||
|
|
@ -68,20 +69,22 @@ void main() {
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
// Cleanup is scoped to the entities created by this test.
|
// Cleanup is scoped to the entities created by this test.
|
||||||
// Delete generated tests to avoid leaving orphans if pack is removed.
|
// Delete generated tests to avoid leaving orphans if pack is removed.
|
||||||
final generatedTests = await (db.select(db.tests)
|
final generatedTests = await (db.select(
|
||||||
..where((t) => t.version.equals('generated')))
|
db.tests,
|
||||||
.get();
|
)..where((t) => t.version.equals('generated'))).get();
|
||||||
for (final t in generatedTests) {
|
for (final t in generatedTests) {
|
||||||
await (db.delete(db.tests)..where((x) => x.id.equals(t.id))).go();
|
await (db.delete(db.tests)..where((x) => x.id.equals(t.id))).go();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (packId != null) {
|
if (packId != null) {
|
||||||
await (db.delete(db.cardPacks)..where((p) => p.id.equals(packId!)))
|
await (db.delete(
|
||||||
.go();
|
db.cardPacks,
|
||||||
|
)..where((p) => p.id.equals(packId!))).go();
|
||||||
}
|
}
|
||||||
if (cardId != null) {
|
if (cardId != null) {
|
||||||
await (db.delete(db.gameCards)..where((c) => c.id.equals(cardId!)))
|
await (db.delete(
|
||||||
.go();
|
db.gameCards,
|
||||||
|
)..where((c) => c.id.equals(cardId!))).go();
|
||||||
}
|
}
|
||||||
|
|
||||||
packId = null;
|
packId = null;
|
||||||
|
|
@ -121,18 +124,20 @@ void main() {
|
||||||
await testManager.updateGeneratedTests(packModel);
|
await testManager.updateGeneratedTests(packModel);
|
||||||
|
|
||||||
final packTests = await db.testDao.getTestsByPackId(packId!);
|
final packTests = await db.testDao.getTestsByPackId(packId!);
|
||||||
final generated =
|
final generated = packTests
|
||||||
packTests.where((t) => t.version == 'generated').toList();
|
.where((t) => t.version == 'generated')
|
||||||
|
.toList();
|
||||||
|
|
||||||
expect(generated, hasLength(1));
|
expect(generated, hasLength(1));
|
||||||
|
|
||||||
final linkedPackId =
|
final linkedPackId = await db.testDao.getPackIdForTest(
|
||||||
await db.testDao.getPackIdForTest(generated.single.id);
|
generated.single.id,
|
||||||
|
);
|
||||||
expect(linkedPackId, equals(packId));
|
expect(linkedPackId, equals(packId));
|
||||||
|
|
||||||
final relations = await (db.select(db.testPackRelations)
|
final relations = await (db.select(
|
||||||
..where((r) => r.packId.equals(packId!)))
|
db.testPackRelations,
|
||||||
.get();
|
)..where((r) => r.packId.equals(packId!))).get();
|
||||||
expect(relations, hasLength(1));
|
expect(relations, hasLength(1));
|
||||||
expect(relations.single.testId, equals(generated.single.id));
|
expect(relations.single.testId, equals(generated.single.id));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@ void main() {
|
||||||
// Подключение к тестовой БД
|
// Подключение к тестовой БД
|
||||||
// Можно использовать переменные окружения для настройки
|
// Можно использовать переменные окружения для настройки
|
||||||
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
|
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
|
||||||
final port = int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
|
final port =
|
||||||
|
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
|
||||||
final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
||||||
final username = Platform.environment['TEST_DB_USER'] ?? 'postgres';
|
final username = Platform.environment['TEST_DB_USER'] ?? 'postgres';
|
||||||
final password = Platform.environment['TEST_DB_PASSWORD'] ?? 'postgres';
|
final password = Platform.environment['TEST_DB_PASSWORD'] ?? 'postgres';
|
||||||
|
|
@ -51,9 +52,7 @@ void main() {
|
||||||
|
|
||||||
// 2. Создать UserData
|
// 2. Создать UserData
|
||||||
await db.userDao.createUserData(
|
await db.userDao.createUserData(
|
||||||
UserDatasCompanion.insert(
|
UserDatasCompanion.insert(userId: testUserId),
|
||||||
userId: testUserId,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3. Создать пак
|
// 3. Создать пак
|
||||||
|
|
@ -194,14 +193,14 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Soft delete
|
// Soft delete
|
||||||
await (db.update(db.wordStatistics)
|
await (db.update(
|
||||||
..where((w) => w.id.equals(stats.id)))
|
db.wordStatistics,
|
||||||
.write(
|
)..where((w) => w.id.equals(stats.id))).write(
|
||||||
WordStatisticsCompanion(
|
WordStatisticsCompanion(
|
||||||
isDeleted: const Value(true),
|
isDeleted: const Value(true),
|
||||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final result = await db.wordStatisticsDao.getByUserAndCard(
|
final result = await db.wordStatisticsDao.getByUserAndCard(
|
||||||
testUserId,
|
testUserId,
|
||||||
|
|
@ -389,7 +388,8 @@ void main() {
|
||||||
// Создать другого пользователя
|
// Создать другого пользователя
|
||||||
final user2 = await db.userDao.createUser(
|
final user2 = await db.userDao.createUser(
|
||||||
UsersCompanion.insert(
|
UsersCompanion.insert(
|
||||||
externalUserId: 'test_user_2_${DateTime.now().millisecondsSinceEpoch}',
|
externalUserId:
|
||||||
|
'test_user_2_${DateTime.now().millisecondsSinceEpoch}',
|
||||||
name: Value('Test User 2'),
|
name: Value('Test User 2'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -444,14 +444,14 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Удалить одну запись
|
// Удалить одну запись
|
||||||
await (db.update(db.wordStatistics)
|
await (db.update(
|
||||||
..where((w) => w.id.equals(stats1.id)))
|
db.wordStatistics,
|
||||||
.write(
|
)..where((w) => w.id.equals(stats1.id))).write(
|
||||||
WordStatisticsCompanion(
|
WordStatisticsCompanion(
|
||||||
isDeleted: const Value(true),
|
isDeleted: const Value(true),
|
||||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// selectActive должен вернуть только активную запись
|
// selectActive должен вернуть только активную запись
|
||||||
final activeStats = await db.wordStatisticsDao.selectActive().get();
|
final activeStats = await db.wordStatisticsDao.selectActive().get();
|
||||||
|
|
@ -468,14 +468,14 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Удалить запись
|
// Удалить запись
|
||||||
await (db.update(db.wordStatistics)
|
await (db.update(
|
||||||
..where((w) => w.id.equals(stats.id)))
|
db.wordStatistics,
|
||||||
.write(
|
)..where((w) => w.id.equals(stats.id))).write(
|
||||||
WordStatisticsCompanion(
|
WordStatisticsCompanion(
|
||||||
isDeleted: const Value(true),
|
isDeleted: const Value(true),
|
||||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final result = await db.wordStatisticsDao.getActiveById(stats.id);
|
final result = await db.wordStatisticsDao.getActiveById(stats.id);
|
||||||
expect(result, isNull);
|
expect(result, isNull);
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,16 @@ void main() {
|
||||||
test('AppDatabase can be instantiated', () {
|
test('AppDatabase can be instantiated', () {
|
||||||
// This test verifies that the database class can be created
|
// This test verifies that the database class can be created
|
||||||
// Actual connection will be tested with real PostgreSQL instance
|
// Actual connection will be tested with real PostgreSQL instance
|
||||||
expect(() => AppDatabase.connect(
|
expect(
|
||||||
host: 'localhost',
|
() => AppDatabase.connect(
|
||||||
port: 5432,
|
host: 'localhost',
|
||||||
database: 'test_db',
|
port: 5432,
|
||||||
username: 'test_user',
|
database: 'test_db',
|
||||||
password: 'test_pass',
|
username: 'test_user',
|
||||||
), returnsNormally);
|
password: 'test_pass',
|
||||||
|
),
|
||||||
|
returnsNormally,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('All tables are registered', () {
|
test('All tables are registered', () {
|
||||||
|
|
|
||||||
|
|
@ -256,11 +256,15 @@ void main() {
|
||||||
|
|
||||||
expect(unlocked.unlockedAt, isNotNull);
|
expect(unlocked.unlockedAt, isNotNull);
|
||||||
expect(
|
expect(
|
||||||
unlocked.unlockedAt!.isAfter(beforeUnlock.subtract(const Duration(seconds: 1))),
|
unlocked.unlockedAt!.isAfter(
|
||||||
|
beforeUnlock.subtract(const Duration(seconds: 1)),
|
||||||
|
),
|
||||||
isTrue,
|
isTrue,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
unlocked.unlockedAt!.isBefore(afterUnlock.add(const Duration(seconds: 1))),
|
unlocked.unlockedAt!.isBefore(
|
||||||
|
afterUnlock.add(const Duration(seconds: 1)),
|
||||||
|
),
|
||||||
isTrue,
|
isTrue,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -526,11 +530,7 @@ void main() {
|
||||||
|
|
||||||
group('edge cases', () {
|
group('edge cases', () {
|
||||||
test('handles empty strings', () {
|
test('handles empty strings', () {
|
||||||
final model = AchievementModel(
|
final model = AchievementModel(id: '', title: '', description: '');
|
||||||
id: '',
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(model.id, '');
|
expect(model.id, '');
|
||||||
expect(model.title, '');
|
expect(model.title, '');
|
||||||
|
|
|
||||||
|
|
@ -83,10 +83,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('converts model with null dates to DTO', () {
|
test('converts model with null dates to DTO', () {
|
||||||
final model = PackProgressModel(
|
final model = PackProgressModel(packId: 'test_pack', totalCards: 10);
|
||||||
packId: 'test_pack',
|
|
||||||
totalCards: 10,
|
|
||||||
);
|
|
||||||
|
|
||||||
final dto = model.toDto();
|
final dto = model.toDto();
|
||||||
|
|
||||||
|
|
@ -134,10 +131,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('creates model from DTO with null dates', () {
|
test('creates model from DTO with null dates', () {
|
||||||
final dto = PackProgressDto(
|
final dto = PackProgressDto(packId: 'test_pack', totalCards: 10);
|
||||||
packId: 'test_pack',
|
|
||||||
totalCards: 10,
|
|
||||||
);
|
|
||||||
|
|
||||||
final model = PackProgressModel.fromDto(dto);
|
final model = PackProgressModel.fromDto(dto);
|
||||||
|
|
||||||
|
|
@ -208,10 +202,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('adds new card to attempts map', () {
|
test('adds new card to attempts map', () {
|
||||||
final model = PackProgressModel(
|
final model = PackProgressModel(packId: 'test_pack', totalCards: 10);
|
||||||
packId: 'test_pack',
|
|
||||||
totalCards: 10,
|
|
||||||
);
|
|
||||||
|
|
||||||
final updated = model.updateCardProgress(
|
final updated = model.updateCardProgress(
|
||||||
cardId: 'new_card',
|
cardId: 'new_card',
|
||||||
|
|
@ -238,9 +229,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sets lastStudyDate to current time', () {
|
test('sets lastStudyDate to current time', () {
|
||||||
final model = PackProgressModel(
|
final model = PackProgressModel(packId: 'test_pack');
|
||||||
packId: 'test_pack',
|
|
||||||
);
|
|
||||||
|
|
||||||
final beforeUpdate = DateTime.now();
|
final beforeUpdate = DateTime.now();
|
||||||
final updated = model.updateCardProgress(
|
final updated = model.updateCardProgress(
|
||||||
|
|
@ -252,19 +241,21 @@ void main() {
|
||||||
|
|
||||||
expect(updated.lastStudyDate, isNotNull);
|
expect(updated.lastStudyDate, isNotNull);
|
||||||
expect(
|
expect(
|
||||||
updated.lastStudyDate!.isAfter(beforeUpdate.subtract(const Duration(seconds: 1))),
|
updated.lastStudyDate!.isAfter(
|
||||||
|
beforeUpdate.subtract(const Duration(seconds: 1)),
|
||||||
|
),
|
||||||
isTrue,
|
isTrue,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
updated.lastStudyDate!.isBefore(afterUpdate.add(const Duration(seconds: 1))),
|
updated.lastStudyDate!.isBefore(
|
||||||
|
afterUpdate.add(const Duration(seconds: 1)),
|
||||||
|
),
|
||||||
isTrue,
|
isTrue,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sets firstStudyDate if null', () {
|
test('sets firstStudyDate if null', () {
|
||||||
final model = PackProgressModel(
|
final model = PackProgressModel(packId: 'test_pack');
|
||||||
packId: 'test_pack',
|
|
||||||
);
|
|
||||||
|
|
||||||
final updated = model.updateCardProgress(
|
final updated = model.updateCardProgress(
|
||||||
cardId: 'card1',
|
cardId: 'card1',
|
||||||
|
|
@ -312,10 +303,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns true when both learnedCards and studyTimeMinutes > 0', () {
|
test('returns true when both learnedCards and studyTimeMinutes > 0', () {
|
||||||
final model = PackProgressModel(
|
final model = PackProgressModel(learnedCards: 5, studyTimeMinutes: 10);
|
||||||
learnedCards: 5,
|
|
||||||
studyTimeMinutes: 10,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(model.hasStarted, isTrue);
|
expect(model.hasStarted, isTrue);
|
||||||
});
|
});
|
||||||
|
|
@ -335,37 +323,25 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('calculates progress correctly', () {
|
test('calculates progress correctly', () {
|
||||||
final model = PackProgressModel(
|
final model = PackProgressModel(totalCards: 10, learnedCards: 5);
|
||||||
totalCards: 10,
|
|
||||||
learnedCards: 5,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(model.progress, 0.5);
|
expect(model.progress, 0.5);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('calculates progress for partial completion', () {
|
test('calculates progress for partial completion', () {
|
||||||
final model = PackProgressModel(
|
final model = PackProgressModel(totalCards: 100, learnedCards: 33);
|
||||||
totalCards: 100,
|
|
||||||
learnedCards: 33,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(model.progress, 0.33);
|
expect(model.progress, 0.33);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns 1.0 when all cards learned', () {
|
test('returns 1.0 when all cards learned', () {
|
||||||
final model = PackProgressModel(
|
final model = PackProgressModel(totalCards: 10, learnedCards: 10);
|
||||||
totalCards: 10,
|
|
||||||
learnedCards: 10,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(model.progress, 1.0);
|
expect(model.progress, 1.0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handles learnedCards exceeding totalCards', () {
|
test('handles learnedCards exceeding totalCards', () {
|
||||||
final model = PackProgressModel(
|
final model = PackProgressModel(totalCards: 10, learnedCards: 15);
|
||||||
totalCards: 10,
|
|
||||||
learnedCards: 15,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(model.progress, 1.5);
|
expect(model.progress, 1.5);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,7 @@ void main() {
|
||||||
test('creates model with required parameters', () {
|
test('creates model with required parameters', () {
|
||||||
final startTime = DateTime.now();
|
final startTime = DateTime.now();
|
||||||
|
|
||||||
final model = StudySessionModel(
|
final model = StudySessionModel(userId: 1, startTime: startTime);
|
||||||
userId: 1,
|
|
||||||
startTime: startTime,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(model.userId, 1);
|
expect(model.userId, 1);
|
||||||
expect(model.startTime, startTime);
|
expect(model.startTime, startTime);
|
||||||
|
|
@ -54,10 +51,7 @@ void main() {
|
||||||
test('creates model with default values', () {
|
test('creates model with default values', () {
|
||||||
final startTime = DateTime.now();
|
final startTime = DateTime.now();
|
||||||
|
|
||||||
final model = StudySessionModel(
|
final model = StudySessionModel(userId: 1, startTime: startTime);
|
||||||
userId: 1,
|
|
||||||
startTime: startTime,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(model.wordsLearned, 0);
|
expect(model.wordsLearned, 0);
|
||||||
expect(model.testsCompleted, 0);
|
expect(model.testsCompleted, 0);
|
||||||
|
|
@ -97,10 +91,7 @@ void main() {
|
||||||
test('converts model with null values to DTO', () {
|
test('converts model with null values to DTO', () {
|
||||||
final startTime = DateTime.now();
|
final startTime = DateTime.now();
|
||||||
|
|
||||||
final model = StudySessionModel(
|
final model = StudySessionModel(userId: 1, startTime: startTime);
|
||||||
userId: 1,
|
|
||||||
startTime: startTime,
|
|
||||||
);
|
|
||||||
|
|
||||||
final dto = model.toDto();
|
final dto = model.toDto();
|
||||||
|
|
||||||
|
|
@ -143,9 +134,7 @@ void main() {
|
||||||
test('creates model from DTO with null values', () {
|
test('creates model from DTO with null values', () {
|
||||||
final startTime = DateTime.now();
|
final startTime = DateTime.now();
|
||||||
|
|
||||||
final dto = StudySessionDto(
|
final dto = StudySessionDto(startTime: startTime);
|
||||||
startTime: startTime,
|
|
||||||
);
|
|
||||||
|
|
||||||
final model = StudySessionModel.fromDto(dto, 1);
|
final model = StudySessionModel.fromDto(dto, 1);
|
||||||
|
|
||||||
|
|
@ -190,10 +179,7 @@ void main() {
|
||||||
|
|
||||||
group('isActive', () {
|
group('isActive', () {
|
||||||
test('returns true when endTime is null', () {
|
test('returns true when endTime is null', () {
|
||||||
final model = StudySessionModel(
|
final model = StudySessionModel(userId: 1, startTime: DateTime.now());
|
||||||
userId: 1,
|
|
||||||
startTime: DateTime.now(),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(model.isActive, isTrue);
|
expect(model.isActive, isTrue);
|
||||||
});
|
});
|
||||||
|
|
@ -227,10 +213,7 @@ void main() {
|
||||||
test('calculates duration using current time when active', () {
|
test('calculates duration using current time when active', () {
|
||||||
final startTime = DateTime.now().subtract(const Duration(minutes: 15));
|
final startTime = DateTime.now().subtract(const Duration(minutes: 15));
|
||||||
|
|
||||||
final model = StudySessionModel(
|
final model = StudySessionModel(userId: 1, startTime: startTime);
|
||||||
userId: 1,
|
|
||||||
startTime: startTime,
|
|
||||||
);
|
|
||||||
|
|
||||||
final duration = model.duration;
|
final duration = model.duration;
|
||||||
expect(duration.inMinutes, greaterThanOrEqualTo(14));
|
expect(duration.inMinutes, greaterThanOrEqualTo(14));
|
||||||
|
|
@ -287,10 +270,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns General Study when neither is set', () {
|
test('returns General Study when neither is set', () {
|
||||||
final model = StudySessionModel(
|
final model = StudySessionModel(userId: 1, startTime: DateTime.now());
|
||||||
userId: 1,
|
|
||||||
startTime: DateTime.now(),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(model.sessionType, 'General Study');
|
expect(model.sessionType, 'General Study');
|
||||||
});
|
});
|
||||||
|
|
@ -416,10 +396,7 @@ void main() {
|
||||||
test('sets endTime to current time', () {
|
test('sets endTime to current time', () {
|
||||||
final startTime = DateTime.now().subtract(const Duration(minutes: 10));
|
final startTime = DateTime.now().subtract(const Duration(minutes: 10));
|
||||||
|
|
||||||
final model = StudySessionModel(
|
final model = StudySessionModel(userId: 1, startTime: startTime);
|
||||||
userId: 1,
|
|
||||||
startTime: startTime,
|
|
||||||
);
|
|
||||||
|
|
||||||
final beforeEnd = DateTime.now();
|
final beforeEnd = DateTime.now();
|
||||||
final ended = model.end(
|
final ended = model.end(
|
||||||
|
|
@ -431,7 +408,9 @@ void main() {
|
||||||
|
|
||||||
expect(ended.endTime, isNotNull);
|
expect(ended.endTime, isNotNull);
|
||||||
expect(
|
expect(
|
||||||
ended.endTime!.isAfter(beforeEnd.subtract(const Duration(seconds: 1))),
|
ended.endTime!.isAfter(
|
||||||
|
beforeEnd.subtract(const Duration(seconds: 1)),
|
||||||
|
),
|
||||||
isTrue,
|
isTrue,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
|
|
@ -441,10 +420,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('updates statistics correctly', () {
|
test('updates statistics correctly', () {
|
||||||
final model = StudySessionModel(
|
final model = StudySessionModel(userId: 1, startTime: DateTime.now());
|
||||||
userId: 1,
|
|
||||||
startTime: DateTime.now(),
|
|
||||||
);
|
|
||||||
|
|
||||||
final ended = model.end(
|
final ended = model.end(
|
||||||
wordsLearned: 15,
|
wordsLearned: 15,
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,21 @@ void main() {
|
||||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO5n5p0AAAAASUVORK5CYII=';
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO5n5p0AAAAASUVORK5CYII=';
|
||||||
|
|
||||||
group('CardImageStorage', () {
|
group('CardImageStorage', () {
|
||||||
test('sanitizeCardsFileName strips cards/ prefix and rejects traversal', () {
|
test(
|
||||||
expect(CardImageStorage.sanitizeCardsFileName('cards/a.png'), 'a.png');
|
'sanitizeCardsFileName strips cards/ prefix and rejects traversal',
|
||||||
expect(CardImageStorage.sanitizeCardsFileName('/cards/a.png'), 'a.png');
|
() {
|
||||||
expect(CardImageStorage.sanitizeCardsFileName('a.png'), 'a.png');
|
expect(CardImageStorage.sanitizeCardsFileName('cards/a.png'), 'a.png');
|
||||||
|
expect(CardImageStorage.sanitizeCardsFileName('/cards/a.png'), 'a.png');
|
||||||
|
expect(CardImageStorage.sanitizeCardsFileName('a.png'), 'a.png');
|
||||||
|
|
||||||
expect(CardImageStorage.sanitizeCardsFileName('../a.png'), isNull);
|
expect(CardImageStorage.sanitizeCardsFileName('../a.png'), isNull);
|
||||||
expect(CardImageStorage.sanitizeCardsFileName('cards/../a.png'), isNull);
|
expect(
|
||||||
expect(CardImageStorage.sanitizeCardsFileName('cards/a/b.png'), isNull);
|
CardImageStorage.sanitizeCardsFileName('cards/../a.png'),
|
||||||
});
|
isNull,
|
||||||
|
);
|
||||||
|
expect(CardImageStorage.sanitizeCardsFileName('cards/a/b.png'), isNull);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('tryParseBase64Image parses data: url and detects png', () {
|
test('tryParseBase64Image parses data: url and detects png', () {
|
||||||
final parsed = CardImageStorage.tryParseBase64Image(
|
final parsed = CardImageStorage.tryParseBase64Image(
|
||||||
|
|
@ -64,4 +70,3 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,18 @@ import 'package:test/test.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('VoiceStorage', () {
|
group('VoiceStorage', () {
|
||||||
test('sanitizeVoiceFileName strips voice/ prefix and rejects traversal', () {
|
test(
|
||||||
expect(VoiceStorage.sanitizeVoiceFileName('voice/a.mp3'), 'a.mp3');
|
'sanitizeVoiceFileName strips voice/ prefix and rejects traversal',
|
||||||
expect(VoiceStorage.sanitizeVoiceFileName('/voice/a.mp3'), 'a.mp3');
|
() {
|
||||||
expect(VoiceStorage.sanitizeVoiceFileName('a.mp3'), 'a.mp3');
|
expect(VoiceStorage.sanitizeVoiceFileName('voice/a.mp3'), 'a.mp3');
|
||||||
|
expect(VoiceStorage.sanitizeVoiceFileName('/voice/a.mp3'), 'a.mp3');
|
||||||
|
expect(VoiceStorage.sanitizeVoiceFileName('a.mp3'), 'a.mp3');
|
||||||
|
|
||||||
expect(VoiceStorage.sanitizeVoiceFileName('../a.mp3'), isNull);
|
expect(VoiceStorage.sanitizeVoiceFileName('../a.mp3'), isNull);
|
||||||
expect(VoiceStorage.sanitizeVoiceFileName('voice/../a.mp3'), isNull);
|
expect(VoiceStorage.sanitizeVoiceFileName('voice/../a.mp3'), isNull);
|
||||||
expect(VoiceStorage.sanitizeVoiceFileName('voice/a/b.mp3'), isNull);
|
expect(VoiceStorage.sanitizeVoiceFileName('voice/a/b.mp3'), isNull);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('tryParseBase64Audio parses data: url and detects mp3', () {
|
test('tryParseBase64Audio parses data: url and detects mp3', () {
|
||||||
final bytes = Uint8List.fromList([0x49, 0x44, 0x33, 0x03, 0x00, 0x00]);
|
final bytes = Uint8List.fromList([0x49, 0x44, 0x33, 0x03, 0x00, 0x00]);
|
||||||
|
|
@ -64,4 +67,3 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@ void main() {
|
||||||
setUpAll(() async {
|
setUpAll(() async {
|
||||||
// Подключение к тестовой БД
|
// Подключение к тестовой БД
|
||||||
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
|
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
|
||||||
final port = int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
|
final port =
|
||||||
|
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
|
||||||
final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
||||||
final username = Platform.environment['TEST_DB_USER'] ?? 'postgres';
|
final username = Platform.environment['TEST_DB_USER'] ?? 'postgres';
|
||||||
final password = Platform.environment['TEST_DB_PASSWORD'] ?? 'postgres';
|
final password = Platform.environment['TEST_DB_PASSWORD'] ?? 'postgres';
|
||||||
|
|
@ -47,7 +48,8 @@ void main() {
|
||||||
// Создать тестовые данные
|
// Создать тестовые данные
|
||||||
final user = await db.userDao.createUser(
|
final user = await db.userDao.createUser(
|
||||||
UsersCompanion.insert(
|
UsersCompanion.insert(
|
||||||
externalUserId: 'smoke_test_user_${DateTime.now().millisecondsSinceEpoch}',
|
externalUserId:
|
||||||
|
'smoke_test_user_${DateTime.now().millisecondsSinceEpoch}',
|
||||||
name: Value('Smoke Test User'),
|
name: Value('Smoke Test User'),
|
||||||
email: Value('smoke@test.com'),
|
email: Value('smoke@test.com'),
|
||||||
),
|
),
|
||||||
|
|
@ -156,7 +158,10 @@ void main() {
|
||||||
expect(packProgress.packId, equals(testPackId));
|
expect(packProgress.packId, equals(testPackId));
|
||||||
expect(packProgress.totalCards, equals(10));
|
expect(packProgress.totalCards, equals(10));
|
||||||
expect(packProgress.learnedCards, equals(2)); // 2 карточки с ответами
|
expect(packProgress.learnedCards, equals(2)); // 2 карточки с ответами
|
||||||
expect(packProgress.averageAccuracy, closeTo(0.5, 0.01)); // 1 правильный, 1 неправильный
|
expect(
|
||||||
|
packProgress.averageAccuracy,
|
||||||
|
closeTo(0.5, 0.01),
|
||||||
|
); // 1 правильный, 1 неправильный
|
||||||
});
|
});
|
||||||
|
|
||||||
test('studyDates рассчитываются из StudySessions', () async {
|
test('studyDates рассчитываются из StudySessions', () async {
|
||||||
|
|
@ -172,16 +177,20 @@ void main() {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final studyDates = await statisticsCalculator.calculateStudyDates(testUserId);
|
final studyDates = await statisticsCalculator.calculateStudyDates(
|
||||||
|
testUserId,
|
||||||
|
);
|
||||||
|
|
||||||
expect(studyDates, isNotEmpty);
|
expect(studyDates, isNotEmpty);
|
||||||
// Проверить что дата сегодняшнего дня присутствует
|
// Проверить что дата сегодняшнего дня присутствует
|
||||||
final today = DateTime(now.year, now.month, now.day);
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
expect(
|
expect(
|
||||||
studyDates.any((d) =>
|
studyDates.any(
|
||||||
d.year == today.year &&
|
(d) =>
|
||||||
d.month == today.month &&
|
d.year == today.year &&
|
||||||
d.day == today.day),
|
d.month == today.month &&
|
||||||
|
d.day == today.day,
|
||||||
|
),
|
||||||
isTrue,
|
isTrue,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -204,18 +213,22 @@ void main() {
|
||||||
userId: testUserId,
|
userId: testUserId,
|
||||||
packId: testPackId,
|
packId: testPackId,
|
||||||
startTime: PgDateTime(now.add(const Duration(hours: 1))),
|
startTime: PgDateTime(now.add(const Duration(hours: 1))),
|
||||||
endTime: Value(PgDateTime(now.add(const Duration(hours: 1, minutes: 20)))),
|
endTime: Value(
|
||||||
|
PgDateTime(now.add(const Duration(hours: 1, minutes: 20))),
|
||||||
|
),
|
||||||
durationMinutes: 20,
|
durationMinutes: 20,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final categoryMinutes = await statisticsCalculator.calculateCategoryMinutes(
|
final categoryMinutes = await statisticsCalculator
|
||||||
testUserId,
|
.calculateCategoryMinutes(testUserId);
|
||||||
);
|
|
||||||
|
|
||||||
expect(categoryMinutes, isNotEmpty);
|
expect(categoryMinutes, isNotEmpty);
|
||||||
// Должно быть минимум 35 минут (15 + 20)
|
// Должно быть минимум 35 минут (15 + 20)
|
||||||
final totalMinutes = categoryMinutes.values.fold<int>(0, (sum, minutes) => sum + minutes);
|
final totalMinutes = categoryMinutes.values.fold<int>(
|
||||||
|
0,
|
||||||
|
(sum, minutes) => sum + minutes,
|
||||||
|
);
|
||||||
expect(totalMinutes, greaterThanOrEqualTo(35));
|
expect(totalMinutes, greaterThanOrEqualTo(35));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -229,14 +242,14 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Soft delete
|
// Soft delete
|
||||||
await (db.update(db.wordStatistics)
|
await (db.update(
|
||||||
..where((w) => w.id.equals(stats.id)))
|
db.wordStatistics,
|
||||||
.write(
|
)..where((w) => w.id.equals(stats.id))).write(
|
||||||
WordStatisticsCompanion(
|
WordStatisticsCompanion(
|
||||||
isDeleted: const Value(true),
|
isDeleted: const Value(true),
|
||||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Проверить что запись не возвращается через getActiveById
|
// Проверить что запись не возвращается через getActiveById
|
||||||
final activeRecord = await db.wordStatisticsDao.getActiveById(stats.id);
|
final activeRecord = await db.wordStatisticsDao.getActiveById(stats.id);
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,10 @@ void main() {
|
||||||
|
|
||||||
expect(sampleAchievementIds.length, equals(19));
|
expect(sampleAchievementIds.length, equals(19));
|
||||||
expect(sampleAchievementIds.every((id) => id.isNotEmpty), isTrue);
|
expect(sampleAchievementIds.every((id) => id.isNotEmpty), isTrue);
|
||||||
expect(sampleAchievementIds.toSet().length, equals(sampleAchievementIds.length)); // All unique
|
expect(
|
||||||
|
sampleAchievementIds.toSet().length,
|
||||||
|
equals(sampleAchievementIds.length),
|
||||||
|
); // All unique
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Achievement categories are properly defined', () {
|
test('Achievement categories are properly defined', () {
|
||||||
|
|
@ -96,8 +99,19 @@ void main() {
|
||||||
|
|
||||||
test('Progressive achievements have increasing difficulty', () {
|
test('Progressive achievements have increasing difficulty', () {
|
||||||
// Test that later achievements require more progress
|
// Test that later achievements require more progress
|
||||||
const wordAchievements = ['words_10', 'words_50', 'words_100', 'words_500', 'words_1000'];
|
const wordAchievements = [
|
||||||
const streakAchievements = ['streak_3', 'streak_7', 'streak_30', 'streak_100'];
|
'words_10',
|
||||||
|
'words_50',
|
||||||
|
'words_100',
|
||||||
|
'words_500',
|
||||||
|
'words_1000',
|
||||||
|
];
|
||||||
|
const streakAchievements = [
|
||||||
|
'streak_3',
|
||||||
|
'streak_7',
|
||||||
|
'streak_30',
|
||||||
|
'streak_100',
|
||||||
|
];
|
||||||
|
|
||||||
expect(wordAchievements.length, equals(5));
|
expect(wordAchievements.length, equals(5));
|
||||||
expect(streakAchievements.length, equals(4));
|
expect(streakAchievements.length, equals(4));
|
||||||
|
|
@ -136,11 +150,11 @@ void main() {
|
||||||
|
|
||||||
final progress1 = calculateProgressMap(5, 2);
|
final progress1 = calculateProgressMap(5, 2);
|
||||||
expect(progress1['words_10'], equals(0.5));
|
expect(progress1['words_10'], equals(0.5));
|
||||||
expect(progress1['streak_3'], equals(2/3));
|
expect(progress1['streak_3'], equals(2 / 3));
|
||||||
|
|
||||||
final progress2 = calculateProgressMap(25, 5);
|
final progress2 = calculateProgressMap(25, 5);
|
||||||
expect(progress2['words_50'], equals(0.5));
|
expect(progress2['words_50'], equals(0.5));
|
||||||
expect(progress2['streak_7'], equals(5/7));
|
expect(progress2['streak_7'], equals(5 / 7));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Achievement progress is properly bounded', () {
|
test('Achievement progress is properly bounded', () {
|
||||||
|
|
|
||||||
|
|
@ -34,12 +34,14 @@ class MockIsar extends Fake implements Isar {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
QueryBuilder<StudySessionModel, StudySessionModel, QWhereClause> get studySessionModels {
|
QueryBuilder<StudySessionModel, StudySessionModel, QWhereClause>
|
||||||
|
get studySessionModels {
|
||||||
return MockQueryBuilder(_sessions);
|
return MockQueryBuilder(_sessions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
QueryBuilder<AchievementModel, AchievementModel, QWhereClause> get achievementModels {
|
QueryBuilder<AchievementModel, AchievementModel, QWhereClause>
|
||||||
|
get achievementModels {
|
||||||
return MockQueryBuilder([]);
|
return MockQueryBuilder([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,18 +135,12 @@ void main() {
|
||||||
testId: 'integration_test',
|
testId: 'integration_test',
|
||||||
sessionToken: 'test_session_${DateTime.now().millisecondsSinceEpoch}',
|
sessionToken: 'test_session_${DateTime.now().millisecondsSinceEpoch}',
|
||||||
packId: 'test_pack',
|
packId: 'test_pack',
|
||||||
words: TestWordsDto(words: [
|
words: TestWordsDto(
|
||||||
TestWordDto(
|
words: [
|
||||||
word: 'hello',
|
TestWordDto(word: 'hello', correct: 1, incorrect: 0),
|
||||||
correct: 1,
|
TestWordDto(word: 'world', correct: 1, incorrect: 0),
|
||||||
incorrect: 0,
|
],
|
||||||
),
|
),
|
||||||
TestWordDto(
|
|
||||||
word: 'world',
|
|
||||||
correct: 1,
|
|
||||||
incorrect: 0,
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Simulate the addTestStatistics call (without the actual database save)
|
// Simulate the addTestStatistics call (without the actual database save)
|
||||||
|
|
@ -208,21 +204,25 @@ void main() {
|
||||||
testId: 'test1',
|
testId: 'test1',
|
||||||
sessionToken: 'session1',
|
sessionToken: 'session1',
|
||||||
packId: 'pack1',
|
packId: 'pack1',
|
||||||
words: TestWordsDto(words: [
|
words: TestWordsDto(
|
||||||
TestWordDto(word: 'word1', correct: 1, incorrect: 0),
|
words: [
|
||||||
TestWordDto(word: 'word2', correct: 0, incorrect: 1),
|
TestWordDto(word: 'word1', correct: 1, incorrect: 0),
|
||||||
]),
|
TestWordDto(word: 'word2', correct: 0, incorrect: 1),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final testStats2 = TestStatisticsDto(
|
final testStats2 = TestStatisticsDto(
|
||||||
testId: 'test2',
|
testId: 'test2',
|
||||||
sessionToken: 'session2',
|
sessionToken: 'session2',
|
||||||
packId: 'pack1',
|
packId: 'pack1',
|
||||||
words: TestWordsDto(words: [
|
words: TestWordsDto(
|
||||||
TestWordDto(word: 'word3', correct: 1, incorrect: 0),
|
words: [
|
||||||
TestWordDto(word: 'word4', correct: 1, incorrect: 0),
|
TestWordDto(word: 'word3', correct: 1, incorrect: 0),
|
||||||
TestWordDto(word: 'word5', correct: 0, incorrect: 1),
|
TestWordDto(word: 'word4', correct: 1, incorrect: 0),
|
||||||
]),
|
TestWordDto(word: 'word5', correct: 0, incorrect: 1),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Test session tracking across multiple tests
|
// Test session tracking across multiple tests
|
||||||
|
|
@ -233,7 +233,9 @@ void main() {
|
||||||
testsCompleted: 1,
|
testsCompleted: 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
final sessionId2 = await sessionTracker.getOrCreateSession(testUser.id!); // Should reuse session
|
final sessionId2 = await sessionTracker.getOrCreateSession(
|
||||||
|
testUser.id!,
|
||||||
|
); // Should reuse session
|
||||||
expect(sessionId2, equals(sessionId1));
|
expect(sessionId2, equals(sessionId1));
|
||||||
|
|
||||||
await sessionTracker.updateSessionProgress(
|
await sessionTracker.updateSessionProgress(
|
||||||
|
|
@ -256,7 +258,10 @@ void main() {
|
||||||
];
|
];
|
||||||
|
|
||||||
final updatedUserData = testUser.userData.value!.copyWith(words: words);
|
final updatedUserData = testUser.userData.value!.copyWith(words: words);
|
||||||
final progress = await achievementManager.getAchievementProgress(testUser.id!, updatedUserData);
|
final progress = await achievementManager.getAchievementProgress(
|
||||||
|
testUser.id!,
|
||||||
|
updatedUserData,
|
||||||
|
);
|
||||||
|
|
||||||
// Should show progress towards word achievements
|
// Should show progress towards word achievements
|
||||||
expect(progress['words_10'], equals(0.5)); // 5/10 = 0.5
|
expect(progress['words_10'], equals(0.5)); // 5/10 = 0.5
|
||||||
|
|
@ -290,7 +295,10 @@ void main() {
|
||||||
testUser.userData.value!,
|
testUser.userData.value!,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(newlyUnlocked, isEmpty); // No achievements should unlock with empty data
|
expect(
|
||||||
|
newlyUnlocked,
|
||||||
|
isEmpty,
|
||||||
|
); // No achievements should unlock with empty data
|
||||||
|
|
||||||
// Test session tracking with non-existent user
|
// Test session tracking with non-existent user
|
||||||
final sessionId = await sessionTracker.getOrCreateSession(999);
|
final sessionId = await sessionTracker.getOrCreateSession(999);
|
||||||
|
|
@ -365,11 +373,11 @@ void main() {
|
||||||
|
|
||||||
// Add some words
|
// Add some words
|
||||||
final updatedUserData1 = testUser.userData.value!.copyWith(
|
final updatedUserData1 = testUser.userData.value!.copyWith(
|
||||||
words: List.generate(6, (i) => WordStatisticsModel(
|
words: List.generate(
|
||||||
word: 'word$i',
|
6,
|
||||||
correct: 1.0,
|
(i) =>
|
||||||
incorrect: 0.0,
|
WordStatisticsModel(word: 'word$i', correct: 1.0, incorrect: 0.0),
|
||||||
)),
|
),
|
||||||
currentStreak: 2,
|
currentStreak: 2,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -379,15 +387,15 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(progress1['words_10'], equals(0.6)); // 6/10 = 0.6
|
expect(progress1['words_10'], equals(0.6)); // 6/10 = 0.6
|
||||||
expect(progress1['streak_3'], equals(2/3)); // 2/3 ≈ 0.67
|
expect(progress1['streak_3'], equals(2 / 3)); // 2/3 ≈ 0.67
|
||||||
|
|
||||||
// Add more progress
|
// Add more progress
|
||||||
final updatedUserData2 = updatedUserData1.copyWith(
|
final updatedUserData2 = updatedUserData1.copyWith(
|
||||||
words: List.generate(8, (i) => WordStatisticsModel(
|
words: List.generate(
|
||||||
word: 'word$i',
|
8,
|
||||||
correct: 1.0,
|
(i) =>
|
||||||
incorrect: 0.0,
|
WordStatisticsModel(word: 'word$i', correct: 1.0, incorrect: 0.0),
|
||||||
)),
|
),
|
||||||
currentStreak: 3,
|
currentStreak: 3,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -397,7 +405,10 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(progress2['words_10'], equals(0.8)); // 8/10 = 0.8
|
expect(progress2['words_10'], equals(0.8)); // 8/10 = 0.8
|
||||||
expect(progress2['streak_3'], equals(1.0)); // 3/3 = 1.0 (achievement unlocked)
|
expect(
|
||||||
|
progress2['streak_3'],
|
||||||
|
equals(1.0),
|
||||||
|
); // 3/3 = 1.0 (achievement unlocked)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,10 @@ void main() {
|
||||||
// 3. Session is tracked
|
// 3. Session is tracked
|
||||||
// 4. Achievements are checked and unlocked
|
// 4. Achievements are checked and unlocked
|
||||||
|
|
||||||
expect(true, isTrue); // Integration tests would be implemented with proper mocking
|
expect(
|
||||||
|
true,
|
||||||
|
isTrue,
|
||||||
|
); // Integration tests would be implemented with proper mocking
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Error handling prevents system crashes', () {
|
test('Error handling prevents system crashes', () {
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,11 @@ void main() {
|
||||||
final yesterday = today.subtract(const Duration(days: 1));
|
final yesterday = today.subtract(const Duration(days: 1));
|
||||||
final threeDaysAgo = today.subtract(const Duration(days: 3));
|
final threeDaysAgo = today.subtract(const Duration(days: 3));
|
||||||
|
|
||||||
final result = calculator.calculateStreak([today, yesterday, threeDaysAgo]);
|
final result = calculator.calculateStreak([
|
||||||
|
today,
|
||||||
|
yesterday,
|
||||||
|
threeDaysAgo,
|
||||||
|
]);
|
||||||
expect(result, 2); // Today and yesterday, gap on day 3
|
expect(result, 2); // Today and yesterday, gap on day 3
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -63,9 +67,15 @@ void main() {
|
||||||
test('handles dates with different times', () {
|
test('handles dates with different times', () {
|
||||||
final todayMorning = DateTime.now().copyWith(hour: 9);
|
final todayMorning = DateTime.now().copyWith(hour: 9);
|
||||||
final todayEvening = DateTime.now().copyWith(hour: 20);
|
final todayEvening = DateTime.now().copyWith(hour: 20);
|
||||||
final yesterday = DateTime.now().subtract(const Duration(days: 1, hours: 10));
|
final yesterday = DateTime.now().subtract(
|
||||||
|
const Duration(days: 1, hours: 10),
|
||||||
|
);
|
||||||
|
|
||||||
final result = calculator.calculateStreak([todayMorning, todayEvening, yesterday]);
|
final result = calculator.calculateStreak([
|
||||||
|
todayMorning,
|
||||||
|
todayEvening,
|
||||||
|
yesterday,
|
||||||
|
]);
|
||||||
expect(result, 2);
|
expect(result, 2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -122,9 +132,7 @@ void main() {
|
||||||
studyTimeMinutes: 45,
|
studyTimeMinutes: 45,
|
||||||
);
|
);
|
||||||
|
|
||||||
final userData = UserDataModel(
|
final userData = UserDataModel(packProgress: [pack1, pack2]);
|
||||||
packProgress: [pack1, pack2],
|
|
||||||
);
|
|
||||||
|
|
||||||
final result = calculator.calculateTotalStudyTime(userData);
|
final result = calculator.calculateTotalStudyTime(userData);
|
||||||
expect(result, 75); // 30 + 45
|
expect(result, 75); // 30 + 45
|
||||||
|
|
@ -224,10 +232,7 @@ void main() {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
final result = calculator.getTimelineStatistics(
|
final result = calculator.getTimelineStatistics(userData, period: 'week');
|
||||||
userData,
|
|
||||||
period: 'week',
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result['period'], 'week');
|
expect(result['period'], 'week');
|
||||||
expect(result.containsKey('totalDays'), true);
|
expect(result.containsKey('totalDays'), true);
|
||||||
|
|
@ -247,10 +252,17 @@ void main() {
|
||||||
|
|
||||||
test('calculates level based on activity', () {
|
test('calculates level based on activity', () {
|
||||||
final userData = UserDataModel(
|
final userData = UserDataModel(
|
||||||
words: List.generate(20, (i) => WordStatisticsModel(word: 'word$i')), // 20 words
|
words: List.generate(
|
||||||
|
20,
|
||||||
|
(i) => WordStatisticsModel(word: 'word$i'),
|
||||||
|
), // 20 words
|
||||||
totalStudyTimeMinutes: 60 * 10, // 10 hours = 600 minutes
|
totalStudyTimeMinutes: 60 * 10, // 10 hours = 600 minutes
|
||||||
packProgress: [
|
packProgress: [
|
||||||
PackProgressModel(packId: 'pack1', totalCards: 10, learnedCards: 10), // 1 completed pack
|
PackProgressModel(
|
||||||
|
packId: 'pack1',
|
||||||
|
totalCards: 10,
|
||||||
|
learnedCards: 10,
|
||||||
|
), // 1 completed pack
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@ void main() {
|
||||||
setUpAll(() async {
|
setUpAll(() async {
|
||||||
// Подключение к тестовой БД
|
// Подключение к тестовой БД
|
||||||
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
|
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
|
||||||
final port = int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
|
final port =
|
||||||
|
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
|
||||||
final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
||||||
final username = Platform.environment['TEST_DB_USER'] ?? 'postgres';
|
final username = Platform.environment['TEST_DB_USER'] ?? 'postgres';
|
||||||
final password = Platform.environment['TEST_DB_PASSWORD'] ?? 'postgres';
|
final password = Platform.environment['TEST_DB_PASSWORD'] ?? 'postgres';
|
||||||
|
|
@ -242,10 +243,7 @@ void main() {
|
||||||
isCorrect: true,
|
isCorrect: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
final packStats = await manager.getPackStatistics(
|
final packStats = await manager.getPackStatistics(testUserId, pack.id);
|
||||||
testUserId,
|
|
||||||
pack.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(packStats.length, equals(2));
|
expect(packStats.length, equals(2));
|
||||||
expect(
|
expect(
|
||||||
|
|
@ -263,10 +261,7 @@ void main() {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final packStats = await manager.getPackStatistics(
|
final packStats = await manager.getPackStatistics(testUserId, pack.id);
|
||||||
testUserId,
|
|
||||||
pack.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(packStats, isEmpty);
|
expect(packStats, isEmpty);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,8 @@ import 'package:mnemo_cards_backend/storage/minio_config.dart';
|
||||||
void main() {
|
void main() {
|
||||||
group('MinioConfig', () {
|
group('MinioConfig', () {
|
||||||
test('should have correct bucket names', () {
|
test('should have correct bucket names', () {
|
||||||
expect(
|
expect(MinioConfig.cardImagesBucket, equals('card-images'));
|
||||||
MinioConfig.cardImagesBucket,
|
expect(MinioConfig.voiceAudioBucket, equals('voice-audio'));
|
||||||
equals('card-images'),
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
MinioConfig.voiceAudioBucket,
|
|
||||||
equals('voice-audio'),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should have correct presigned URL expiry', () {
|
test('should have correct presigned URL expiry', () {
|
||||||
|
|
@ -44,14 +38,8 @@ void main() {
|
||||||
|
|
||||||
test('should have reasonable presigned URL expiry', () {
|
test('should have reasonable presigned URL expiry', () {
|
||||||
// Presigned URLs should expire after 4 hours (14400 seconds)
|
// Presigned URLs should expire after 4 hours (14400 seconds)
|
||||||
expect(
|
expect(MinioConfig.presignedUrlExpirySeconds, equals(14400));
|
||||||
MinioConfig.presignedUrlExpirySeconds,
|
expect(MinioConfig.presignedUrlExpirySeconds, greaterThan(0));
|
||||||
equals(14400),
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
MinioConfig.presignedUrlExpirySeconds,
|
|
||||||
greaterThan(0),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,4 +79,3 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,7 @@ void main() {
|
||||||
final generator = SimpleQuestionGenerator(
|
final generator = SimpleQuestionGenerator(
|
||||||
data,
|
data,
|
||||||
seed: 1,
|
seed: 1,
|
||||||
possibleTypes: {
|
possibleTypes: {SimpleQuestionType.audio_translation},
|
||||||
SimpleQuestionType.audio_translation,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final answerCard = data.items.first; // card_1 with UUID audio
|
final answerCard = data.items.first; // card_1 with UUID audio
|
||||||
|
|
@ -82,9 +80,7 @@ void main() {
|
||||||
final generator = SimpleQuestionGenerator(
|
final generator = SimpleQuestionGenerator(
|
||||||
data,
|
data,
|
||||||
seed: 1,
|
seed: 1,
|
||||||
possibleTypes: {
|
possibleTypes: {SimpleQuestionType.audio_translation},
|
||||||
SimpleQuestionType.audio_translation,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final answerCard = data.items.first;
|
final answerCard = data.items.first;
|
||||||
|
|
@ -116,9 +112,7 @@ void main() {
|
||||||
final generator = SimpleQuestionGenerator(
|
final generator = SimpleQuestionGenerator(
|
||||||
data,
|
data,
|
||||||
seed: 1,
|
seed: 1,
|
||||||
possibleTypes: {
|
possibleTypes: {SimpleQuestionType.audio_translation},
|
||||||
SimpleQuestionType.audio_translation,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final answerCard = data.items.first;
|
final answerCard = data.items.first;
|
||||||
|
|
@ -151,9 +145,7 @@ void main() {
|
||||||
final generator = SimpleQuestionGenerator(
|
final generator = SimpleQuestionGenerator(
|
||||||
data,
|
data,
|
||||||
seed: 1,
|
seed: 1,
|
||||||
possibleTypes: {
|
possibleTypes: {SimpleQuestionType.audio_images},
|
||||||
SimpleQuestionType.audio_images,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final answerCard = data.items.first;
|
final answerCard = data.items.first;
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,13 @@ void main() {
|
||||||
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
|
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
|
||||||
final port =
|
final port =
|
||||||
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
|
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
|
||||||
final database =
|
final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
||||||
Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
final username =
|
||||||
final username = Platform.environment['TEST_DB_USER'] ??
|
Platform.environment['TEST_DB_USER'] ??
|
||||||
Platform.environment['DB_USER'] ??
|
Platform.environment['DB_USER'] ??
|
||||||
'mnemo_user';
|
'mnemo_user';
|
||||||
final password = Platform.environment['TEST_DB_PASSWORD'] ??
|
final password =
|
||||||
|
Platform.environment['TEST_DB_PASSWORD'] ??
|
||||||
Platform.environment['DB_PASSWORD'] ??
|
Platform.environment['DB_PASSWORD'] ??
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
|
@ -38,12 +39,12 @@ void main() {
|
||||||
await Migrator(db).createAll();
|
await Migrator(db).createAll();
|
||||||
|
|
||||||
// Initialize MinioService
|
// Initialize MinioService
|
||||||
final minioEndpoint = Platform.environment['MINIO_ENDPOINT'] ??
|
final minioEndpoint =
|
||||||
'localhost:9000';
|
Platform.environment['MINIO_ENDPOINT'] ?? 'localhost:9000';
|
||||||
final minioAccessKey = Platform.environment['MINIO_ACCESS_KEY'] ??
|
final minioAccessKey =
|
||||||
'minioadmin';
|
Platform.environment['MINIO_ACCESS_KEY'] ?? 'minioadmin';
|
||||||
final minioSecretKey = Platform.environment['MINIO_SECRET_KEY'] ??
|
final minioSecretKey =
|
||||||
'minioadmin';
|
Platform.environment['MINIO_SECRET_KEY'] ?? 'minioadmin';
|
||||||
|
|
||||||
minioService = MinioService(
|
minioService = MinioService(
|
||||||
endpoint: minioEndpoint,
|
endpoint: minioEndpoint,
|
||||||
|
|
@ -71,99 +72,100 @@ void main() {
|
||||||
testId = null;
|
testId = null;
|
||||||
}
|
}
|
||||||
if (packId != null) {
|
if (packId != null) {
|
||||||
await (db.delete(db.cardPacks)..where((p) => p.id.equals(packId!)))
|
await (db.delete(
|
||||||
.go();
|
db.cardPacks,
|
||||||
|
)..where((p) => p.id.equals(packId!))).go();
|
||||||
packId = null;
|
packId = null;
|
||||||
}
|
}
|
||||||
if (cardId != null) {
|
if (cardId != null) {
|
||||||
await (db.delete(db.gameCards)..where((c) => c.id.equals(cardId!)))
|
await (db.delete(
|
||||||
.go();
|
db.gameCards,
|
||||||
|
)..where((c) => c.id.equals(cardId!))).go();
|
||||||
cardId = null;
|
cardId = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('matrix question buttons should have imageUrl from card images',
|
test(
|
||||||
() async {
|
'matrix question buttons should have imageUrl from card images',
|
||||||
// Create pack
|
() async {
|
||||||
packId = await db.packDao.createPack(
|
// Create pack
|
||||||
CardPacksCompanion.insert(
|
packId = await db.packDao.createPack(
|
||||||
title: 'Test Pack',
|
CardPacksCompanion.insert(
|
||||||
subtitle: 'subtitle',
|
title: 'Test Pack',
|
||||||
size: 1,
|
subtitle: 'subtitle',
|
||||||
),
|
size: 1,
|
||||||
);
|
|
||||||
|
|
||||||
// Create card with image (using UUID as objectId in MinIO)
|
|
||||||
const imageObjectId = '12345678-1234-1234-1234-123456789abc';
|
|
||||||
cardId = await db.packDao.createCard(
|
|
||||||
GameCardsCompanion.insert(
|
|
||||||
original: 'perro',
|
|
||||||
translation: 'dog',
|
|
||||||
image: imageObjectId,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await db.packDao.addCardToPack(
|
|
||||||
packId: packId!,
|
|
||||||
cardId: cardId!,
|
|
||||||
order: 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create test with matrix question (without buttons - they'll be generated)
|
|
||||||
testId = await db.testDao.createTest(
|
|
||||||
TestsCompanion.insert(
|
|
||||||
name: 'Matrix Test',
|
|
||||||
color: const Value('#ff0000'),
|
|
||||||
version: const Value('1.0'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await db.testDao.linkTestToPack(testId!, packId!);
|
|
||||||
|
|
||||||
// Create matrix question without buttons
|
|
||||||
await db.testDao.createTestQuestion(
|
|
||||||
TestQuestionsCompanion.insert(
|
|
||||||
testId: testId!,
|
|
||||||
orderIndex: const Value(0),
|
|
||||||
questionType: TestQuestionType.matrix.name,
|
|
||||||
word: 'test',
|
|
||||||
answer: '',
|
|
||||||
options: const Value('[]'), // Empty buttons - will be generated
|
|
||||||
uiData: const Value('{"matrixSize": 1}'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fetch test
|
|
||||||
final user = UserModel(id: 'test-user');
|
|
||||||
final result = await testManager.fetchTest(testId!, user);
|
|
||||||
|
|
||||||
expect(result, isNotNull);
|
|
||||||
expect(result!.questions, hasLength(1));
|
|
||||||
|
|
||||||
final question = result.questions.first;
|
|
||||||
expect(question.questionType, equals(TestQuestionType.matrix));
|
|
||||||
|
|
||||||
// Check buttons
|
|
||||||
if (question is MatrixTestQuestion) {
|
|
||||||
expect(question.buttons, hasLength(1));
|
|
||||||
|
|
||||||
final button = question.buttons.first;
|
|
||||||
expect(button.id, equals(cardId));
|
|
||||||
expect(button.image, equals(imageObjectId));
|
|
||||||
expect(button.imageUrl, isNotNull);
|
|
||||||
expect(button.imageUrl, isNot(equals(imageObjectId)));
|
|
||||||
// imageUrl should be a presigned URL or API endpoint
|
|
||||||
expect(
|
|
||||||
button.imageUrl,
|
|
||||||
anyOf(
|
|
||||||
startsWith('http'),
|
|
||||||
startsWith('/api/v2/packs'),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
fail('Expected MatrixTestQuestion');
|
// Create card with image (using UUID as objectId in MinIO)
|
||||||
}
|
const imageObjectId = '12345678-1234-1234-1234-123456789abc';
|
||||||
});
|
cardId = await db.packDao.createCard(
|
||||||
|
GameCardsCompanion.insert(
|
||||||
|
original: 'perro',
|
||||||
|
translation: 'dog',
|
||||||
|
image: imageObjectId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.packDao.addCardToPack(
|
||||||
|
packId: packId!,
|
||||||
|
cardId: cardId!,
|
||||||
|
order: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create test with matrix question (without buttons - they'll be generated)
|
||||||
|
testId = await db.testDao.createTest(
|
||||||
|
TestsCompanion.insert(
|
||||||
|
name: 'Matrix Test',
|
||||||
|
color: const Value('#ff0000'),
|
||||||
|
version: const Value('1.0'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.testDao.linkTestToPack(testId!, packId!);
|
||||||
|
|
||||||
|
// Create matrix question without buttons
|
||||||
|
await db.testDao.createTestQuestion(
|
||||||
|
TestQuestionsCompanion.insert(
|
||||||
|
testId: testId!,
|
||||||
|
orderIndex: const Value(0),
|
||||||
|
questionType: TestQuestionType.matrix.name,
|
||||||
|
word: 'test',
|
||||||
|
answer: '',
|
||||||
|
options: const Value('[]'), // Empty buttons - will be generated
|
||||||
|
uiData: const Value('{"matrixSize": 1}'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fetch test
|
||||||
|
final user = UserModel(id: 'test-user');
|
||||||
|
final result = await testManager.fetchTest(testId!, user);
|
||||||
|
|
||||||
|
expect(result, isNotNull);
|
||||||
|
expect(result!.questions, hasLength(1));
|
||||||
|
|
||||||
|
final question = result.questions.first;
|
||||||
|
expect(question.questionType, equals(TestQuestionType.matrix));
|
||||||
|
|
||||||
|
// Check buttons
|
||||||
|
if (question is MatrixTestQuestion) {
|
||||||
|
expect(question.buttons, hasLength(1));
|
||||||
|
|
||||||
|
final button = question.buttons.first;
|
||||||
|
expect(button.id, equals(cardId));
|
||||||
|
expect(button.image, equals(imageObjectId));
|
||||||
|
expect(button.imageUrl, isNotNull);
|
||||||
|
expect(button.imageUrl, isNot(equals(imageObjectId)));
|
||||||
|
// imageUrl should be a presigned URL or API endpoint
|
||||||
|
expect(
|
||||||
|
button.imageUrl,
|
||||||
|
anyOf(startsWith('http'), startsWith('/api/v2/packs')),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
fail('Expected MatrixTestQuestion');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('test question with image should have imageUrl', () async {
|
test('test question with image should have imageUrl', () async {
|
||||||
// Create pack
|
// Create pack
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,7 @@ void main() async {
|
||||||
username: 'mnemo_user',
|
username: 'mnemo_user',
|
||||||
password: 'dev_password_change_me',
|
password: 'dev_password_change_me',
|
||||||
),
|
),
|
||||||
settings: pg.ConnectionSettings(
|
settings: pg.ConnectionSettings(sslMode: pg.SslMode.disable),
|
||||||
sslMode: pg.SslMode.disable,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
print('✅ PostgreSQL connection successful');
|
print('✅ PostgreSQL connection successful');
|
||||||
|
|
@ -27,7 +25,6 @@ void main() async {
|
||||||
await connection.close();
|
await connection.close();
|
||||||
print('✅ Database connection closed');
|
print('✅ Database connection closed');
|
||||||
print('🎉 PostgreSQL backend can connect to database!');
|
print('🎉 PostgreSQL backend can connect to database!');
|
||||||
|
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
print('❌ Error: $e');
|
print('❌ Error: $e');
|
||||||
print('Stack trace: $s');
|
print('Stack trace: $s');
|
||||||
|
|
|
||||||
|
|
@ -55,3 +55,4 @@ echo " - favicon.png (32x32)"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue