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 {
|
||||
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);
|
||||
print('🔍 PaymentManager.createPayment: Calling paymentDao.createPayment');
|
||||
final paymentId = await _db.paymentDao.createPayment(companion);
|
||||
|
|
@ -43,7 +45,9 @@ class PaymentManager {
|
|||
print('🔍 PaymentManager.createPayment: Getting payment by id');
|
||||
final payment = await _db.paymentDao.getPaymentById(paymentId);
|
||||
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');
|
||||
}
|
||||
print('✅ PaymentManager.createPayment: Payment retrieved successfully');
|
||||
|
|
@ -299,7 +303,7 @@ class PaymentManager {
|
|||
try {
|
||||
final payment = await _db.paymentDao.getPaymentByExternalToken(token);
|
||||
if (payment == null) {
|
||||
log('Payment not found for token: $token');
|
||||
print('Payment not found for token: $token');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -336,15 +340,21 @@ class PaymentManager {
|
|||
List<MnemoCardsProductDto> products = const [],
|
||||
}) async {
|
||||
print('🔍 PaymentManager.createYookassaUrl: Starting');
|
||||
print('🔍 PaymentManager.createYookassaUrl: amount=$amount, userId=$userId, products=${products.length}');
|
||||
|
||||
print('🔍 PaymentManager.createYookassaUrl: Calling YooMoneyHandler.createPayment');
|
||||
print(
|
||||
'🔍 PaymentManager.createYookassaUrl: amount=$amount, userId=$userId, products=${products.length}',
|
||||
);
|
||||
|
||||
print(
|
||||
'🔍 PaymentManager.createYookassaUrl: Calling YooMoneyHandler.createPayment',
|
||||
);
|
||||
final yookassaPayment = await _yooMoneyHandler.createPayment(
|
||||
amount: amount,
|
||||
description: description,
|
||||
userId: userId,
|
||||
);
|
||||
print('✅ PaymentManager.createYookassaUrl: YooKassa payment created, id=${yookassaPayment.id}');
|
||||
print(
|
||||
'✅ PaymentManager.createYookassaUrl: YooKassa payment created, id=${yookassaPayment.id}',
|
||||
);
|
||||
|
||||
if (yookassaPayment.confirmationUrl == null) {
|
||||
print('❌ PaymentManager.createYookassaUrl: confirmationUrl is null');
|
||||
|
|
@ -368,7 +378,9 @@ class PaymentManager {
|
|||
await createPayment(paymentDto, userId);
|
||||
print('✅ PaymentManager.createYookassaUrl: Payment created in database');
|
||||
|
||||
print('✅ PaymentManager.createYookassaUrl: Returning confirmationUrl and paymentId');
|
||||
print(
|
||||
'✅ PaymentManager.createYookassaUrl: Returning confirmationUrl and paymentId',
|
||||
);
|
||||
return YookassaPaymentResult(
|
||||
confirmationUrl: yookassaPayment.confirmationUrl!,
|
||||
paymentId: yookassaPayment.id,
|
||||
|
|
|
|||
|
|
@ -30,12 +30,10 @@ class YooMoneyHandler {
|
|||
final _uuid = const Uuid();
|
||||
static const String _baseUrl = 'https://api.yookassa.ru/v3';
|
||||
|
||||
YooMoneyHandler({
|
||||
required String shopId,
|
||||
required String secretKey,
|
||||
}) : _shopId = shopId,
|
||||
_secretKey = secretKey,
|
||||
_returnUrlBase = Platform.environment['YOOKASSA_RETURN_URL'] {
|
||||
YooMoneyHandler({required String shopId, required String secretKey})
|
||||
: _shopId = shopId,
|
||||
_secretKey = secretKey,
|
||||
_returnUrlBase = Platform.environment['YOOKASSA_RETURN_URL'] {
|
||||
// Validate credentials
|
||||
if (_shopId.isEmpty || _secretKey.isEmpty) {
|
||||
log(
|
||||
|
|
@ -49,9 +47,7 @@ class YooMoneyHandler {
|
|||
_dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: _baseUrl,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -113,17 +109,11 @@ class YooMoneyHandler {
|
|||
|
||||
// Create payment request according to CreatePaymentRequest schema
|
||||
final requestBody = {
|
||||
'amount': {
|
||||
'value': formattedAmount,
|
||||
'currency': 'RUB',
|
||||
},
|
||||
'amount': {'value': formattedAmount, 'currency': 'RUB'},
|
||||
'description': description.length > 128
|
||||
? description.substring(0, 128)
|
||||
: description, // Max 128 chars per spec
|
||||
'confirmation': {
|
||||
'type': 'redirect',
|
||||
'return_url': returnUrl,
|
||||
},
|
||||
'confirmation': {'type': 'redirect', 'return_url': returnUrl},
|
||||
'capture': true, // Auto-capture payment when succeeded
|
||||
'metadata': {
|
||||
'userId': userId,
|
||||
|
|
@ -146,11 +136,7 @@ class YooMoneyHandler {
|
|||
final response = await _dio!.post<Map<String, dynamic>>(
|
||||
'/payments',
|
||||
data: requestBody,
|
||||
options: Options(
|
||||
headers: {
|
||||
'Idempotence-Key': idempotenceKey,
|
||||
},
|
||||
),
|
||||
options: Options(headers: {'Idempotence-Key': idempotenceKey}),
|
||||
);
|
||||
|
||||
final paymentData = response.data;
|
||||
|
|
@ -175,30 +161,15 @@ class YooMoneyHandler {
|
|||
if (confirmationType == 'redirect') {
|
||||
confirmationUrl = confirmation['confirmation_url'] as String?;
|
||||
} else {
|
||||
log(
|
||||
'Payment created with confirmation type: $confirmationType',
|
||||
name: 'YooMoneyHandler',
|
||||
);
|
||||
print('Payment created with confirmation type: $confirmationType');
|
||||
}
|
||||
}
|
||||
|
||||
if (confirmationUrl == null) {
|
||||
log(
|
||||
'Warning: No confirmation URL in payment response',
|
||||
name: 'YooMoneyHandler',
|
||||
error: jsonEncode(paymentData),
|
||||
);
|
||||
print('Warning: No confirmation URL in payment response');
|
||||
}
|
||||
|
||||
log(
|
||||
'YooKassa payment created successfully',
|
||||
name: 'YooMoneyHandler',
|
||||
error: {
|
||||
'paymentId': paymentId,
|
||||
'status': paymentStatus,
|
||||
'hasConfirmationUrl': confirmationUrl != null,
|
||||
},
|
||||
);
|
||||
print('YooKassa payment created successfully');
|
||||
|
||||
return YookassaPayment(
|
||||
id: paymentId,
|
||||
|
|
@ -206,28 +177,15 @@ class YooMoneyHandler {
|
|||
confirmationUrl: confirmationUrl,
|
||||
);
|
||||
} on DioException catch (e, stackTrace) {
|
||||
log(
|
||||
'YooKassa API error when creating payment',
|
||||
name: 'YooMoneyHandler',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
print('YooKassa API error when creating payment $e $stackTrace');
|
||||
if (e.response != null) {
|
||||
log(
|
||||
'YooKassa error response: ${e.response?.data}',
|
||||
name: 'YooMoneyHandler',
|
||||
);
|
||||
print('YooKassa error response: ${e.response?.data}');
|
||||
}
|
||||
throw Exception(
|
||||
'Failed to create YooKassa payment: ${e.message ?? 'Unknown error'}',
|
||||
);
|
||||
} on Exception catch (e, stackTrace) {
|
||||
log(
|
||||
'Unexpected error when creating YooKassa payment',
|
||||
name: 'YooMoneyHandler',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
print('Unexpected error when creating YooKassa payment $e $stackTrace');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
|
@ -242,11 +200,7 @@ class YooMoneyHandler {
|
|||
}
|
||||
|
||||
try {
|
||||
log(
|
||||
'Checking YooKassa payment status',
|
||||
name: 'YooMoneyHandler',
|
||||
error: {'paymentId': paymentId},
|
||||
);
|
||||
print('Checking YooKassa payment status $paymentId');
|
||||
|
||||
// Get payment info from YooKassa API
|
||||
// According to spec: GET /v3/payments/{payment_id}
|
||||
|
|
@ -274,15 +228,8 @@ class YooMoneyHandler {
|
|||
}
|
||||
}
|
||||
|
||||
log(
|
||||
'YooKassa payment status retrieved',
|
||||
name: 'YooMoneyHandler',
|
||||
error: {
|
||||
'paymentId': id,
|
||||
'status': paymentStatus,
|
||||
'paid': paid,
|
||||
},
|
||||
);
|
||||
print('''YooKassa payment status retrieved
|
||||
${{'paymentId': id, 'status': paymentStatus, 'paid': paid}}''');
|
||||
|
||||
return YookassaPayment(
|
||||
id: id,
|
||||
|
|
@ -290,28 +237,15 @@ class YooMoneyHandler {
|
|||
confirmationUrl: confirmationUrl,
|
||||
);
|
||||
} on DioException catch (e, stackTrace) {
|
||||
log(
|
||||
'YooKassa API error when checking payment',
|
||||
name: 'YooMoneyHandler',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
print('YooKassa API error when checking payment $e $stackTrace');
|
||||
if (e.response != null) {
|
||||
log(
|
||||
'YooKassa error response: ${e.response?.data}',
|
||||
name: 'YooMoneyHandler',
|
||||
);
|
||||
print('YooKassa error response: ${e.response?.data}');
|
||||
}
|
||||
throw Exception(
|
||||
'Failed to check YooKassa payment: ${e.message ?? 'Unknown error'}',
|
||||
);
|
||||
} on Exception catch (e, stackTrace) {
|
||||
log(
|
||||
'Unexpected error when checking YooKassa payment',
|
||||
name: 'YooMoneyHandler',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
print('Unexpected error when checking YooKassa payment $e $stackTrace');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -602,10 +602,7 @@ class PacksApiV2 {
|
|||
/// to allow image preview in public pack listings
|
||||
@Route.get('/packs/<packId>/cover')
|
||||
@OpenApiRouteHttp()
|
||||
Future<Response> getPackCover(
|
||||
Request request,
|
||||
String packId,
|
||||
) async {
|
||||
Future<Response> getPackCover(Request request, String packId) async {
|
||||
try {
|
||||
if (packId.isEmpty) {
|
||||
return _badRequest('Invalid pack ID');
|
||||
|
|
|
|||
|
|
@ -23,11 +23,7 @@ class PurchasesApiV2 {
|
|||
final PackManager _packManager;
|
||||
final AppDatabase _db;
|
||||
|
||||
PurchasesApiV2(
|
||||
this._paymentManager,
|
||||
this._packManager,
|
||||
this._db,
|
||||
);
|
||||
PurchasesApiV2(this._paymentManager, this._packManager, this._db);
|
||||
|
||||
Response _ok(Object? object, {Map<String, String> headers = const {}}) =>
|
||||
Response.ok(
|
||||
|
|
@ -70,10 +66,7 @@ class PurchasesApiV2 {
|
|||
/// Create purchase for a pack
|
||||
@Route.post('/purchases/packs/<packId>')
|
||||
@OpenApiRouteHttp()
|
||||
Future<Response> createPackPurchase(
|
||||
Request request,
|
||||
String packId,
|
||||
) async {
|
||||
Future<Response> createPackPurchase(Request request, String packId) async {
|
||||
try {
|
||||
print('🔍 createPackPurchase: Starting for packId=$packId');
|
||||
final user = request.user;
|
||||
|
|
@ -87,7 +80,9 @@ class PurchasesApiV2 {
|
|||
return _unauthorized();
|
||||
}
|
||||
|
||||
print('🔍 createPackPurchase: Getting pack information for packId=$packId');
|
||||
print(
|
||||
'🔍 createPackPurchase: Getting pack information for packId=$packId',
|
||||
);
|
||||
// Get pack information
|
||||
final pack = await _packManager.getPack(packId);
|
||||
if (pack == null) {
|
||||
|
|
@ -96,12 +91,11 @@ class PurchasesApiV2 {
|
|||
}
|
||||
print('✅ createPackPurchase: Pack found: ${pack.title}');
|
||||
|
||||
print('🔍 createPackPurchase: Checking pack access for userId=${user.id}, packId=$packId');
|
||||
// Check if already purchased
|
||||
final hasAccess = await _db.userDao.hasPackAccess(
|
||||
user.id!,
|
||||
packId,
|
||||
print(
|
||||
'🔍 createPackPurchase: Checking pack access for userId=${user.id}, packId=$packId',
|
||||
);
|
||||
// Check if already purchased
|
||||
final hasAccess = await _db.userDao.hasPackAccess(user.id!, packId);
|
||||
if (hasAccess) {
|
||||
print('❌ createPackPurchase: Pack already purchased');
|
||||
return _badRequest('Pack is already purchased');
|
||||
|
|
@ -129,10 +123,7 @@ class PurchasesApiV2 {
|
|||
|
||||
// Create products list
|
||||
final products = [
|
||||
MnemoCardsProductDto(
|
||||
type: MnemoCardsProductType.pack,
|
||||
id: packId,
|
||||
),
|
||||
MnemoCardsProductDto(type: MnemoCardsProductType.pack, id: packId),
|
||||
];
|
||||
|
||||
print('🔍 createPackPurchase: Creating YooKassa payment URL');
|
||||
|
|
@ -143,11 +134,14 @@ class PurchasesApiV2 {
|
|||
userId: user.id!,
|
||||
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
|
||||
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({
|
||||
'purchaseUrl': paymentResult.confirmationUrl,
|
||||
|
|
@ -207,10 +201,7 @@ class PurchasesApiV2 {
|
|||
}
|
||||
|
||||
// Check if already purchased
|
||||
final hasAccess = await _db.userDao.hasPackAccess(
|
||||
user.id!,
|
||||
productId,
|
||||
);
|
||||
final hasAccess = await _db.userDao.hasPackAccess(user.id!, productId);
|
||||
if (hasAccess) {
|
||||
return _badRequest('Pack is already purchased');
|
||||
}
|
||||
|
|
@ -230,10 +221,7 @@ class PurchasesApiV2 {
|
|||
|
||||
description = 'Покупка пакета: ${pack.title}';
|
||||
products = [
|
||||
MnemoCardsProductDto(
|
||||
type: MnemoCardsProductType.pack,
|
||||
id: productId,
|
||||
),
|
||||
MnemoCardsProductDto(type: MnemoCardsProductType.pack, id: productId),
|
||||
];
|
||||
} else if (productType == MnemoCardsProductType.subscription) {
|
||||
// Get subscription plan
|
||||
|
|
@ -292,7 +280,8 @@ class PurchasesApiV2 {
|
|||
|
||||
// Build return URL for payment verification
|
||||
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({
|
||||
'purchaseUrl': paymentResult.confirmationUrl,
|
||||
|
|
@ -309,10 +298,7 @@ class PurchasesApiV2 {
|
|||
/// Verify payment status
|
||||
@Route.get('/purchases/payments/<paymentId>/verify')
|
||||
@OpenApiRouteHttp()
|
||||
Future<Response> verifyPayment(
|
||||
Request request,
|
||||
String paymentId,
|
||||
) async {
|
||||
Future<Response> verifyPayment(Request request, String paymentId) async {
|
||||
try {
|
||||
final user = request.user;
|
||||
if (user == null) {
|
||||
|
|
@ -360,10 +346,7 @@ class PurchasesApiV2 {
|
|||
/// Check pack purchase status
|
||||
@Route.get('/purchases/packs/<packId>/status')
|
||||
@OpenApiRouteHttp()
|
||||
Future<Response> getPackPurchaseStatus(
|
||||
Request request,
|
||||
String packId,
|
||||
) async {
|
||||
Future<Response> getPackPurchaseStatus(Request request, String packId) async {
|
||||
try {
|
||||
final user = request.user;
|
||||
if (user == null) {
|
||||
|
|
@ -384,13 +367,14 @@ class PurchasesApiV2 {
|
|||
final isPurchased = await _db.userDao.hasPackAccess(user.id!, packId);
|
||||
|
||||
// Check subscription access
|
||||
final activeSubscription = await _db.subscriptionDao.getActiveSubscription(
|
||||
user.id!,
|
||||
);
|
||||
final hasSubscriptionAccess = activeSubscription != null &&
|
||||
final activeSubscription = await _db.subscriptionDao
|
||||
.getActiveSubscription(user.id!);
|
||||
final hasSubscriptionAccess =
|
||||
activeSubscription != null &&
|
||||
(activeSubscription.features is List &&
|
||||
(activeSubscription.features as List)
|
||||
.contains(SubscriptionFeatureEnum.packs.name));
|
||||
(activeSubscription.features as List).contains(
|
||||
SubscriptionFeatureEnum.packs.name,
|
||||
));
|
||||
|
||||
return _ok({
|
||||
'packId': packId,
|
||||
|
|
@ -399,11 +383,14 @@ class PurchasesApiV2 {
|
|||
'hasSubscriptionAccess': hasSubscriptionAccess,
|
||||
});
|
||||
} 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());
|
||||
}
|
||||
}
|
||||
|
||||
Router get router => _$PurchasesApiV2Router(this);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,8 +44,12 @@ mixin SoftDeleteMixin<T extends Table, D> on DatabaseAccessor<AppDatabase> {
|
|||
///
|
||||
/// Возвращает null если запись не найдена или удалена.
|
||||
Future<D?> getActiveById(String id) async {
|
||||
print('🔍 SoftDeleteMixin.getActiveById: id=$id, table=${table.entityName}');
|
||||
print('🔍 SoftDeleteMixin.getActiveById: SQL: SELECT * FROM ${table.entityName} WHERE id = ? AND is_deleted = false LIMIT 1');
|
||||
print(
|
||||
'🔍 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()
|
||||
..where((t) {
|
||||
final idColumn = (t as dynamic).id;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
|
|||
/// Получить пак по ID
|
||||
Future<CardPack?> getPackById(String id) async {
|
||||
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)
|
||||
..where((p) => p.id.equals(id))
|
||||
..limit(1);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ class PaymentDao extends DatabaseAccessor<AppDatabase>
|
|||
print('🔍 PaymentDao.getPaymentById: paymentId=$id');
|
||||
print('🔍 PaymentDao.getPaymentById: Calling getActiveById');
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -51,48 +53,68 @@ class PaymentDao extends DatabaseAccessor<AppDatabase>
|
|||
/// Создать платеж
|
||||
Future<String> createPayment(PaymentsCompanion payment) async {
|
||||
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: amount=${payment.amount.value}');
|
||||
|
||||
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
// Query back to get the id using type-safe queries
|
||||
// Use externalToken if available, otherwise query by unique fields
|
||||
if (inserted.externalToken != null && inserted.externalToken!.isNotEmpty) {
|
||||
print('🔍 PaymentDao.createPayment: Querying by externalToken=${inserted.externalToken}');
|
||||
final foundPayment = await getPaymentByExternalToken(inserted.externalToken!);
|
||||
print(
|
||||
'🔍 PaymentDao.createPayment: Querying by externalToken=${inserted.externalToken}',
|
||||
);
|
||||
final foundPayment = await getPaymentByExternalToken(
|
||||
inserted.externalToken!,
|
||||
);
|
||||
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;
|
||||
}
|
||||
print('⚠️ PaymentDao.createPayment: No payment found by externalToken');
|
||||
}
|
||||
|
||||
|
||||
// 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)
|
||||
..where((p) =>
|
||||
p.userId.equals(payment.userId.value) &
|
||||
p.date.equals(inserted.date) &
|
||||
p.amount.equals(inserted.amount))
|
||||
..where(
|
||||
(p) =>
|
||||
p.userId.equals(payment.userId.value) &
|
||||
p.date.equals(inserted.date) &
|
||||
p.amount.equals(inserted.amount),
|
||||
)
|
||||
..orderBy([(p) => OrderingTerm.desc(p.createdAt)])
|
||||
..limit(1);
|
||||
|
||||
|
||||
final results = await query.get();
|
||||
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;
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,10 +26,12 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
|
|||
|
||||
/// Получить пользователя с UserData
|
||||
Future<UserWithData?> getUserWithDataById(String id) async {
|
||||
final query = select(users).join([
|
||||
leftOuterJoin(userDatas, userDatas.userId.equalsExp(users.id)),
|
||||
])..where(users.id.equals(id))
|
||||
..limit(1);
|
||||
final query =
|
||||
select(users).join([
|
||||
leftOuterJoin(userDatas, userDatas.userId.equalsExp(users.id)),
|
||||
])
|
||||
..where(users.id.equals(id))
|
||||
..limit(1);
|
||||
|
||||
final results = await query.get();
|
||||
if (results.isEmpty) return null;
|
||||
|
|
@ -146,7 +148,9 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
|
|||
/// Получить UserData пользователя
|
||||
Future<UserData?> getUserData(String userId) async {
|
||||
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)
|
||||
..where((ud) => ud.userId.equals(userId))
|
||||
..limit(1);
|
||||
|
|
@ -202,8 +206,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
|
|||
final query = select(tokens)
|
||||
..where(
|
||||
(t) =>
|
||||
t.externalUserId.equals(externalUserId) &
|
||||
t.isDeleted.equals(false),
|
||||
t.externalUserId.equals(externalUserId) & t.isDeleted.equals(false),
|
||||
)
|
||||
..orderBy([(t) => OrderingTerm.desc(t.created)])
|
||||
..limit(1);
|
||||
|
|
@ -216,9 +219,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
|
|||
Future<Token?> getTokenByUserId(String userId) async {
|
||||
final query = select(tokens)
|
||||
..where((t) => t.userId.equals(userId) & t.isDeleted.equals(false))
|
||||
..where(
|
||||
(t) => t.expires.isBiggerThanValue(PgDateTime(DateTime.now())),
|
||||
)
|
||||
..where((t) => t.expires.isBiggerThanValue(PgDateTime(DateTime.now())))
|
||||
..orderBy([(t) => OrderingTerm.desc(t.created)])
|
||||
..limit(1);
|
||||
|
||||
|
|
@ -397,13 +398,17 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
|
|||
/// Проверить, есть ли у пользователя доступ к паку
|
||||
Future<bool> hasPackAccess(String userId, String packId) async {
|
||||
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)
|
||||
..where((up) => up.userId.equals(userId) & up.packId.equals(packId))
|
||||
..limit(1);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -337,9 +337,9 @@ class AppDatabase extends _$AppDatabase {
|
|||
// 2. Удаляем все старые вопросы (soft delete)
|
||||
print('Soft-deleting all existing test questions...');
|
||||
final now = PgDateTime(DateTime.now());
|
||||
await (update(testQuestions)
|
||||
..where((tq) => tq.isDeleted.equals(false)))
|
||||
.write(
|
||||
await (update(
|
||||
testQuestions,
|
||||
)..where((tq) => tq.isDeleted.equals(false))).write(
|
||||
TestQuestionsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
deletedAt: Value(now),
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ extension CardPackToDto on CardPack {
|
|||
String? coverUrl;
|
||||
if (cover != null && cover!.isNotEmpty) {
|
||||
final coverValue = cover!.trim();
|
||||
|
||||
|
||||
// If it's already a remote URL, use it directly
|
||||
if (CardImageStorage.isRemoteUrl(coverValue)) {
|
||||
coverUrl = coverValue;
|
||||
|
|
|
|||
|
|
@ -40,9 +40,10 @@ class PackDtoConverter {
|
|||
String? coverUrl;
|
||||
if (model.cover != null && model.cover!.isNotEmpty) {
|
||||
final coverValue = model.cover!.trim();
|
||||
|
||||
|
||||
// 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;
|
||||
}
|
||||
// If it's a UUID or any other value, use API endpoint
|
||||
|
|
|
|||
|
|
@ -131,30 +131,40 @@ class InputButtonsQuestionGenerator implements QuestionGenerator {
|
|||
slotPositions.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Map template slot positions to answer letter positions
|
||||
// Answer may contain spaces, so we need to map slots to letters (excluding spaces)
|
||||
final answerLetters = answer.replaceAll(' ', '');
|
||||
final templateToAnswerIndex = <int, int>{};
|
||||
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] == '|') {
|
||||
templateToAnswerIndex[i] = answerLetterIndex;
|
||||
answerLetterIndex++;
|
||||
}
|
||||
// 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 positionsToReveal = shuffledPositions.take(visibleLetters).toList();
|
||||
|
||||
|
||||
// Replace slots with actual letters from answer
|
||||
for (final pos in positionsToReveal) {
|
||||
final answerLetterIndex = templateToAnswerIndex[pos];
|
||||
if (answerLetterIndex != null && answerLetterIndex < answerLetters.length) {
|
||||
template = template.replaceRange(pos, pos + 1, answerLetters[answerLetterIndex]);
|
||||
if (answerLetterIndex != null &&
|
||||
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 UserModel adminUser;
|
||||
|
||||
Request buildAdminRequest(
|
||||
String method,
|
||||
String url, {
|
||||
Object? body,
|
||||
}) {
|
||||
Request buildAdminRequest(String method, String url, {Object? body}) {
|
||||
return Request(
|
||||
method,
|
||||
Uri.parse(url),
|
||||
body: body == null ? null : jsonEncode(body),
|
||||
).change(
|
||||
context: {
|
||||
'user': adminUser,
|
||||
'accessService': accessService,
|
||||
},
|
||||
);
|
||||
).change(context: {'user': adminUser, 'accessService': accessService});
|
||||
}
|
||||
|
||||
setUpAll(() async {
|
||||
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
|
||||
final port =
|
||||
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
|
||||
final database =
|
||||
Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
||||
final username = Platform.environment['TEST_DB_USER'] ??
|
||||
final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
||||
final username =
|
||||
Platform.environment['TEST_DB_USER'] ??
|
||||
Platform.environment['DB_USER'] ??
|
||||
'mnemo_user';
|
||||
final password = Platform.environment['TEST_DB_PASSWORD'] ??
|
||||
final password =
|
||||
Platform.environment['TEST_DB_PASSWORD'] ??
|
||||
Platform.environment['DB_PASSWORD'] ??
|
||||
'';
|
||||
|
||||
|
|
@ -78,7 +70,10 @@ void main() {
|
|||
final discountsManager = DiscountsManager(db);
|
||||
final productsPriceResolver = ProductsPriceResolver(discountsManager, db);
|
||||
final adsManager = AdsManager();
|
||||
final packDtoConverter = PackDtoConverter(productsPriceResolver, adsManager);
|
||||
final packDtoConverter = PackDtoConverter(
|
||||
productsPriceResolver,
|
||||
adsManager,
|
||||
);
|
||||
final packManager = PackManager(db, packDtoConverter);
|
||||
|
||||
final resourceLoader = ResourceLoader(packManager);
|
||||
|
|
@ -107,23 +102,25 @@ void main() {
|
|||
Future<void> cleanup() async {
|
||||
// Delete relations first.
|
||||
if (testId != null) {
|
||||
await (db.delete(db.testPackRelations)
|
||||
..where((r) => r.testId.equals(testId!)))
|
||||
.go();
|
||||
await (db.delete(
|
||||
db.testPackRelations,
|
||||
)..where((r) => r.testId.equals(testId!))).go();
|
||||
|
||||
await (db.delete(db.testQuestions)
|
||||
..where((q) => q.testId.equals(testId!)))
|
||||
.go();
|
||||
await (db.delete(
|
||||
db.testQuestions,
|
||||
)..where((q) => q.testId.equals(testId!))).go();
|
||||
|
||||
await (db.delete(db.tests)..where((t) => t.id.equals(testId!))).go();
|
||||
}
|
||||
|
||||
if (packId != null) {
|
||||
await (db.delete(db.cardPackCards)
|
||||
..where((c) => c.packId.equals(packId!)))
|
||||
.go();
|
||||
await (db.delete(
|
||||
db.cardPackCards,
|
||||
)..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) {
|
||||
|
|
@ -131,7 +128,9 @@ void main() {
|
|||
await (db.delete(db.gameCards)..where((c) => c.id.equals(cardId))).go();
|
||||
|
||||
// Remove possible image files in data/cards
|
||||
final cardsDir = Directory('${PackManagerUtils.assetsDirectory.path}/cards');
|
||||
final cardsDir = Directory(
|
||||
'${PackManagerUtils.assetsDirectory.path}/cards',
|
||||
);
|
||||
final candidates = [
|
||||
File('${cardsDir.path}/$cardId.png'),
|
||||
File('${cardsDir.path}/$cardId.webp'),
|
||||
|
|
@ -159,144 +158,162 @@ void main() {
|
|||
await cleanup();
|
||||
});
|
||||
|
||||
test('getTest returns image URLs (no base64) and cards are linked to pack',
|
||||
() async {
|
||||
// Create pack
|
||||
packId = await db.packDao.createPack(
|
||||
CardPacksCompanion.insert(
|
||||
title: 'pack for admin tests images',
|
||||
subtitle: 'subtitle',
|
||||
size: 0,
|
||||
),
|
||||
);
|
||||
test(
|
||||
'getTest returns image URLs (no base64) and cards are linked to pack',
|
||||
() async {
|
||||
// Create pack
|
||||
packId = await db.packDao.createPack(
|
||||
CardPacksCompanion.insert(
|
||||
title: 'pack for admin tests images',
|
||||
subtitle: 'subtitle',
|
||||
size: 0,
|
||||
),
|
||||
);
|
||||
|
||||
// Create test with base64 cover + base64 question/button images
|
||||
final createRequest = buildAdminRequest(
|
||||
'POST',
|
||||
'http://localhost/api/v2/admin/tests',
|
||||
body: {
|
||||
'name': 'test with images',
|
||||
'cover': oneByOnePngBase64,
|
||||
'questions': [
|
||||
{
|
||||
'questionType': 'simple',
|
||||
'word': 'word',
|
||||
'answer': 'btn1',
|
||||
'image': oneByOnePngBase64,
|
||||
'buttons': [
|
||||
{
|
||||
'id': 'btn1',
|
||||
'text': 'ok',
|
||||
'image': oneByOnePngBase64,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
// Create test with base64 cover + base64 question/button images
|
||||
final createRequest = buildAdminRequest(
|
||||
'POST',
|
||||
'http://localhost/api/v2/admin/tests',
|
||||
body: {
|
||||
'name': 'test with images',
|
||||
'cover': oneByOnePngBase64,
|
||||
'questions': [
|
||||
{
|
||||
'questionType': 'simple',
|
||||
'word': 'word',
|
||||
'answer': 'btn1',
|
||||
'image': oneByOnePngBase64,
|
||||
'buttons': [
|
||||
{'id': 'btn1', 'text': 'ok', 'image': oneByOnePngBase64},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
final createResp = await adminTestsApi.upsertTest(createRequest);
|
||||
expect(createResp.statusCode, anyOf(equals(201), equals(200)));
|
||||
final createBody = jsonDecode(await createResp.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
testId = (createBody['test'] as Map<String, dynamic>)['id'] as String;
|
||||
expect(testId, isNotEmpty);
|
||||
final createResp = await adminTestsApi.upsertTest(createRequest);
|
||||
expect(createResp.statusCode, anyOf(equals(201), equals(200)));
|
||||
final createBody =
|
||||
jsonDecode(await createResp.readAsString()) as Map<String, dynamic>;
|
||||
testId = (createBody['test'] as Map<String, dynamic>)['id'] as String;
|
||||
expect(testId, isNotEmpty);
|
||||
|
||||
// Link test to pack via admin packs API (this must also link referenced cards)
|
||||
final linkRequest = buildAdminRequest(
|
||||
'POST',
|
||||
'http://localhost/api/v2/admin/packs',
|
||||
body: {
|
||||
'id': packId,
|
||||
'title': 'pack for admin tests images',
|
||||
'addTestIds': [testId],
|
||||
},
|
||||
);
|
||||
// Link test to pack via admin packs API (this must also link referenced cards)
|
||||
final linkRequest = buildAdminRequest(
|
||||
'POST',
|
||||
'http://localhost/api/v2/admin/packs',
|
||||
body: {
|
||||
'id': packId,
|
||||
'title': 'pack for admin tests images',
|
||||
'addTestIds': [testId],
|
||||
},
|
||||
);
|
||||
|
||||
final linkResp = await adminPacksApi.upsertPack(linkRequest);
|
||||
expect(linkResp.statusCode, equals(200));
|
||||
final linkResp = await adminPacksApi.upsertPack(linkRequest);
|
||||
expect(linkResp.statusCode, equals(200));
|
||||
|
||||
// Fetch test and verify URLs
|
||||
final getReq = buildAdminRequest(
|
||||
'GET',
|
||||
'http://localhost/api/v2/admin/tests/$testId',
|
||||
);
|
||||
final getResp = await adminTestsApi.getTest(getReq, testId!);
|
||||
expect(getResp.statusCode, equals(200));
|
||||
// Fetch test and verify URLs
|
||||
final getReq = buildAdminRequest(
|
||||
'GET',
|
||||
'http://localhost/api/v2/admin/tests/$testId',
|
||||
);
|
||||
final getResp = await adminTestsApi.getTest(getReq, testId!);
|
||||
expect(getResp.statusCode, equals(200));
|
||||
|
||||
final body = jsonDecode(await getResp.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final body =
|
||||
jsonDecode(await getResp.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
final cover = body['cover'] as String?;
|
||||
expect(cover, isNotNull);
|
||||
expect(cover, isNot(contains(oneByOnePngBase64)));
|
||||
expect(cover, startsWith('/api/v2/packs/$packId/cards/'));
|
||||
expect(cover, endsWith('/image'));
|
||||
final cover = body['cover'] as String?;
|
||||
expect(cover, isNotNull);
|
||||
expect(cover, isNot(contains(oneByOnePngBase64)));
|
||||
expect(cover, startsWith('/api/v2/packs/$packId/cards/'));
|
||||
expect(cover, endsWith('/image'));
|
||||
|
||||
final questions = body['questions'] as List<dynamic>;
|
||||
expect(questions, hasLength(1));
|
||||
final q0 = questions.first as Map<String, dynamic>;
|
||||
final questions = body['questions'] as List<dynamic>;
|
||||
expect(questions, hasLength(1));
|
||||
final q0 = questions.first as Map<String, dynamic>;
|
||||
|
||||
final qImage = q0['image'] as String?;
|
||||
expect(qImage, isNotNull);
|
||||
expect(qImage, isNot(contains(oneByOnePngBase64)));
|
||||
expect(qImage, startsWith('/api/v2/packs/$packId/cards/'));
|
||||
expect(qImage, endsWith('/image'));
|
||||
final qImage = q0['image'] as String?;
|
||||
expect(qImage, isNotNull);
|
||||
expect(qImage, isNot(contains(oneByOnePngBase64)));
|
||||
expect(qImage, startsWith('/api/v2/packs/$packId/cards/'));
|
||||
expect(qImage, endsWith('/image'));
|
||||
|
||||
final buttons = q0['buttons'] as List<dynamic>;
|
||||
expect(buttons, hasLength(1));
|
||||
final b0 = buttons.first as Map<String, dynamic>;
|
||||
final bImage = b0['image'] as String?;
|
||||
expect(bImage, isNotNull);
|
||||
expect(bImage, isNot(contains(oneByOnePngBase64)));
|
||||
expect(bImage, startsWith('/api/v2/packs/$packId/cards/'));
|
||||
expect(bImage, endsWith('/image'));
|
||||
final buttons = q0['buttons'] as List<dynamic>;
|
||||
expect(buttons, hasLength(1));
|
||||
final b0 = buttons.first as Map<String, dynamic>;
|
||||
final bImage = b0['image'] as String?;
|
||||
expect(bImage, isNotNull);
|
||||
expect(bImage, isNot(contains(oneByOnePngBase64)));
|
||||
expect(bImage, startsWith('/api/v2/packs/$packId/cards/'));
|
||||
expect(bImage, endsWith('/image'));
|
||||
|
||||
// Extract card ids from URLs and ensure they are linked to the pack.
|
||||
final extractCardId = (String url) {
|
||||
final match = RegExp(r'/cards/([^/]+)/image$').firstMatch(url);
|
||||
return match?.group(1);
|
||||
};
|
||||
// Extract card ids from URLs and ensure they are linked to the pack.
|
||||
final extractCardId = (String url) {
|
||||
final match = RegExp(r'/cards/([^/]+)/image$').firstMatch(url);
|
||||
return match?.group(1);
|
||||
};
|
||||
|
||||
final coverCardId = extractCardId(cover!);
|
||||
final qCardId = extractCardId(qImage!);
|
||||
final bCardId = extractCardId(bImage!);
|
||||
final coverCardId = extractCardId(cover!);
|
||||
final qCardId = extractCardId(qImage!);
|
||||
final bCardId = extractCardId(bImage!);
|
||||
|
||||
expect(coverCardId, isNotNull);
|
||||
expect(qCardId, isNotNull);
|
||||
expect(bCardId, isNotNull);
|
||||
expect(coverCardId, isNotNull);
|
||||
expect(qCardId, isNotNull);
|
||||
expect(bCardId, isNotNull);
|
||||
|
||||
createdCardIds.addAll([coverCardId!, qCardId!, bCardId!]);
|
||||
createdCardIds.addAll([coverCardId!, qCardId!, bCardId!]);
|
||||
|
||||
final linked = await (db.select(db.cardPackCards)
|
||||
..where((c) => c.packId.equals(packId!) &
|
||||
c.cardId.isIn([coverCardId, qCardId, bCardId])))
|
||||
.get();
|
||||
expect(linked.map((e) => e.cardId).toSet(),
|
||||
containsAll([coverCardId, qCardId, bCardId]));
|
||||
final linked =
|
||||
await (db.select(db.cardPackCards)..where(
|
||||
(c) =>
|
||||
c.packId.equals(packId!) &
|
||||
c.cardId.isIn([coverCardId, qCardId, bCardId]),
|
||||
))
|
||||
.get();
|
||||
expect(
|
||||
linked.map((e) => e.cardId).toSet(),
|
||||
containsAll([coverCardId, qCardId, bCardId]),
|
||||
);
|
||||
|
||||
// Also ensure DB stores cardIds, not API URLs/base64.
|
||||
final dbQuestions = await db.testDao.getTestQuestions(testId!);
|
||||
expect(dbQuestions, hasLength(1));
|
||||
final qDb = dbQuestions.single;
|
||||
final options = jsonDecode(qDb.options) as List<dynamic>;
|
||||
final uiData = jsonDecode(qDb.uiData) as Map<String, dynamic>;
|
||||
// Also ensure DB stores cardIds, not API URLs/base64.
|
||||
final dbQuestions = await db.testDao.getTestQuestions(testId!);
|
||||
expect(dbQuestions, hasLength(1));
|
||||
final qDb = dbQuestions.single;
|
||||
final options = jsonDecode(qDb.options) as List<dynamic>;
|
||||
final uiData = jsonDecode(qDb.uiData) as Map<String, dynamic>;
|
||||
|
||||
final storedUiImage = uiData['image']?.toString();
|
||||
expect(storedUiImage, isNotNull);
|
||||
expect(RegExp(r'^[0-9a-f\-]{36}$', caseSensitive: false)
|
||||
.hasMatch(storedUiImage!), isTrue);
|
||||
final storedUiImage = uiData['image']?.toString();
|
||||
expect(storedUiImage, isNotNull);
|
||||
expect(
|
||||
RegExp(
|
||||
r'^[0-9a-f\-]{36}$',
|
||||
caseSensitive: false,
|
||||
).hasMatch(storedUiImage!),
|
||||
isTrue,
|
||||
);
|
||||
|
||||
final storedBtnImage =
|
||||
(options.first as Map<String, dynamic>)['image']?.toString();
|
||||
expect(storedBtnImage, isNotNull);
|
||||
expect(RegExp(r'^[0-9a-f\-]{36}$', caseSensitive: false)
|
||||
.hasMatch(storedBtnImage!), isTrue);
|
||||
final storedBtnImage = (options.first as Map<String, dynamic>)['image']
|
||||
?.toString();
|
||||
expect(storedBtnImage, isNotNull);
|
||||
expect(
|
||||
RegExp(
|
||||
r'^[0-9a-f\-]{36}$',
|
||||
caseSensitive: false,
|
||||
).hasMatch(storedBtnImage!),
|
||||
isTrue,
|
||||
);
|
||||
|
||||
final storedCover = (await db.testDao.getTestById(testId!))!.cover;
|
||||
expect(storedCover, isNotNull);
|
||||
expect(RegExp(r'^[0-9a-f\-]{36}$', caseSensitive: false)
|
||||
.hasMatch(storedCover!), isTrue);
|
||||
});
|
||||
final storedCover = (await db.testDao.getTestById(testId!))!.cover;
|
||||
expect(storedCover, isNotNull);
|
||||
expect(
|
||||
RegExp(
|
||||
r'^[0-9a-f\-]{36}$',
|
||||
caseSensitive: false,
|
||||
).hasMatch(storedCover!),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,11 @@ void main() {
|
|||
});
|
||||
|
||||
authApiV2 = AuthApiV2(
|
||||
userManager, mockGoogleApi, jwtService, telegramAuthCodeService);
|
||||
userManager,
|
||||
mockGoogleApi,
|
||||
jwtService,
|
||||
telegramAuthCodeService,
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
|
|
@ -101,8 +105,9 @@ void main() {
|
|||
const email = 'user@example.com';
|
||||
const name = 'Test User';
|
||||
|
||||
when(mockGoogleApi.getGoogleUserId(googleIdToken))
|
||||
.thenAnswer((_) async => googleUserId);
|
||||
when(
|
||||
mockGoogleApi.getGoogleUserId(googleIdToken),
|
||||
).thenAnswer((_) async => googleUserId);
|
||||
|
||||
final request = Request(
|
||||
'POST',
|
||||
|
|
@ -151,8 +156,9 @@ void main() {
|
|||
test('should return 401 when Google token is invalid', () async {
|
||||
const googleIdToken = 'invalid_google_token';
|
||||
|
||||
when(mockGoogleApi.getGoogleUserId(googleIdToken))
|
||||
.thenAnswer((_) async => null);
|
||||
when(
|
||||
mockGoogleApi.getGoogleUserId(googleIdToken),
|
||||
).thenAnswer((_) async => null);
|
||||
|
||||
final request = Request(
|
||||
'POST',
|
||||
|
|
@ -267,9 +273,7 @@ void main() {
|
|||
final request = Request(
|
||||
'GET',
|
||||
Uri.parse('http://localhost/api/v2/auth/me'),
|
||||
headers: {
|
||||
'Authorization': 'Bearer ${tokens.accessToken}',
|
||||
},
|
||||
headers: {'Authorization': 'Bearer ${tokens.accessToken}'},
|
||||
).change(context: {'user': testUser});
|
||||
|
||||
final response = await authApiV2.getCurrentUser(request);
|
||||
|
|
@ -340,9 +344,7 @@ void main() {
|
|||
'POST',
|
||||
Uri.parse('http://localhost/api/v2/auth/logout'),
|
||||
body: '',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ${tokens.accessToken}',
|
||||
},
|
||||
headers: {'Authorization': 'Bearer ${tokens.accessToken}'},
|
||||
).change(context: {'user': testUser});
|
||||
|
||||
final response = await authApiV2.logout(request);
|
||||
|
|
@ -369,117 +371,132 @@ void main() {
|
|||
});
|
||||
|
||||
group('AuthApiV2 - Telegram Web Code Flow', () {
|
||||
test('should create, claim, and authenticate using web-generated code',
|
||||
() async {
|
||||
// Step 1: create code from web
|
||||
final createRequest = Request(
|
||||
'POST',
|
||||
Uri.parse('http://localhost/api/v2/auth/telegram/web-code'),
|
||||
body: '',
|
||||
);
|
||||
test(
|
||||
'should create, claim, and authenticate using web-generated code',
|
||||
() async {
|
||||
// Step 1: create code from web
|
||||
final createRequest = Request(
|
||||
'POST',
|
||||
Uri.parse('http://localhost/api/v2/auth/telegram/web-code'),
|
||||
body: '',
|
||||
);
|
||||
|
||||
final createResponse =
|
||||
await authApiV2.createWebTelegramCode(createRequest);
|
||||
final createBody = jsonDecode(await createResponse.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final createResponse = await authApiV2.createWebTelegramCode(
|
||||
createRequest,
|
||||
);
|
||||
final createBody =
|
||||
jsonDecode(await createResponse.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(createResponse.statusCode, equals(200));
|
||||
final code = createBody['code'] as String?;
|
||||
expect(code, isNotNull);
|
||||
expect(createBody['status'], equals('pending'));
|
||||
expect(createBody['remainingSeconds'], greaterThan(0));
|
||||
expect(createBody['remainingSeconds'], lessThanOrEqualTo(300));
|
||||
expect(createResponse.statusCode, equals(200));
|
||||
final code = createBody['code'] as String?;
|
||||
expect(code, isNotNull);
|
||||
expect(createBody['status'], equals('pending'));
|
||||
expect(createBody['remainingSeconds'], greaterThan(0));
|
||||
expect(createBody['remainingSeconds'], lessThanOrEqualTo(300));
|
||||
|
||||
// Step 2: status should be pending before claim
|
||||
final statusRequest = Request(
|
||||
'GET',
|
||||
Uri.parse('http://localhost/api/v2/auth/telegram/code-status/$code'),
|
||||
);
|
||||
// Step 2: status should be pending before claim
|
||||
final statusRequest = Request(
|
||||
'GET',
|
||||
Uri.parse('http://localhost/api/v2/auth/telegram/code-status/$code'),
|
||||
);
|
||||
|
||||
final statusResponse =
|
||||
await authApiV2.getTelegramCodeStatus(statusRequest, code!);
|
||||
final statusBody = jsonDecode(await statusResponse.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final statusResponse = await authApiV2.getTelegramCodeStatus(
|
||||
statusRequest,
|
||||
code!,
|
||||
);
|
||||
final statusBody =
|
||||
jsonDecode(await statusResponse.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(statusResponse.statusCode, equals(200));
|
||||
expect(statusBody['status'], equals('pending'));
|
||||
expect(statusBody['isClaimed'], isFalse);
|
||||
expect(statusBody['isUsed'], isFalse);
|
||||
expect(statusResponse.statusCode, equals(200));
|
||||
expect(statusBody['status'], equals('pending'));
|
||||
expect(statusBody['isClaimed'], isFalse);
|
||||
expect(statusBody['isUsed'], isFalse);
|
||||
|
||||
// Step 3: authentication before claim should fail
|
||||
final preAuthRequest = Request(
|
||||
'POST',
|
||||
Uri.parse('http://localhost/api/v2/auth/oauth/telegram'),
|
||||
body: jsonEncode({'code': code}),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
// Step 3: authentication before claim should fail
|
||||
final preAuthRequest = Request(
|
||||
'POST',
|
||||
Uri.parse('http://localhost/api/v2/auth/oauth/telegram'),
|
||||
body: jsonEncode({'code': code}),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
|
||||
final preAuthResponse =
|
||||
await authApiV2.authenticateTelegram(preAuthRequest);
|
||||
expect(preAuthResponse.statusCode, equals(401));
|
||||
final preAuthResponse = await authApiV2.authenticateTelegram(
|
||||
preAuthRequest,
|
||||
);
|
||||
expect(preAuthResponse.statusCode, equals(401));
|
||||
|
||||
// Step 4: claim code via bot
|
||||
final claimRequest = Request(
|
||||
'POST',
|
||||
Uri.parse('http://localhost/api/v2/auth/telegram/claim-code'),
|
||||
body: jsonEncode({
|
||||
'code': code,
|
||||
'telegramUserId': 'telegram_user_1',
|
||||
'telegramUsername': 'webuser',
|
||||
'firstName': 'Web',
|
||||
'lastName': 'User',
|
||||
}),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
// Step 4: claim code via bot
|
||||
final claimRequest = Request(
|
||||
'POST',
|
||||
Uri.parse('http://localhost/api/v2/auth/telegram/claim-code'),
|
||||
body: jsonEncode({
|
||||
'code': code,
|
||||
'telegramUserId': 'telegram_user_1',
|
||||
'telegramUsername': 'webuser',
|
||||
'firstName': 'Web',
|
||||
'lastName': 'User',
|
||||
}),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
|
||||
final claimResponse = await authApiV2.claimTelegramCode(claimRequest);
|
||||
final claimBody = jsonDecode(await claimResponse.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final claimResponse = await authApiV2.claimTelegramCode(claimRequest);
|
||||
final claimBody =
|
||||
jsonDecode(await claimResponse.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(claimResponse.statusCode, equals(200));
|
||||
expect(claimBody['status'], equals('claimed'));
|
||||
expect(claimBody['remainingSeconds'], greaterThan(0));
|
||||
expect(claimResponse.statusCode, equals(200));
|
||||
expect(claimBody['status'], equals('claimed'));
|
||||
expect(claimBody['remainingSeconds'], greaterThan(0));
|
||||
|
||||
// Step 5: status should be claimed
|
||||
final claimedStatusResponse =
|
||||
await authApiV2.getTelegramCodeStatus(statusRequest, code);
|
||||
final claimedStatusBody =
|
||||
jsonDecode(await claimedStatusResponse.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
// Step 5: status should be claimed
|
||||
final claimedStatusResponse = await authApiV2.getTelegramCodeStatus(
|
||||
statusRequest,
|
||||
code,
|
||||
);
|
||||
final claimedStatusBody =
|
||||
jsonDecode(await claimedStatusResponse.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(claimedStatusResponse.statusCode, equals(200));
|
||||
expect(claimedStatusBody['status'], equals('claimed'));
|
||||
expect(claimedStatusBody['isClaimed'], isTrue);
|
||||
expect(claimedStatusBody['isUsed'], isFalse);
|
||||
expect(claimedStatusResponse.statusCode, equals(200));
|
||||
expect(claimedStatusBody['status'], equals('claimed'));
|
||||
expect(claimedStatusBody['isClaimed'], isTrue);
|
||||
expect(claimedStatusBody['isUsed'], isFalse);
|
||||
|
||||
// Step 6: authentication after claim should succeed
|
||||
final authRequest = Request(
|
||||
'POST',
|
||||
Uri.parse('http://localhost/api/v2/auth/oauth/telegram'),
|
||||
body: jsonEncode({'code': code}),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
// Step 6: authentication after claim should succeed
|
||||
final authRequest = Request(
|
||||
'POST',
|
||||
Uri.parse('http://localhost/api/v2/auth/oauth/telegram'),
|
||||
body: jsonEncode({'code': code}),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
|
||||
final authResponse = await authApiV2.authenticateTelegram(authRequest);
|
||||
final authBody =
|
||||
jsonDecode(await authResponse.readAsString()) as Map<String, dynamic>;
|
||||
final authResponse = await authApiV2.authenticateTelegram(authRequest);
|
||||
final authBody =
|
||||
jsonDecode(await authResponse.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(authResponse.statusCode, equals(200));
|
||||
expect(authBody['user'], isNotNull);
|
||||
expect(authBody['accessToken'], isNotEmpty);
|
||||
expect(authBody['refreshToken'], isNotEmpty);
|
||||
expect(authResponse.statusCode, equals(200));
|
||||
expect(authBody['user'], isNotNull);
|
||||
expect(authBody['accessToken'], isNotEmpty);
|
||||
expect(authBody['refreshToken'], isNotEmpty);
|
||||
|
||||
// Step 7: status reflects used code
|
||||
final usedStatusResponse =
|
||||
await authApiV2.getTelegramCodeStatus(statusRequest, code);
|
||||
final usedStatusBody = jsonDecode(await usedStatusResponse.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
// Step 7: status reflects used code
|
||||
final usedStatusResponse = await authApiV2.getTelegramCodeStatus(
|
||||
statusRequest,
|
||||
code,
|
||||
);
|
||||
final usedStatusBody =
|
||||
jsonDecode(await usedStatusResponse.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(usedStatusResponse.statusCode, equals(200));
|
||||
expect(usedStatusBody['status'], equals('used'));
|
||||
expect(usedStatusBody['isClaimed'], isTrue);
|
||||
expect(usedStatusBody['isUsed'], isTrue);
|
||||
});
|
||||
expect(usedStatusResponse.statusCode, equals(200));
|
||||
expect(usedStatusBody['status'], equals('used'));
|
||||
expect(usedStatusBody['isClaimed'], isTrue);
|
||||
expect(usedStatusBody['isUsed'], isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
test('should return error when claiming unknown code', () async {
|
||||
final claimRequest = Request(
|
||||
|
|
|
|||
|
|
@ -74,10 +74,10 @@ void main() {
|
|||
handler = const Pipeline()
|
||||
.addMiddleware(authorizeV2(userManager, jwtService))
|
||||
.addHandler((request) {
|
||||
final user = request.context['user'] as UserModel?;
|
||||
final body = user == null ? 'no-user' : 'user-${user.id}';
|
||||
return Response.ok(body);
|
||||
});
|
||||
final user = request.context['user'] as UserModel?;
|
||||
final body = user == null ? 'no-user' : 'user-${user.id}';
|
||||
return Response.ok(body);
|
||||
});
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
|
|
@ -93,8 +93,11 @@ void main() {
|
|||
if (token != null) {
|
||||
headers['authorization'] = 'Bearer $token';
|
||||
}
|
||||
final request =
|
||||
Request(method, Uri.parse('http://localhost$path'), headers: headers);
|
||||
final request = Request(
|
||||
method,
|
||||
Uri.parse('http://localhost$path'),
|
||||
headers: headers,
|
||||
);
|
||||
return Future.sync(() => handler(request));
|
||||
}
|
||||
|
||||
|
|
@ -106,8 +109,7 @@ void main() {
|
|||
});
|
||||
|
||||
test('attaches user context when token present on GET /packs/<id>', () async {
|
||||
final response =
|
||||
await _makeRequest('/packs/10', token: accessToken);
|
||||
final response = await _makeRequest('/packs/10', token: accessToken);
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(await response.readAsString(), equals('user-${testUser.id}'));
|
||||
|
|
@ -117,13 +119,11 @@ void main() {
|
|||
final response = await _makeRequest('/packs/10/tests');
|
||||
|
||||
expect(response.statusCode, equals(401));
|
||||
expect(await response.readAsString(),
|
||||
contains('"error":"Unauthorized"'));
|
||||
expect(await response.readAsString(), contains('"error":"Unauthorized"'));
|
||||
});
|
||||
|
||||
test('allows GET /packs/<id>/tests when token provided', () async {
|
||||
final response =
|
||||
await _makeRequest('/packs/10/tests', token: accessToken);
|
||||
final response = await _makeRequest('/packs/10/tests', token: accessToken);
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(await response.readAsString(), equals('user-${testUser.id}'));
|
||||
|
|
@ -133,8 +133,6 @@ void main() {
|
|||
final response = await _makeRequest('/packs/10', token: 'invalid');
|
||||
|
||||
expect(response.statusCode, equals(401));
|
||||
expect(await response.readAsString(),
|
||||
contains('"error":"Unauthorized"'));
|
||||
expect(await response.readAsString(), contains('"error":"Unauthorized"'));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ void main() {
|
|||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody, isA<List>());
|
||||
expect(responseBody.length, greaterThanOrEqualTo(1));
|
||||
|
||||
|
||||
// Verify game structure
|
||||
final game = responseBody[0] as Map<String, dynamic>;
|
||||
expect(game['id'], isA<String>());
|
||||
|
|
@ -45,12 +45,12 @@ void main() {
|
|||
final responseBody = jsonDecode(await response.readAsString()) as List;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
|
||||
|
||||
final funnyLettersGame = responseBody.firstWhere(
|
||||
(game) => (game as Map<String, dynamic>)['id'] == 'funny_letters',
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
|
||||
expect(funnyLettersGame, isNotNull);
|
||||
final game = funnyLettersGame as Map<String, dynamic>;
|
||||
expect(game['title'], equals('Funny letters'));
|
||||
|
|
@ -64,9 +64,12 @@ void main() {
|
|||
Uri.parse('http://localhost/api/v2/games/nonexistent_game/assets'),
|
||||
);
|
||||
|
||||
final response = await gamesApiV2.getGameAssets(request, 'nonexistent_game');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final response = await gamesApiV2.getGameAssets(
|
||||
request,
|
||||
'nonexistent_game',
|
||||
);
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(404));
|
||||
expect(responseBody['error'], equals('Not Found'));
|
||||
|
|
@ -82,13 +85,13 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await gamesApiV2.getGameAssets(request, 'funny_letters');
|
||||
|
||||
|
||||
// The response will be either 200 (if assets exist) or 404
|
||||
expect(response.statusCode, isIn([200, 404]));
|
||||
|
||||
|
||||
if (response.statusCode == 404) {
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(responseBody['error'], equals('Not Found'));
|
||||
} else {
|
||||
// If file exists, verify it returns binary data
|
||||
|
|
@ -104,18 +107,12 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await gamesApiV2.getGameAssets(request, 'funny_letters');
|
||||
|
||||
|
||||
// Only check headers if the file exists (status 200)
|
||||
if (response.statusCode == 200) {
|
||||
expect(response.headers['content-type'], equals('application/zip'));
|
||||
expect(
|
||||
response.headers['content-disposition'],
|
||||
contains('attachment'),
|
||||
);
|
||||
expect(
|
||||
response.headers['cache-control'],
|
||||
contains('max-age=86400'),
|
||||
);
|
||||
expect(response.headers['content-disposition'], contains('attachment'));
|
||||
expect(response.headers['cache-control'], contains('max-age=86400'));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -126,17 +123,16 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await gamesApiV2.getGameAssets(request, 'funny_letters');
|
||||
|
||||
|
||||
// May be 404 (assets not found) or 200 (success)
|
||||
expect(response.statusCode, isIn([200, 404]));
|
||||
|
||||
|
||||
if (response.statusCode == 404) {
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
// Should say "assets not found" not "game not found"
|
||||
expect(responseBody['message'], isNot(contains('Game not found')));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ void main() {
|
|||
setUpAll(() async {
|
||||
// Initialize Isar for testing with in-memory database
|
||||
await Isar.initializeIsarCore(download: true);
|
||||
|
||||
|
||||
// Create temporary directory for test database
|
||||
final testDir = Directory.systemTemp.createTempSync('isar_test_');
|
||||
|
||||
|
||||
// Include all schemas (use same as IsarConnector to avoid dependency issues)
|
||||
testIsar = await Isar.open(
|
||||
[
|
||||
|
|
@ -43,14 +43,14 @@ void main() {
|
|||
name: 'test_db',
|
||||
inspector: false,
|
||||
);
|
||||
|
||||
|
||||
// Set the global isar for JwtService to use
|
||||
backend_main.isar = testIsar;
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
jwtService = JwtService();
|
||||
|
||||
|
||||
// Create a test user
|
||||
await testIsar.writeTxn(() async {
|
||||
final user = UserModel.empty.copyWith(
|
||||
|
|
@ -81,7 +81,7 @@ void main() {
|
|||
expect(tokens.accessToken, isNotEmpty);
|
||||
expect(tokens.refreshToken, isNotEmpty);
|
||||
expect(tokens.expiresIn, equals(3600)); // 1 hour
|
||||
|
||||
|
||||
// Tokens should have JWT format (3 parts separated by dots)
|
||||
expect(tokens.accessToken.split('.').length, equals(3));
|
||||
expect(tokens.refreshToken.split('.').length, equals(3));
|
||||
|
|
@ -209,7 +209,7 @@ void main() {
|
|||
test('should verify non-expired tokens', () async {
|
||||
// Create fresh tokens
|
||||
final tokens = await jwtService.generateTokens(testUser);
|
||||
|
||||
|
||||
// The token should be valid immediately after creation
|
||||
final userId = jwtService.verifyAccessToken(tokens.accessToken);
|
||||
expect(userId, equals(testUser.id.toString()));
|
||||
|
|
@ -234,7 +234,9 @@ void main() {
|
|||
.findFirst();
|
||||
if (token != null) {
|
||||
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(),
|
||||
);
|
||||
|
||||
final context = <String, Object?>{
|
||||
'accessService': accessService,
|
||||
};
|
||||
final context = <String, Object?>{'accessService': accessService};
|
||||
|
||||
if (user != null) {
|
||||
context['user'] = user;
|
||||
|
|
@ -87,7 +85,7 @@ void main() {
|
|||
|
||||
// Without proper multipart body, it will fail validation, but we can test access
|
||||
final response = await mediaApiV2.uploadCardImage(request);
|
||||
|
||||
|
||||
// Should not be 403 (access denied), but might be 400 (bad request) due to missing file
|
||||
expect(response.statusCode, isNot(403));
|
||||
});
|
||||
|
|
@ -109,11 +107,13 @@ void main() {
|
|||
final response = await mediaApiV2.uploadCardImage(request);
|
||||
|
||||
expect(response.statusCode, equals(403));
|
||||
verifyNever(mockMinioService.uploadFile(
|
||||
bucket: anyNamed('bucket'),
|
||||
bytes: anyNamed('bytes'),
|
||||
contentType: anyNamed('contentType'),
|
||||
));
|
||||
verifyNever(
|
||||
mockMinioService.uploadFile(
|
||||
bucket: anyNamed('bucket'),
|
||||
bytes: anyNamed('bytes'),
|
||||
contentType: anyNamed('contentType'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('should return 400 when file size exceeds limit', () async {
|
||||
|
|
@ -142,16 +142,20 @@ void main() {
|
|||
const testObjectId = 'test-uuid-456';
|
||||
const testPresignedUrl = 'https://minio.example.com/presigned-url-2';
|
||||
|
||||
when(mockMinioService.uploadFile(
|
||||
bucket: MinioConfig.cardImagesBucket,
|
||||
bytes: anyNamed('bytes'),
|
||||
contentType: 'image/jpeg',
|
||||
)).thenAnswer((_) async => testObjectId);
|
||||
when(
|
||||
mockMinioService.uploadFile(
|
||||
bucket: MinioConfig.cardImagesBucket,
|
||||
bytes: anyNamed('bytes'),
|
||||
contentType: 'image/jpeg',
|
||||
),
|
||||
).thenAnswer((_) async => testObjectId);
|
||||
|
||||
when(mockMinioService.getPresignedUrl(
|
||||
bucket: MinioConfig.cardImagesBucket,
|
||||
objectId: testObjectId,
|
||||
)).thenAnswer((_) async => testPresignedUrl);
|
||||
when(
|
||||
mockMinioService.getPresignedUrl(
|
||||
bucket: MinioConfig.cardImagesBucket,
|
||||
objectId: testObjectId,
|
||||
),
|
||||
).thenAnswer((_) async => testPresignedUrl);
|
||||
|
||||
// Test images now use the same bucket as card images
|
||||
expect(testObjectId, isNotEmpty);
|
||||
|
|
@ -164,16 +168,20 @@ void main() {
|
|||
const testObjectId = 'test-uuid-789';
|
||||
const testPresignedUrl = 'https://minio.example.com/presigned-url-3';
|
||||
|
||||
when(mockMinioService.uploadFile(
|
||||
bucket: MinioConfig.voiceAudioBucket,
|
||||
bytes: anyNamed('bytes'),
|
||||
contentType: 'audio/mpeg',
|
||||
)).thenAnswer((_) async => testObjectId);
|
||||
when(
|
||||
mockMinioService.uploadFile(
|
||||
bucket: MinioConfig.voiceAudioBucket,
|
||||
bytes: anyNamed('bytes'),
|
||||
contentType: 'audio/mpeg',
|
||||
),
|
||||
).thenAnswer((_) async => testObjectId);
|
||||
|
||||
when(mockMinioService.getPresignedUrl(
|
||||
bucket: MinioConfig.voiceAudioBucket,
|
||||
objectId: testObjectId,
|
||||
)).thenAnswer((_) async => testPresignedUrl);
|
||||
when(
|
||||
mockMinioService.getPresignedUrl(
|
||||
bucket: MinioConfig.voiceAudioBucket,
|
||||
objectId: testObjectId,
|
||||
),
|
||||
).thenAnswer((_) async => testPresignedUrl);
|
||||
|
||||
// Similar to uploadCardImage test
|
||||
expect(testObjectId, isNotEmpty);
|
||||
|
|
@ -205,11 +213,13 @@ void main() {
|
|||
const testPresignedUrl = 'https://minio.example.com/presigned-url-4';
|
||||
const testBucket = MinioConfig.cardImagesBucket;
|
||||
|
||||
when(mockMinioService.getPresignedUrl(
|
||||
bucket: testBucket,
|
||||
objectId: testObjectId,
|
||||
expirySeconds: anyNamed('expirySeconds'),
|
||||
)).thenAnswer((_) async => testPresignedUrl);
|
||||
when(
|
||||
mockMinioService.getPresignedUrl(
|
||||
bucket: testBucket,
|
||||
objectId: testObjectId,
|
||||
expirySeconds: anyNamed('expirySeconds'),
|
||||
),
|
||||
).thenAnswer((_) async => testPresignedUrl);
|
||||
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
|
|
@ -221,29 +231,33 @@ void main() {
|
|||
testBucket,
|
||||
testObjectId,
|
||||
);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['url'], equals(testPresignedUrl));
|
||||
expect(responseBody['expiresAt'], isNotNull);
|
||||
|
||||
verify(mockMinioService.getPresignedUrl(
|
||||
bucket: testBucket,
|
||||
objectId: testObjectId,
|
||||
expirySeconds: anyNamed('expirySeconds'),
|
||||
)).called(1);
|
||||
verify(
|
||||
mockMinioService.getPresignedUrl(
|
||||
bucket: testBucket,
|
||||
objectId: testObjectId,
|
||||
expirySeconds: anyNamed('expirySeconds'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('should return 404 when object does not exist', () async {
|
||||
const testObjectId = 'non-existent-id';
|
||||
const testBucket = MinioConfig.cardImagesBucket;
|
||||
|
||||
when(mockMinioService.getPresignedUrl(
|
||||
bucket: testBucket,
|
||||
objectId: testObjectId,
|
||||
expirySeconds: anyNamed('expirySeconds'),
|
||||
)).thenAnswer((_) async => null);
|
||||
when(
|
||||
mockMinioService.getPresignedUrl(
|
||||
bucket: testBucket,
|
||||
objectId: testObjectId,
|
||||
expirySeconds: anyNamed('expirySeconds'),
|
||||
),
|
||||
).thenAnswer((_) async => null);
|
||||
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
|
|
@ -275,11 +289,13 @@ void main() {
|
|||
);
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
verifyNever(mockMinioService.getPresignedUrl(
|
||||
bucket: anyNamed('bucket'),
|
||||
objectId: anyNamed('objectId'),
|
||||
expirySeconds: anyNamed('expirySeconds'),
|
||||
));
|
||||
verifyNever(
|
||||
mockMinioService.getPresignedUrl(
|
||||
bucket: anyNamed('bucket'),
|
||||
objectId: anyNamed('objectId'),
|
||||
expirySeconds: anyNamed('expirySeconds'),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -288,10 +304,9 @@ void main() {
|
|||
const testObjectId = 'test-uuid-delete';
|
||||
const testBucket = MinioConfig.cardImagesBucket;
|
||||
|
||||
when(mockMinioService.deleteFile(
|
||||
bucket: testBucket,
|
||||
objectId: testObjectId,
|
||||
)).thenAnswer((_) async => Future.value());
|
||||
when(
|
||||
mockMinioService.deleteFile(bucket: testBucket, objectId: testObjectId),
|
||||
).thenAnswer((_) async => Future.value());
|
||||
|
||||
final request = buildRequest(
|
||||
'DELETE',
|
||||
|
|
@ -304,16 +319,15 @@ void main() {
|
|||
testBucket,
|
||||
testObjectId,
|
||||
);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['success'], isTrue);
|
||||
|
||||
verify(mockMinioService.deleteFile(
|
||||
bucket: testBucket,
|
||||
objectId: testObjectId,
|
||||
)).called(1);
|
||||
verify(
|
||||
mockMinioService.deleteFile(bucket: testBucket, objectId: testObjectId),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('should return 403 when user is not admin', () async {
|
||||
|
|
@ -340,10 +354,12 @@ void main() {
|
|||
);
|
||||
|
||||
expect(response.statusCode, equals(403));
|
||||
verifyNever(mockMinioService.deleteFile(
|
||||
bucket: anyNamed('bucket'),
|
||||
objectId: anyNamed('objectId'),
|
||||
));
|
||||
verifyNever(
|
||||
mockMinioService.deleteFile(
|
||||
bucket: anyNamed('bucket'),
|
||||
objectId: anyNamed('objectId'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('should return 400 for invalid bucket name', () async {
|
||||
|
|
@ -363,10 +379,12 @@ void main() {
|
|||
);
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
verifyNever(mockMinioService.deleteFile(
|
||||
bucket: anyNamed('bucket'),
|
||||
objectId: anyNamed('objectId'),
|
||||
));
|
||||
verifyNever(
|
||||
mockMinioService.deleteFile(
|
||||
bucket: anyNamed('bucket'),
|
||||
objectId: anyNamed('objectId'),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,11 +26,7 @@ void main() {
|
|||
late PacksApiV2 packsApiV2;
|
||||
late UserModel testUser;
|
||||
|
||||
Request buildRequest(
|
||||
String method,
|
||||
String url, {
|
||||
UserModel? user,
|
||||
}) {
|
||||
Request buildRequest(String method, String url, {UserModel? user}) {
|
||||
final resourceLoader = ResourceLoader(packManager);
|
||||
final accessService = AccessService(
|
||||
PackAccessPolicy(resourceLoader),
|
||||
|
|
@ -53,7 +49,7 @@ void main() {
|
|||
// Initialize Isar for testing
|
||||
await Isar.initializeIsarCore(download: true);
|
||||
final testDir = Directory.systemTemp.createTempSync('isar_test_');
|
||||
|
||||
|
||||
testIsar = await Isar.open(
|
||||
[
|
||||
CardPackModelSchema,
|
||||
|
|
@ -78,7 +74,7 @@ void main() {
|
|||
name: 'test_db',
|
||||
inspector: false,
|
||||
);
|
||||
|
||||
|
||||
backend_main.isar = testIsar;
|
||||
});
|
||||
|
||||
|
|
@ -87,11 +83,8 @@ void main() {
|
|||
final discountsManager = const DiscountsManager();
|
||||
final productsPriceResolver = ProductsPriceResolver(discountsManager);
|
||||
final adsManager = AdsManager();
|
||||
packDtoConverter = PackDtoConverter(
|
||||
productsPriceResolver,
|
||||
adsManager,
|
||||
);
|
||||
|
||||
packDtoConverter = PackDtoConverter(productsPriceResolver, adsManager);
|
||||
|
||||
packManager = PackManager(packDtoConverter);
|
||||
testManager = TestManager(packDtoConverter);
|
||||
packsApiV2 = PacksApiV2(packManager, testManager);
|
||||
|
|
@ -139,19 +132,16 @@ void main() {
|
|||
);
|
||||
|
||||
await testIsar.gameCardModels.putAll([card1, card2]);
|
||||
|
||||
|
||||
// Link cards to pack
|
||||
final savedPack = (await testIsar.cardPackModels.get(10))!;
|
||||
await savedPack.cards.load();
|
||||
savedPack.cards.addAll([card1, card2]);
|
||||
await savedPack.cards.save();
|
||||
|
||||
|
||||
// Update cardsOrder
|
||||
await testIsar.cardPackModels.put(
|
||||
savedPack.copyWith(
|
||||
cardsOrder: [card1.id!, card2.id!],
|
||||
size: 2,
|
||||
),
|
||||
savedPack.copyWith(cardsOrder: [card1.id!, card2.id!], size: 2),
|
||||
);
|
||||
|
||||
final privatePack = CardPackModel(
|
||||
|
|
@ -183,10 +173,7 @@ void main() {
|
|||
await savedPrivatePack.cards.save();
|
||||
|
||||
await testIsar.cardPackModels.put(
|
||||
savedPrivatePack.copyWith(
|
||||
cardsOrder: [privateCard.id!],
|
||||
size: 1,
|
||||
),
|
||||
savedPrivatePack.copyWith(cardsOrder: [privateCard.id!], size: 1),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -213,8 +200,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getPacks(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['items'], isA<List>());
|
||||
|
|
@ -225,14 +212,11 @@ void main() {
|
|||
});
|
||||
|
||||
test('should return packs with default pagination', () async {
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'http://localhost/api/v2/packs',
|
||||
);
|
||||
final request = buildRequest('GET', 'http://localhost/api/v2/packs');
|
||||
|
||||
final response = await packsApiV2.getPacks(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['page'], equals(1));
|
||||
|
|
@ -246,14 +230,15 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getPacks(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final items = responseBody['items'] as List;
|
||||
// Should find testPack with "Test" in title
|
||||
final found = items.any((pack) =>
|
||||
(pack['title'] as String).contains('Test'));
|
||||
final found = items.any(
|
||||
(pack) => (pack['title'] as String).contains('Test'),
|
||||
);
|
||||
expect(found, isTrue);
|
||||
});
|
||||
|
||||
|
|
@ -264,8 +249,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getPacks(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
expect(responseBody['error'], equals('Bad Request'));
|
||||
|
|
@ -279,64 +264,67 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getPacks(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
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', () {
|
||||
test('should return pack details for public pack (id=10)', () async {
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'http://localhost/api/v2/packs/10',
|
||||
);
|
||||
final request = buildRequest('GET', 'http://localhost/api/v2/packs/10');
|
||||
|
||||
final response = await packsApiV2.getPack(request, '10');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['id'], equals('10'));
|
||||
expect(responseBody['title'], equals('Test Pack'));
|
||||
});
|
||||
|
||||
test('should return pack with purchase status for authenticated user', () async {
|
||||
// First, add pack to user
|
||||
await testIsar.writeTxn(() async {
|
||||
final user = await testIsar.userModels.get(1);
|
||||
if (user != null) {
|
||||
await user.packs.load();
|
||||
final pack = await testIsar.cardPackModels.get(10);
|
||||
if (pack != null) {
|
||||
user.packs.add(pack);
|
||||
await user.packs.save();
|
||||
test(
|
||||
'should return pack with purchase status for authenticated user',
|
||||
() async {
|
||||
// First, add pack to user
|
||||
await testIsar.writeTxn(() async {
|
||||
final user = await testIsar.userModels.get(1);
|
||||
if (user != null) {
|
||||
await user.packs.load();
|
||||
final pack = await testIsar.cardPackModels.get(10);
|
||||
if (pack != null) {
|
||||
user.packs.add(pack);
|
||||
await user.packs.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Reload user to get updated packs
|
||||
final updatedUser = await testIsar.userModels.get(1);
|
||||
expect(updatedUser, isNotNull);
|
||||
// Reload user to get updated packs
|
||||
final updatedUser = await testIsar.userModels.get(1);
|
||||
expect(updatedUser, isNotNull);
|
||||
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'http://localhost/api/v2/packs/10',
|
||||
user: updatedUser,
|
||||
);
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'http://localhost/api/v2/packs/10',
|
||||
user: updatedUser,
|
||||
);
|
||||
|
||||
final response = await packsApiV2.getPack(request, '10');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final response = await packsApiV2.getPack(request, '10');
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['id'], equals('10'));
|
||||
// isPurchased may be false since pack id=10 is public, so just check it exists
|
||||
expect(responseBody.containsKey('isPurchased'), isTrue);
|
||||
});
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['id'], equals('10'));
|
||||
// isPurchased may be false since pack id=10 is public, so just check it exists
|
||||
expect(responseBody.containsKey('isPurchased'), isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
test('should return 404 for non-existent pack', () async {
|
||||
final request = buildRequest(
|
||||
|
|
@ -346,45 +334,48 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getPack(request, '999');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(404));
|
||||
expect(responseBody['error'], equals('Not Found'));
|
||||
});
|
||||
|
||||
test('should return buy page for unauthenticated private pack access', () async {
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'http://localhost/api/v2/packs/11',
|
||||
);
|
||||
test(
|
||||
'should return buy page for unauthenticated private pack access',
|
||||
() async {
|
||||
final request = buildRequest('GET', 'http://localhost/api/v2/packs/11');
|
||||
|
||||
final response = await packsApiV2.getPack(request, '11');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final response = await packsApiV2.getPack(request, '11');
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['id'], equals('11'));
|
||||
expect(responseBody['price'], equals('199'));
|
||||
expect(responseBody.containsKey('cards'), isTrue);
|
||||
});
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['id'], equals('11'));
|
||||
expect(responseBody['price'], equals('199'));
|
||||
expect(responseBody.containsKey('cards'), isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
test('should return buy page for authenticated user without pack access', () async {
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'http://localhost/api/v2/packs/11',
|
||||
user: testUser,
|
||||
);
|
||||
test(
|
||||
'should return buy page for authenticated user without pack access',
|
||||
() async {
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'http://localhost/api/v2/packs/11',
|
||||
user: testUser,
|
||||
);
|
||||
|
||||
final response = await packsApiV2.getPack(request, '11');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final response = await packsApiV2.getPack(request, '11');
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['id'], equals('11'));
|
||||
expect(responseBody['price'], equals('199'));
|
||||
expect(responseBody.containsKey('cards'), isTrue);
|
||||
});
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['id'], equals('11'));
|
||||
expect(responseBody['price'], equals('199'));
|
||||
expect(responseBody.containsKey('cards'), isTrue);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('PacksApiV2 - Get Pack Cards', () {
|
||||
|
|
@ -396,20 +387,19 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getPackCards(request, '10');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['items'], isA<List>());
|
||||
expect(responseBody['total'], isA<int>());
|
||||
expect(responseBody['page'], equals(1));
|
||||
expect(responseBody['limit'], equals(10));
|
||||
|
||||
|
||||
final items = responseBody['items'] as List;
|
||||
expect(items.length, greaterThanOrEqualTo(0));
|
||||
});
|
||||
|
||||
|
||||
test('should return 404 for non-existent pack', () async {
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
|
|
@ -418,8 +408,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getPackCards(request, '999');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(404));
|
||||
expect(responseBody['error'], equals('Not Found'));
|
||||
|
|
@ -433,8 +423,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getPackCards(request, '10');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
expect(responseBody['error'], equals('Bad Request'));
|
||||
|
|
@ -450,8 +440,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getCardImage(request, '10', '999');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(404));
|
||||
expect(responseBody['error'], equals('Not Found'));
|
||||
|
|
@ -465,8 +455,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getCardImage(request, '999', '1');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(404));
|
||||
expect(responseBody['error'], equals('Not Found'));
|
||||
|
|
@ -480,8 +470,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getCardImage(request, '10', 'invalid');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
expect(responseBody['error'], equals('Bad Request'));
|
||||
|
|
@ -495,8 +485,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getCardImage(request, 'invalid', '1');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
expect(responseBody['error'], equals('Bad Request'));
|
||||
|
|
@ -524,28 +514,31 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getCardImage(request, '99', '1');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(404));
|
||||
expect(responseBody['error'], equals('Not Found'));
|
||||
});
|
||||
|
||||
test('should allow access without authentication for enabled packs', () async {
|
||||
// Request without user context (public access)
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'http://localhost/api/v2/packs/10/cards/1/image',
|
||||
);
|
||||
test(
|
||||
'should allow access without authentication for enabled packs',
|
||||
() async {
|
||||
// Request without user context (public access)
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'http://localhost/api/v2/packs/10/cards/1/image',
|
||||
);
|
||||
|
||||
// This should work even without auth because pack 10 is enabled
|
||||
// and images are now accessible for enabled packs
|
||||
final response = await packsApiV2.getCardImage(request, '10', '1');
|
||||
|
||||
// Note: This might return 404 if image file doesn't exist,
|
||||
// but it should not return 401 Unauthorized
|
||||
expect(response.statusCode, isNot(equals(401)));
|
||||
});
|
||||
// This should work even without auth because pack 10 is enabled
|
||||
// and images are now accessible for enabled packs
|
||||
final response = await packsApiV2.getCardImage(request, '10', '1');
|
||||
|
||||
// Note: This might return 404 if image file doesn't exist,
|
||||
// but it should not return 401 Unauthorized
|
||||
expect(response.statusCode, isNot(equals(401)));
|
||||
},
|
||||
);
|
||||
|
||||
test('should return 404 if card does not belong to pack', () async {
|
||||
// Create another pack and card
|
||||
|
|
@ -580,8 +573,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getCardImage(request, '10', '3');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(404));
|
||||
expect(responseBody['error'], equals('Not Found'));
|
||||
|
|
@ -597,8 +590,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getCardImageBack(request, '10', '1');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(404));
|
||||
expect(responseBody['error'], equals('Not Found'));
|
||||
|
|
@ -612,8 +605,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getCardImageBack(request, '10', '999');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(404));
|
||||
expect(responseBody['error'], equals('Not Found'));
|
||||
|
|
@ -627,8 +620,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getCardImageBack(request, '999', '1');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(404));
|
||||
expect(responseBody['error'], equals('Not Found'));
|
||||
|
|
@ -646,8 +639,8 @@ void main() {
|
|||
'10',
|
||||
'invalid',
|
||||
);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
expect(responseBody['error'], equals('Bad Request'));
|
||||
|
|
@ -665,8 +658,8 @@ void main() {
|
|||
'invalid',
|
||||
'1',
|
||||
);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
expect(responseBody['error'], equals('Bad Request'));
|
||||
|
|
@ -680,28 +673,31 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getCardImageBack(request, '99', '1');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(404));
|
||||
expect(responseBody['error'], equals('Not Found'));
|
||||
});
|
||||
|
||||
test('should allow access without authentication for enabled packs', () async {
|
||||
// Request without user context (public access)
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'http://localhost/api/v2/packs/10/cards/1/imageBack',
|
||||
);
|
||||
test(
|
||||
'should allow access without authentication for enabled packs',
|
||||
() async {
|
||||
// Request without user context (public access)
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'http://localhost/api/v2/packs/10/cards/1/imageBack',
|
||||
);
|
||||
|
||||
// This should work even without auth because pack 10 is enabled
|
||||
// and images are now accessible for enabled packs
|
||||
final response = await packsApiV2.getCardImageBack(request, '10', '1');
|
||||
|
||||
// Note: This might return 404 if image file doesn't exist,
|
||||
// but it should not return 401 Unauthorized
|
||||
expect(response.statusCode, isNot(equals(401)));
|
||||
});
|
||||
// This should work even without auth because pack 10 is enabled
|
||||
// and images are now accessible for enabled packs
|
||||
final response = await packsApiV2.getCardImageBack(request, '10', '1');
|
||||
|
||||
// Note: This might return 404 if image file doesn't exist,
|
||||
// but it should not return 401 Unauthorized
|
||||
expect(response.statusCode, isNot(equals(401)));
|
||||
},
|
||||
);
|
||||
|
||||
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)
|
||||
|
|
@ -711,8 +707,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getCardImageBack(request, '10', '3');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(404));
|
||||
expect(responseBody['error'], equals('Not Found'));
|
||||
|
|
@ -727,43 +723,46 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getPackTests(request, '10');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(401));
|
||||
expect(responseBody['error'], equals('Unauthorized'));
|
||||
});
|
||||
|
||||
test('should return tests list for authenticated user with pack access', () async {
|
||||
// First, add pack to user so they have access
|
||||
await testIsar.writeTxn(() async {
|
||||
final user = await testIsar.userModels.get(1);
|
||||
if (user != null) {
|
||||
await user.packs.load();
|
||||
final pack = await testIsar.cardPackModels.get(10);
|
||||
if (pack != null) {
|
||||
user.packs.add(pack);
|
||||
await user.packs.save();
|
||||
test(
|
||||
'should return tests list for authenticated user with pack access',
|
||||
() async {
|
||||
// First, add pack to user so they have access
|
||||
await testIsar.writeTxn(() async {
|
||||
final user = await testIsar.userModels.get(1);
|
||||
if (user != null) {
|
||||
await user.packs.load();
|
||||
final pack = await testIsar.cardPackModels.get(10);
|
||||
if (pack != null) {
|
||||
user.packs.add(pack);
|
||||
await user.packs.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Reload user to get updated packs
|
||||
final updatedUser = await testIsar.userModels.get(1);
|
||||
expect(updatedUser, isNotNull);
|
||||
// Reload user to get updated packs
|
||||
final updatedUser = await testIsar.userModels.get(1);
|
||||
expect(updatedUser, isNotNull);
|
||||
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'http://localhost/api/v2/packs/10/tests',
|
||||
user: updatedUser,
|
||||
);
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'http://localhost/api/v2/packs/10/tests',
|
||||
user: updatedUser,
|
||||
);
|
||||
|
||||
final response = await packsApiV2.getPackTests(request, '10');
|
||||
final responseBody = jsonDecode(await response.readAsString());
|
||||
final response = await packsApiV2.getPackTests(request, '10');
|
||||
final responseBody = jsonDecode(await response.readAsString());
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody, isA<List>());
|
||||
});
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody, isA<List>());
|
||||
},
|
||||
);
|
||||
|
||||
test('should return 404 for non-existent pack', () async {
|
||||
final request = buildRequest(
|
||||
|
|
@ -773,8 +772,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getPackTests(request, '999');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(404));
|
||||
expect(responseBody['error'], equals('Not Found'));
|
||||
|
|
@ -789,8 +788,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getPackBuyPage(request, '11');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['id'], equals('11'));
|
||||
|
|
@ -798,55 +797,61 @@ void main() {
|
|||
expect(responseBody.containsKey('cards'), isTrue);
|
||||
});
|
||||
|
||||
test('should return buy page for authenticated user without pack', () async {
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'http://localhost/api/v2/packs/11/buy',
|
||||
user: testUser,
|
||||
);
|
||||
test(
|
||||
'should return buy page for authenticated user without pack',
|
||||
() async {
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'http://localhost/api/v2/packs/11/buy',
|
||||
user: testUser,
|
||||
);
|
||||
|
||||
final response = await packsApiV2.getPackBuyPage(request, '11');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final response = await packsApiV2.getPackBuyPage(request, '11');
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['id'], equals('11'));
|
||||
expect(responseBody['price'], equals('199'));
|
||||
expect(responseBody.containsKey('cards'), isTrue);
|
||||
});
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['id'], equals('11'));
|
||||
expect(responseBody['price'], equals('199'));
|
||||
expect(responseBody.containsKey('cards'), isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
test('should return pack info with isPurchased=true for authenticated user who already owns pack', () async {
|
||||
// First, add pack to user
|
||||
await testIsar.writeTxn(() async {
|
||||
final user = await testIsar.userModels.get(1);
|
||||
if (user != null) {
|
||||
await user.packs.load();
|
||||
final pack = await testIsar.cardPackModels.get(11);
|
||||
if (pack != null) {
|
||||
user.packs.add(pack);
|
||||
await user.packs.save();
|
||||
test(
|
||||
'should return pack info with isPurchased=true for authenticated user who already owns pack',
|
||||
() async {
|
||||
// First, add pack to user
|
||||
await testIsar.writeTxn(() async {
|
||||
final user = await testIsar.userModels.get(1);
|
||||
if (user != null) {
|
||||
await user.packs.load();
|
||||
final pack = await testIsar.cardPackModels.get(11);
|
||||
if (pack != null) {
|
||||
user.packs.add(pack);
|
||||
await user.packs.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Reload user to get updated packs
|
||||
final updatedUser = await testIsar.userModels.get(1);
|
||||
expect(updatedUser, isNotNull);
|
||||
// Reload user to get updated packs
|
||||
final updatedUser = await testIsar.userModels.get(1);
|
||||
expect(updatedUser, isNotNull);
|
||||
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'http://localhost/api/v2/packs/11/buy',
|
||||
user: updatedUser,
|
||||
);
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'http://localhost/api/v2/packs/11/buy',
|
||||
user: updatedUser,
|
||||
);
|
||||
|
||||
final response = await packsApiV2.getPackBuyPage(request, '11');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final response = await packsApiV2.getPackBuyPage(request, '11');
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['id'], equals('11'));
|
||||
expect(responseBody['isPurchased'], equals(true));
|
||||
});
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['id'], equals('11'));
|
||||
expect(responseBody['isPurchased'], equals(true));
|
||||
},
|
||||
);
|
||||
|
||||
test('should return 404 for non-existent pack (unauthenticated)', () async {
|
||||
final request = buildRequest(
|
||||
|
|
@ -855,8 +860,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getPackBuyPage(request, '999');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(404));
|
||||
expect(responseBody['error'], equals('Not Found'));
|
||||
|
|
@ -870,8 +875,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getPackBuyPage(request, '999');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(404));
|
||||
expect(responseBody['error'], equals('Not Found'));
|
||||
|
|
@ -884,12 +889,11 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await packsApiV2.getPackBuyPage(request, 'invalid');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
expect(responseBody['error'], equals('Bad Request'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,18 +26,16 @@ void main() {
|
|||
late UserModel testUser;
|
||||
late UserDataModel testUserData;
|
||||
|
||||
Request buildRequest(
|
||||
String method,
|
||||
String url, {
|
||||
UserModel? user,
|
||||
}) {
|
||||
Request buildRequest(String method, String url, {UserModel? user}) {
|
||||
final context = <String, Object?>{};
|
||||
|
||||
if (user != null) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -107,10 +105,7 @@ void main() {
|
|||
await testIsar.userModels.put(user);
|
||||
testUser = user;
|
||||
|
||||
final userData = UserDataModel(
|
||||
tags: ['premium', 'beta'],
|
||||
words: [],
|
||||
);
|
||||
final userData = UserDataModel(tags: ['premium', 'beta'], words: []);
|
||||
await testIsar.userDataModels.put(userData);
|
||||
userData.user.value = user;
|
||||
await userData.user.save();
|
||||
|
|
@ -137,7 +132,8 @@ void main() {
|
|||
final response = await promocodesApiV2.listPromocodes(request);
|
||||
|
||||
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'));
|
||||
});
|
||||
|
||||
|
|
@ -146,7 +142,8 @@ void main() {
|
|||
final response = await promocodesApiV2.listPromocodes(request);
|
||||
|
||||
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'] as List).isEmpty, isTrue);
|
||||
});
|
||||
|
|
@ -184,11 +181,13 @@ void main() {
|
|||
final response = await promocodesApiV2.listPromocodes(request);
|
||||
|
||||
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'] 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['promoCodes'], isA<List>());
|
||||
expect((campaign['promoCodes'] as List).length, equals(2));
|
||||
|
|
@ -238,10 +237,14 @@ void main() {
|
|||
final response = await promocodesApiV2.listPromocodes(request);
|
||||
|
||||
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;
|
||||
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 {
|
||||
|
|
@ -300,10 +303,14 @@ void main() {
|
|||
final response = await promocodesApiV2.listPromocodes(request);
|
||||
|
||||
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;
|
||||
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 {
|
||||
|
|
@ -368,10 +375,13 @@ void main() {
|
|||
final response = await promocodesApiV2.listPromocodes(request);
|
||||
|
||||
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;
|
||||
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('NOTAGS'));
|
||||
expect(templates, isNot(contains('NONMATCHING')));
|
||||
|
|
@ -411,7 +421,8 @@ void main() {
|
|||
final response = await promocodesApiV2.listPromocodes(request);
|
||||
|
||||
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;
|
||||
expect(campaigns.isEmpty, isTrue);
|
||||
});
|
||||
|
|
@ -435,8 +446,14 @@ void main() {
|
|||
);
|
||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||
|
||||
final promoCode1 = PromoCodeModel(code: 'LIMITED1', activations: 1); // Already used
|
||||
final promoCode2 = PromoCodeModel(code: 'LIMITED2', activations: 0); // Available
|
||||
final promoCode1 = PromoCodeModel(
|
||||
code: 'LIMITED1',
|
||||
activations: 1,
|
||||
); // Already used
|
||||
final promoCode2 = PromoCodeModel(
|
||||
code: 'LIMITED2',
|
||||
activations: 0,
|
||||
); // Available
|
||||
await testIsar.promoCodeModels.putAll([promoCode1, promoCode2]);
|
||||
|
||||
campaign.promoCodes.addAll([promoCode1, promoCode2]);
|
||||
|
|
@ -447,7 +464,8 @@ void main() {
|
|||
final response = await promocodesApiV2.listPromocodes(request);
|
||||
|
||||
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;
|
||||
expect(campaigns.length, equals(1));
|
||||
|
||||
|
|
@ -493,7 +511,8 @@ void main() {
|
|||
final response = await promocodesApiV2.listPromocodes(request);
|
||||
|
||||
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;
|
||||
expect(campaigns.length, equals(1));
|
||||
|
||||
|
|
@ -503,61 +522,76 @@ void main() {
|
|||
expect(promoCodes.first, equals('AVAILABLE1'));
|
||||
});
|
||||
|
||||
test('returns proper JSON format with campaign and code information', () async {
|
||||
final now = DateTime.now();
|
||||
final start = now.subtract(const Duration(days: 1));
|
||||
final finish = now.add(const Duration(days: 1));
|
||||
test(
|
||||
'returns proper JSON format with campaign and code information',
|
||||
() 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: 'FORMAT',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: [],
|
||||
name: 'Test Campaign',
|
||||
await testIsar.writeTxn(() async {
|
||||
final campaign = PromoCodesCampaignModel(
|
||||
template: 'FORMAT',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: [],
|
||||
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');
|
||||
await testIsar.promoCodeModels.put(promoCode);
|
||||
campaign.promoCodes.add(promoCode);
|
||||
await campaign.promoCodes.save();
|
||||
});
|
||||
expect(response.statusCode, equals(200));
|
||||
final body =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body.containsKey('campaigns'), isTrue);
|
||||
|
||||
final request = buildRequest('GET', '/api/v2/promocodes', user: testUser);
|
||||
final response = await promocodesApiV2.listPromocodes(request);
|
||||
final campaigns = body['campaigns'] as List;
|
||||
expect(campaigns.length, equals(1));
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body.containsKey('campaigns'), isTrue);
|
||||
|
||||
final campaigns = body['campaigns'] as List;
|
||||
expect(campaigns.length, equals(1));
|
||||
|
||||
final campaign = campaigns.first as Map<String, dynamic>;
|
||||
expect(campaign.containsKey('id'), isTrue);
|
||||
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>());
|
||||
});
|
||||
final campaign = campaigns.first as Map<String, dynamic>;
|
||||
expect(campaign.containsKey('id'), isTrue);
|
||||
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', () {
|
||||
test('returns 401 for unauthenticated requests', () async {
|
||||
final request = buildRequest('GET', '/api/v2/promocodes/TEST123/validate');
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'TEST123');
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'/api/v2/promocodes/TEST123/validate',
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(
|
||||
request,
|
||||
'TEST123',
|
||||
);
|
||||
|
||||
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'));
|
||||
});
|
||||
|
||||
|
|
@ -567,10 +601,14 @@ void main() {
|
|||
'/api/v2/promocodes/NONEXISTENT/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'NONEXISTENT');
|
||||
final response = await promocodesApiV2.validatePromocode(
|
||||
request,
|
||||
'NONEXISTENT',
|
||||
);
|
||||
|
||||
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['message'], equals('Промокод не найден'));
|
||||
});
|
||||
|
|
@ -584,7 +622,8 @@ void main() {
|
|||
final response = await promocodesApiV2.validatePromocode(request, '');
|
||||
|
||||
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'));
|
||||
});
|
||||
|
||||
|
|
@ -618,10 +657,14 @@ void main() {
|
|||
'/api/v2/promocodes/VALID123/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'VALID123');
|
||||
final response = await promocodesApiV2.validatePromocode(
|
||||
request,
|
||||
'VALID123',
|
||||
);
|
||||
|
||||
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['message'], equals('Промокод действителен'));
|
||||
});
|
||||
|
|
@ -656,10 +699,14 @@ void main() {
|
|||
'/api/v2/promocodes/INACTIVE123/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'INACTIVE123');
|
||||
final response = await promocodesApiV2.validatePromocode(
|
||||
request,
|
||||
'INACTIVE123',
|
||||
);
|
||||
|
||||
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['message'], equals('Промокод недействителен'));
|
||||
});
|
||||
|
|
@ -694,10 +741,14 @@ void main() {
|
|||
'/api/v2/promocodes/FUTURE123/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'FUTURE123');
|
||||
final response = await promocodesApiV2.validatePromocode(
|
||||
request,
|
||||
'FUTURE123',
|
||||
);
|
||||
|
||||
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['message'], equals('Промокод еще не активен'));
|
||||
});
|
||||
|
|
@ -732,176 +783,211 @@ void main() {
|
|||
'/api/v2/promocodes/EXPIRED123/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'EXPIRED123');
|
||||
final response = await promocodesApiV2.validatePromocode(
|
||||
request,
|
||||
'EXPIRED123',
|
||||
);
|
||||
|
||||
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['message'], equals('Промокод истек'));
|
||||
});
|
||||
|
||||
test('returns valid: false for promocode that reached activation limit', () async {
|
||||
final now = DateTime.now();
|
||||
final start = now.subtract(const Duration(days: 1));
|
||||
final finish = now.add(const Duration(days: 1));
|
||||
test(
|
||||
'returns valid: false for promocode that reached activation limit',
|
||||
() 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: 'EXHAUSTED',
|
||||
products: [],
|
||||
activationsPerCode: 1,
|
||||
activationsPerUser: 10,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: [],
|
||||
await testIsar.writeTxn(() async {
|
||||
final campaign = PromoCodesCampaignModel(
|
||||
template: 'EXHAUSTED',
|
||||
products: [],
|
||||
activationsPerCode: 1,
|
||||
activationsPerUser: 10,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
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 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,
|
||||
);
|
||||
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: [],
|
||||
final response = await promocodesApiV2.validatePromocode(
|
||||
request,
|
||||
'EXHAUSTED123',
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||
|
||||
final promoCode = PromoCodeModel(code: 'USED123', activations: 0);
|
||||
await testIsar.promoCodeModels.put(promoCode);
|
||||
campaign.promoCodes.add(promoCode);
|
||||
await campaign.promoCodes.save();
|
||||
expect(response.statusCode, equals(200));
|
||||
final body =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['valid'], equals(false));
|
||||
expect(body['message'], equals('Промокод исчерпан'));
|
||||
},
|
||||
);
|
||||
|
||||
// User has already activated this code
|
||||
testUserData.activatedPromoCodes.add(promoCode);
|
||||
await testUserData.activatedPromoCodes.save();
|
||||
await testIsar.userDataModels.put(testUserData);
|
||||
});
|
||||
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));
|
||||
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'/api/v2/promocodes/USED123/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'USED123');
|
||||
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);
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['valid'], equals(false));
|
||||
expect(body['message'], equals('Промокод уже был активирован'));
|
||||
});
|
||||
final promoCode = PromoCodeModel(code: 'USED123', activations: 0);
|
||||
await testIsar.promoCodeModels.put(promoCode);
|
||||
campaign.promoCodes.add(promoCode);
|
||||
await campaign.promoCodes.save();
|
||||
|
||||
test('returns valid: false when user reached campaign activation limit', () async {
|
||||
final now = DateTime.now();
|
||||
final start = now.subtract(const Duration(days: 1));
|
||||
final finish = now.add(const Duration(days: 1));
|
||||
// User has already activated this code
|
||||
testUserData.activatedPromoCodes.add(promoCode);
|
||||
await testUserData.activatedPromoCodes.save();
|
||||
await testIsar.userDataModels.put(testUserData);
|
||||
});
|
||||
|
||||
await testIsar.writeTxn(() async {
|
||||
final campaign = PromoCodesCampaignModel(
|
||||
template: 'LIMITED',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1, // User can only activate once
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: [],
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'/api/v2/promocodes/USED123/validate',
|
||||
user: testUser,
|
||||
);
|
||||
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'
|
||||
final response = await promocodesApiV2.validatePromocode(
|
||||
request,
|
||||
'USED123',
|
||||
);
|
||||
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();
|
||||
});
|
||||
expect(response.statusCode, equals(200));
|
||||
final body =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['valid'], equals(false));
|
||||
expect(body['message'], equals('Промокод уже был активирован'));
|
||||
},
|
||||
);
|
||||
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'/api/v2/promocodes/TAGGED123/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'TAGGED123');
|
||||
test(
|
||||
'returns valid: false when user reached campaign activation limit',
|
||||
() async {
|
||||
final now = DateTime.now();
|
||||
final start = now.subtract(const Duration(days: 1));
|
||||
final finish = now.add(const Duration(days: 1));
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['valid'], equals(false));
|
||||
expect(body['message'], equals('Промокод недействителен'));
|
||||
});
|
||||
await testIsar.writeTxn(() async {
|
||||
final campaign = PromoCodesCampaignModel(
|
||||
template: 'LIMITED',
|
||||
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 {
|
||||
final now = DateTime.now();
|
||||
|
|
@ -933,118 +1019,139 @@ void main() {
|
|||
'/api/v2/promocodes/MATCHING123/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'MATCHING123');
|
||||
final response = await promocodesApiV2.validatePromocode(
|
||||
request,
|
||||
'MATCHING123',
|
||||
);
|
||||
|
||||
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['message'], equals('Промокод действителен'));
|
||||
});
|
||||
|
||||
test('returns valid: false for individual promocode for another user', () async {
|
||||
final now = DateTime.now();
|
||||
final start = now.subtract(const Duration(days: 1));
|
||||
final finish = now.add(const Duration(days: 1));
|
||||
test(
|
||||
'returns valid: false for individual promocode for another user',
|
||||
() async {
|
||||
final now = DateTime.now();
|
||||
final start = now.subtract(const Duration(days: 1));
|
||||
final finish = now.add(const Duration(days: 1));
|
||||
|
||||
late UserModel otherUser;
|
||||
late UserDataModel otherUserData;
|
||||
late UserModel otherUser;
|
||||
late UserDataModel otherUserData;
|
||||
|
||||
await testIsar.writeTxn(() async {
|
||||
// Create another user
|
||||
otherUser = UserModel.empty.copyWith(
|
||||
id: 2,
|
||||
name: 'Other User',
|
||||
email: 'other@example.com',
|
||||
await testIsar.writeTxn(() async {
|
||||
// Create another user
|
||||
otherUser = UserModel.empty.copyWith(
|
||||
id: 2,
|
||||
name: 'Other User',
|
||||
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);
|
||||
|
||||
otherUserData = UserDataModel(
|
||||
tags: [],
|
||||
words: [],
|
||||
final response = await promocodesApiV2.validatePromocode(
|
||||
request,
|
||||
'INDIVIDUAL123',
|
||||
);
|
||||
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: [],
|
||||
expect(response.statusCode, equals(200));
|
||||
final body =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['valid'], equals(false));
|
||||
expect(
|
||||
body['message'],
|
||||
equals('Это промокод для другого пользователя'),
|
||||
);
|
||||
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();
|
||||
test(
|
||||
'returns valid: true for individual promocode for current user',
|
||||
() async {
|
||||
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
|
||||
promoCode.userData.value = otherUserData;
|
||||
await promoCode.userData.save();
|
||||
});
|
||||
await testIsar.writeTxn(() async {
|
||||
final campaign = PromoCodesCampaignModel(
|
||||
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(
|
||||
'GET',
|
||||
'/api/v2/promocodes/INDIVIDUAL123/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'INDIVIDUAL123');
|
||||
final promoCode = PromoCodeModel(code: 'MYCODE123', activations: 0);
|
||||
await testIsar.promoCodeModels.put(promoCode);
|
||||
campaign.promoCodes.add(promoCode);
|
||||
await campaign.promoCodes.save();
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['valid'], equals(false));
|
||||
expect(body['message'], equals('Это промокод для другого пользователя'));
|
||||
});
|
||||
// Assign promocode to current user
|
||||
promoCode.userData.value = testUserData;
|
||||
await promoCode.userData.save();
|
||||
});
|
||||
|
||||
test('returns valid: true for individual promocode for current 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: 'MYCODE',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: [],
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'/api/v2/promocodes/MYCODE123/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(
|
||||
request,
|
||||
'MYCODE123',
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||
|
||||
final promoCode = PromoCodeModel(code: 'MYCODE123', activations: 0);
|
||||
await testIsar.promoCodeModels.put(promoCode);
|
||||
campaign.promoCodes.add(promoCode);
|
||||
await campaign.promoCodes.save();
|
||||
|
||||
// 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('Промокод действителен'));
|
||||
});
|
||||
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 {
|
||||
final now = DateTime.now();
|
||||
|
|
@ -1077,10 +1184,14 @@ void main() {
|
|||
'/api/v2/promocodes/case123/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'case123');
|
||||
final response = await promocodesApiV2.validatePromocode(
|
||||
request,
|
||||
'case123',
|
||||
);
|
||||
|
||||
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['message'], equals('Промокод действителен'));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ void main() {
|
|||
// Initialize Isar for testing
|
||||
await Isar.initializeIsarCore(download: true);
|
||||
final testDir = Directory.systemTemp.createTempSync('isar_test_');
|
||||
|
||||
|
||||
testIsar = await Isar.open(
|
||||
[
|
||||
CardPackModelSchema,
|
||||
|
|
@ -55,7 +55,7 @@ void main() {
|
|||
name: 'test_db',
|
||||
inspector: false,
|
||||
);
|
||||
|
||||
|
||||
backend_main.isar = testIsar;
|
||||
});
|
||||
|
||||
|
|
@ -64,13 +64,10 @@ void main() {
|
|||
final discountsManager = const DiscountsManager();
|
||||
final productsPriceResolver = ProductsPriceResolver(discountsManager);
|
||||
final adsManager = AdsManager();
|
||||
packDtoConverter = PackDtoConverter(
|
||||
productsPriceResolver,
|
||||
adsManager,
|
||||
);
|
||||
|
||||
packDtoConverter = PackDtoConverter(productsPriceResolver, adsManager);
|
||||
|
||||
packManager = PackManager(packDtoConverter);
|
||||
|
||||
|
||||
final subscriptionManager = SubscriptionManager();
|
||||
final yooMoneyHandler = YooMoneyHandler(packManager);
|
||||
final rustorePurchaseHandler = RustorePurchaseHandler();
|
||||
|
|
@ -81,7 +78,7 @@ void main() {
|
|||
rustorePurchaseHandler,
|
||||
productsPriceResolver,
|
||||
);
|
||||
|
||||
|
||||
purchasesApiV2 = PurchasesApiV2(paymentManager, packManager);
|
||||
|
||||
// Create test user
|
||||
|
|
@ -141,8 +138,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await purchasesApiV2.createPackPurchase(request, '20');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(401));
|
||||
expect(responseBody['error'], equals('Unauthorized'));
|
||||
|
|
@ -155,8 +152,8 @@ void main() {
|
|||
).change(context: {'user': testUser});
|
||||
|
||||
final response = await purchasesApiV2.createPackPurchase(request, '999');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(404));
|
||||
expect(responseBody['error'], equals('Not Found'));
|
||||
|
|
@ -185,8 +182,8 @@ void main() {
|
|||
).change(context: {'user': updatedUser!});
|
||||
|
||||
final response = await purchasesApiV2.createPackPurchase(request, '20');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
expect(responseBody['error'], equals('Bad Request'));
|
||||
|
|
@ -205,8 +202,8 @@ void main() {
|
|||
request,
|
||||
'20',
|
||||
);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['packId'], equals('20'));
|
||||
|
|
@ -241,8 +238,8 @@ void main() {
|
|||
request,
|
||||
'20',
|
||||
);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['isPurchased'], isTrue);
|
||||
|
|
@ -259,8 +256,8 @@ void main() {
|
|||
request,
|
||||
'20',
|
||||
);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(401));
|
||||
expect(responseBody['error'], equals('Unauthorized'));
|
||||
|
|
@ -276,8 +273,8 @@ void main() {
|
|||
request,
|
||||
'invalid',
|
||||
);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
expect(responseBody['error'], equals('Bad Request'));
|
||||
|
|
@ -289,15 +286,12 @@ void main() {
|
|||
final request = Request(
|
||||
'POST',
|
||||
Uri.parse('http://localhost/api/v2/purchases/payments'),
|
||||
body: jsonEncode({
|
||||
'productId': '20',
|
||||
'productType': 'pack',
|
||||
}),
|
||||
body: jsonEncode({'productId': '20', 'productType': 'pack'}),
|
||||
);
|
||||
|
||||
final response = await purchasesApiV2.createPayment(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(401));
|
||||
expect(responseBody['error'], equals('Unauthorized'));
|
||||
|
|
@ -307,14 +301,12 @@ void main() {
|
|||
final request = Request(
|
||||
'POST',
|
||||
Uri.parse('http://localhost/api/v2/purchases/payments'),
|
||||
body: jsonEncode({
|
||||
'productType': 'pack',
|
||||
}),
|
||||
body: jsonEncode({'productType': 'pack'}),
|
||||
).change(context: {'user': testUser});
|
||||
|
||||
final response = await purchasesApiV2.createPayment(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
expect(responseBody['error'], equals('Bad Request'));
|
||||
|
|
@ -329,8 +321,8 @@ void main() {
|
|||
).change(context: {'user': testUser});
|
||||
|
||||
final response = await purchasesApiV2.createPayment(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
expect(responseBody['error'], equals('Bad Request'));
|
||||
|
|
@ -351,8 +343,8 @@ void main() {
|
|||
request,
|
||||
'payment123',
|
||||
);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(401));
|
||||
expect(responseBody['error'], equals('Unauthorized'));
|
||||
|
|
@ -370,8 +362,8 @@ void main() {
|
|||
request,
|
||||
'payment123',
|
||||
);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
expect(responseBody['error'], equals('Bad Request'));
|
||||
|
|
@ -392,13 +384,13 @@ void main() {
|
|||
request,
|
||||
'payment123',
|
||||
);
|
||||
|
||||
|
||||
// Should return a response (either success or failure)
|
||||
expect(response.statusCode, isIn([200, 500]));
|
||||
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(responseBody['paymentId'], equals('payment123'));
|
||||
expect(responseBody['status'], isA<String>());
|
||||
expect(responseBody['result'], isA<bool>());
|
||||
|
|
@ -406,4 +398,3 @@ void main() {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -87,20 +87,23 @@ void main() {
|
|||
});
|
||||
|
||||
group('SubscriptionsApiV2 - Get Plans', () {
|
||||
test('should return 200 with empty array when no plans available', () async {
|
||||
final request = Request(
|
||||
'GET',
|
||||
Uri.parse('http://localhost/api/v2/subscriptions/plans'),
|
||||
);
|
||||
test(
|
||||
'should return 200 with empty array when no plans available',
|
||||
() async {
|
||||
final request = Request(
|
||||
'GET',
|
||||
Uri.parse('http://localhost/api/v2/subscriptions/plans'),
|
||||
);
|
||||
|
||||
final response = await subscriptionsApiV2.getPlans(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final response = await subscriptionsApiV2.getPlans(request);
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['plans'], isA<List>());
|
||||
expect(responseBody['plans'], isEmpty);
|
||||
});
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['plans'], isA<List>());
|
||||
expect(responseBody['plans'], isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test('should return 200 with list of plans when plans exist', () async {
|
||||
// Create test subscription plans
|
||||
|
|
@ -147,8 +150,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await subscriptionsApiV2.getPlans(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['plans'], isA<List>());
|
||||
|
|
@ -194,8 +197,8 @@ void main() {
|
|||
).change(context: {'user': testUser});
|
||||
|
||||
final response = await subscriptionsApiV2.getPlans(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['plans'], isA<List>());
|
||||
|
|
@ -228,8 +231,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await subscriptionsApiV2.getPlans(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(response.headers['content-type'], contains('application/json'));
|
||||
|
|
@ -249,57 +252,56 @@ void main() {
|
|||
expect(plan.containsKey('ui'), isTrue);
|
||||
});
|
||||
|
||||
test('should handle multiple plans with different payment systems', () async {
|
||||
// Create test subscription plans with different payment systems
|
||||
await testIsar.writeTxn(() async {
|
||||
final plan1 = SubscriptionPlanModel(
|
||||
id: 1,
|
||||
price: '299',
|
||||
currency: 'RUB',
|
||||
durationDays: 30,
|
||||
features: [SubscriptionFeatureEnum.packs],
|
||||
paymentSystem: PaymentSystem.yookassa,
|
||||
paymentId: 'plan_1',
|
||||
ui: SubscriptionPlanUI(
|
||||
title: 'YooKassa Plan',
|
||||
),
|
||||
test(
|
||||
'should handle multiple plans with different payment systems',
|
||||
() async {
|
||||
// Create test subscription plans with different payment systems
|
||||
await testIsar.writeTxn(() async {
|
||||
final plan1 = SubscriptionPlanModel(
|
||||
id: 1,
|
||||
price: '299',
|
||||
currency: 'RUB',
|
||||
durationDays: 30,
|
||||
features: [SubscriptionFeatureEnum.packs],
|
||||
paymentSystem: PaymentSystem.yookassa,
|
||||
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(
|
||||
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 response = await subscriptionsApiV2.getPlans(request);
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
final request = Request(
|
||||
'GET',
|
||||
Uri.parse('http://localhost/api/v2/subscriptions/plans'),
|
||||
);
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['plans'], hasLength(2));
|
||||
|
||||
final response = await subscriptionsApiV2.getPlans(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['plans'], hasLength(2));
|
||||
|
||||
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'));
|
||||
});
|
||||
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', () {
|
||||
|
|
@ -310,8 +312,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await subscriptionsApiV2.getStatus(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(401));
|
||||
expect(responseBody['error'], equals('unauthorized'));
|
||||
|
|
@ -325,8 +327,8 @@ void main() {
|
|||
).change(context: {'user': testUser});
|
||||
|
||||
final response = await subscriptionsApiV2.getStatus(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['active'], isFalse);
|
||||
|
|
@ -334,52 +336,55 @@ void main() {
|
|||
expect(responseBody.containsKey('finish'), isFalse);
|
||||
});
|
||||
|
||||
test('should return active: true with dates when subscription is active', () async {
|
||||
final now = DateTime.now();
|
||||
final startDate = now.subtract(const Duration(days: 5));
|
||||
final finishDate = now.add(const Duration(days: 25));
|
||||
test(
|
||||
'should return active: true with dates when subscription is active',
|
||||
() async {
|
||||
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
|
||||
await testIsar.writeTxn(() async {
|
||||
final subscription = UserSubscriptionModel(
|
||||
start: startDate,
|
||||
finish: finishDate,
|
||||
features: [SubscriptionFeatureEnum.packs],
|
||||
);
|
||||
await testIsar.userSubscriptionModels.put(subscription);
|
||||
|
||||
final user = await testIsar.userModels.get(1);
|
||||
if (user != null) {
|
||||
user.subscriptionModel.value = subscription;
|
||||
await user.subscriptionModel.save();
|
||||
}
|
||||
});
|
||||
// Create active subscription for test user
|
||||
await testIsar.writeTxn(() async {
|
||||
final subscription = UserSubscriptionModel(
|
||||
start: startDate,
|
||||
finish: finishDate,
|
||||
features: [SubscriptionFeatureEnum.packs],
|
||||
);
|
||||
await testIsar.userSubscriptionModels.put(subscription);
|
||||
|
||||
// Reload user with subscription
|
||||
final userWithSubscription = await testIsar.userModels.get(1);
|
||||
expect(userWithSubscription, isNotNull);
|
||||
await userWithSubscription!.subscriptionModel.load();
|
||||
final user = await testIsar.userModels.get(1);
|
||||
if (user != null) {
|
||||
user.subscriptionModel.value = subscription;
|
||||
await user.subscriptionModel.save();
|
||||
}
|
||||
});
|
||||
|
||||
final request = Request(
|
||||
'GET',
|
||||
Uri.parse('http://localhost/api/v2/subscriptions/status'),
|
||||
).change(context: {'user': userWithSubscription});
|
||||
// Reload user with subscription
|
||||
final userWithSubscription = await testIsar.userModels.get(1);
|
||||
expect(userWithSubscription, isNotNull);
|
||||
await userWithSubscription!.subscriptionModel.load();
|
||||
|
||||
final response = await subscriptionsApiV2.getStatus(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final request = Request(
|
||||
'GET',
|
||||
Uri.parse('http://localhost/api/v2/subscriptions/status'),
|
||||
).change(context: {'user': userWithSubscription});
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['active'], isTrue);
|
||||
expect(responseBody.containsKey('start'), isTrue);
|
||||
expect(responseBody.containsKey('finish'), isTrue);
|
||||
|
||||
final start = DateTime.parse(responseBody['start'] as String);
|
||||
final finish = DateTime.parse(responseBody['finish'] as String);
|
||||
|
||||
expect(start, equals(startDate));
|
||||
expect(finish, equals(finishDate));
|
||||
});
|
||||
final response = await subscriptionsApiV2.getStatus(request);
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['active'], isTrue);
|
||||
expect(responseBody.containsKey('start'), isTrue);
|
||||
expect(responseBody.containsKey('finish'), isTrue);
|
||||
|
||||
final start = DateTime.parse(responseBody['start'] as String);
|
||||
final finish = DateTime.parse(responseBody['finish'] as String);
|
||||
|
||||
expect(start, equals(startDate));
|
||||
expect(finish, equals(finishDate));
|
||||
},
|
||||
);
|
||||
|
||||
test('should return active: false when subscription is expired', () async {
|
||||
final now = DateTime.now();
|
||||
|
|
@ -394,7 +399,7 @@ void main() {
|
|||
features: [SubscriptionFeatureEnum.packs],
|
||||
);
|
||||
await testIsar.userSubscriptionModels.put(subscription);
|
||||
|
||||
|
||||
final user = await testIsar.userModels.get(1);
|
||||
if (user != null) {
|
||||
user.subscriptionModel.value = subscription;
|
||||
|
|
@ -413,8 +418,8 @@ void main() {
|
|||
).change(context: {'user': userWithSubscription});
|
||||
|
||||
final response = await subscriptionsApiV2.getStatus(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['active'], isFalse);
|
||||
|
|
@ -430,10 +435,13 @@ void main() {
|
|||
final subscription = UserSubscriptionModel(
|
||||
start: startDate,
|
||||
finish: finishDate,
|
||||
features: [SubscriptionFeatureEnum.packs, SubscriptionFeatureEnum.ads],
|
||||
features: [
|
||||
SubscriptionFeatureEnum.packs,
|
||||
SubscriptionFeatureEnum.ads,
|
||||
],
|
||||
);
|
||||
await testIsar.userSubscriptionModels.put(subscription);
|
||||
|
||||
|
||||
final user = await testIsar.userModels.get(1);
|
||||
if (user != null) {
|
||||
user.subscriptionModel.value = subscription;
|
||||
|
|
@ -451,20 +459,20 @@ void main() {
|
|||
).change(context: {'user': userWithSubscription});
|
||||
|
||||
final response = await subscriptionsApiV2.getStatus(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(response.headers['content-type'], contains('application/json'));
|
||||
expect(responseBody.containsKey('active'), isTrue);
|
||||
expect(responseBody['active'], isA<bool>());
|
||||
|
||||
|
||||
if (responseBody['active'] as bool) {
|
||||
expect(responseBody.containsKey('start'), isTrue);
|
||||
expect(responseBody.containsKey('finish'), isTrue);
|
||||
expect(responseBody['start'], isA<String>());
|
||||
expect(responseBody['finish'], isA<String>());
|
||||
|
||||
|
||||
// Verify ISO8601 format
|
||||
expect(
|
||||
() => DateTime.parse(responseBody['start'] as String),
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ void main() {
|
|||
// Initialize Isar for testing
|
||||
await Isar.initializeIsarCore(download: true);
|
||||
final testDir = Directory.systemTemp.createTempSync('isar_test_');
|
||||
|
||||
|
||||
testIsar = await Isar.open(
|
||||
[
|
||||
CardPackModelSchema,
|
||||
|
|
@ -53,7 +53,7 @@ void main() {
|
|||
name: 'test_db',
|
||||
inspector: false,
|
||||
);
|
||||
|
||||
|
||||
backend_main.isar = testIsar;
|
||||
});
|
||||
|
||||
|
|
@ -62,16 +62,13 @@ void main() {
|
|||
final discountsManager = const DiscountsManager();
|
||||
final productsPriceResolver = ProductsPriceResolver(discountsManager);
|
||||
final adsManager = AdsManager();
|
||||
packDtoConverter = PackDtoConverter(
|
||||
productsPriceResolver,
|
||||
adsManager,
|
||||
);
|
||||
|
||||
packDtoConverter = PackDtoConverter(productsPriceResolver, adsManager);
|
||||
|
||||
testManager = TestManager(packDtoConverter);
|
||||
|
||||
|
||||
final freePacksDistributor = const FreePacksDistributor();
|
||||
userManager = UserManager(freePacksDistributor);
|
||||
|
||||
|
||||
testsApiV2 = TestsApiV2(testManager, userManager);
|
||||
|
||||
// Create test user
|
||||
|
|
@ -93,11 +90,7 @@ void main() {
|
|||
|
||||
// Create test model
|
||||
await testIsar.writeTxn(() async {
|
||||
final test = TestModel(
|
||||
id: 100,
|
||||
name: 'Test Test',
|
||||
version: '1.0',
|
||||
);
|
||||
final test = TestModel(id: 100, name: 'Test Test', version: '1.0');
|
||||
await testIsar.testModels.put(test);
|
||||
});
|
||||
});
|
||||
|
|
@ -125,8 +118,8 @@ void main() {
|
|||
).change(context: {'user': testUser});
|
||||
|
||||
final response = await testsApiV2.getTest(request, '100');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['id'], equals('100'));
|
||||
|
|
@ -140,8 +133,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await testsApiV2.getTest(request, '100');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(401));
|
||||
expect(responseBody['error'], equals('Unauthorized'));
|
||||
|
|
@ -154,8 +147,8 @@ void main() {
|
|||
).change(context: {'user': testUser});
|
||||
|
||||
final response = await testsApiV2.getTest(request, '999');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(404));
|
||||
expect(responseBody['error'], equals('Not Found'));
|
||||
|
|
@ -168,8 +161,8 @@ void main() {
|
|||
).change(context: {'user': testUser});
|
||||
|
||||
final response = await testsApiV2.getTest(request, 'invalid');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
expect(responseBody['error'], equals('Bad Request'));
|
||||
|
|
@ -200,8 +193,8 @@ void main() {
|
|||
).change(context: {'user': testUser});
|
||||
|
||||
final response = await testsApiV2.submitTestResults(request, '100');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['success'], equals(true));
|
||||
|
|
@ -222,8 +215,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await testsApiV2.submitTestResults(request, '100');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(401));
|
||||
expect(responseBody['error'], equals('Unauthorized'));
|
||||
|
|
@ -243,8 +236,8 @@ void main() {
|
|||
).change(context: {'user': testUser});
|
||||
|
||||
final response = await testsApiV2.submitTestResults(request, '100');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
expect(responseBody['error'], equals('Bad Request'));
|
||||
|
|
@ -259,12 +252,15 @@ void main() {
|
|||
).change(context: {'user': testUser});
|
||||
|
||||
final response = await testsApiV2.submitTestResults(request, '100');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
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 {
|
||||
|
|
@ -275,8 +271,8 @@ void main() {
|
|||
).change(context: {'user': testUser});
|
||||
|
||||
final response = await testsApiV2.submitTestResults(request, '100');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
expect(responseBody['error'], equals('Bad Request'));
|
||||
|
|
@ -297,8 +293,8 @@ void main() {
|
|||
).change(context: {'user': testUser});
|
||||
|
||||
final response = await testsApiV2.submitTestResults(request, 'invalid');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
expect(responseBody['error'], equals('Bad Request'));
|
||||
|
|
@ -313,8 +309,8 @@ void main() {
|
|||
).change(context: {'user': testUser});
|
||||
|
||||
final response = await testsApiV2.getTestHistory(request, '100');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['items'], isA<List>());
|
||||
|
|
@ -370,8 +366,8 @@ void main() {
|
|||
).change(context: {'user': updatedUser!});
|
||||
|
||||
final response = await testsApiV2.getTestHistory(request, '100');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['items'], isA<List>());
|
||||
|
|
@ -411,8 +407,8 @@ void main() {
|
|||
).change(context: {'user': updatedUser!});
|
||||
|
||||
final response = await testsApiV2.getTestHistory(request, '100');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['items'].length, lessThanOrEqualTo(2));
|
||||
|
|
@ -428,8 +424,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await testsApiV2.getTestHistory(request, '100');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(401));
|
||||
expect(responseBody['error'], equals('Unauthorized'));
|
||||
|
|
@ -442,8 +438,8 @@ void main() {
|
|||
).change(context: {'user': testUser});
|
||||
|
||||
final response = await testsApiV2.getTestHistory(request, 'invalid');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
expect(responseBody['error'], equals('Bad Request'));
|
||||
|
|
@ -456,8 +452,8 @@ void main() {
|
|||
).change(context: {'user': testUser});
|
||||
|
||||
final response = await testsApiV2.getTestHistory(request, '100');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
expect(responseBody['error'], equals('Bad Request'));
|
||||
|
|
@ -471,13 +467,15 @@ void main() {
|
|||
).change(context: {'user': testUser});
|
||||
|
||||
final response = await testsApiV2.getTestHistory(request, '100');
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
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,
|
||||
String? body,
|
||||
}) {
|
||||
var request = Request(
|
||||
method,
|
||||
Uri.parse(url),
|
||||
body: body,
|
||||
);
|
||||
var request = Request(method, Uri.parse(url), body: body);
|
||||
if (user != null) {
|
||||
request = request.change(
|
||||
context: {'user': user},
|
||||
);
|
||||
request = request.change(context: {'user': user});
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
|
@ -88,7 +82,7 @@ void main() {
|
|||
|
||||
setUp(() async {
|
||||
statisticsCalculator = StatisticsCalculator();
|
||||
|
||||
|
||||
// Set up PaymentManager dependencies
|
||||
final discountsManager = const DiscountsManager();
|
||||
final productsPriceResolver = ProductsPriceResolver(discountsManager);
|
||||
|
|
@ -108,18 +102,14 @@ void main() {
|
|||
rustorePurchaseHandler,
|
||||
productsPriceResolver,
|
||||
);
|
||||
|
||||
|
||||
userManager = UserManager(
|
||||
const FreePacksDistributor(),
|
||||
SessionTracker(testIsar),
|
||||
statisticsCalculator,
|
||||
AchievementManager(testIsar),
|
||||
);
|
||||
usersApiV2 = UsersApiV2(
|
||||
userManager,
|
||||
paymentManager,
|
||||
statisticsCalculator,
|
||||
);
|
||||
usersApiV2 = UsersApiV2(userManager, paymentManager, statisticsCalculator);
|
||||
|
||||
// Create test user with statistics data
|
||||
await testIsar.writeTxn(() async {
|
||||
|
|
@ -191,7 +181,7 @@ void main() {
|
|||
),
|
||||
],
|
||||
)..user.value = user;
|
||||
|
||||
|
||||
// Save userData first
|
||||
await testIsar.userDataModels.put(userData);
|
||||
// Then link it to user
|
||||
|
|
@ -199,7 +189,7 @@ void main() {
|
|||
await user.userData.save();
|
||||
// Save user again to persist the link
|
||||
await testIsar.userModels.put(user);
|
||||
|
||||
|
||||
// Reload user to ensure userData link is properly loaded
|
||||
testUser = (await testIsar.userModels.get(1))!;
|
||||
await testUser.userData.load();
|
||||
|
|
@ -235,7 +225,7 @@ void main() {
|
|||
if (fetchedUser != null) {
|
||||
await fetchedUser.userData.load();
|
||||
}
|
||||
|
||||
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'http://localhost/api/v2/users/me/statistics/detailed',
|
||||
|
|
@ -243,8 +233,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.getDetailedStatistics(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
// The endpoint should return 200 with UserDataDto or 404 if userData not found
|
||||
if (response.statusCode == 404) {
|
||||
|
|
@ -253,23 +243,23 @@ void main() {
|
|||
expect(responseBody['error'], equals('user_data_not_found'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
// Verify response structure - UserDataDto should have these fields
|
||||
expect(responseBody, isA<Map>());
|
||||
expect(responseBody.isNotEmpty, isTrue);
|
||||
|
||||
|
||||
// Verify that response contains expected UserDataDto fields
|
||||
// Note: Some fields may be null in JSON serialization
|
||||
if (responseBody.containsKey('totalStudyTimeMinutes') &&
|
||||
if (responseBody.containsKey('totalStudyTimeMinutes') &&
|
||||
responseBody['totalStudyTimeMinutes'] != null) {
|
||||
expect(responseBody['totalStudyTimeMinutes'], equals(180));
|
||||
}
|
||||
if (responseBody.containsKey('currentStreak') &&
|
||||
if (responseBody.containsKey('currentStreak') &&
|
||||
responseBody['currentStreak'] != null) {
|
||||
expect(responseBody['currentStreak'], equals(3));
|
||||
}
|
||||
if (responseBody.containsKey('longestStreak') &&
|
||||
if (responseBody.containsKey('longestStreak') &&
|
||||
responseBody['longestStreak'] != null) {
|
||||
expect(responseBody['longestStreak'], equals(5));
|
||||
}
|
||||
|
|
@ -300,8 +290,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.getDetailedStatistics(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(401));
|
||||
expect(responseBody['error'], equals('unauthorized'));
|
||||
|
|
@ -315,8 +305,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.getDetailedStatistics(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(404));
|
||||
expect(responseBody['error'], equals('user_data_not_found'));
|
||||
|
|
@ -394,8 +384,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.getPacksStatistics(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(401));
|
||||
expect(responseBody['error'], equals('unauthorized'));
|
||||
|
|
@ -411,8 +401,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.getWordsStatistics(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['words'], isA<List>());
|
||||
|
|
@ -431,8 +421,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.getWordsStatistics(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect((responseBody['words'] as List).length, equals(2));
|
||||
|
|
@ -446,8 +436,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response2 = await usersApiV2.getWordsStatistics(request2);
|
||||
final responseBody2 = jsonDecode(await response2.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody2 =
|
||||
jsonDecode(await response2.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(responseBody2['totalCount'], equals(3));
|
||||
expect((responseBody2['words'] as List).length, equals(1));
|
||||
|
|
@ -462,8 +452,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.getWordsStatistics(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final words = responseBody['words'] as List;
|
||||
|
|
@ -476,10 +466,15 @@ void main() {
|
|||
expect(word['incorrect'], isA<double>());
|
||||
}
|
||||
// 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++) {
|
||||
expect(difficulties[i], greaterThanOrEqualTo(difficulties[i + 1]),
|
||||
reason: 'Words should be sorted by difficulty descending');
|
||||
expect(
|
||||
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 responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final words = responseBody['words'] as List;
|
||||
|
|
@ -508,8 +503,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.getWordsStatistics(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['words'], isA<List>());
|
||||
|
|
@ -523,8 +518,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.getWordsStatistics(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final words = responseBody['words'] as List;
|
||||
|
|
@ -541,8 +536,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.getWordsStatistics(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['pageSize'], equals(50)); // Default limit
|
||||
|
|
@ -557,8 +552,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.getWordsStatistics(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['pageSize'], lessThanOrEqualTo(100));
|
||||
|
|
@ -572,8 +567,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.getWordsStatistics(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['page'], equals(0)); // Should clamp to 0
|
||||
|
|
@ -587,8 +582,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.getWordsStatistics(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['words'], isA<List>());
|
||||
|
|
@ -604,8 +599,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.getWordsStatistics(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(401));
|
||||
expect(responseBody['error'], equals('unauthorized'));
|
||||
|
|
@ -621,8 +616,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.getTimelineStatistics(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['period'], equals('month'));
|
||||
|
|
@ -643,8 +638,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.getTimelineStatistics(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['period'], equals('week'));
|
||||
|
|
@ -660,8 +655,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.getTimelineStatistics(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['period'], isA<String>());
|
||||
|
|
@ -675,8 +670,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.getTimelineStatistics(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
// Should still return valid response, ignoring invalid date
|
||||
expect(response.statusCode, equals(200));
|
||||
|
|
@ -691,8 +686,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.getTimelineStatistics(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['totalDays'], equals(0));
|
||||
|
|
@ -713,8 +708,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.getTimelineStatistics(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(401));
|
||||
expect(responseBody['error'], equals('unauthorized'));
|
||||
|
|
@ -735,8 +730,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.recordStudySession(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['result'], equals(true));
|
||||
|
|
@ -752,8 +747,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.recordStudySession(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
expect(responseBody['error'], equals('bad_request'));
|
||||
|
|
@ -769,8 +764,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.recordStudySession(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
expect(responseBody['error'], equals('bad_request'));
|
||||
|
|
@ -786,8 +781,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.recordStudySession(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
expect(responseBody['error'], equals('bad_request'));
|
||||
|
|
@ -805,8 +800,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.recordStudySession(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(401));
|
||||
expect(responseBody['error'], equals('unauthorized'));
|
||||
|
|
@ -854,8 +849,8 @@ void main() {
|
|||
);
|
||||
|
||||
final response = await usersApiV2.getAchievements(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
final responseBody =
|
||||
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(401));
|
||||
expect(responseBody['error'], equals('unauthorized'));
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import 'package:mnemo_cards_backend/database/tables/users.dart';
|
|||
import 'package:mnemo_cards_backend/database/tables/packs.dart';
|
||||
|
||||
/// Unit тесты для SoftDeleteMixin
|
||||
///
|
||||
///
|
||||
/// Тестирует функциональность soft delete на примере WordStatisticsDao
|
||||
/// Требования:
|
||||
/// - PostgreSQL должен быть запущен на localhost:5432
|
||||
|
|
@ -19,7 +19,8 @@ void main() {
|
|||
setUpAll(() async {
|
||||
// Подключение к тестовой БД
|
||||
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 username = Platform.environment['TEST_DB_USER'] ?? 'postgres';
|
||||
final password = Platform.environment['TEST_DB_PASSWORD'] ?? 'postgres';
|
||||
|
|
@ -114,14 +115,14 @@ void main() {
|
|||
expect(activeStats.length, equals(3));
|
||||
|
||||
// Удалить одну запись (soft delete)
|
||||
await (db.update(db.wordStatistics)
|
||||
..where((w) => w.id.equals(stats2.id)))
|
||||
.write(
|
||||
WordStatisticsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
await (db.update(
|
||||
db.wordStatistics,
|
||||
)..where((w) => w.id.equals(stats2.id))).write(
|
||||
WordStatisticsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
|
||||
// selectActive должен вернуть только активные
|
||||
activeStats = await db.wordStatisticsDao.selectActive().get();
|
||||
|
|
@ -145,14 +146,14 @@ void main() {
|
|||
);
|
||||
|
||||
// Удалить запись
|
||||
await (db.update(db.wordStatistics)
|
||||
..where((w) => w.id.equals(stats.id)))
|
||||
.write(
|
||||
WordStatisticsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
await (db.update(
|
||||
db.wordStatistics,
|
||||
)..where((w) => w.id.equals(stats.id))).write(
|
||||
WordStatisticsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
|
||||
final activeStats = await db.wordStatisticsDao.selectActive().get();
|
||||
expect(activeStats, isEmpty);
|
||||
|
|
@ -162,7 +163,8 @@ void main() {
|
|||
// Создать записи для разных пользователей
|
||||
final user2 = await db.userDao.createUser(
|
||||
UsersCompanion.insert(
|
||||
externalUserId: 'test_user_2_${DateTime.now().millisecondsSinceEpoch}',
|
||||
externalUserId:
|
||||
'test_user_2_${DateTime.now().millisecondsSinceEpoch}',
|
||||
name: Value('Test User 2'),
|
||||
),
|
||||
);
|
||||
|
|
@ -185,32 +187,35 @@ void main() {
|
|||
);
|
||||
|
||||
// selectActive с where должен фильтровать и по isDeleted, и по условию
|
||||
final user1Stats = await (db.wordStatisticsDao.selectActive()
|
||||
..where((w) => w.userId.equals(testUserId)))
|
||||
.get();
|
||||
final user1Stats =
|
||||
await (db.wordStatisticsDao.selectActive()
|
||||
..where((w) => w.userId.equals(testUserId)))
|
||||
.get();
|
||||
expect(user1Stats.length, equals(1));
|
||||
expect(user1Stats.first.id, equals(stats1.id));
|
||||
|
||||
// Удалить запись пользователя 1
|
||||
await (db.update(db.wordStatistics)
|
||||
..where((w) => w.id.equals(stats1.id)))
|
||||
.write(
|
||||
WordStatisticsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
await (db.update(
|
||||
db.wordStatistics,
|
||||
)..where((w) => w.id.equals(stats1.id))).write(
|
||||
WordStatisticsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
|
||||
// Теперь selectActive для пользователя 1 должен вернуть пустой список
|
||||
final user1StatsAfterDelete = await (db.wordStatisticsDao.selectActive()
|
||||
..where((w) => w.userId.equals(testUserId)))
|
||||
.get();
|
||||
final user1StatsAfterDelete =
|
||||
await (db.wordStatisticsDao.selectActive()
|
||||
..where((w) => w.userId.equals(testUserId)))
|
||||
.get();
|
||||
expect(user1StatsAfterDelete, isEmpty);
|
||||
|
||||
// Но запись пользователя 2 все еще активна
|
||||
final user2Stats = await (db.wordStatisticsDao.selectActive()
|
||||
..where((w) => w.userId.equals(user2.id)))
|
||||
.get();
|
||||
final user2Stats =
|
||||
await (db.wordStatisticsDao.selectActive()
|
||||
..where((w) => w.userId.equals(user2.id)))
|
||||
.get();
|
||||
expect(user2Stats.length, equals(1));
|
||||
expect(user2Stats.first.id, equals(stats2.id));
|
||||
});
|
||||
|
|
@ -233,7 +238,9 @@ void main() {
|
|||
});
|
||||
|
||||
test('возвращает null если запись не существует', () async {
|
||||
final result = await db.wordStatisticsDao.getActiveById('non_existent_id');
|
||||
final result = await db.wordStatisticsDao.getActiveById(
|
||||
'non_existent_id',
|
||||
);
|
||||
expect(result, isNull);
|
||||
});
|
||||
|
||||
|
|
@ -246,14 +253,14 @@ void main() {
|
|||
);
|
||||
|
||||
// Удалить запись
|
||||
await (db.update(db.wordStatistics)
|
||||
..where((w) => w.id.equals(stats.id)))
|
||||
.write(
|
||||
WordStatisticsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
await (db.update(
|
||||
db.wordStatistics,
|
||||
)..where((w) => w.id.equals(stats.id))).write(
|
||||
WordStatisticsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
|
||||
final result = await db.wordStatisticsDao.getActiveById(stats.id);
|
||||
expect(result, isNull);
|
||||
|
|
@ -268,14 +275,14 @@ void main() {
|
|||
);
|
||||
|
||||
// Удалить запись
|
||||
await (db.update(db.wordStatistics)
|
||||
..where((w) => w.id.equals(stats.id)))
|
||||
.write(
|
||||
WordStatisticsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
await (db.update(
|
||||
db.wordStatistics,
|
||||
)..where((w) => w.id.equals(stats.id))).write(
|
||||
WordStatisticsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
|
||||
// Проверить что запись физически существует в БД, но getActiveById не возвращает её
|
||||
final allRecords = await db.select(db.wordStatistics).get();
|
||||
|
|
|
|||
|
|
@ -12,12 +12,13 @@ void main() {
|
|||
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
|
||||
final port =
|
||||
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
|
||||
final database =
|
||||
Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
||||
final username = Platform.environment['TEST_DB_USER'] ??
|
||||
final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
||||
final username =
|
||||
Platform.environment['TEST_DB_USER'] ??
|
||||
Platform.environment['DB_USER'] ??
|
||||
'mnemo_user';
|
||||
final password = Platform.environment['TEST_DB_PASSWORD'] ??
|
||||
final password =
|
||||
Platform.environment['TEST_DB_PASSWORD'] ??
|
||||
Platform.environment['DB_PASSWORD'] ??
|
||||
'';
|
||||
|
||||
|
|
@ -44,9 +45,9 @@ void main() {
|
|||
tearDown(() async {
|
||||
// Keep cleanup scoped to generated tests only to avoid touching other data
|
||||
// that might exist in the shared test DB.
|
||||
final allGenerated = await (db.select(db.tests)
|
||||
..where((t) => t.version.equals('generated')))
|
||||
.get();
|
||||
final allGenerated = await (db.select(
|
||||
db.tests,
|
||||
)..where((t) => t.version.equals('generated'))).get();
|
||||
|
||||
for (final t in allGenerated) {
|
||||
await (db.delete(db.tests)..where((x) => x.id.equals(t.id))).go();
|
||||
|
|
@ -84,53 +85,58 @@ void main() {
|
|||
expect(stillThere, isNull);
|
||||
});
|
||||
|
||||
test('hardDeleteOldSoftDeletedGeneratedTests removes old soft-deleted tests',
|
||||
() async {
|
||||
final now = DateTime.now();
|
||||
test(
|
||||
'hardDeleteOldSoftDeletedGeneratedTests removes old soft-deleted tests',
|
||||
() async {
|
||||
final now = DateTime.now();
|
||||
|
||||
// Create a pack so we have a normal relation entry.
|
||||
final packId = await db.packDao.createPack(
|
||||
CardPacksCompanion.insert(
|
||||
title: 'pack for generated test cleanup',
|
||||
subtitle: 'subtitle',
|
||||
size: 1,
|
||||
),
|
||||
);
|
||||
// Create a pack so we have a normal relation entry.
|
||||
final packId = await db.packDao.createPack(
|
||||
CardPacksCompanion.insert(
|
||||
title: 'pack for generated test cleanup',
|
||||
subtitle: 'subtitle',
|
||||
size: 1,
|
||||
),
|
||||
);
|
||||
|
||||
// Create a generated test and link it to the pack.
|
||||
final testId = await db.testDao.createTest(
|
||||
TestsCompanion.insert(
|
||||
name: 'linked generated test',
|
||||
version: const Value('generated'),
|
||||
createdAt: Value(PgDateTime(now.subtract(const Duration(days: 10)))),
|
||||
updatedAt: Value(PgDateTime(now.subtract(const Duration(days: 10)))),
|
||||
),
|
||||
);
|
||||
await db.testDao.linkTestToPack(testId, packId);
|
||||
// Create a generated test and link it to the pack.
|
||||
final testId = await db.testDao.createTest(
|
||||
TestsCompanion.insert(
|
||||
name: 'linked generated test',
|
||||
version: const Value('generated'),
|
||||
createdAt: Value(
|
||||
PgDateTime(now.subtract(const Duration(days: 10))),
|
||||
),
|
||||
updatedAt: Value(
|
||||
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.
|
||||
await (db.update(db.tests)..where((t) => t.id.equals(testId))).write(
|
||||
TestsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
deletedAt: Value(PgDateTime(now.subtract(const Duration(days: 9)))),
|
||||
updatedAt: Value(PgDateTime(now.subtract(const Duration(days: 9)))),
|
||||
),
|
||||
);
|
||||
// Soft delete it long ago so it's eligible for TTL purge.
|
||||
await (db.update(db.tests)..where((t) => t.id.equals(testId))).write(
|
||||
TestsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
deletedAt: Value(PgDateTime(now.subtract(const Duration(days: 9)))),
|
||||
updatedAt: Value(PgDateTime(now.subtract(const Duration(days: 9)))),
|
||||
),
|
||||
);
|
||||
|
||||
final deleted = await db.testDao.hardDeleteOldSoftDeletedGeneratedTests(
|
||||
olderThan: const Duration(days: 7),
|
||||
);
|
||||
final deleted = await db.testDao.hardDeleteOldSoftDeletedGeneratedTests(
|
||||
olderThan: const Duration(days: 7),
|
||||
);
|
||||
|
||||
expect(deleted, equals(1));
|
||||
final stillThere = await db.testDao.getTestById(testId);
|
||||
expect(stillThere, isNull);
|
||||
expect(deleted, equals(1));
|
||||
final stillThere = await db.testDao.getTestById(testId);
|
||||
expect(stillThere, isNull);
|
||||
|
||||
// Cleanup the pack relation/pack.
|
||||
await (db.delete(db.testPackRelations)
|
||||
..where((r) => r.packId.equals(packId)))
|
||||
.go();
|
||||
await (db.delete(db.cardPacks)..where((p) => p.id.equals(packId))).go();
|
||||
});
|
||||
// Cleanup the pack relation/pack.
|
||||
await (db.delete(
|
||||
db.testPackRelations,
|
||||
)..where((r) => r.packId.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 port =
|
||||
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
|
||||
final database =
|
||||
Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
||||
final username = Platform.environment['TEST_DB_USER'] ??
|
||||
final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
||||
final username =
|
||||
Platform.environment['TEST_DB_USER'] ??
|
||||
Platform.environment['DB_USER'] ??
|
||||
'mnemo_user';
|
||||
final password = Platform.environment['TEST_DB_PASSWORD'] ??
|
||||
final password =
|
||||
Platform.environment['TEST_DB_PASSWORD'] ??
|
||||
Platform.environment['DB_PASSWORD'] ??
|
||||
'';
|
||||
|
||||
|
|
@ -40,13 +41,13 @@ void main() {
|
|||
await Migrator(db).createAll();
|
||||
|
||||
// Initialize MinioService
|
||||
final minioEndpoint = Platform.environment['MINIO_ENDPOINT'] ??
|
||||
'localhost:9000';
|
||||
final minioAccessKey = Platform.environment['MINIO_ACCESS_KEY'] ??
|
||||
'minioadmin';
|
||||
final minioSecretKey = Platform.environment['MINIO_SECRET_KEY'] ??
|
||||
'minioadmin';
|
||||
|
||||
final minioEndpoint =
|
||||
Platform.environment['MINIO_ENDPOINT'] ?? 'localhost:9000';
|
||||
final minioAccessKey =
|
||||
Platform.environment['MINIO_ACCESS_KEY'] ?? 'minioadmin';
|
||||
final minioSecretKey =
|
||||
Platform.environment['MINIO_SECRET_KEY'] ?? 'minioadmin';
|
||||
|
||||
minioService = MinioService(
|
||||
endpoint: minioEndpoint,
|
||||
accessKey: minioAccessKey,
|
||||
|
|
@ -68,20 +69,22 @@ void main() {
|
|||
tearDown(() async {
|
||||
// Cleanup is scoped to the entities created by this test.
|
||||
// Delete generated tests to avoid leaving orphans if pack is removed.
|
||||
final generatedTests = await (db.select(db.tests)
|
||||
..where((t) => t.version.equals('generated')))
|
||||
.get();
|
||||
final generatedTests = await (db.select(
|
||||
db.tests,
|
||||
)..where((t) => t.version.equals('generated'))).get();
|
||||
for (final t in generatedTests) {
|
||||
await (db.delete(db.tests)..where((x) => x.id.equals(t.id))).go();
|
||||
}
|
||||
|
||||
if (packId != null) {
|
||||
await (db.delete(db.cardPacks)..where((p) => p.id.equals(packId!)))
|
||||
.go();
|
||||
await (db.delete(
|
||||
db.cardPacks,
|
||||
)..where((p) => p.id.equals(packId!))).go();
|
||||
}
|
||||
if (cardId != null) {
|
||||
await (db.delete(db.gameCards)..where((c) => c.id.equals(cardId!)))
|
||||
.go();
|
||||
await (db.delete(
|
||||
db.gameCards,
|
||||
)..where((c) => c.id.equals(cardId!))).go();
|
||||
}
|
||||
|
||||
packId = null;
|
||||
|
|
@ -121,18 +124,20 @@ void main() {
|
|||
await testManager.updateGeneratedTests(packModel);
|
||||
|
||||
final packTests = await db.testDao.getTestsByPackId(packId!);
|
||||
final generated =
|
||||
packTests.where((t) => t.version == 'generated').toList();
|
||||
final generated = packTests
|
||||
.where((t) => t.version == 'generated')
|
||||
.toList();
|
||||
|
||||
expect(generated, hasLength(1));
|
||||
|
||||
final linkedPackId =
|
||||
await db.testDao.getPackIdForTest(generated.single.id);
|
||||
final linkedPackId = await db.testDao.getPackIdForTest(
|
||||
generated.single.id,
|
||||
);
|
||||
expect(linkedPackId, equals(packId));
|
||||
|
||||
final relations = await (db.select(db.testPackRelations)
|
||||
..where((r) => r.packId.equals(packId!)))
|
||||
.get();
|
||||
final relations = await (db.select(
|
||||
db.testPackRelations,
|
||||
)..where((r) => r.packId.equals(packId!))).get();
|
||||
expect(relations, hasLength(1));
|
||||
expect(relations.single.testId, equals(generated.single.id));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import 'package:mnemo_cards_backend/database/tables/packs.dart';
|
|||
import 'package:mnemo_cards_backend/database/tables/relations.dart';
|
||||
|
||||
/// Unit тесты для WordStatisticsDao
|
||||
///
|
||||
///
|
||||
/// Требования:
|
||||
/// - PostgreSQL должен быть запущен на localhost:5432
|
||||
/// - Тестовая БД: mnemo_cards_test
|
||||
|
|
@ -22,7 +22,8 @@ void main() {
|
|||
// Подключение к тестовой БД
|
||||
// Можно использовать переменные окружения для настройки
|
||||
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 username = Platform.environment['TEST_DB_USER'] ?? 'postgres';
|
||||
final password = Platform.environment['TEST_DB_PASSWORD'] ?? 'postgres';
|
||||
|
|
@ -51,9 +52,7 @@ void main() {
|
|||
|
||||
// 2. Создать UserData
|
||||
await db.userDao.createUserData(
|
||||
UserDatasCompanion.insert(
|
||||
userId: testUserId,
|
||||
),
|
||||
UserDatasCompanion.insert(userId: testUserId),
|
||||
);
|
||||
|
||||
// 3. Создать пак
|
||||
|
|
@ -194,14 +193,14 @@ void main() {
|
|||
);
|
||||
|
||||
// Soft delete
|
||||
await (db.update(db.wordStatistics)
|
||||
..where((w) => w.id.equals(stats.id)))
|
||||
.write(
|
||||
WordStatisticsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
await (db.update(
|
||||
db.wordStatistics,
|
||||
)..where((w) => w.id.equals(stats.id))).write(
|
||||
WordStatisticsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
|
||||
final result = await db.wordStatisticsDao.getByUserAndCard(
|
||||
testUserId,
|
||||
|
|
@ -389,7 +388,8 @@ void main() {
|
|||
// Создать другого пользователя
|
||||
final user2 = await db.userDao.createUser(
|
||||
UsersCompanion.insert(
|
||||
externalUserId: 'test_user_2_${DateTime.now().millisecondsSinceEpoch}',
|
||||
externalUserId:
|
||||
'test_user_2_${DateTime.now().millisecondsSinceEpoch}',
|
||||
name: Value('Test User 2'),
|
||||
),
|
||||
);
|
||||
|
|
@ -444,14 +444,14 @@ void main() {
|
|||
);
|
||||
|
||||
// Удалить одну запись
|
||||
await (db.update(db.wordStatistics)
|
||||
..where((w) => w.id.equals(stats1.id)))
|
||||
.write(
|
||||
WordStatisticsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
await (db.update(
|
||||
db.wordStatistics,
|
||||
)..where((w) => w.id.equals(stats1.id))).write(
|
||||
WordStatisticsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
|
||||
// selectActive должен вернуть только активную запись
|
||||
final activeStats = await db.wordStatisticsDao.selectActive().get();
|
||||
|
|
@ -468,14 +468,14 @@ void main() {
|
|||
);
|
||||
|
||||
// Удалить запись
|
||||
await (db.update(db.wordStatistics)
|
||||
..where((w) => w.id.equals(stats.id)))
|
||||
.write(
|
||||
WordStatisticsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
await (db.update(
|
||||
db.wordStatistics,
|
||||
)..where((w) => w.id.equals(stats.id))).write(
|
||||
WordStatisticsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
|
||||
final result = await db.wordStatisticsDao.getActiveById(stats.id);
|
||||
expect(result, isNull);
|
||||
|
|
|
|||
|
|
@ -6,13 +6,16 @@ void main() {
|
|||
test('AppDatabase can be instantiated', () {
|
||||
// This test verifies that the database class can be created
|
||||
// Actual connection will be tested with real PostgreSQL instance
|
||||
expect(() => AppDatabase.connect(
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
database: 'test_db',
|
||||
username: 'test_user',
|
||||
password: 'test_pass',
|
||||
), returnsNormally);
|
||||
expect(
|
||||
() => AppDatabase.connect(
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
database: 'test_db',
|
||||
username: 'test_user',
|
||||
password: 'test_pass',
|
||||
),
|
||||
returnsNormally,
|
||||
);
|
||||
});
|
||||
|
||||
test('All tables are registered', () {
|
||||
|
|
@ -24,7 +27,7 @@ void main() {
|
|||
username: 'test_user',
|
||||
password: 'test_pass',
|
||||
);
|
||||
|
||||
|
||||
// Check that DAOs are available
|
||||
expect(db.userDao, isNotNull);
|
||||
expect(db.packDao, isNotNull);
|
||||
|
|
|
|||
|
|
@ -256,11 +256,15 @@ void main() {
|
|||
|
||||
expect(unlocked.unlockedAt, isNotNull);
|
||||
expect(
|
||||
unlocked.unlockedAt!.isAfter(beforeUnlock.subtract(const Duration(seconds: 1))),
|
||||
unlocked.unlockedAt!.isAfter(
|
||||
beforeUnlock.subtract(const Duration(seconds: 1)),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
unlocked.unlockedAt!.isBefore(afterUnlock.add(const Duration(seconds: 1))),
|
||||
unlocked.unlockedAt!.isBefore(
|
||||
afterUnlock.add(const Duration(seconds: 1)),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
|
@ -526,11 +530,7 @@ void main() {
|
|||
|
||||
group('edge cases', () {
|
||||
test('handles empty strings', () {
|
||||
final model = AchievementModel(
|
||||
id: '',
|
||||
title: '',
|
||||
description: '',
|
||||
);
|
||||
final model = AchievementModel(id: '', title: '', description: '');
|
||||
|
||||
expect(model.id, '');
|
||||
expect(model.title, '');
|
||||
|
|
|
|||
|
|
@ -83,10 +83,7 @@ void main() {
|
|||
});
|
||||
|
||||
test('converts model with null dates to DTO', () {
|
||||
final model = PackProgressModel(
|
||||
packId: 'test_pack',
|
||||
totalCards: 10,
|
||||
);
|
||||
final model = PackProgressModel(packId: 'test_pack', totalCards: 10);
|
||||
|
||||
final dto = model.toDto();
|
||||
|
||||
|
|
@ -134,10 +131,7 @@ void main() {
|
|||
});
|
||||
|
||||
test('creates model from DTO with null dates', () {
|
||||
final dto = PackProgressDto(
|
||||
packId: 'test_pack',
|
||||
totalCards: 10,
|
||||
);
|
||||
final dto = PackProgressDto(packId: 'test_pack', totalCards: 10);
|
||||
|
||||
final model = PackProgressModel.fromDto(dto);
|
||||
|
||||
|
|
@ -208,10 +202,7 @@ void main() {
|
|||
});
|
||||
|
||||
test('adds new card to attempts map', () {
|
||||
final model = PackProgressModel(
|
||||
packId: 'test_pack',
|
||||
totalCards: 10,
|
||||
);
|
||||
final model = PackProgressModel(packId: 'test_pack', totalCards: 10);
|
||||
|
||||
final updated = model.updateCardProgress(
|
||||
cardId: 'new_card',
|
||||
|
|
@ -238,9 +229,7 @@ void main() {
|
|||
});
|
||||
|
||||
test('sets lastStudyDate to current time', () {
|
||||
final model = PackProgressModel(
|
||||
packId: 'test_pack',
|
||||
);
|
||||
final model = PackProgressModel(packId: 'test_pack');
|
||||
|
||||
final beforeUpdate = DateTime.now();
|
||||
final updated = model.updateCardProgress(
|
||||
|
|
@ -252,19 +241,21 @@ void main() {
|
|||
|
||||
expect(updated.lastStudyDate, isNotNull);
|
||||
expect(
|
||||
updated.lastStudyDate!.isAfter(beforeUpdate.subtract(const Duration(seconds: 1))),
|
||||
updated.lastStudyDate!.isAfter(
|
||||
beforeUpdate.subtract(const Duration(seconds: 1)),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
updated.lastStudyDate!.isBefore(afterUpdate.add(const Duration(seconds: 1))),
|
||||
updated.lastStudyDate!.isBefore(
|
||||
afterUpdate.add(const Duration(seconds: 1)),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('sets firstStudyDate if null', () {
|
||||
final model = PackProgressModel(
|
||||
packId: 'test_pack',
|
||||
);
|
||||
final model = PackProgressModel(packId: 'test_pack');
|
||||
|
||||
final updated = model.updateCardProgress(
|
||||
cardId: 'card1',
|
||||
|
|
@ -312,10 +303,7 @@ void main() {
|
|||
});
|
||||
|
||||
test('returns true when both learnedCards and studyTimeMinutes > 0', () {
|
||||
final model = PackProgressModel(
|
||||
learnedCards: 5,
|
||||
studyTimeMinutes: 10,
|
||||
);
|
||||
final model = PackProgressModel(learnedCards: 5, studyTimeMinutes: 10);
|
||||
|
||||
expect(model.hasStarted, isTrue);
|
||||
});
|
||||
|
|
@ -335,37 +323,25 @@ void main() {
|
|||
});
|
||||
|
||||
test('calculates progress correctly', () {
|
||||
final model = PackProgressModel(
|
||||
totalCards: 10,
|
||||
learnedCards: 5,
|
||||
);
|
||||
final model = PackProgressModel(totalCards: 10, learnedCards: 5);
|
||||
|
||||
expect(model.progress, 0.5);
|
||||
});
|
||||
|
||||
test('calculates progress for partial completion', () {
|
||||
final model = PackProgressModel(
|
||||
totalCards: 100,
|
||||
learnedCards: 33,
|
||||
);
|
||||
final model = PackProgressModel(totalCards: 100, learnedCards: 33);
|
||||
|
||||
expect(model.progress, 0.33);
|
||||
});
|
||||
|
||||
test('returns 1.0 when all cards learned', () {
|
||||
final model = PackProgressModel(
|
||||
totalCards: 10,
|
||||
learnedCards: 10,
|
||||
);
|
||||
final model = PackProgressModel(totalCards: 10, learnedCards: 10);
|
||||
|
||||
expect(model.progress, 1.0);
|
||||
});
|
||||
|
||||
test('handles learnedCards exceeding totalCards', () {
|
||||
final model = PackProgressModel(
|
||||
totalCards: 10,
|
||||
learnedCards: 15,
|
||||
);
|
||||
final model = PackProgressModel(totalCards: 10, learnedCards: 15);
|
||||
|
||||
expect(model.progress, 1.5);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,10 +8,7 @@ void main() {
|
|||
test('creates model with required parameters', () {
|
||||
final startTime = DateTime.now();
|
||||
|
||||
final model = StudySessionModel(
|
||||
userId: 1,
|
||||
startTime: startTime,
|
||||
);
|
||||
final model = StudySessionModel(userId: 1, startTime: startTime);
|
||||
|
||||
expect(model.userId, 1);
|
||||
expect(model.startTime, startTime);
|
||||
|
|
@ -54,10 +51,7 @@ void main() {
|
|||
test('creates model with default values', () {
|
||||
final startTime = DateTime.now();
|
||||
|
||||
final model = StudySessionModel(
|
||||
userId: 1,
|
||||
startTime: startTime,
|
||||
);
|
||||
final model = StudySessionModel(userId: 1, startTime: startTime);
|
||||
|
||||
expect(model.wordsLearned, 0);
|
||||
expect(model.testsCompleted, 0);
|
||||
|
|
@ -97,10 +91,7 @@ void main() {
|
|||
test('converts model with null values to DTO', () {
|
||||
final startTime = DateTime.now();
|
||||
|
||||
final model = StudySessionModel(
|
||||
userId: 1,
|
||||
startTime: startTime,
|
||||
);
|
||||
final model = StudySessionModel(userId: 1, startTime: startTime);
|
||||
|
||||
final dto = model.toDto();
|
||||
|
||||
|
|
@ -143,9 +134,7 @@ void main() {
|
|||
test('creates model from DTO with null values', () {
|
||||
final startTime = DateTime.now();
|
||||
|
||||
final dto = StudySessionDto(
|
||||
startTime: startTime,
|
||||
);
|
||||
final dto = StudySessionDto(startTime: startTime);
|
||||
|
||||
final model = StudySessionModel.fromDto(dto, 1);
|
||||
|
||||
|
|
@ -190,10 +179,7 @@ void main() {
|
|||
|
||||
group('isActive', () {
|
||||
test('returns true when endTime is null', () {
|
||||
final model = StudySessionModel(
|
||||
userId: 1,
|
||||
startTime: DateTime.now(),
|
||||
);
|
||||
final model = StudySessionModel(userId: 1, startTime: DateTime.now());
|
||||
|
||||
expect(model.isActive, isTrue);
|
||||
});
|
||||
|
|
@ -227,10 +213,7 @@ void main() {
|
|||
test('calculates duration using current time when active', () {
|
||||
final startTime = DateTime.now().subtract(const Duration(minutes: 15));
|
||||
|
||||
final model = StudySessionModel(
|
||||
userId: 1,
|
||||
startTime: startTime,
|
||||
);
|
||||
final model = StudySessionModel(userId: 1, startTime: startTime);
|
||||
|
||||
final duration = model.duration;
|
||||
expect(duration.inMinutes, greaterThanOrEqualTo(14));
|
||||
|
|
@ -287,10 +270,7 @@ void main() {
|
|||
});
|
||||
|
||||
test('returns General Study when neither is set', () {
|
||||
final model = StudySessionModel(
|
||||
userId: 1,
|
||||
startTime: DateTime.now(),
|
||||
);
|
||||
final model = StudySessionModel(userId: 1, startTime: DateTime.now());
|
||||
|
||||
expect(model.sessionType, 'General Study');
|
||||
});
|
||||
|
|
@ -416,10 +396,7 @@ void main() {
|
|||
test('sets endTime to current time', () {
|
||||
final startTime = DateTime.now().subtract(const Duration(minutes: 10));
|
||||
|
||||
final model = StudySessionModel(
|
||||
userId: 1,
|
||||
startTime: startTime,
|
||||
);
|
||||
final model = StudySessionModel(userId: 1, startTime: startTime);
|
||||
|
||||
final beforeEnd = DateTime.now();
|
||||
final ended = model.end(
|
||||
|
|
@ -431,7 +408,9 @@ void main() {
|
|||
|
||||
expect(ended.endTime, isNotNull);
|
||||
expect(
|
||||
ended.endTime!.isAfter(beforeEnd.subtract(const Duration(seconds: 1))),
|
||||
ended.endTime!.isAfter(
|
||||
beforeEnd.subtract(const Duration(seconds: 1)),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
|
|
@ -441,10 +420,7 @@ void main() {
|
|||
});
|
||||
|
||||
test('updates statistics correctly', () {
|
||||
final model = StudySessionModel(
|
||||
userId: 1,
|
||||
startTime: DateTime.now(),
|
||||
);
|
||||
final model = StudySessionModel(userId: 1, startTime: DateTime.now());
|
||||
|
||||
final ended = model.end(
|
||||
wordsLearned: 15,
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ void main() {
|
|||
|
||||
final json = user.toJson();
|
||||
final deserialized = UserModel.fromJson(json);
|
||||
|
||||
|
||||
expect(deserialized.email, equals('user@example.com'));
|
||||
expect(deserialized.telegram, equals('@testuser'));
|
||||
expect(deserialized.admin, isTrue);
|
||||
|
|
|
|||
|
|
@ -8,15 +8,21 @@ void main() {
|
|||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO5n5p0AAAAASUVORK5CYII=';
|
||||
|
||||
group('CardImageStorage', () {
|
||||
test('sanitizeCardsFileName strips cards/ prefix and rejects traversal', () {
|
||||
expect(CardImageStorage.sanitizeCardsFileName('cards/a.png'), 'a.png');
|
||||
expect(CardImageStorage.sanitizeCardsFileName('/cards/a.png'), 'a.png');
|
||||
expect(CardImageStorage.sanitizeCardsFileName('a.png'), 'a.png');
|
||||
test(
|
||||
'sanitizeCardsFileName strips cards/ prefix and rejects traversal',
|
||||
() {
|
||||
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('cards/../a.png'), isNull);
|
||||
expect(CardImageStorage.sanitizeCardsFileName('cards/a/b.png'), isNull);
|
||||
});
|
||||
expect(CardImageStorage.sanitizeCardsFileName('../a.png'), isNull);
|
||||
expect(
|
||||
CardImageStorage.sanitizeCardsFileName('cards/../a.png'),
|
||||
isNull,
|
||||
);
|
||||
expect(CardImageStorage.sanitizeCardsFileName('cards/a/b.png'), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
test('tryParseBase64Image parses data: url and detects png', () {
|
||||
final parsed = CardImageStorage.tryParseBase64Image(
|
||||
|
|
@ -64,4 +70,3 @@ void main() {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,15 +7,18 @@ import 'package:test/test.dart';
|
|||
|
||||
void main() {
|
||||
group('VoiceStorage', () {
|
||||
test('sanitizeVoiceFileName strips voice/ prefix and rejects traversal', () {
|
||||
expect(VoiceStorage.sanitizeVoiceFileName('voice/a.mp3'), 'a.mp3');
|
||||
expect(VoiceStorage.sanitizeVoiceFileName('/voice/a.mp3'), 'a.mp3');
|
||||
expect(VoiceStorage.sanitizeVoiceFileName('a.mp3'), 'a.mp3');
|
||||
test(
|
||||
'sanitizeVoiceFileName strips voice/ prefix and rejects traversal',
|
||||
() {
|
||||
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('voice/../a.mp3'), isNull);
|
||||
expect(VoiceStorage.sanitizeVoiceFileName('voice/a/b.mp3'), isNull);
|
||||
});
|
||||
expect(VoiceStorage.sanitizeVoiceFileName('../a.mp3'), isNull);
|
||||
expect(VoiceStorage.sanitizeVoiceFileName('voice/../a.mp3'), isNull);
|
||||
expect(VoiceStorage.sanitizeVoiceFileName('voice/a/b.mp3'), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
test('tryParseBase64Audio parses data: url and detects mp3', () {
|
||||
final bytes = Uint8List.fromList([0x49, 0x44, 0x33, 0x03, 0x00, 0x00]);
|
||||
|
|
@ -64,4 +67,3 @@ void main() {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ import 'package:mnemo_cards_backend/statistics/word_statistics_manager.dart';
|
|||
import 'package:mnemo_cards_backend/statistics/statistics_calculator.dart';
|
||||
|
||||
/// Smoke тесты для проверки базовой функциональности после деплоя
|
||||
///
|
||||
///
|
||||
/// Эти тесты проверяют:
|
||||
/// - Создание БД и таблиц
|
||||
/// - Основные CRUD операции
|
||||
/// - Интеграцию WordStatisticsManager с TestManager
|
||||
/// - Расчет статистики через StatisticsCalculator
|
||||
///
|
||||
///
|
||||
/// Требования:
|
||||
/// - PostgreSQL должен быть запущен
|
||||
/// - Тестовая БД: mnemo_cards_test
|
||||
|
|
@ -28,7 +28,8 @@ void main() {
|
|||
setUpAll(() async {
|
||||
// Подключение к тестовой БД
|
||||
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 username = Platform.environment['TEST_DB_USER'] ?? 'postgres';
|
||||
final password = Platform.environment['TEST_DB_PASSWORD'] ?? 'postgres';
|
||||
|
|
@ -47,7 +48,8 @@ void main() {
|
|||
// Создать тестовые данные
|
||||
final user = await db.userDao.createUser(
|
||||
UsersCompanion.insert(
|
||||
externalUserId: 'smoke_test_user_${DateTime.now().millisecondsSinceEpoch}',
|
||||
externalUserId:
|
||||
'smoke_test_user_${DateTime.now().millisecondsSinceEpoch}',
|
||||
name: Value('Smoke Test User'),
|
||||
email: Value('smoke@test.com'),
|
||||
),
|
||||
|
|
@ -156,7 +158,10 @@ void main() {
|
|||
expect(packProgress.packId, equals(testPackId));
|
||||
expect(packProgress.totalCards, equals(10));
|
||||
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 {
|
||||
|
|
@ -172,16 +177,20 @@ void main() {
|
|||
),
|
||||
);
|
||||
|
||||
final studyDates = await statisticsCalculator.calculateStudyDates(testUserId);
|
||||
final studyDates = await statisticsCalculator.calculateStudyDates(
|
||||
testUserId,
|
||||
);
|
||||
|
||||
expect(studyDates, isNotEmpty);
|
||||
// Проверить что дата сегодняшнего дня присутствует
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
expect(
|
||||
studyDates.any((d) =>
|
||||
d.year == today.year &&
|
||||
d.month == today.month &&
|
||||
d.day == today.day),
|
||||
studyDates.any(
|
||||
(d) =>
|
||||
d.year == today.year &&
|
||||
d.month == today.month &&
|
||||
d.day == today.day,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
|
@ -204,18 +213,22 @@ void main() {
|
|||
userId: testUserId,
|
||||
packId: testPackId,
|
||||
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,
|
||||
),
|
||||
);
|
||||
|
||||
final categoryMinutes = await statisticsCalculator.calculateCategoryMinutes(
|
||||
testUserId,
|
||||
);
|
||||
final categoryMinutes = await statisticsCalculator
|
||||
.calculateCategoryMinutes(testUserId);
|
||||
|
||||
expect(categoryMinutes, isNotEmpty);
|
||||
// Должно быть минимум 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));
|
||||
});
|
||||
|
||||
|
|
@ -229,14 +242,14 @@ void main() {
|
|||
);
|
||||
|
||||
// Soft delete
|
||||
await (db.update(db.wordStatistics)
|
||||
..where((w) => w.id.equals(stats.id)))
|
||||
.write(
|
||||
WordStatisticsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
await (db.update(
|
||||
db.wordStatistics,
|
||||
)..where((w) => w.id.equals(stats.id))).write(
|
||||
WordStatisticsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
deletedAt: Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
|
||||
// Проверить что запись не возвращается через getActiveById
|
||||
final activeRecord = await db.wordStatisticsDao.getActiveById(stats.id);
|
||||
|
|
|
|||
|
|
@ -34,7 +34,10 @@ void main() {
|
|||
|
||||
expect(sampleAchievementIds.length, equals(19));
|
||||
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', () {
|
||||
|
|
@ -96,8 +99,19 @@ void main() {
|
|||
|
||||
test('Progressive achievements have increasing difficulty', () {
|
||||
// Test that later achievements require more progress
|
||||
const wordAchievements = ['words_10', 'words_50', 'words_100', 'words_500', 'words_1000'];
|
||||
const streakAchievements = ['streak_3', 'streak_7', 'streak_30', 'streak_100'];
|
||||
const wordAchievements = [
|
||||
'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(streakAchievements.length, equals(4));
|
||||
|
|
@ -136,11 +150,11 @@ void main() {
|
|||
|
||||
final progress1 = calculateProgressMap(5, 2);
|
||||
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);
|
||||
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', () {
|
||||
|
|
|
|||
|
|
@ -34,12 +34,14 @@ class MockIsar extends Fake implements Isar {
|
|||
}
|
||||
|
||||
@override
|
||||
QueryBuilder<StudySessionModel, StudySessionModel, QWhereClause> get studySessionModels {
|
||||
QueryBuilder<StudySessionModel, StudySessionModel, QWhereClause>
|
||||
get studySessionModels {
|
||||
return MockQueryBuilder(_sessions);
|
||||
}
|
||||
|
||||
@override
|
||||
QueryBuilder<AchievementModel, AchievementModel, QWhereClause> get achievementModels {
|
||||
QueryBuilder<AchievementModel, AchievementModel, QWhereClause>
|
||||
get achievementModels {
|
||||
return MockQueryBuilder([]);
|
||||
}
|
||||
|
||||
|
|
@ -133,18 +135,12 @@ void main() {
|
|||
testId: 'integration_test',
|
||||
sessionToken: 'test_session_${DateTime.now().millisecondsSinceEpoch}',
|
||||
packId: 'test_pack',
|
||||
words: TestWordsDto(words: [
|
||||
TestWordDto(
|
||||
word: 'hello',
|
||||
correct: 1,
|
||||
incorrect: 0,
|
||||
),
|
||||
TestWordDto(
|
||||
word: 'world',
|
||||
correct: 1,
|
||||
incorrect: 0,
|
||||
),
|
||||
]),
|
||||
words: TestWordsDto(
|
||||
words: [
|
||||
TestWordDto(word: 'hello', correct: 1, incorrect: 0),
|
||||
TestWordDto(word: 'world', correct: 1, incorrect: 0),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// Simulate the addTestStatistics call (without the actual database save)
|
||||
|
|
@ -208,21 +204,25 @@ void main() {
|
|||
testId: 'test1',
|
||||
sessionToken: 'session1',
|
||||
packId: 'pack1',
|
||||
words: TestWordsDto(words: [
|
||||
TestWordDto(word: 'word1', correct: 1, incorrect: 0),
|
||||
TestWordDto(word: 'word2', correct: 0, incorrect: 1),
|
||||
]),
|
||||
words: TestWordsDto(
|
||||
words: [
|
||||
TestWordDto(word: 'word1', correct: 1, incorrect: 0),
|
||||
TestWordDto(word: 'word2', correct: 0, incorrect: 1),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final testStats2 = TestStatisticsDto(
|
||||
testId: 'test2',
|
||||
sessionToken: 'session2',
|
||||
packId: 'pack1',
|
||||
words: TestWordsDto(words: [
|
||||
TestWordDto(word: 'word3', correct: 1, incorrect: 0),
|
||||
TestWordDto(word: 'word4', correct: 1, incorrect: 0),
|
||||
TestWordDto(word: 'word5', correct: 0, incorrect: 1),
|
||||
]),
|
||||
words: TestWordsDto(
|
||||
words: [
|
||||
TestWordDto(word: 'word3', correct: 1, incorrect: 0),
|
||||
TestWordDto(word: 'word4', correct: 1, incorrect: 0),
|
||||
TestWordDto(word: 'word5', correct: 0, incorrect: 1),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// Test session tracking across multiple tests
|
||||
|
|
@ -233,7 +233,9 @@ void main() {
|
|||
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));
|
||||
|
||||
await sessionTracker.updateSessionProgress(
|
||||
|
|
@ -256,7 +258,10 @@ void main() {
|
|||
];
|
||||
|
||||
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
|
||||
expect(progress['words_10'], equals(0.5)); // 5/10 = 0.5
|
||||
|
|
@ -290,7 +295,10 @@ void main() {
|
|||
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
|
||||
final sessionId = await sessionTracker.getOrCreateSession(999);
|
||||
|
|
@ -365,11 +373,11 @@ void main() {
|
|||
|
||||
// Add some words
|
||||
final updatedUserData1 = testUser.userData.value!.copyWith(
|
||||
words: List.generate(6, (i) => WordStatisticsModel(
|
||||
word: 'word$i',
|
||||
correct: 1.0,
|
||||
incorrect: 0.0,
|
||||
)),
|
||||
words: List.generate(
|
||||
6,
|
||||
(i) =>
|
||||
WordStatisticsModel(word: 'word$i', correct: 1.0, incorrect: 0.0),
|
||||
),
|
||||
currentStreak: 2,
|
||||
);
|
||||
|
||||
|
|
@ -379,15 +387,15 @@ void main() {
|
|||
);
|
||||
|
||||
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
|
||||
final updatedUserData2 = updatedUserData1.copyWith(
|
||||
words: List.generate(8, (i) => WordStatisticsModel(
|
||||
word: 'word$i',
|
||||
correct: 1.0,
|
||||
incorrect: 0.0,
|
||||
)),
|
||||
words: List.generate(
|
||||
8,
|
||||
(i) =>
|
||||
WordStatisticsModel(word: 'word$i', correct: 1.0, incorrect: 0.0),
|
||||
),
|
||||
currentStreak: 3,
|
||||
);
|
||||
|
||||
|
|
@ -397,7 +405,10 @@ void main() {
|
|||
);
|
||||
|
||||
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
|
||||
// 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', () {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,11 @@ void main() {
|
|||
final yesterday = today.subtract(const Duration(days: 1));
|
||||
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
|
||||
});
|
||||
|
||||
|
|
@ -63,9 +67,15 @@ void main() {
|
|||
test('handles dates with different times', () {
|
||||
final todayMorning = DateTime.now().copyWith(hour: 9);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
@ -122,9 +132,7 @@ void main() {
|
|||
studyTimeMinutes: 45,
|
||||
);
|
||||
|
||||
final userData = UserDataModel(
|
||||
packProgress: [pack1, pack2],
|
||||
);
|
||||
final userData = UserDataModel(packProgress: [pack1, pack2]);
|
||||
|
||||
final result = calculator.calculateTotalStudyTime(userData);
|
||||
expect(result, 75); // 30 + 45
|
||||
|
|
@ -224,10 +232,7 @@ void main() {
|
|||
],
|
||||
);
|
||||
|
||||
final result = calculator.getTimelineStatistics(
|
||||
userData,
|
||||
period: 'week',
|
||||
);
|
||||
final result = calculator.getTimelineStatistics(userData, period: 'week');
|
||||
|
||||
expect(result['period'], 'week');
|
||||
expect(result.containsKey('totalDays'), true);
|
||||
|
|
@ -247,10 +252,17 @@ void main() {
|
|||
|
||||
test('calculates level based on activity', () {
|
||||
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
|
||||
packProgress: [
|
||||
PackProgressModel(packId: 'pack1', totalCards: 10, learnedCards: 10), // 1 completed pack
|
||||
PackProgressModel(
|
||||
packId: 'pack1',
|
||||
totalCards: 10,
|
||||
learnedCards: 10,
|
||||
), // 1 completed pack
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import 'package:mnemo_cards_backend/database/tables/users.dart';
|
|||
import 'package:mnemo_cards_backend/database/tables/packs.dart';
|
||||
|
||||
/// Unit тесты для WordStatisticsManager
|
||||
///
|
||||
///
|
||||
/// Требования:
|
||||
/// - PostgreSQL должен быть запущен на localhost:5432
|
||||
/// - Тестовая БД: mnemo_cards_test
|
||||
|
|
@ -20,7 +20,8 @@ void main() {
|
|||
setUpAll(() async {
|
||||
// Подключение к тестовой БД
|
||||
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 username = Platform.environment['TEST_DB_USER'] ?? 'postgres';
|
||||
final password = Platform.environment['TEST_DB_PASSWORD'] ?? 'postgres';
|
||||
|
|
@ -242,10 +243,7 @@ void main() {
|
|||
isCorrect: true,
|
||||
);
|
||||
|
||||
final packStats = await manager.getPackStatistics(
|
||||
testUserId,
|
||||
pack.id,
|
||||
);
|
||||
final packStats = await manager.getPackStatistics(testUserId, pack.id);
|
||||
|
||||
expect(packStats.length, equals(2));
|
||||
expect(
|
||||
|
|
@ -263,10 +261,7 @@ void main() {
|
|||
),
|
||||
);
|
||||
|
||||
final packStats = await manager.getPackStatistics(
|
||||
testUserId,
|
||||
pack.id,
|
||||
);
|
||||
final packStats = await manager.getPackStatistics(testUserId, pack.id);
|
||||
|
||||
expect(packStats, isEmpty);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,14 +4,8 @@ import 'package:mnemo_cards_backend/storage/minio_config.dart';
|
|||
void main() {
|
||||
group('MinioConfig', () {
|
||||
test('should have correct bucket names', () {
|
||||
expect(
|
||||
MinioConfig.cardImagesBucket,
|
||||
equals('card-images'),
|
||||
);
|
||||
expect(
|
||||
MinioConfig.voiceAudioBucket,
|
||||
equals('voice-audio'),
|
||||
);
|
||||
expect(MinioConfig.cardImagesBucket, equals('card-images'));
|
||||
expect(MinioConfig.voiceAudioBucket, equals('voice-audio'));
|
||||
});
|
||||
|
||||
test('should have correct presigned URL expiry', () {
|
||||
|
|
@ -34,7 +28,7 @@ void main() {
|
|||
// Test that bucket names are correctly defined
|
||||
expect(MinioConfig.cardImagesBucket, isNotEmpty);
|
||||
expect(MinioConfig.voiceAudioBucket, isNotEmpty);
|
||||
|
||||
|
||||
// Test that bucket names are different
|
||||
expect(
|
||||
MinioConfig.cardImagesBucket,
|
||||
|
|
@ -44,14 +38,8 @@ void main() {
|
|||
|
||||
test('should have reasonable presigned URL expiry', () {
|
||||
// Presigned URLs should expire after 4 hours (14400 seconds)
|
||||
expect(
|
||||
MinioConfig.presignedUrlExpirySeconds,
|
||||
equals(14400),
|
||||
);
|
||||
expect(
|
||||
MinioConfig.presignedUrlExpirySeconds,
|
||||
greaterThan(0),
|
||||
);
|
||||
expect(MinioConfig.presignedUrlExpirySeconds, equals(14400));
|
||||
expect(MinioConfig.presignedUrlExpirySeconds, greaterThan(0));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,4 +79,3 @@ void main() {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,9 +46,7 @@ void main() {
|
|||
final generator = SimpleQuestionGenerator(
|
||||
data,
|
||||
seed: 1,
|
||||
possibleTypes: {
|
||||
SimpleQuestionType.audio_translation,
|
||||
},
|
||||
possibleTypes: {SimpleQuestionType.audio_translation},
|
||||
);
|
||||
|
||||
final answerCard = data.items.first; // card_1 with UUID audio
|
||||
|
|
@ -82,9 +80,7 @@ void main() {
|
|||
final generator = SimpleQuestionGenerator(
|
||||
data,
|
||||
seed: 1,
|
||||
possibleTypes: {
|
||||
SimpleQuestionType.audio_translation,
|
||||
},
|
||||
possibleTypes: {SimpleQuestionType.audio_translation},
|
||||
);
|
||||
|
||||
final answerCard = data.items.first;
|
||||
|
|
@ -116,9 +112,7 @@ void main() {
|
|||
final generator = SimpleQuestionGenerator(
|
||||
data,
|
||||
seed: 1,
|
||||
possibleTypes: {
|
||||
SimpleQuestionType.audio_translation,
|
||||
},
|
||||
possibleTypes: {SimpleQuestionType.audio_translation},
|
||||
);
|
||||
|
||||
final answerCard = data.items.first;
|
||||
|
|
@ -151,9 +145,7 @@ void main() {
|
|||
final generator = SimpleQuestionGenerator(
|
||||
data,
|
||||
seed: 1,
|
||||
possibleTypes: {
|
||||
SimpleQuestionType.audio_images,
|
||||
},
|
||||
possibleTypes: {SimpleQuestionType.audio_images},
|
||||
);
|
||||
|
||||
final answerCard = data.items.first;
|
||||
|
|
@ -167,4 +159,4 @@ void main() {
|
|||
expect(sq.text, isNull); // No text for audio questions
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,12 +17,13 @@ void main() {
|
|||
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
|
||||
final port =
|
||||
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
|
||||
final database =
|
||||
Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
||||
final username = Platform.environment['TEST_DB_USER'] ??
|
||||
final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
|
||||
final username =
|
||||
Platform.environment['TEST_DB_USER'] ??
|
||||
Platform.environment['DB_USER'] ??
|
||||
'mnemo_user';
|
||||
final password = Platform.environment['TEST_DB_PASSWORD'] ??
|
||||
final password =
|
||||
Platform.environment['TEST_DB_PASSWORD'] ??
|
||||
Platform.environment['DB_PASSWORD'] ??
|
||||
'';
|
||||
|
||||
|
|
@ -38,13 +39,13 @@ void main() {
|
|||
await Migrator(db).createAll();
|
||||
|
||||
// Initialize MinioService
|
||||
final minioEndpoint = Platform.environment['MINIO_ENDPOINT'] ??
|
||||
'localhost:9000';
|
||||
final minioAccessKey = Platform.environment['MINIO_ACCESS_KEY'] ??
|
||||
'minioadmin';
|
||||
final minioSecretKey = Platform.environment['MINIO_SECRET_KEY'] ??
|
||||
'minioadmin';
|
||||
|
||||
final minioEndpoint =
|
||||
Platform.environment['MINIO_ENDPOINT'] ?? 'localhost:9000';
|
||||
final minioAccessKey =
|
||||
Platform.environment['MINIO_ACCESS_KEY'] ?? 'minioadmin';
|
||||
final minioSecretKey =
|
||||
Platform.environment['MINIO_SECRET_KEY'] ?? 'minioadmin';
|
||||
|
||||
minioService = MinioService(
|
||||
endpoint: minioEndpoint,
|
||||
accessKey: minioAccessKey,
|
||||
|
|
@ -71,99 +72,100 @@ void main() {
|
|||
testId = null;
|
||||
}
|
||||
if (packId != null) {
|
||||
await (db.delete(db.cardPacks)..where((p) => p.id.equals(packId!)))
|
||||
.go();
|
||||
await (db.delete(
|
||||
db.cardPacks,
|
||||
)..where((p) => p.id.equals(packId!))).go();
|
||||
packId = null;
|
||||
}
|
||||
if (cardId != null) {
|
||||
await (db.delete(db.gameCards)..where((c) => c.id.equals(cardId!)))
|
||||
.go();
|
||||
await (db.delete(
|
||||
db.gameCards,
|
||||
)..where((c) => c.id.equals(cardId!))).go();
|
||||
cardId = null;
|
||||
}
|
||||
});
|
||||
|
||||
test('matrix question buttons should have imageUrl from card images',
|
||||
() async {
|
||||
// Create pack
|
||||
packId = await db.packDao.createPack(
|
||||
CardPacksCompanion.insert(
|
||||
title: 'Test Pack',
|
||||
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'),
|
||||
test(
|
||||
'matrix question buttons should have imageUrl from card images',
|
||||
() async {
|
||||
// Create pack
|
||||
packId = await db.packDao.createPack(
|
||||
CardPacksCompanion.insert(
|
||||
title: 'Test Pack',
|
||||
subtitle: 'subtitle',
|
||||
size: 1,
|
||||
),
|
||||
);
|
||||
} 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 {
|
||||
// Create pack
|
||||
|
|
@ -202,7 +204,7 @@ void main() {
|
|||
await db.testDao.linkTestToPack(testId!, packId!);
|
||||
|
||||
const imageObjectId = '87654321-4321-4321-4321-cba987654321';
|
||||
|
||||
|
||||
// Create question with image
|
||||
await db.testDao.createTestQuestion(
|
||||
TestQuestionsCompanion.insert(
|
||||
|
|
@ -222,10 +224,10 @@ void main() {
|
|||
|
||||
expect(result, isNotNull);
|
||||
expect(result!.questions, hasLength(1));
|
||||
|
||||
|
||||
final question = result.questions.first;
|
||||
expect(question.questionType, equals(TestQuestionType.input_buttons));
|
||||
|
||||
|
||||
// Check image and imageUrl
|
||||
if (question is InputButtonsTestQuestion) {
|
||||
expect(question.image, equals(imageObjectId));
|
||||
|
|
@ -264,7 +266,7 @@ void main() {
|
|||
);
|
||||
|
||||
const coverObjectId = 'aaaabbbb-cccc-dddd-eeee-ffff00001111';
|
||||
|
||||
|
||||
// Create test with cover
|
||||
testId = await db.testDao.createTest(
|
||||
TestsCompanion.insert(
|
||||
|
|
|
|||
|
|
@ -13,9 +13,7 @@ void main() async {
|
|||
username: 'mnemo_user',
|
||||
password: 'dev_password_change_me',
|
||||
),
|
||||
settings: pg.ConnectionSettings(
|
||||
sslMode: pg.SslMode.disable,
|
||||
),
|
||||
settings: pg.ConnectionSettings(sslMode: pg.SslMode.disable),
|
||||
);
|
||||
|
||||
print('✅ PostgreSQL connection successful');
|
||||
|
|
@ -27,9 +25,8 @@ void main() async {
|
|||
await connection.close();
|
||||
print('✅ Database connection closed');
|
||||
print('🎉 PostgreSQL backend can connect to database!');
|
||||
|
||||
} catch (e, s) {
|
||||
print('❌ Error: $e');
|
||||
print('Stack trace: $s');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,3 +55,4 @@ echo " - favicon.png (32x32)"
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue