From 4ed18398933dbf296b2cbc57669d43c7d752aa9f Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 8 Jan 2026 16:02:47 +0300 Subject: [PATCH] format and logs --- .../DATABASE_IMPROVEMENT_PLAN_DETAILED.md | 1 + .../FINAL_VERIFICATION_REPORT.md | 1 + .../docs/SHOULD_EXTRACT_DATABASE.md | 1 + .../fix_is_blacklisted_nulls.sql | 1 + .../lib/api/purchase/payment_manager.dart | 28 +- .../lib/api/purchase/yoo_money.dart | 106 +-- .../lib/api/v2/packs_api_v2.dart | 5 +- .../lib/api/v2/purchases_api_v2.dart | 79 +- .../daos/mixins/soft_delete_mixin.dart | 8 +- .../lib/database/daos/pack_dao.dart | 4 +- .../lib/database/daos/payment_dao.dart | 62 +- .../lib/database/daos/user_dao.dart | 29 +- .../lib/database/database.dart | 6 +- .../lib/packs/card_pack_drift_extension.dart | 2 +- .../lib/packs/pack_dto_converter.dart | 5 +- .../input_buttons_question_generator.dart | 26 +- .../001_remove_deprecated_fields.sql | 1 + .../migrations/002_add_enum_types.sql | 1 + .../migrations/003_add_composite_indexes.sql | 1 + .../admin_tests_api_v2_image_urls_test.dart | 311 +++---- .../test/api/v2/auth_api_v2_test.dart | 227 +++--- .../test/api/v2/authorize_v2_test.dart | 28 +- .../test/api/v2/games_api_v2_test.dart | 44 +- .../test/api/v2/jwt_service_test.dart | 17 +- .../test/api/v2/media_api_v2_test.dart | 152 ++-- .../test/api/v2/packs_api_v2_test.dart | 490 ++++++------ .../test/api/v2/promocodes_api_v2_test.dart | 757 ++++++++++-------- .../test/api/v2/purchases_api_v2_test.dart | 81 +- .../api/v2/subscriptions_api_v2_test.dart | 246 +++--- .../test/api/v2/tests_api_v2_test.dart | 100 ++- .../api/v2/users_api_v2_statistics_test.dart | 159 ++-- .../daos/mixins/soft_delete_mixin_test.dart | 113 +-- .../daos/test_dao_generated_cleanup_test.dart | 104 +-- ...test_manager_generated_pack_link_test.dart | 55 +- .../daos/word_statistics_dao_test.dart | 60 +- .../test/database/database_test.dart | 19 +- .../test/models/achievement_model_test.dart | 14 +- .../test/models/pack_progress_model_test.dart | 56 +- .../test/models/study_session_model_test.dart | 48 +- .../test/models/user_model_telegram_test.dart | 2 +- .../test/packs/card_image_storage_test.dart | 23 +- .../test/packs/voice_storage_test.dart | 20 +- .../test/smoke/smoke_tests.dart | 59 +- .../statistics/achievement_manager_test.dart | 24 +- .../automatic_tracking_integration_test.dart | 87 +- .../test/statistics/session_tracker_test.dart | 5 +- .../statistics_calculator_test.dart | 36 +- .../word_statistics_manager_test.dart | 15 +- .../test/storage/minio_service_test.dart | 22 +- .../matrix_question_generator_test.dart | 1 - .../simple_question_generator_test.dart | 18 +- .../test_manager_image_conversion_test.dart | 200 ++--- mnemo_cards_backend/test_postgres.dart | 7 +- mnemo_cards_web_v2/generate_icons.sh | 1 + 54 files changed, 2055 insertions(+), 1913 deletions(-) diff --git a/mnemo_cards_backend/DATABASE_IMPROVEMENT_PLAN_DETAILED.md b/mnemo_cards_backend/DATABASE_IMPROVEMENT_PLAN_DETAILED.md index f977160..dc778e6 100644 --- a/mnemo_cards_backend/DATABASE_IMPROVEMENT_PLAN_DETAILED.md +++ b/mnemo_cards_backend/DATABASE_IMPROVEMENT_PLAN_DETAILED.md @@ -965,3 +965,4 @@ void main() { + diff --git a/mnemo_cards_backend/FINAL_VERIFICATION_REPORT.md b/mnemo_cards_backend/FINAL_VERIFICATION_REPORT.md index 575be80..f5d9c34 100644 --- a/mnemo_cards_backend/FINAL_VERIFICATION_REPORT.md +++ b/mnemo_cards_backend/FINAL_VERIFICATION_REPORT.md @@ -281,3 +281,4 @@ Built with build_runner/jit in 1s; wrote 0 outputs. + diff --git a/mnemo_cards_backend/docs/SHOULD_EXTRACT_DATABASE.md b/mnemo_cards_backend/docs/SHOULD_EXTRACT_DATABASE.md index 7bcf320..ee7dd5f 100644 --- a/mnemo_cards_backend/docs/SHOULD_EXTRACT_DATABASE.md +++ b/mnemo_cards_backend/docs/SHOULD_EXTRACT_DATABASE.md @@ -321,3 +321,4 @@ import 'package:mnemo_cards_backend/database/database.dart'; + diff --git a/mnemo_cards_backend/fix_is_blacklisted_nulls.sql b/mnemo_cards_backend/fix_is_blacklisted_nulls.sql index 99655bc..af2ba05 100644 --- a/mnemo_cards_backend/fix_is_blacklisted_nulls.sql +++ b/mnemo_cards_backend/fix_is_blacklisted_nulls.sql @@ -30,3 +30,4 @@ GROUP BY is_blacklisted; + diff --git a/mnemo_cards_backend/lib/api/purchase/payment_manager.dart b/mnemo_cards_backend/lib/api/purchase/payment_manager.dart index 99f5143..34bb125 100644 --- a/mnemo_cards_backend/lib/api/purchase/payment_manager.dart +++ b/mnemo_cards_backend/lib/api/purchase/payment_manager.dart @@ -35,7 +35,9 @@ class PaymentManager { /// Создать платеж в базе данных Future 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 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, diff --git a/mnemo_cards_backend/lib/api/purchase/yoo_money.dart b/mnemo_cards_backend/lib/api/purchase/yoo_money.dart index 0b02057..aab97a4 100644 --- a/mnemo_cards_backend/lib/api/purchase/yoo_money.dart +++ b/mnemo_cards_backend/lib/api/purchase/yoo_money.dart @@ -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>( '/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; } } diff --git a/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart b/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart index 989bb17..5cdc93c 100644 --- a/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart @@ -602,10 +602,7 @@ class PacksApiV2 { /// to allow image preview in public pack listings @Route.get('/packs//cover') @OpenApiRouteHttp() - Future getPackCover( - Request request, - String packId, - ) async { + Future getPackCover(Request request, String packId) async { try { if (packId.isEmpty) { return _badRequest('Invalid pack ID'); diff --git a/mnemo_cards_backend/lib/api/v2/purchases_api_v2.dart b/mnemo_cards_backend/lib/api/v2/purchases_api_v2.dart index 1f3ec9c..f8dc02a 100644 --- a/mnemo_cards_backend/lib/api/v2/purchases_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/purchases_api_v2.dart @@ -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 headers = const {}}) => Response.ok( @@ -70,10 +66,7 @@ class PurchasesApiV2 { /// Create purchase for a pack @Route.post('/purchases/packs/') @OpenApiRouteHttp() - Future createPackPurchase( - Request request, - String packId, - ) async { + Future 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//verify') @OpenApiRouteHttp() - Future verifyPayment( - Request request, - String paymentId, - ) async { + Future 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//status') @OpenApiRouteHttp() - Future getPackPurchaseStatus( - Request request, - String packId, - ) async { + Future 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); } - diff --git a/mnemo_cards_backend/lib/database/daos/mixins/soft_delete_mixin.dart b/mnemo_cards_backend/lib/database/daos/mixins/soft_delete_mixin.dart index 3bc8890..a46daa5 100644 --- a/mnemo_cards_backend/lib/database/daos/mixins/soft_delete_mixin.dart +++ b/mnemo_cards_backend/lib/database/daos/mixins/soft_delete_mixin.dart @@ -44,8 +44,12 @@ mixin SoftDeleteMixin on DatabaseAccessor { /// /// Возвращает null если запись не найдена или удалена. Future 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; diff --git a/mnemo_cards_backend/lib/database/daos/pack_dao.dart b/mnemo_cards_backend/lib/database/daos/pack_dao.dart index 172eef2..edc49e1 100644 --- a/mnemo_cards_backend/lib/database/daos/pack_dao.dart +++ b/mnemo_cards_backend/lib/database/daos/pack_dao.dart @@ -24,7 +24,9 @@ class PackDao extends DatabaseAccessor with _$PackDaoMixin { /// Получить пак по ID Future 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); diff --git a/mnemo_cards_backend/lib/database/daos/payment_dao.dart b/mnemo_cards_backend/lib/database/daos/payment_dao.dart index 8c2bca9..5d2fde2 100644 --- a/mnemo_cards_backend/lib/database/daos/payment_dao.dart +++ b/mnemo_cards_backend/lib/database/daos/payment_dao.dart @@ -19,7 +19,9 @@ class PaymentDao extends DatabaseAccessor 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 /// Создать платеж Future 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'); } diff --git a/mnemo_cards_backend/lib/database/daos/user_dao.dart b/mnemo_cards_backend/lib/database/daos/user_dao.dart index 83ab708..6a213b2 100644 --- a/mnemo_cards_backend/lib/database/daos/user_dao.dart +++ b/mnemo_cards_backend/lib/database/daos/user_dao.dart @@ -26,10 +26,12 @@ class UserDao extends DatabaseAccessor with _$UserDaoMixin { /// Получить пользователя с UserData Future 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 with _$UserDaoMixin { /// Получить UserData пользователя Future 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 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 with _$UserDaoMixin { Future 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 with _$UserDaoMixin { /// Проверить, есть ли у пользователя доступ к паку Future 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; } diff --git a/mnemo_cards_backend/lib/database/database.dart b/mnemo_cards_backend/lib/database/database.dart index 2d079d0..c38e458 100644 --- a/mnemo_cards_backend/lib/database/database.dart +++ b/mnemo_cards_backend/lib/database/database.dart @@ -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), diff --git a/mnemo_cards_backend/lib/packs/card_pack_drift_extension.dart b/mnemo_cards_backend/lib/packs/card_pack_drift_extension.dart index 5b1c9cf..bf7d129 100644 --- a/mnemo_cards_backend/lib/packs/card_pack_drift_extension.dart +++ b/mnemo_cards_backend/lib/packs/card_pack_drift_extension.dart @@ -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; diff --git a/mnemo_cards_backend/lib/packs/pack_dto_converter.dart b/mnemo_cards_backend/lib/packs/pack_dto_converter.dart index 30e3183..4404058 100644 --- a/mnemo_cards_backend/lib/packs/pack_dto_converter.dart +++ b/mnemo_cards_backend/lib/packs/pack_dto_converter.dart @@ -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 diff --git a/mnemo_cards_backend/lib/tests/generators/question_generators/input_buttons_question_generator.dart b/mnemo_cards_backend/lib/tests/generators/question_generators/input_buttons_question_generator.dart index 3a5a570..0c1e184 100644 --- a/mnemo_cards_backend/lib/tests/generators/question_generators/input_buttons_question_generator.dart +++ b/mnemo_cards_backend/lib/tests/generators/question_generators/input_buttons_question_generator.dart @@ -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 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.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], + ); } } } diff --git a/mnemo_cards_backend/migrations/001_remove_deprecated_fields.sql b/mnemo_cards_backend/migrations/001_remove_deprecated_fields.sql index 213d881..2c6226c 100644 --- a/mnemo_cards_backend/migrations/001_remove_deprecated_fields.sql +++ b/mnemo_cards_backend/migrations/001_remove_deprecated_fields.sql @@ -50,3 +50,4 @@ VACUUM FULL ANALYZE payments; + diff --git a/mnemo_cards_backend/migrations/002_add_enum_types.sql b/mnemo_cards_backend/migrations/002_add_enum_types.sql index 2e98c21..48a8939 100644 --- a/mnemo_cards_backend/migrations/002_add_enum_types.sql +++ b/mnemo_cards_backend/migrations/002_add_enum_types.sql @@ -123,3 +123,4 @@ DROP TYPE IF EXISTS grant_type CASCADE; + diff --git a/mnemo_cards_backend/migrations/003_add_composite_indexes.sql b/mnemo_cards_backend/migrations/003_add_composite_indexes.sql index 230bb65..1676df2 100644 --- a/mnemo_cards_backend/migrations/003_add_composite_indexes.sql +++ b/mnemo_cards_backend/migrations/003_add_composite_indexes.sql @@ -195,3 +195,4 @@ PRINT 'Migration 003 completed successfully!'; + diff --git a/mnemo_cards_backend/test/api/v2/admin_tests_api_v2_image_urls_test.dart b/mnemo_cards_backend/test/api/v2/admin_tests_api_v2_image_urls_test.dart index d487673..3cf659a 100644 --- a/mnemo_cards_backend/test/api/v2/admin_tests_api_v2_image_urls_test.dart +++ b/mnemo_cards_backend/test/api/v2/admin_tests_api_v2_image_urls_test.dart @@ -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 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; - testId = (createBody['test'] as Map)['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; + testId = (createBody['test'] as Map)['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; + final body = + jsonDecode(await getResp.readAsString()) as Map; - 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; - expect(questions, hasLength(1)); - final q0 = questions.first as Map; + final questions = body['questions'] as List; + expect(questions, hasLength(1)); + final q0 = questions.first as Map; - 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; - expect(buttons, hasLength(1)); - final b0 = buttons.first as Map; - 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; + expect(buttons, hasLength(1)); + final b0 = buttons.first as Map; + 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; - final uiData = jsonDecode(qDb.uiData) as Map; + // 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; + final uiData = jsonDecode(qDb.uiData) as Map; - 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)['image']?.toString(); - expect(storedBtnImage, isNotNull); - expect(RegExp(r'^[0-9a-f\-]{36}$', caseSensitive: false) - .hasMatch(storedBtnImage!), isTrue); + final storedBtnImage = (options.first as Map)['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, + ); + }, + ); }); } diff --git a/mnemo_cards_backend/test/api/v2/auth_api_v2_test.dart b/mnemo_cards_backend/test/api/v2/auth_api_v2_test.dart index 5b1bea3..eae7b82 100644 --- a/mnemo_cards_backend/test/api/v2/auth_api_v2_test.dart +++ b/mnemo_cards_backend/test/api/v2/auth_api_v2_test.dart @@ -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; + final createResponse = await authApiV2.createWebTelegramCode( + createRequest, + ); + final createBody = + jsonDecode(await createResponse.readAsString()) + as Map; - 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; + final statusResponse = await authApiV2.getTelegramCodeStatus( + statusRequest, + code!, + ); + final statusBody = + jsonDecode(await statusResponse.readAsString()) + as Map; - 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; + final claimResponse = await authApiV2.claimTelegramCode(claimRequest); + final claimBody = + jsonDecode(await claimResponse.readAsString()) + as Map; - 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; + // Step 5: status should be claimed + final claimedStatusResponse = await authApiV2.getTelegramCodeStatus( + statusRequest, + code, + ); + final claimedStatusBody = + jsonDecode(await claimedStatusResponse.readAsString()) + as Map; - 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; + final authResponse = await authApiV2.authenticateTelegram(authRequest); + final authBody = + jsonDecode(await authResponse.readAsString()) + as Map; - 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; + // Step 7: status reflects used code + final usedStatusResponse = await authApiV2.getTelegramCodeStatus( + statusRequest, + code, + ); + final usedStatusBody = + jsonDecode(await usedStatusResponse.readAsString()) + as Map; - 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( diff --git a/mnemo_cards_backend/test/api/v2/authorize_v2_test.dart b/mnemo_cards_backend/test/api/v2/authorize_v2_test.dart index f4bbf5b..f12cc56 100644 --- a/mnemo_cards_backend/test/api/v2/authorize_v2_test.dart +++ b/mnemo_cards_backend/test/api/v2/authorize_v2_test.dart @@ -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/', () 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//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"')); }); } - diff --git a/mnemo_cards_backend/test/api/v2/games_api_v2_test.dart b/mnemo_cards_backend/test/api/v2/games_api_v2_test.dart index 72dc575..e688cfe 100644 --- a/mnemo_cards_backend/test/api/v2/games_api_v2_test.dart +++ b/mnemo_cards_backend/test/api/v2/games_api_v2_test.dart @@ -26,7 +26,7 @@ void main() { expect(response.statusCode, equals(200)); expect(responseBody, isA()); expect(responseBody.length, greaterThanOrEqualTo(1)); - + // Verify game structure final game = responseBody[0] as Map; expect(game['id'], isA()); @@ -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)['id'] == 'funny_letters', orElse: () => null, ); - + expect(funnyLettersGame, isNotNull); final game = funnyLettersGame as Map; 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; + final response = await gamesApiV2.getGameAssets( + request, + 'nonexistent_game', + ); + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; // Should say "assets not found" not "game not found" expect(responseBody['message'], isNot(contains('Game not found'))); } }); }); } - diff --git a/mnemo_cards_backend/test/api/v2/jwt_service_test.dart b/mnemo_cards_backend/test/api/v2/jwt_service_test.dart index 0c430de..606694f 100644 --- a/mnemo_cards_backend/test/api/v2/jwt_service_test.dart +++ b/mnemo_cards_backend/test/api/v2/jwt_service_test.dart @@ -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() { }); }); } - diff --git a/mnemo_cards_backend/test/api/v2/media_api_v2_test.dart b/mnemo_cards_backend/test/api/v2/media_api_v2_test.dart index 3097a7e..f86b42a 100644 --- a/mnemo_cards_backend/test/api/v2/media_api_v2_test.dart +++ b/mnemo_cards_backend/test/api/v2/media_api_v2_test.dart @@ -40,9 +40,7 @@ void main() { AdminAccessPolicy(), ); - final context = { - 'accessService': accessService, - }; + final context = {'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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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'), + ), + ); }); }); } diff --git a/mnemo_cards_backend/test/api/v2/packs_api_v2_test.dart b/mnemo_cards_backend/test/api/v2/packs_api_v2_test.dart index 789fb8c..84d24c1 100644 --- a/mnemo_cards_backend/test/api/v2/packs_api_v2_test.dart +++ b/mnemo_cards_backend/test/api/v2/packs_api_v2_test.dart @@ -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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; expect(response.statusCode, equals(200)); expect(responseBody['items'], isA()); @@ -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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final response = await packsApiV2.getPack(request, '10'); + final responseBody = + jsonDecode(await response.readAsString()) as Map; - 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final response = await packsApiV2.getPack(request, '11'); + final responseBody = + jsonDecode(await response.readAsString()) as Map; - 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; + final response = await packsApiV2.getPack(request, '11'); + final responseBody = + jsonDecode(await response.readAsString()) as Map; - 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; expect(response.statusCode, equals(200)); expect(responseBody['items'], isA()); expect(responseBody['total'], isA()); 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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()); - }); + expect(response.statusCode, equals(200)); + expect(responseBody, isA()); + }, + ); 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final response = await packsApiV2.getPackBuyPage(request, '11'); + final responseBody = + jsonDecode(await response.readAsString()) as Map; - 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; + final response = await packsApiV2.getPackBuyPage(request, '11'); + final responseBody = + jsonDecode(await response.readAsString()) as Map; - 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; expect(response.statusCode, equals(400)); expect(responseBody['error'], equals('Bad Request')); }); }); } - diff --git a/mnemo_cards_backend/test/api/v2/promocodes_api_v2_test.dart b/mnemo_cards_backend/test/api/v2/promocodes_api_v2_test.dart index b636a90..60ea380 100644 --- a/mnemo_cards_backend/test/api/v2/promocodes_api_v2_test.dart +++ b/mnemo_cards_backend/test/api/v2/promocodes_api_v2_test.dart @@ -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 = {}; 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; + final body = + jsonDecode(await response.readAsString()) as Map; 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; + final body = + jsonDecode(await response.readAsString()) as Map; expect(body['campaigns'], isA()); 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; + final body = + jsonDecode(await response.readAsString()) as Map; expect(body['campaigns'], isA()); expect((body['campaigns'] as List).length, equals(1)); - final campaign = (body['campaigns'] as List).first as Map; + final campaign = + (body['campaigns'] as List).first as Map; expect(campaign['status'], equals('active')); expect(campaign['promoCodes'], isA()); 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; + final body = + jsonDecode(await response.readAsString()) as Map; final campaigns = body['campaigns'] as List; expect(campaigns.length, equals(1)); - expect((campaigns.first as Map)['template'], equals('ACTIVE')); + expect( + (campaigns.first as Map)['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; + final body = + jsonDecode(await response.readAsString()) as Map; final campaigns = body['campaigns'] as List; expect(campaigns.length, equals(1)); - expect((campaigns.first as Map)['template'], equals('ACTIVE')); + expect( + (campaigns.first as Map)['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; + final body = + jsonDecode(await response.readAsString()) as Map; final campaigns = body['campaigns'] as List; expect(campaigns.length, equals(2)); - final templates = campaigns.map((c) => (c as Map)['template']).toList(); + final templates = campaigns + .map((c) => (c as Map)['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; + final body = + jsonDecode(await response.readAsString()) as Map; 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; + final body = + jsonDecode(await response.readAsString()) as Map; 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; + final body = + jsonDecode(await response.readAsString()) as Map; 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; + 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; - expect(body.containsKey('campaigns'), isTrue); - - final campaigns = body['campaigns'] as List; - expect(campaigns.length, equals(1)); - - final campaign = campaigns.first as Map; - 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()); - }); + final campaign = campaigns.first as Map; + 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()); + }, + ); }); 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; + final body = + jsonDecode(await response.readAsString()) as Map; 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; + final body = + jsonDecode(await response.readAsString()) as Map; 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; + final body = + jsonDecode(await response.readAsString()) as Map; 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; + final body = + jsonDecode(await response.readAsString()) as Map; 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; + final body = + jsonDecode(await response.readAsString()) as Map; 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; + final body = + jsonDecode(await response.readAsString()) as Map; 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; + final body = + jsonDecode(await response.readAsString()) as Map; 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; - 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; + 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; - 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; - 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; + 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; - 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; + 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; + 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; + final body = + jsonDecode(await response.readAsString()) as Map; 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; + 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; - 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; - expect(body['valid'], equals(true)); - expect(body['message'], equals('Промокод действителен')); - }); + expect(response.statusCode, equals(200)); + final body = + jsonDecode(await response.readAsString()) as Map; + 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; + final body = + jsonDecode(await response.readAsString()) as Map; expect(body['valid'], equals(true)); expect(body['message'], equals('Промокод действителен')); }); diff --git a/mnemo_cards_backend/test/api/v2/purchases_api_v2_test.dart b/mnemo_cards_backend/test/api/v2/purchases_api_v2_test.dart index 73aa117..b901366 100644 --- a/mnemo_cards_backend/test/api/v2/purchases_api_v2_test.dart +++ b/mnemo_cards_backend/test/api/v2/purchases_api_v2_test.dart @@ -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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; expect(responseBody['paymentId'], equals('payment123')); expect(responseBody['status'], isA()); expect(responseBody['result'], isA()); @@ -406,4 +398,3 @@ void main() { }); }); } - diff --git a/mnemo_cards_backend/test/api/v2/subscriptions_api_v2_test.dart b/mnemo_cards_backend/test/api/v2/subscriptions_api_v2_test.dart index 855c554..302956b 100644 --- a/mnemo_cards_backend/test/api/v2/subscriptions_api_v2_test.dart +++ b/mnemo_cards_backend/test/api/v2/subscriptions_api_v2_test.dart @@ -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; + final response = await subscriptionsApiV2.getPlans(request); + final responseBody = + jsonDecode(await response.readAsString()) as Map; - expect(response.statusCode, equals(200)); - expect(responseBody['plans'], isA()); - expect(responseBody['plans'], isEmpty); - }); + expect(response.statusCode, equals(200)); + expect(responseBody['plans'], isA()); + 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; expect(response.statusCode, equals(200)); expect(responseBody['plans'], isA()); @@ -194,8 +197,8 @@ void main() { ).change(context: {'user': testUser}); final response = await subscriptionsApiV2.getPlans(request); - final responseBody = jsonDecode(await response.readAsString()) - as Map; + final responseBody = + jsonDecode(await response.readAsString()) as Map; expect(response.statusCode, equals(200)); expect(responseBody['plans'], isA()); @@ -228,8 +231,8 @@ void main() { ); final response = await subscriptionsApiV2.getPlans(request); - final responseBody = jsonDecode(await response.readAsString()) - as Map; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; - 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; - - expect(response.statusCode, equals(200)); - expect(responseBody['plans'], hasLength(2)); - - final plans = responseBody['plans'] as List; - final paymentSystems = plans - .map((p) => (p as Map)['paymentSystem']) - .toList(); - expect(paymentSystems, contains('yookassa')); - expect(paymentSystems, contains('google')); - }); + final plans = responseBody['plans'] as List; + final paymentSystems = plans + .map((p) => (p as Map)['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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + 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; + + 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; expect(response.statusCode, equals(200)); expect(response.headers['content-type'], contains('application/json')); expect(responseBody.containsKey('active'), isTrue); expect(responseBody['active'], isA()); - + if (responseBody['active'] as bool) { expect(responseBody.containsKey('start'), isTrue); expect(responseBody.containsKey('finish'), isTrue); expect(responseBody['start'], isA()); expect(responseBody['finish'], isA()); - + // Verify ISO8601 format expect( () => DateTime.parse(responseBody['start'] as String), diff --git a/mnemo_cards_backend/test/api/v2/tests_api_v2_test.dart b/mnemo_cards_backend/test/api/v2/tests_api_v2_test.dart index 5d63972..20e2be4 100644 --- a/mnemo_cards_backend/test/api/v2/tests_api_v2_test.dart +++ b/mnemo_cards_backend/test/api/v2/tests_api_v2_test.dart @@ -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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; expect(response.statusCode, equals(200)); expect(responseBody['items'], isA()); @@ -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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; expect(response.statusCode, equals(200)); expect(responseBody['items'], isA()); @@ -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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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'), + ); }); }); } - diff --git a/mnemo_cards_backend/test/api/v2/users_api_v2_statistics_test.dart b/mnemo_cards_backend/test/api/v2/users_api_v2_statistics_test.dart index 4960ebb..e169927 100644 --- a/mnemo_cards_backend/test/api/v2/users_api_v2_statistics_test.dart +++ b/mnemo_cards_backend/test/api/v2/users_api_v2_statistics_test.dart @@ -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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; // 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()); 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; expect(response.statusCode, equals(200)); expect(responseBody['words'], isA()); @@ -431,8 +421,8 @@ void main() { ); final response = await usersApiV2.getWordsStatistics(request); - final responseBody = jsonDecode(await response.readAsString()) - as Map; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody2 = + jsonDecode(await response2.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; expect(response.statusCode, equals(200)); final words = responseBody['words'] as List; @@ -476,10 +466,15 @@ void main() { expect(word['incorrect'], isA()); } // 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; expect(response.statusCode, equals(200)); expect(responseBody['words'], isA()); @@ -523,8 +518,8 @@ void main() { ); final response = await usersApiV2.getWordsStatistics(request); - final responseBody = jsonDecode(await response.readAsString()) - as Map; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; expect(response.statusCode, equals(200)); expect(responseBody['words'], isA()); @@ -604,8 +599,8 @@ void main() { ); final response = await usersApiV2.getWordsStatistics(request); - final responseBody = jsonDecode(await response.readAsString()) - as Map; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; expect(response.statusCode, equals(200)); expect(responseBody['period'], isA()); @@ -675,8 +670,8 @@ void main() { ); final response = await usersApiV2.getTimelineStatistics(request); - final responseBody = jsonDecode(await response.readAsString()) - as Map; + final responseBody = + jsonDecode(await response.readAsString()) as Map; // 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; 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; + final responseBody = + jsonDecode(await response.readAsString()) as Map; expect(response.statusCode, equals(401)); expect(responseBody['error'], equals('unauthorized')); diff --git a/mnemo_cards_backend/test/database/daos/mixins/soft_delete_mixin_test.dart b/mnemo_cards_backend/test/database/daos/mixins/soft_delete_mixin_test.dart index 957b9f2..e3b8fc4 100644 --- a/mnemo_cards_backend/test/database/daos/mixins/soft_delete_mixin_test.dart +++ b/mnemo_cards_backend/test/database/daos/mixins/soft_delete_mixin_test.dart @@ -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(); diff --git a/mnemo_cards_backend/test/database/daos/test_dao_generated_cleanup_test.dart b/mnemo_cards_backend/test/database/daos/test_dao_generated_cleanup_test.dart index 4f84484..4979315 100644 --- a/mnemo_cards_backend/test/database/daos/test_dao_generated_cleanup_test.dart +++ b/mnemo_cards_backend/test/database/daos/test_dao_generated_cleanup_test.dart @@ -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(); + }, + ); }); } - diff --git a/mnemo_cards_backend/test/database/daos/test_manager_generated_pack_link_test.dart b/mnemo_cards_backend/test/database/daos/test_manager_generated_pack_link_test.dart index 25045c3..fde6c6f 100644 --- a/mnemo_cards_backend/test/database/daos/test_manager_generated_pack_link_test.dart +++ b/mnemo_cards_backend/test/database/daos/test_manager_generated_pack_link_test.dart @@ -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)); }); diff --git a/mnemo_cards_backend/test/database/daos/word_statistics_dao_test.dart b/mnemo_cards_backend/test/database/daos/word_statistics_dao_test.dart index 805ecff..a2548a2 100644 --- a/mnemo_cards_backend/test/database/daos/word_statistics_dao_test.dart +++ b/mnemo_cards_backend/test/database/daos/word_statistics_dao_test.dart @@ -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); diff --git a/mnemo_cards_backend/test/database/database_test.dart b/mnemo_cards_backend/test/database/database_test.dart index dcc952b..22183e4 100644 --- a/mnemo_cards_backend/test/database/database_test.dart +++ b/mnemo_cards_backend/test/database/database_test.dart @@ -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); diff --git a/mnemo_cards_backend/test/models/achievement_model_test.dart b/mnemo_cards_backend/test/models/achievement_model_test.dart index b4f6983..34b24b7 100644 --- a/mnemo_cards_backend/test/models/achievement_model_test.dart +++ b/mnemo_cards_backend/test/models/achievement_model_test.dart @@ -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, ''); diff --git a/mnemo_cards_backend/test/models/pack_progress_model_test.dart b/mnemo_cards_backend/test/models/pack_progress_model_test.dart index 82fa7ae..dbaeefe 100644 --- a/mnemo_cards_backend/test/models/pack_progress_model_test.dart +++ b/mnemo_cards_backend/test/models/pack_progress_model_test.dart @@ -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); }); diff --git a/mnemo_cards_backend/test/models/study_session_model_test.dart b/mnemo_cards_backend/test/models/study_session_model_test.dart index f6a43d4..c49879f 100644 --- a/mnemo_cards_backend/test/models/study_session_model_test.dart +++ b/mnemo_cards_backend/test/models/study_session_model_test.dart @@ -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, diff --git a/mnemo_cards_backend/test/models/user_model_telegram_test.dart b/mnemo_cards_backend/test/models/user_model_telegram_test.dart index e5ea200..4c7947a 100644 --- a/mnemo_cards_backend/test/models/user_model_telegram_test.dart +++ b/mnemo_cards_backend/test/models/user_model_telegram_test.dart @@ -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); diff --git a/mnemo_cards_backend/test/packs/card_image_storage_test.dart b/mnemo_cards_backend/test/packs/card_image_storage_test.dart index c35af71..526f77a 100644 --- a/mnemo_cards_backend/test/packs/card_image_storage_test.dart +++ b/mnemo_cards_backend/test/packs/card_image_storage_test.dart @@ -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() { }); }); } - diff --git a/mnemo_cards_backend/test/packs/voice_storage_test.dart b/mnemo_cards_backend/test/packs/voice_storage_test.dart index 7a6161b..8b91389 100644 --- a/mnemo_cards_backend/test/packs/voice_storage_test.dart +++ b/mnemo_cards_backend/test/packs/voice_storage_test.dart @@ -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() { }); }); } - diff --git a/mnemo_cards_backend/test/smoke/smoke_tests.dart b/mnemo_cards_backend/test/smoke/smoke_tests.dart index 1a42ebb..7591e90 100644 --- a/mnemo_cards_backend/test/smoke/smoke_tests.dart +++ b/mnemo_cards_backend/test/smoke/smoke_tests.dart @@ -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(0, (sum, minutes) => sum + minutes); + final totalMinutes = categoryMinutes.values.fold( + 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); diff --git a/mnemo_cards_backend/test/statistics/achievement_manager_test.dart b/mnemo_cards_backend/test/statistics/achievement_manager_test.dart index a6d9205..6ba99ec 100644 --- a/mnemo_cards_backend/test/statistics/achievement_manager_test.dart +++ b/mnemo_cards_backend/test/statistics/achievement_manager_test.dart @@ -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', () { diff --git a/mnemo_cards_backend/test/statistics/automatic_tracking_integration_test.dart b/mnemo_cards_backend/test/statistics/automatic_tracking_integration_test.dart index 9445403..5f85ac7 100644 --- a/mnemo_cards_backend/test/statistics/automatic_tracking_integration_test.dart +++ b/mnemo_cards_backend/test/statistics/automatic_tracking_integration_test.dart @@ -34,12 +34,14 @@ class MockIsar extends Fake implements Isar { } @override - QueryBuilder get studySessionModels { + QueryBuilder + get studySessionModels { return MockQueryBuilder(_sessions); } @override - QueryBuilder get achievementModels { + QueryBuilder + 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) }); }); } diff --git a/mnemo_cards_backend/test/statistics/session_tracker_test.dart b/mnemo_cards_backend/test/statistics/session_tracker_test.dart index 223f0e4..c1551bf 100644 --- a/mnemo_cards_backend/test/statistics/session_tracker_test.dart +++ b/mnemo_cards_backend/test/statistics/session_tracker_test.dart @@ -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', () { diff --git a/mnemo_cards_backend/test/statistics/statistics_calculator_test.dart b/mnemo_cards_backend/test/statistics/statistics_calculator_test.dart index 37258a8..a952c85 100644 --- a/mnemo_cards_backend/test/statistics/statistics_calculator_test.dart +++ b/mnemo_cards_backend/test/statistics/statistics_calculator_test.dart @@ -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 ], ); diff --git a/mnemo_cards_backend/test/statistics/word_statistics_manager_test.dart b/mnemo_cards_backend/test/statistics/word_statistics_manager_test.dart index 9028ba5..eeed02a 100644 --- a/mnemo_cards_backend/test/statistics/word_statistics_manager_test.dart +++ b/mnemo_cards_backend/test/statistics/word_statistics_manager_test.dart @@ -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); }); diff --git a/mnemo_cards_backend/test/storage/minio_service_test.dart b/mnemo_cards_backend/test/storage/minio_service_test.dart index 9eb96e1..f416d7d 100644 --- a/mnemo_cards_backend/test/storage/minio_service_test.dart +++ b/mnemo_cards_backend/test/storage/minio_service_test.dart @@ -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)); }); }); } diff --git a/mnemo_cards_backend/test/tests/generators/question_generators/matrix_question_generator_test.dart b/mnemo_cards_backend/test/tests/generators/question_generators/matrix_question_generator_test.dart index 49b0edd..56a3509 100644 --- a/mnemo_cards_backend/test/tests/generators/question_generators/matrix_question_generator_test.dart +++ b/mnemo_cards_backend/test/tests/generators/question_generators/matrix_question_generator_test.dart @@ -79,4 +79,3 @@ void main() { }); }); } - diff --git a/mnemo_cards_backend/test/tests/generators/question_generators/simple_question_generator_test.dart b/mnemo_cards_backend/test/tests/generators/question_generators/simple_question_generator_test.dart index 619aaf6..b40ff28 100644 --- a/mnemo_cards_backend/test/tests/generators/question_generators/simple_question_generator_test.dart +++ b/mnemo_cards_backend/test/tests/generators/question_generators/simple_question_generator_test.dart @@ -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 }); }); -} \ No newline at end of file +} diff --git a/mnemo_cards_backend/test/tests/test_manager_image_conversion_test.dart b/mnemo_cards_backend/test/tests/test_manager_image_conversion_test.dart index 2090223..d41510d 100644 --- a/mnemo_cards_backend/test/tests/test_manager_image_conversion_test.dart +++ b/mnemo_cards_backend/test/tests/test_manager_image_conversion_test.dart @@ -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( diff --git a/mnemo_cards_backend/test_postgres.dart b/mnemo_cards_backend/test_postgres.dart index 98b3e4c..93c3286 100644 --- a/mnemo_cards_backend/test_postgres.dart +++ b/mnemo_cards_backend/test_postgres.dart @@ -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'); } -} \ No newline at end of file +} diff --git a/mnemo_cards_web_v2/generate_icons.sh b/mnemo_cards_web_v2/generate_icons.sh index 02dbb09..bb433da 100755 --- a/mnemo_cards_web_v2/generate_icons.sh +++ b/mnemo_cards_web_v2/generate_icons.sh @@ -55,3 +55,4 @@ echo " - favicon.png (32x32)" +