format and logs
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Web App CI / test (push) Waiting to run
Web App CI / build (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions

This commit is contained in:
Dmitry 2026-01-08 16:02:47 +03:00
parent feeced1543
commit 4ed1839893
54 changed files with 2055 additions and 1913 deletions

View file

@ -965,3 +965,4 @@ void main() {

View file

@ -281,3 +281,4 @@ Built with build_runner/jit in 1s; wrote 0 outputs.

View file

@ -321,3 +321,4 @@ import 'package:mnemo_cards_backend/database/database.dart';

View file

@ -30,3 +30,4 @@ GROUP BY is_blacklisted;

View file

@ -35,7 +35,9 @@ class PaymentManager {
/// Создать платеж в базе данных /// Создать платеж в базе данных
Future<PaymentDto> createPayment(PaymentDto paymentDto, String userId) async { Future<PaymentDto> createPayment(PaymentDto paymentDto, String userId) async {
print('🔍 PaymentManager.createPayment: Starting'); print('🔍 PaymentManager.createPayment: Starting');
print('🔍 PaymentManager.createPayment: userId=$userId, externalToken=${paymentDto.externalToken}'); print(
'🔍 PaymentManager.createPayment: userId=$userId, externalToken=${paymentDto.externalToken}',
);
final companion = paymentDto.toCompanion(userId); final companion = paymentDto.toCompanion(userId);
print('🔍 PaymentManager.createPayment: Calling paymentDao.createPayment'); print('🔍 PaymentManager.createPayment: Calling paymentDao.createPayment');
final paymentId = await _db.paymentDao.createPayment(companion); final paymentId = await _db.paymentDao.createPayment(companion);
@ -43,7 +45,9 @@ class PaymentManager {
print('🔍 PaymentManager.createPayment: Getting payment by id'); print('🔍 PaymentManager.createPayment: Getting payment by id');
final payment = await _db.paymentDao.getPaymentById(paymentId); final payment = await _db.paymentDao.getPaymentById(paymentId);
if (payment == null) { if (payment == null) {
print('❌ PaymentManager.createPayment: Payment not found after creation, id=$paymentId'); print(
'❌ PaymentManager.createPayment: Payment not found after creation, id=$paymentId',
);
throw Exception('Failed to create payment'); throw Exception('Failed to create payment');
} }
print('✅ PaymentManager.createPayment: Payment retrieved successfully'); print('✅ PaymentManager.createPayment: Payment retrieved successfully');
@ -299,7 +303,7 @@ class PaymentManager {
try { try {
final payment = await _db.paymentDao.getPaymentByExternalToken(token); final payment = await _db.paymentDao.getPaymentByExternalToken(token);
if (payment == null) { if (payment == null) {
log('Payment not found for token: $token'); print('Payment not found for token: $token');
return false; return false;
} }
@ -336,15 +340,21 @@ class PaymentManager {
List<MnemoCardsProductDto> products = const [], List<MnemoCardsProductDto> products = const [],
}) async { }) async {
print('🔍 PaymentManager.createYookassaUrl: Starting'); print('🔍 PaymentManager.createYookassaUrl: Starting');
print('🔍 PaymentManager.createYookassaUrl: amount=$amount, userId=$userId, products=${products.length}'); print(
'🔍 PaymentManager.createYookassaUrl: amount=$amount, userId=$userId, products=${products.length}',
);
print('🔍 PaymentManager.createYookassaUrl: Calling YooMoneyHandler.createPayment'); print(
'🔍 PaymentManager.createYookassaUrl: Calling YooMoneyHandler.createPayment',
);
final yookassaPayment = await _yooMoneyHandler.createPayment( final yookassaPayment = await _yooMoneyHandler.createPayment(
amount: amount, amount: amount,
description: description, description: description,
userId: userId, userId: userId,
); );
print('✅ PaymentManager.createYookassaUrl: YooKassa payment created, id=${yookassaPayment.id}'); print(
'✅ PaymentManager.createYookassaUrl: YooKassa payment created, id=${yookassaPayment.id}',
);
if (yookassaPayment.confirmationUrl == null) { if (yookassaPayment.confirmationUrl == null) {
print('❌ PaymentManager.createYookassaUrl: confirmationUrl is null'); print('❌ PaymentManager.createYookassaUrl: confirmationUrl is null');
@ -368,7 +378,9 @@ class PaymentManager {
await createPayment(paymentDto, userId); await createPayment(paymentDto, userId);
print('✅ PaymentManager.createYookassaUrl: Payment created in database'); print('✅ PaymentManager.createYookassaUrl: Payment created in database');
print('✅ PaymentManager.createYookassaUrl: Returning confirmationUrl and paymentId'); print(
'✅ PaymentManager.createYookassaUrl: Returning confirmationUrl and paymentId',
);
return YookassaPaymentResult( return YookassaPaymentResult(
confirmationUrl: yookassaPayment.confirmationUrl!, confirmationUrl: yookassaPayment.confirmationUrl!,
paymentId: yookassaPayment.id, paymentId: yookassaPayment.id,

View file

@ -30,10 +30,8 @@ class YooMoneyHandler {
final _uuid = const Uuid(); final _uuid = const Uuid();
static const String _baseUrl = 'https://api.yookassa.ru/v3'; static const String _baseUrl = 'https://api.yookassa.ru/v3';
YooMoneyHandler({ YooMoneyHandler({required String shopId, required String secretKey})
required String shopId, : _shopId = shopId,
required String secretKey,
}) : _shopId = shopId,
_secretKey = secretKey, _secretKey = secretKey,
_returnUrlBase = Platform.environment['YOOKASSA_RETURN_URL'] { _returnUrlBase = Platform.environment['YOOKASSA_RETURN_URL'] {
// Validate credentials // Validate credentials
@ -49,9 +47,7 @@ class YooMoneyHandler {
_dio = Dio( _dio = Dio(
BaseOptions( BaseOptions(
baseUrl: _baseUrl, baseUrl: _baseUrl,
headers: { headers: {'Content-Type': 'application/json'},
'Content-Type': 'application/json',
},
), ),
); );
@ -113,17 +109,11 @@ class YooMoneyHandler {
// Create payment request according to CreatePaymentRequest schema // Create payment request according to CreatePaymentRequest schema
final requestBody = { final requestBody = {
'amount': { 'amount': {'value': formattedAmount, 'currency': 'RUB'},
'value': formattedAmount,
'currency': 'RUB',
},
'description': description.length > 128 'description': description.length > 128
? description.substring(0, 128) ? description.substring(0, 128)
: description, // Max 128 chars per spec : description, // Max 128 chars per spec
'confirmation': { 'confirmation': {'type': 'redirect', 'return_url': returnUrl},
'type': 'redirect',
'return_url': returnUrl,
},
'capture': true, // Auto-capture payment when succeeded 'capture': true, // Auto-capture payment when succeeded
'metadata': { 'metadata': {
'userId': userId, 'userId': userId,
@ -146,11 +136,7 @@ class YooMoneyHandler {
final response = await _dio!.post<Map<String, dynamic>>( final response = await _dio!.post<Map<String, dynamic>>(
'/payments', '/payments',
data: requestBody, data: requestBody,
options: Options( options: Options(headers: {'Idempotence-Key': idempotenceKey}),
headers: {
'Idempotence-Key': idempotenceKey,
},
),
); );
final paymentData = response.data; final paymentData = response.data;
@ -175,30 +161,15 @@ class YooMoneyHandler {
if (confirmationType == 'redirect') { if (confirmationType == 'redirect') {
confirmationUrl = confirmation['confirmation_url'] as String?; confirmationUrl = confirmation['confirmation_url'] as String?;
} else { } else {
log( print('Payment created with confirmation type: $confirmationType');
'Payment created with confirmation type: $confirmationType',
name: 'YooMoneyHandler',
);
} }
} }
if (confirmationUrl == null) { if (confirmationUrl == null) {
log( print('Warning: No confirmation URL in payment response');
'Warning: No confirmation URL in payment response',
name: 'YooMoneyHandler',
error: jsonEncode(paymentData),
);
} }
log( print('YooKassa payment created successfully');
'YooKassa payment created successfully',
name: 'YooMoneyHandler',
error: {
'paymentId': paymentId,
'status': paymentStatus,
'hasConfirmationUrl': confirmationUrl != null,
},
);
return YookassaPayment( return YookassaPayment(
id: paymentId, id: paymentId,
@ -206,28 +177,15 @@ class YooMoneyHandler {
confirmationUrl: confirmationUrl, confirmationUrl: confirmationUrl,
); );
} on DioException catch (e, stackTrace) { } on DioException catch (e, stackTrace) {
log( print('YooKassa API error when creating payment $e $stackTrace');
'YooKassa API error when creating payment',
name: 'YooMoneyHandler',
error: e,
stackTrace: stackTrace,
);
if (e.response != null) { if (e.response != null) {
log( print('YooKassa error response: ${e.response?.data}');
'YooKassa error response: ${e.response?.data}',
name: 'YooMoneyHandler',
);
} }
throw Exception( throw Exception(
'Failed to create YooKassa payment: ${e.message ?? 'Unknown error'}', 'Failed to create YooKassa payment: ${e.message ?? 'Unknown error'}',
); );
} on Exception catch (e, stackTrace) { } on Exception catch (e, stackTrace) {
log( print('Unexpected error when creating YooKassa payment $e $stackTrace');
'Unexpected error when creating YooKassa payment',
name: 'YooMoneyHandler',
error: e,
stackTrace: stackTrace,
);
rethrow; rethrow;
} }
} }
@ -242,11 +200,7 @@ class YooMoneyHandler {
} }
try { try {
log( print('Checking YooKassa payment status $paymentId');
'Checking YooKassa payment status',
name: 'YooMoneyHandler',
error: {'paymentId': paymentId},
);
// Get payment info from YooKassa API // Get payment info from YooKassa API
// According to spec: GET /v3/payments/{payment_id} // According to spec: GET /v3/payments/{payment_id}
@ -274,15 +228,8 @@ class YooMoneyHandler {
} }
} }
log( print('''YooKassa payment status retrieved
'YooKassa payment status retrieved', ${{'paymentId': id, 'status': paymentStatus, 'paid': paid}}''');
name: 'YooMoneyHandler',
error: {
'paymentId': id,
'status': paymentStatus,
'paid': paid,
},
);
return YookassaPayment( return YookassaPayment(
id: id, id: id,
@ -290,28 +237,15 @@ class YooMoneyHandler {
confirmationUrl: confirmationUrl, confirmationUrl: confirmationUrl,
); );
} on DioException catch (e, stackTrace) { } on DioException catch (e, stackTrace) {
log( print('YooKassa API error when checking payment $e $stackTrace');
'YooKassa API error when checking payment',
name: 'YooMoneyHandler',
error: e,
stackTrace: stackTrace,
);
if (e.response != null) { if (e.response != null) {
log( print('YooKassa error response: ${e.response?.data}');
'YooKassa error response: ${e.response?.data}',
name: 'YooMoneyHandler',
);
} }
throw Exception( throw Exception(
'Failed to check YooKassa payment: ${e.message ?? 'Unknown error'}', 'Failed to check YooKassa payment: ${e.message ?? 'Unknown error'}',
); );
} on Exception catch (e, stackTrace) { } on Exception catch (e, stackTrace) {
log( print('Unexpected error when checking YooKassa payment $e $stackTrace');
'Unexpected error when checking YooKassa payment',
name: 'YooMoneyHandler',
error: e,
stackTrace: stackTrace,
);
rethrow; rethrow;
} }
} }

View file

@ -602,10 +602,7 @@ class PacksApiV2 {
/// to allow image preview in public pack listings /// to allow image preview in public pack listings
@Route.get('/packs/<packId>/cover') @Route.get('/packs/<packId>/cover')
@OpenApiRouteHttp() @OpenApiRouteHttp()
Future<Response> getPackCover( Future<Response> getPackCover(Request request, String packId) async {
Request request,
String packId,
) async {
try { try {
if (packId.isEmpty) { if (packId.isEmpty) {
return _badRequest('Invalid pack ID'); return _badRequest('Invalid pack ID');

View file

@ -23,11 +23,7 @@ class PurchasesApiV2 {
final PackManager _packManager; final PackManager _packManager;
final AppDatabase _db; final AppDatabase _db;
PurchasesApiV2( PurchasesApiV2(this._paymentManager, this._packManager, this._db);
this._paymentManager,
this._packManager,
this._db,
);
Response _ok(Object? object, {Map<String, String> headers = const {}}) => Response _ok(Object? object, {Map<String, String> headers = const {}}) =>
Response.ok( Response.ok(
@ -70,10 +66,7 @@ class PurchasesApiV2 {
/// Create purchase for a pack /// Create purchase for a pack
@Route.post('/purchases/packs/<packId>') @Route.post('/purchases/packs/<packId>')
@OpenApiRouteHttp() @OpenApiRouteHttp()
Future<Response> createPackPurchase( Future<Response> createPackPurchase(Request request, String packId) async {
Request request,
String packId,
) async {
try { try {
print('🔍 createPackPurchase: Starting for packId=$packId'); print('🔍 createPackPurchase: Starting for packId=$packId');
final user = request.user; final user = request.user;
@ -87,7 +80,9 @@ class PurchasesApiV2 {
return _unauthorized(); return _unauthorized();
} }
print('🔍 createPackPurchase: Getting pack information for packId=$packId'); print(
'🔍 createPackPurchase: Getting pack information for packId=$packId',
);
// Get pack information // Get pack information
final pack = await _packManager.getPack(packId); final pack = await _packManager.getPack(packId);
if (pack == null) { if (pack == null) {
@ -96,12 +91,11 @@ class PurchasesApiV2 {
} }
print('✅ createPackPurchase: Pack found: ${pack.title}'); print('✅ createPackPurchase: Pack found: ${pack.title}');
print('🔍 createPackPurchase: Checking pack access for userId=${user.id}, packId=$packId'); print(
// Check if already purchased '🔍 createPackPurchase: Checking pack access for userId=${user.id}, packId=$packId',
final hasAccess = await _db.userDao.hasPackAccess(
user.id!,
packId,
); );
// Check if already purchased
final hasAccess = await _db.userDao.hasPackAccess(user.id!, packId);
if (hasAccess) { if (hasAccess) {
print('❌ createPackPurchase: Pack already purchased'); print('❌ createPackPurchase: Pack already purchased');
return _badRequest('Pack is already purchased'); return _badRequest('Pack is already purchased');
@ -129,10 +123,7 @@ class PurchasesApiV2 {
// Create products list // Create products list
final products = [ final products = [
MnemoCardsProductDto( MnemoCardsProductDto(type: MnemoCardsProductType.pack, id: packId),
type: MnemoCardsProductType.pack,
id: packId,
),
]; ];
print('🔍 createPackPurchase: Creating YooKassa payment URL'); print('🔍 createPackPurchase: Creating YooKassa payment URL');
@ -143,11 +134,14 @@ class PurchasesApiV2 {
userId: user.id!, userId: user.id!,
products: products, products: products,
); );
print('✅ createPackPurchase: Payment URL created: ${paymentResult.confirmationUrl}, paymentId=${paymentResult.paymentId}'); print(
'✅ createPackPurchase: Payment URL created: ${paymentResult.confirmationUrl}, paymentId=${paymentResult.paymentId}',
);
// Build return URL for payment verification // Build return URL for payment verification
final baseUri = request.requestedUri; final baseUri = request.requestedUri;
final checkUrl = '${baseUri.scheme}://${baseUri.host}${baseUri.hasPort ? ':${baseUri.port}' : ''}/api/v2/purchases/payments/${paymentResult.paymentId}/verify?productId=$packId&productType=pack'; final checkUrl =
'${baseUri.scheme}://${baseUri.host}${baseUri.hasPort ? ':${baseUri.port}' : ''}/api/v2/purchases/payments/${paymentResult.paymentId}/verify?productId=$packId&productType=pack';
return _ok({ return _ok({
'purchaseUrl': paymentResult.confirmationUrl, 'purchaseUrl': paymentResult.confirmationUrl,
@ -207,10 +201,7 @@ class PurchasesApiV2 {
} }
// Check if already purchased // Check if already purchased
final hasAccess = await _db.userDao.hasPackAccess( final hasAccess = await _db.userDao.hasPackAccess(user.id!, productId);
user.id!,
productId,
);
if (hasAccess) { if (hasAccess) {
return _badRequest('Pack is already purchased'); return _badRequest('Pack is already purchased');
} }
@ -230,10 +221,7 @@ class PurchasesApiV2 {
description = 'Покупка пакета: ${pack.title}'; description = 'Покупка пакета: ${pack.title}';
products = [ products = [
MnemoCardsProductDto( MnemoCardsProductDto(type: MnemoCardsProductType.pack, id: productId),
type: MnemoCardsProductType.pack,
id: productId,
),
]; ];
} else if (productType == MnemoCardsProductType.subscription) { } else if (productType == MnemoCardsProductType.subscription) {
// Get subscription plan // Get subscription plan
@ -292,7 +280,8 @@ class PurchasesApiV2 {
// Build return URL for payment verification // Build return URL for payment verification
final baseUri = request.requestedUri; final baseUri = request.requestedUri;
final checkUrl = '${baseUri.scheme}://${baseUri.host}${baseUri.hasPort ? ':${baseUri.port}' : ''}/api/v2/purchases/payments/${paymentResult.paymentId}/verify?productId=$productId&productType=$productTypeStr'; final checkUrl =
'${baseUri.scheme}://${baseUri.host}${baseUri.hasPort ? ':${baseUri.port}' : ''}/api/v2/purchases/payments/${paymentResult.paymentId}/verify?productId=$productId&productType=$productTypeStr';
return _ok({ return _ok({
'purchaseUrl': paymentResult.confirmationUrl, 'purchaseUrl': paymentResult.confirmationUrl,
@ -309,10 +298,7 @@ class PurchasesApiV2 {
/// Verify payment status /// Verify payment status
@Route.get('/purchases/payments/<paymentId>/verify') @Route.get('/purchases/payments/<paymentId>/verify')
@OpenApiRouteHttp() @OpenApiRouteHttp()
Future<Response> verifyPayment( Future<Response> verifyPayment(Request request, String paymentId) async {
Request request,
String paymentId,
) async {
try { try {
final user = request.user; final user = request.user;
if (user == null) { if (user == null) {
@ -360,10 +346,7 @@ class PurchasesApiV2 {
/// Check pack purchase status /// Check pack purchase status
@Route.get('/purchases/packs/<packId>/status') @Route.get('/purchases/packs/<packId>/status')
@OpenApiRouteHttp() @OpenApiRouteHttp()
Future<Response> getPackPurchaseStatus( Future<Response> getPackPurchaseStatus(Request request, String packId) async {
Request request,
String packId,
) async {
try { try {
final user = request.user; final user = request.user;
if (user == null) { if (user == null) {
@ -384,13 +367,14 @@ class PurchasesApiV2 {
final isPurchased = await _db.userDao.hasPackAccess(user.id!, packId); final isPurchased = await _db.userDao.hasPackAccess(user.id!, packId);
// Check subscription access // Check subscription access
final activeSubscription = await _db.subscriptionDao.getActiveSubscription( final activeSubscription = await _db.subscriptionDao
user.id!, .getActiveSubscription(user.id!);
); final hasSubscriptionAccess =
final hasSubscriptionAccess = activeSubscription != null && activeSubscription != null &&
(activeSubscription.features is List && (activeSubscription.features is List &&
(activeSubscription.features as List) (activeSubscription.features as List).contains(
.contains(SubscriptionFeatureEnum.packs.name)); SubscriptionFeatureEnum.packs.name,
));
return _ok({ return _ok({
'packId': packId, 'packId': packId,
@ -399,11 +383,14 @@ class PurchasesApiV2 {
'hasSubscriptionAccess': hasSubscriptionAccess, 'hasSubscriptionAccess': hasSubscriptionAccess,
}); });
} catch (e, s) { } catch (e, s) {
developer.log('Error in getPackPurchaseStatus: $e', error: e, stackTrace: s); developer.log(
'Error in getPackPurchaseStatus: $e',
error: e,
stackTrace: s,
);
return _internalServerError(e.toString()); return _internalServerError(e.toString());
} }
} }
Router get router => _$PurchasesApiV2Router(this); Router get router => _$PurchasesApiV2Router(this);
} }

View file

@ -44,8 +44,12 @@ mixin SoftDeleteMixin<T extends Table, D> on DatabaseAccessor<AppDatabase> {
/// ///
/// Возвращает null если запись не найдена или удалена. /// Возвращает null если запись не найдена или удалена.
Future<D?> getActiveById(String id) async { Future<D?> getActiveById(String id) async {
print('🔍 SoftDeleteMixin.getActiveById: id=$id, table=${table.entityName}'); print(
print('🔍 SoftDeleteMixin.getActiveById: SQL: SELECT * FROM ${table.entityName} WHERE id = ? AND is_deleted = false LIMIT 1'); '🔍 SoftDeleteMixin.getActiveById: id=$id, table=${table.entityName}',
);
print(
'🔍 SoftDeleteMixin.getActiveById: SQL: SELECT * FROM ${table.entityName} WHERE id = ? AND is_deleted = false LIMIT 1',
);
final query = selectActive() final query = selectActive()
..where((t) { ..where((t) {
final idColumn = (t as dynamic).id; final idColumn = (t as dynamic).id;

View file

@ -24,7 +24,9 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
/// Получить пак по ID /// Получить пак по ID
Future<CardPack?> getPackById(String id) async { Future<CardPack?> getPackById(String id) async {
print('🔍 PackDao.getPackById: packId=$id'); print('🔍 PackDao.getPackById: packId=$id');
print('🔍 PackDao.getPackById: SQL: SELECT * FROM card_packs WHERE id = ? LIMIT 1'); print(
'🔍 PackDao.getPackById: SQL: SELECT * FROM card_packs WHERE id = ? LIMIT 1',
);
final query = select(cardPacks) final query = select(cardPacks)
..where((p) => p.id.equals(id)) ..where((p) => p.id.equals(id))
..limit(1); ..limit(1);

View file

@ -19,7 +19,9 @@ class PaymentDao extends DatabaseAccessor<AppDatabase>
print('🔍 PaymentDao.getPaymentById: paymentId=$id'); print('🔍 PaymentDao.getPaymentById: paymentId=$id');
print('🔍 PaymentDao.getPaymentById: Calling getActiveById'); print('🔍 PaymentDao.getPaymentById: Calling getActiveById');
final result = await getActiveById(id); final result = await getActiveById(id);
print('✅ PaymentDao.getPaymentById: Result=${result != null ? "found" : "not found"}'); print(
'✅ PaymentDao.getPaymentById: Result=${result != null ? "found" : "not found"}',
);
return result; return result;
} }
@ -51,48 +53,68 @@ class PaymentDao extends DatabaseAccessor<AppDatabase>
/// Создать платеж /// Создать платеж
Future<String> createPayment(PaymentsCompanion payment) async { Future<String> createPayment(PaymentsCompanion payment) async {
print('🔍 PaymentDao.createPayment: Starting payment creation'); print('🔍 PaymentDao.createPayment: Starting payment creation');
print('🔍 PaymentDao.createPayment: externalToken=${payment.externalToken.value}'); print(
'🔍 PaymentDao.createPayment: externalToken=${payment.externalToken.value}',
);
print('🔍 PaymentDao.createPayment: userId=${payment.userId.value}'); print('🔍 PaymentDao.createPayment: userId=${payment.userId.value}');
print('🔍 PaymentDao.createPayment: amount=${payment.amount.value}'); print('🔍 PaymentDao.createPayment: amount=${payment.amount.value}');
final inserted = await into(payments).insertReturning(payment); final inserted = await into(payments).insertReturning(payment);
print('✅ PaymentDao.createPayment: Payment inserted, date=${inserted.date.dateTime}'); print(
'✅ PaymentDao.createPayment: Payment inserted, date=${inserted.date.dateTime}',
);
// Try to get id from inserted object first // Try to get id from inserted object first
if (inserted.id.isNotEmpty) { if (inserted.id.isNotEmpty) {
print('✅ PaymentDao.createPayment: Got id from inserted object, id=${inserted.id}'); print(
'✅ PaymentDao.createPayment: Got id from inserted object, id=${inserted.id}',
);
return inserted.id; return inserted.id;
} }
// Query back to get the id using type-safe queries // Query back to get the id using type-safe queries
// Use externalToken if available, otherwise query by unique fields // Use externalToken if available, otherwise query by unique fields
if (inserted.externalToken != null && inserted.externalToken!.isNotEmpty) { if (inserted.externalToken != null && inserted.externalToken!.isNotEmpty) {
print('🔍 PaymentDao.createPayment: Querying by externalToken=${inserted.externalToken}'); print(
final foundPayment = await getPaymentByExternalToken(inserted.externalToken!); '🔍 PaymentDao.createPayment: Querying by externalToken=${inserted.externalToken}',
);
final foundPayment = await getPaymentByExternalToken(
inserted.externalToken!,
);
if (foundPayment != null && foundPayment.id.isNotEmpty) { if (foundPayment != null && foundPayment.id.isNotEmpty) {
print('✅ PaymentDao.createPayment: Found payment by externalToken, id=${foundPayment.id}'); print(
'✅ PaymentDao.createPayment: Found payment by externalToken, id=${foundPayment.id}',
);
return foundPayment.id; return foundPayment.id;
} }
print('⚠️ PaymentDao.createPayment: No payment found by externalToken'); print('⚠️ PaymentDao.createPayment: No payment found by externalToken');
} }
// Fallback: query by userId, date, and amount using type-safe queries // Fallback: query by userId, date, and amount using type-safe queries
print('🔍 PaymentDao.createPayment: Querying by userId, date, amount (fallback)'); print(
'🔍 PaymentDao.createPayment: Querying by userId, date, amount (fallback)',
);
final query = select(payments) final query = select(payments)
..where((p) => ..where(
(p) =>
p.userId.equals(payment.userId.value) & p.userId.equals(payment.userId.value) &
p.date.equals(inserted.date) & p.date.equals(inserted.date) &
p.amount.equals(inserted.amount)) p.amount.equals(inserted.amount),
)
..orderBy([(p) => OrderingTerm.desc(p.createdAt)]) ..orderBy([(p) => OrderingTerm.desc(p.createdAt)])
..limit(1); ..limit(1);
final results = await query.get(); final results = await query.get();
if (results.isNotEmpty && results.first.id.isNotEmpty) { if (results.isNotEmpty && results.first.id.isNotEmpty) {
print('✅ PaymentDao.createPayment: Found payment by fallback query, id=${results.first.id}'); print(
'✅ PaymentDao.createPayment: Found payment by fallback query, id=${results.first.id}',
);
return results.first.id; return results.first.id;
} }
print('❌ PaymentDao.createPayment: Failed to retrieve payment ID after creation'); print(
'❌ PaymentDao.createPayment: Failed to retrieve payment ID after creation',
);
throw Exception('Failed to retrieve payment ID after creation'); throw Exception('Failed to retrieve payment ID after creation');
} }

View file

@ -26,9 +26,11 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
/// Получить пользователя с UserData /// Получить пользователя с UserData
Future<UserWithData?> getUserWithDataById(String id) async { Future<UserWithData?> getUserWithDataById(String id) async {
final query = select(users).join([ final query =
select(users).join([
leftOuterJoin(userDatas, userDatas.userId.equalsExp(users.id)), leftOuterJoin(userDatas, userDatas.userId.equalsExp(users.id)),
])..where(users.id.equals(id)) ])
..where(users.id.equals(id))
..limit(1); ..limit(1);
final results = await query.get(); final results = await query.get();
@ -146,7 +148,9 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
/// Получить UserData пользователя /// Получить UserData пользователя
Future<UserData?> getUserData(String userId) async { Future<UserData?> getUserData(String userId) async {
print('🔍 UserDao.getUserData: userId=$userId'); print('🔍 UserDao.getUserData: userId=$userId');
print('🔍 UserDao.getUserData: SQL: SELECT * FROM user_datas WHERE user_id = ? LIMIT 1'); print(
'🔍 UserDao.getUserData: SQL: SELECT * FROM user_datas WHERE user_id = ? LIMIT 1',
);
final query = select(userDatas) final query = select(userDatas)
..where((ud) => ud.userId.equals(userId)) ..where((ud) => ud.userId.equals(userId))
..limit(1); ..limit(1);
@ -202,8 +206,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
final query = select(tokens) final query = select(tokens)
..where( ..where(
(t) => (t) =>
t.externalUserId.equals(externalUserId) & t.externalUserId.equals(externalUserId) & t.isDeleted.equals(false),
t.isDeleted.equals(false),
) )
..orderBy([(t) => OrderingTerm.desc(t.created)]) ..orderBy([(t) => OrderingTerm.desc(t.created)])
..limit(1); ..limit(1);
@ -216,9 +219,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
Future<Token?> getTokenByUserId(String userId) async { Future<Token?> getTokenByUserId(String userId) async {
final query = select(tokens) final query = select(tokens)
..where((t) => t.userId.equals(userId) & t.isDeleted.equals(false)) ..where((t) => t.userId.equals(userId) & t.isDeleted.equals(false))
..where( ..where((t) => t.expires.isBiggerThanValue(PgDateTime(DateTime.now())))
(t) => t.expires.isBiggerThanValue(PgDateTime(DateTime.now())),
)
..orderBy([(t) => OrderingTerm.desc(t.created)]) ..orderBy([(t) => OrderingTerm.desc(t.created)])
..limit(1); ..limit(1);
@ -397,13 +398,17 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
/// Проверить, есть ли у пользователя доступ к паку /// Проверить, есть ли у пользователя доступ к паку
Future<bool> hasPackAccess(String userId, String packId) async { Future<bool> hasPackAccess(String userId, String packId) async {
print('🔍 UserDao.hasPackAccess: userId=$userId, packId=$packId'); print('🔍 UserDao.hasPackAccess: userId=$userId, packId=$packId');
print('🔍 UserDao.hasPackAccess: SQL: SELECT * FROM user_packs WHERE user_id = ? AND pack_id = ? LIMIT 1'); print(
'🔍 UserDao.hasPackAccess: SQL: SELECT * FROM user_packs WHERE user_id = ? AND pack_id = ? LIMIT 1',
);
final query = select(userPacks) final query = select(userPacks)
..where((up) => up.userId.equals(userId) & up.packId.equals(packId)) ..where((up) => up.userId.equals(userId) & up.packId.equals(packId))
..limit(1); ..limit(1);
final results = await query.get(); final results = await query.get();
print('✅ UserDao.hasPackAccess: Result count=${results.length}, hasAccess=${results.isNotEmpty}'); print(
'✅ UserDao.hasPackAccess: Result count=${results.length}, hasAccess=${results.isNotEmpty}',
);
return results.isNotEmpty; return results.isNotEmpty;
} }

View file

@ -337,9 +337,9 @@ class AppDatabase extends _$AppDatabase {
// 2. Удаляем все старые вопросы (soft delete) // 2. Удаляем все старые вопросы (soft delete)
print('Soft-deleting all existing test questions...'); print('Soft-deleting all existing test questions...');
final now = PgDateTime(DateTime.now()); final now = PgDateTime(DateTime.now());
await (update(testQuestions) await (update(
..where((tq) => tq.isDeleted.equals(false))) testQuestions,
.write( )..where((tq) => tq.isDeleted.equals(false))).write(
TestQuestionsCompanion( TestQuestionsCompanion(
isDeleted: const Value(true), isDeleted: const Value(true),
deletedAt: Value(now), deletedAt: Value(now),

View file

@ -42,7 +42,8 @@ class PackDtoConverter {
final coverValue = model.cover!.trim(); final coverValue = model.cover!.trim();
// If it's already a remote URL, use it directly // If it's already a remote URL, use it directly
if (coverValue.startsWith('http://') || coverValue.startsWith('https://')) { if (coverValue.startsWith('http://') ||
coverValue.startsWith('https://')) {
coverUrl = coverValue; coverUrl = coverValue;
} }
// If it's a UUID or any other value, use API endpoint // If it's a UUID or any other value, use API endpoint

View file

@ -138,7 +138,11 @@ class InputButtonsQuestionGenerator implements QuestionGenerator {
final templateToAnswerIndex = <int, int>{}; final templateToAnswerIndex = <int, int>{};
int answerLetterIndex = 0; int answerLetterIndex = 0;
for (int i = 0; i < template.length && answerLetterIndex < answerLetters.length; i++) { for (
int i = 0;
i < template.length && answerLetterIndex < answerLetters.length;
i++
) {
if (template[i] == '_' || template[i] == '|') { if (template[i] == '_' || template[i] == '|') {
templateToAnswerIndex[i] = answerLetterIndex; templateToAnswerIndex[i] = answerLetterIndex;
answerLetterIndex++; answerLetterIndex++;
@ -146,15 +150,21 @@ class InputButtonsQuestionGenerator implements QuestionGenerator {
// Skip other characters in template (visible letters, spaces) // Skip other characters in template (visible letters, spaces)
} }
int visibleLetters = (visibleButtonsPercent * slotPositions.length).floor(); int visibleLetters = (visibleButtonsPercent * slotPositions.length)
.floor();
final shuffledPositions = List<int>.from(slotPositions)..shuffle(random); final shuffledPositions = List<int>.from(slotPositions)..shuffle(random);
final positionsToReveal = shuffledPositions.take(visibleLetters).toList(); final positionsToReveal = shuffledPositions.take(visibleLetters).toList();
// Replace slots with actual letters from answer // Replace slots with actual letters from answer
for (final pos in positionsToReveal) { for (final pos in positionsToReveal) {
final answerLetterIndex = templateToAnswerIndex[pos]; final answerLetterIndex = templateToAnswerIndex[pos];
if (answerLetterIndex != null && answerLetterIndex < answerLetters.length) { if (answerLetterIndex != null &&
template = template.replaceRange(pos, pos + 1, answerLetters[answerLetterIndex]); answerLetterIndex < answerLetters.length) {
template = template.replaceRange(
pos,
pos + 1,
answerLetters[answerLetterIndex],
);
} }
} }
} }

View file

@ -50,3 +50,4 @@ VACUUM FULL ANALYZE payments;

View file

@ -123,3 +123,4 @@ DROP TYPE IF EXISTS grant_type CASCADE;

View file

@ -195,3 +195,4 @@ PRINT 'Migration 003 completed successfully!';

View file

@ -29,33 +29,25 @@ void main() {
late AccessService accessService; late AccessService accessService;
late UserModel adminUser; late UserModel adminUser;
Request buildAdminRequest( Request buildAdminRequest(String method, String url, {Object? body}) {
String method,
String url, {
Object? body,
}) {
return Request( return Request(
method, method,
Uri.parse(url), Uri.parse(url),
body: body == null ? null : jsonEncode(body), body: body == null ? null : jsonEncode(body),
).change( ).change(context: {'user': adminUser, 'accessService': accessService});
context: {
'user': adminUser,
'accessService': accessService,
},
);
} }
setUpAll(() async { setUpAll(() async {
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost'; final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
final port = final port =
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432; int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
final database = final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test'; final username =
final username = Platform.environment['TEST_DB_USER'] ?? Platform.environment['TEST_DB_USER'] ??
Platform.environment['DB_USER'] ?? Platform.environment['DB_USER'] ??
'mnemo_user'; 'mnemo_user';
final password = Platform.environment['TEST_DB_PASSWORD'] ?? final password =
Platform.environment['TEST_DB_PASSWORD'] ??
Platform.environment['DB_PASSWORD'] ?? Platform.environment['DB_PASSWORD'] ??
''; '';
@ -78,7 +70,10 @@ void main() {
final discountsManager = DiscountsManager(db); final discountsManager = DiscountsManager(db);
final productsPriceResolver = ProductsPriceResolver(discountsManager, db); final productsPriceResolver = ProductsPriceResolver(discountsManager, db);
final adsManager = AdsManager(); final adsManager = AdsManager();
final packDtoConverter = PackDtoConverter(productsPriceResolver, adsManager); final packDtoConverter = PackDtoConverter(
productsPriceResolver,
adsManager,
);
final packManager = PackManager(db, packDtoConverter); final packManager = PackManager(db, packDtoConverter);
final resourceLoader = ResourceLoader(packManager); final resourceLoader = ResourceLoader(packManager);
@ -107,23 +102,25 @@ void main() {
Future<void> cleanup() async { Future<void> cleanup() async {
// Delete relations first. // Delete relations first.
if (testId != null) { if (testId != null) {
await (db.delete(db.testPackRelations) await (db.delete(
..where((r) => r.testId.equals(testId!))) db.testPackRelations,
.go(); )..where((r) => r.testId.equals(testId!))).go();
await (db.delete(db.testQuestions) await (db.delete(
..where((q) => q.testId.equals(testId!))) db.testQuestions,
.go(); )..where((q) => q.testId.equals(testId!))).go();
await (db.delete(db.tests)..where((t) => t.id.equals(testId!))).go(); await (db.delete(db.tests)..where((t) => t.id.equals(testId!))).go();
} }
if (packId != null) { if (packId != null) {
await (db.delete(db.cardPackCards) await (db.delete(
..where((c) => c.packId.equals(packId!))) db.cardPackCards,
.go(); )..where((c) => c.packId.equals(packId!))).go();
await (db.delete(db.cardPacks)..where((p) => p.id.equals(packId!))).go(); await (db.delete(
db.cardPacks,
)..where((p) => p.id.equals(packId!))).go();
} }
for (final cardId in createdCardIds) { for (final cardId in createdCardIds) {
@ -131,7 +128,9 @@ void main() {
await (db.delete(db.gameCards)..where((c) => c.id.equals(cardId))).go(); await (db.delete(db.gameCards)..where((c) => c.id.equals(cardId))).go();
// Remove possible image files in data/cards // Remove possible image files in data/cards
final cardsDir = Directory('${PackManagerUtils.assetsDirectory.path}/cards'); final cardsDir = Directory(
'${PackManagerUtils.assetsDirectory.path}/cards',
);
final candidates = [ final candidates = [
File('${cardsDir.path}/$cardId.png'), File('${cardsDir.path}/$cardId.png'),
File('${cardsDir.path}/$cardId.webp'), File('${cardsDir.path}/$cardId.webp'),
@ -159,7 +158,8 @@ void main() {
await cleanup(); await cleanup();
}); });
test('getTest returns image URLs (no base64) and cards are linked to pack', test(
'getTest returns image URLs (no base64) and cards are linked to pack',
() async { () async {
// Create pack // Create pack
packId = await db.packDao.createPack( packId = await db.packDao.createPack(
@ -184,11 +184,7 @@ void main() {
'answer': 'btn1', 'answer': 'btn1',
'image': oneByOnePngBase64, 'image': oneByOnePngBase64,
'buttons': [ 'buttons': [
{ {'id': 'btn1', 'text': 'ok', 'image': oneByOnePngBase64},
'id': 'btn1',
'text': 'ok',
'image': oneByOnePngBase64,
},
], ],
}, },
], ],
@ -197,8 +193,8 @@ void main() {
final createResp = await adminTestsApi.upsertTest(createRequest); final createResp = await adminTestsApi.upsertTest(createRequest);
expect(createResp.statusCode, anyOf(equals(201), equals(200))); expect(createResp.statusCode, anyOf(equals(201), equals(200)));
final createBody = jsonDecode(await createResp.readAsString()) final createBody =
as Map<String, dynamic>; jsonDecode(await createResp.readAsString()) as Map<String, dynamic>;
testId = (createBody['test'] as Map<String, dynamic>)['id'] as String; testId = (createBody['test'] as Map<String, dynamic>)['id'] as String;
expect(testId, isNotEmpty); expect(testId, isNotEmpty);
@ -224,8 +220,8 @@ void main() {
final getResp = await adminTestsApi.getTest(getReq, testId!); final getResp = await adminTestsApi.getTest(getReq, testId!);
expect(getResp.statusCode, equals(200)); expect(getResp.statusCode, equals(200));
final body = jsonDecode(await getResp.readAsString()) final body =
as Map<String, dynamic>; jsonDecode(await getResp.readAsString()) as Map<String, dynamic>;
final cover = body['cover'] as String?; final cover = body['cover'] as String?;
expect(cover, isNotNull); expect(cover, isNotNull);
@ -268,12 +264,17 @@ void main() {
createdCardIds.addAll([coverCardId!, qCardId!, bCardId!]); createdCardIds.addAll([coverCardId!, qCardId!, bCardId!]);
final linked = await (db.select(db.cardPackCards) final linked =
..where((c) => c.packId.equals(packId!) & await (db.select(db.cardPackCards)..where(
c.cardId.isIn([coverCardId, qCardId, bCardId]))) (c) =>
c.packId.equals(packId!) &
c.cardId.isIn([coverCardId, qCardId, bCardId]),
))
.get(); .get();
expect(linked.map((e) => e.cardId).toSet(), expect(
containsAll([coverCardId, qCardId, bCardId])); linked.map((e) => e.cardId).toSet(),
containsAll([coverCardId, qCardId, bCardId]),
);
// Also ensure DB stores cardIds, not API URLs/base64. // Also ensure DB stores cardIds, not API URLs/base64.
final dbQuestions = await db.testDao.getTestQuestions(testId!); final dbQuestions = await db.testDao.getTestQuestions(testId!);
@ -284,19 +285,35 @@ void main() {
final storedUiImage = uiData['image']?.toString(); final storedUiImage = uiData['image']?.toString();
expect(storedUiImage, isNotNull); expect(storedUiImage, isNotNull);
expect(RegExp(r'^[0-9a-f\-]{36}$', caseSensitive: false) expect(
.hasMatch(storedUiImage!), isTrue); RegExp(
r'^[0-9a-f\-]{36}$',
caseSensitive: false,
).hasMatch(storedUiImage!),
isTrue,
);
final storedBtnImage = final storedBtnImage = (options.first as Map<String, dynamic>)['image']
(options.first as Map<String, dynamic>)['image']?.toString(); ?.toString();
expect(storedBtnImage, isNotNull); expect(storedBtnImage, isNotNull);
expect(RegExp(r'^[0-9a-f\-]{36}$', caseSensitive: false) expect(
.hasMatch(storedBtnImage!), isTrue); RegExp(
r'^[0-9a-f\-]{36}$',
caseSensitive: false,
).hasMatch(storedBtnImage!),
isTrue,
);
final storedCover = (await db.testDao.getTestById(testId!))!.cover; final storedCover = (await db.testDao.getTestById(testId!))!.cover;
expect(storedCover, isNotNull); expect(storedCover, isNotNull);
expect(RegExp(r'^[0-9a-f\-]{36}$', caseSensitive: false) expect(
.hasMatch(storedCover!), isTrue); RegExp(
}); r'^[0-9a-f\-]{36}$',
caseSensitive: false,
).hasMatch(storedCover!),
isTrue,
);
},
);
}); });
} }

View file

@ -78,7 +78,11 @@ void main() {
}); });
authApiV2 = AuthApiV2( authApiV2 = AuthApiV2(
userManager, mockGoogleApi, jwtService, telegramAuthCodeService); userManager,
mockGoogleApi,
jwtService,
telegramAuthCodeService,
);
}); });
tearDown(() async { tearDown(() async {
@ -101,8 +105,9 @@ void main() {
const email = 'user@example.com'; const email = 'user@example.com';
const name = 'Test User'; const name = 'Test User';
when(mockGoogleApi.getGoogleUserId(googleIdToken)) when(
.thenAnswer((_) async => googleUserId); mockGoogleApi.getGoogleUserId(googleIdToken),
).thenAnswer((_) async => googleUserId);
final request = Request( final request = Request(
'POST', 'POST',
@ -151,8 +156,9 @@ void main() {
test('should return 401 when Google token is invalid', () async { test('should return 401 when Google token is invalid', () async {
const googleIdToken = 'invalid_google_token'; const googleIdToken = 'invalid_google_token';
when(mockGoogleApi.getGoogleUserId(googleIdToken)) when(
.thenAnswer((_) async => null); mockGoogleApi.getGoogleUserId(googleIdToken),
).thenAnswer((_) async => null);
final request = Request( final request = Request(
'POST', 'POST',
@ -267,9 +273,7 @@ void main() {
final request = Request( final request = Request(
'GET', 'GET',
Uri.parse('http://localhost/api/v2/auth/me'), Uri.parse('http://localhost/api/v2/auth/me'),
headers: { headers: {'Authorization': 'Bearer ${tokens.accessToken}'},
'Authorization': 'Bearer ${tokens.accessToken}',
},
).change(context: {'user': testUser}); ).change(context: {'user': testUser});
final response = await authApiV2.getCurrentUser(request); final response = await authApiV2.getCurrentUser(request);
@ -340,9 +344,7 @@ void main() {
'POST', 'POST',
Uri.parse('http://localhost/api/v2/auth/logout'), Uri.parse('http://localhost/api/v2/auth/logout'),
body: '', body: '',
headers: { headers: {'Authorization': 'Bearer ${tokens.accessToken}'},
'Authorization': 'Bearer ${tokens.accessToken}',
},
).change(context: {'user': testUser}); ).change(context: {'user': testUser});
final response = await authApiV2.logout(request); final response = await authApiV2.logout(request);
@ -369,7 +371,8 @@ void main() {
}); });
group('AuthApiV2 - Telegram Web Code Flow', () { group('AuthApiV2 - Telegram Web Code Flow', () {
test('should create, claim, and authenticate using web-generated code', test(
'should create, claim, and authenticate using web-generated code',
() async { () async {
// Step 1: create code from web // Step 1: create code from web
final createRequest = Request( final createRequest = Request(
@ -378,9 +381,11 @@ void main() {
body: '', body: '',
); );
final createResponse = final createResponse = await authApiV2.createWebTelegramCode(
await authApiV2.createWebTelegramCode(createRequest); createRequest,
final createBody = jsonDecode(await createResponse.readAsString()) );
final createBody =
jsonDecode(await createResponse.readAsString())
as Map<String, dynamic>; as Map<String, dynamic>;
expect(createResponse.statusCode, equals(200)); expect(createResponse.statusCode, equals(200));
@ -396,9 +401,12 @@ void main() {
Uri.parse('http://localhost/api/v2/auth/telegram/code-status/$code'), Uri.parse('http://localhost/api/v2/auth/telegram/code-status/$code'),
); );
final statusResponse = final statusResponse = await authApiV2.getTelegramCodeStatus(
await authApiV2.getTelegramCodeStatus(statusRequest, code!); statusRequest,
final statusBody = jsonDecode(await statusResponse.readAsString()) code!,
);
final statusBody =
jsonDecode(await statusResponse.readAsString())
as Map<String, dynamic>; as Map<String, dynamic>;
expect(statusResponse.statusCode, equals(200)); expect(statusResponse.statusCode, equals(200));
@ -414,8 +422,9 @@ void main() {
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
final preAuthResponse = final preAuthResponse = await authApiV2.authenticateTelegram(
await authApiV2.authenticateTelegram(preAuthRequest); preAuthRequest,
);
expect(preAuthResponse.statusCode, equals(401)); expect(preAuthResponse.statusCode, equals(401));
// Step 4: claim code via bot // Step 4: claim code via bot
@ -433,7 +442,8 @@ void main() {
); );
final claimResponse = await authApiV2.claimTelegramCode(claimRequest); final claimResponse = await authApiV2.claimTelegramCode(claimRequest);
final claimBody = jsonDecode(await claimResponse.readAsString()) final claimBody =
jsonDecode(await claimResponse.readAsString())
as Map<String, dynamic>; as Map<String, dynamic>;
expect(claimResponse.statusCode, equals(200)); expect(claimResponse.statusCode, equals(200));
@ -441,8 +451,10 @@ void main() {
expect(claimBody['remainingSeconds'], greaterThan(0)); expect(claimBody['remainingSeconds'], greaterThan(0));
// Step 5: status should be claimed // Step 5: status should be claimed
final claimedStatusResponse = final claimedStatusResponse = await authApiV2.getTelegramCodeStatus(
await authApiV2.getTelegramCodeStatus(statusRequest, code); statusRequest,
code,
);
final claimedStatusBody = final claimedStatusBody =
jsonDecode(await claimedStatusResponse.readAsString()) jsonDecode(await claimedStatusResponse.readAsString())
as Map<String, dynamic>; as Map<String, dynamic>;
@ -462,7 +474,8 @@ void main() {
final authResponse = await authApiV2.authenticateTelegram(authRequest); final authResponse = await authApiV2.authenticateTelegram(authRequest);
final authBody = final authBody =
jsonDecode(await authResponse.readAsString()) as Map<String, dynamic>; jsonDecode(await authResponse.readAsString())
as Map<String, dynamic>;
expect(authResponse.statusCode, equals(200)); expect(authResponse.statusCode, equals(200));
expect(authBody['user'], isNotNull); expect(authBody['user'], isNotNull);
@ -470,16 +483,20 @@ void main() {
expect(authBody['refreshToken'], isNotEmpty); expect(authBody['refreshToken'], isNotEmpty);
// Step 7: status reflects used code // Step 7: status reflects used code
final usedStatusResponse = final usedStatusResponse = await authApiV2.getTelegramCodeStatus(
await authApiV2.getTelegramCodeStatus(statusRequest, code); statusRequest,
final usedStatusBody = jsonDecode(await usedStatusResponse.readAsString()) code,
);
final usedStatusBody =
jsonDecode(await usedStatusResponse.readAsString())
as Map<String, dynamic>; as Map<String, dynamic>;
expect(usedStatusResponse.statusCode, equals(200)); expect(usedStatusResponse.statusCode, equals(200));
expect(usedStatusBody['status'], equals('used')); expect(usedStatusBody['status'], equals('used'));
expect(usedStatusBody['isClaimed'], isTrue); expect(usedStatusBody['isClaimed'], isTrue);
expect(usedStatusBody['isUsed'], isTrue); expect(usedStatusBody['isUsed'], isTrue);
}); },
);
test('should return error when claiming unknown code', () async { test('should return error when claiming unknown code', () async {
final claimRequest = Request( final claimRequest = Request(

View file

@ -93,8 +93,11 @@ void main() {
if (token != null) { if (token != null) {
headers['authorization'] = 'Bearer $token'; headers['authorization'] = 'Bearer $token';
} }
final request = final request = Request(
Request(method, Uri.parse('http://localhost$path'), headers: headers); method,
Uri.parse('http://localhost$path'),
headers: headers,
);
return Future.sync(() => handler(request)); return Future.sync(() => handler(request));
} }
@ -106,8 +109,7 @@ void main() {
}); });
test('attaches user context when token present on GET /packs/<id>', () async { test('attaches user context when token present on GET /packs/<id>', () async {
final response = final response = await _makeRequest('/packs/10', token: accessToken);
await _makeRequest('/packs/10', token: accessToken);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(await response.readAsString(), equals('user-${testUser.id}')); expect(await response.readAsString(), equals('user-${testUser.id}'));
@ -117,13 +119,11 @@ void main() {
final response = await _makeRequest('/packs/10/tests'); final response = await _makeRequest('/packs/10/tests');
expect(response.statusCode, equals(401)); expect(response.statusCode, equals(401));
expect(await response.readAsString(), expect(await response.readAsString(), contains('"error":"Unauthorized"'));
contains('"error":"Unauthorized"'));
}); });
test('allows GET /packs/<id>/tests when token provided', () async { test('allows GET /packs/<id>/tests when token provided', () async {
final response = final response = await _makeRequest('/packs/10/tests', token: accessToken);
await _makeRequest('/packs/10/tests', token: accessToken);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(await response.readAsString(), equals('user-${testUser.id}')); expect(await response.readAsString(), equals('user-${testUser.id}'));
@ -133,8 +133,6 @@ void main() {
final response = await _makeRequest('/packs/10', token: 'invalid'); final response = await _makeRequest('/packs/10', token: 'invalid');
expect(response.statusCode, equals(401)); expect(response.statusCode, equals(401));
expect(await response.readAsString(), expect(await response.readAsString(), contains('"error":"Unauthorized"'));
contains('"error":"Unauthorized"'));
}); });
} }

View file

@ -64,9 +64,12 @@ void main() {
Uri.parse('http://localhost/api/v2/games/nonexistent_game/assets'), Uri.parse('http://localhost/api/v2/games/nonexistent_game/assets'),
); );
final response = await gamesApiV2.getGameAssets(request, 'nonexistent_game'); final response = await gamesApiV2.getGameAssets(
final responseBody = jsonDecode(await response.readAsString()) request,
as Map<String, dynamic>; 'nonexistent_game',
);
final responseBody =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(404)); expect(response.statusCode, equals(404));
expect(responseBody['error'], equals('Not Found')); expect(responseBody['error'], equals('Not Found'));
@ -87,8 +90,8 @@ void main() {
expect(response.statusCode, isIn([200, 404])); expect(response.statusCode, isIn([200, 404]));
if (response.statusCode == 404) { if (response.statusCode == 404) {
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(responseBody['error'], equals('Not Found')); expect(responseBody['error'], equals('Not Found'));
} else { } else {
// If file exists, verify it returns binary data // If file exists, verify it returns binary data
@ -108,14 +111,8 @@ void main() {
// Only check headers if the file exists (status 200) // Only check headers if the file exists (status 200)
if (response.statusCode == 200) { if (response.statusCode == 200) {
expect(response.headers['content-type'], equals('application/zip')); expect(response.headers['content-type'], equals('application/zip'));
expect( expect(response.headers['content-disposition'], contains('attachment'));
response.headers['content-disposition'], expect(response.headers['cache-control'], contains('max-age=86400'));
contains('attachment'),
);
expect(
response.headers['cache-control'],
contains('max-age=86400'),
);
} }
}); });
@ -131,12 +128,11 @@ void main() {
expect(response.statusCode, isIn([200, 404])); expect(response.statusCode, isIn([200, 404]));
if (response.statusCode == 404) { if (response.statusCode == 404) {
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
// Should say "assets not found" not "game not found" // Should say "assets not found" not "game not found"
expect(responseBody['message'], isNot(contains('Game not found'))); expect(responseBody['message'], isNot(contains('Game not found')));
} }
}); });
}); });
} }

View file

@ -234,7 +234,9 @@ void main() {
.findFirst(); .findFirst();
if (token != null) { if (token != null) {
await testIsar.refreshTokenModels.put( await testIsar.refreshTokenModels.put(
token.copyWith(expiresAt: DateTime.now().subtract(Duration(hours: 1))), token.copyWith(
expiresAt: DateTime.now().subtract(Duration(hours: 1)),
),
); );
} }
}); });
@ -274,4 +276,3 @@ void main() {
}); });
}); });
} }

View file

@ -40,9 +40,7 @@ void main() {
AdminAccessPolicy(), AdminAccessPolicy(),
); );
final context = <String, Object?>{ final context = <String, Object?>{'accessService': accessService};
'accessService': accessService,
};
if (user != null) { if (user != null) {
context['user'] = user; context['user'] = user;
@ -109,11 +107,13 @@ void main() {
final response = await mediaApiV2.uploadCardImage(request); final response = await mediaApiV2.uploadCardImage(request);
expect(response.statusCode, equals(403)); expect(response.statusCode, equals(403));
verifyNever(mockMinioService.uploadFile( verifyNever(
mockMinioService.uploadFile(
bucket: anyNamed('bucket'), bucket: anyNamed('bucket'),
bytes: anyNamed('bytes'), bytes: anyNamed('bytes'),
contentType: anyNamed('contentType'), contentType: anyNamed('contentType'),
)); ),
);
}); });
test('should return 400 when file size exceeds limit', () async { test('should return 400 when file size exceeds limit', () async {
@ -142,16 +142,20 @@ void main() {
const testObjectId = 'test-uuid-456'; const testObjectId = 'test-uuid-456';
const testPresignedUrl = 'https://minio.example.com/presigned-url-2'; const testPresignedUrl = 'https://minio.example.com/presigned-url-2';
when(mockMinioService.uploadFile( when(
mockMinioService.uploadFile(
bucket: MinioConfig.cardImagesBucket, bucket: MinioConfig.cardImagesBucket,
bytes: anyNamed('bytes'), bytes: anyNamed('bytes'),
contentType: 'image/jpeg', contentType: 'image/jpeg',
)).thenAnswer((_) async => testObjectId); ),
).thenAnswer((_) async => testObjectId);
when(mockMinioService.getPresignedUrl( when(
mockMinioService.getPresignedUrl(
bucket: MinioConfig.cardImagesBucket, bucket: MinioConfig.cardImagesBucket,
objectId: testObjectId, objectId: testObjectId,
)).thenAnswer((_) async => testPresignedUrl); ),
).thenAnswer((_) async => testPresignedUrl);
// Test images now use the same bucket as card images // Test images now use the same bucket as card images
expect(testObjectId, isNotEmpty); expect(testObjectId, isNotEmpty);
@ -164,16 +168,20 @@ void main() {
const testObjectId = 'test-uuid-789'; const testObjectId = 'test-uuid-789';
const testPresignedUrl = 'https://minio.example.com/presigned-url-3'; const testPresignedUrl = 'https://minio.example.com/presigned-url-3';
when(mockMinioService.uploadFile( when(
mockMinioService.uploadFile(
bucket: MinioConfig.voiceAudioBucket, bucket: MinioConfig.voiceAudioBucket,
bytes: anyNamed('bytes'), bytes: anyNamed('bytes'),
contentType: 'audio/mpeg', contentType: 'audio/mpeg',
)).thenAnswer((_) async => testObjectId); ),
).thenAnswer((_) async => testObjectId);
when(mockMinioService.getPresignedUrl( when(
mockMinioService.getPresignedUrl(
bucket: MinioConfig.voiceAudioBucket, bucket: MinioConfig.voiceAudioBucket,
objectId: testObjectId, objectId: testObjectId,
)).thenAnswer((_) async => testPresignedUrl); ),
).thenAnswer((_) async => testPresignedUrl);
// Similar to uploadCardImage test // Similar to uploadCardImage test
expect(testObjectId, isNotEmpty); expect(testObjectId, isNotEmpty);
@ -205,11 +213,13 @@ void main() {
const testPresignedUrl = 'https://minio.example.com/presigned-url-4'; const testPresignedUrl = 'https://minio.example.com/presigned-url-4';
const testBucket = MinioConfig.cardImagesBucket; const testBucket = MinioConfig.cardImagesBucket;
when(mockMinioService.getPresignedUrl( when(
mockMinioService.getPresignedUrl(
bucket: testBucket, bucket: testBucket,
objectId: testObjectId, objectId: testObjectId,
expirySeconds: anyNamed('expirySeconds'), expirySeconds: anyNamed('expirySeconds'),
)).thenAnswer((_) async => testPresignedUrl); ),
).thenAnswer((_) async => testPresignedUrl);
final request = buildRequest( final request = buildRequest(
'GET', 'GET',
@ -221,29 +231,33 @@ void main() {
testBucket, testBucket,
testObjectId, testObjectId,
); );
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['url'], equals(testPresignedUrl)); expect(responseBody['url'], equals(testPresignedUrl));
expect(responseBody['expiresAt'], isNotNull); expect(responseBody['expiresAt'], isNotNull);
verify(mockMinioService.getPresignedUrl( verify(
mockMinioService.getPresignedUrl(
bucket: testBucket, bucket: testBucket,
objectId: testObjectId, objectId: testObjectId,
expirySeconds: anyNamed('expirySeconds'), expirySeconds: anyNamed('expirySeconds'),
)).called(1); ),
).called(1);
}); });
test('should return 404 when object does not exist', () async { test('should return 404 when object does not exist', () async {
const testObjectId = 'non-existent-id'; const testObjectId = 'non-existent-id';
const testBucket = MinioConfig.cardImagesBucket; const testBucket = MinioConfig.cardImagesBucket;
when(mockMinioService.getPresignedUrl( when(
mockMinioService.getPresignedUrl(
bucket: testBucket, bucket: testBucket,
objectId: testObjectId, objectId: testObjectId,
expirySeconds: anyNamed('expirySeconds'), expirySeconds: anyNamed('expirySeconds'),
)).thenAnswer((_) async => null); ),
).thenAnswer((_) async => null);
final request = buildRequest( final request = buildRequest(
'GET', 'GET',
@ -275,11 +289,13 @@ void main() {
); );
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
verifyNever(mockMinioService.getPresignedUrl( verifyNever(
mockMinioService.getPresignedUrl(
bucket: anyNamed('bucket'), bucket: anyNamed('bucket'),
objectId: anyNamed('objectId'), objectId: anyNamed('objectId'),
expirySeconds: anyNamed('expirySeconds'), expirySeconds: anyNamed('expirySeconds'),
)); ),
);
}); });
}); });
@ -288,10 +304,9 @@ void main() {
const testObjectId = 'test-uuid-delete'; const testObjectId = 'test-uuid-delete';
const testBucket = MinioConfig.cardImagesBucket; const testBucket = MinioConfig.cardImagesBucket;
when(mockMinioService.deleteFile( when(
bucket: testBucket, mockMinioService.deleteFile(bucket: testBucket, objectId: testObjectId),
objectId: testObjectId, ).thenAnswer((_) async => Future.value());
)).thenAnswer((_) async => Future.value());
final request = buildRequest( final request = buildRequest(
'DELETE', 'DELETE',
@ -304,16 +319,15 @@ void main() {
testBucket, testBucket,
testObjectId, testObjectId,
); );
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['success'], isTrue); expect(responseBody['success'], isTrue);
verify(mockMinioService.deleteFile( verify(
bucket: testBucket, mockMinioService.deleteFile(bucket: testBucket, objectId: testObjectId),
objectId: testObjectId, ).called(1);
)).called(1);
}); });
test('should return 403 when user is not admin', () async { test('should return 403 when user is not admin', () async {
@ -340,10 +354,12 @@ void main() {
); );
expect(response.statusCode, equals(403)); expect(response.statusCode, equals(403));
verifyNever(mockMinioService.deleteFile( verifyNever(
mockMinioService.deleteFile(
bucket: anyNamed('bucket'), bucket: anyNamed('bucket'),
objectId: anyNamed('objectId'), objectId: anyNamed('objectId'),
)); ),
);
}); });
test('should return 400 for invalid bucket name', () async { test('should return 400 for invalid bucket name', () async {
@ -363,10 +379,12 @@ void main() {
); );
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
verifyNever(mockMinioService.deleteFile( verifyNever(
mockMinioService.deleteFile(
bucket: anyNamed('bucket'), bucket: anyNamed('bucket'),
objectId: anyNamed('objectId'), objectId: anyNamed('objectId'),
)); ),
);
}); });
}); });
} }

View file

@ -26,11 +26,7 @@ void main() {
late PacksApiV2 packsApiV2; late PacksApiV2 packsApiV2;
late UserModel testUser; late UserModel testUser;
Request buildRequest( Request buildRequest(String method, String url, {UserModel? user}) {
String method,
String url, {
UserModel? user,
}) {
final resourceLoader = ResourceLoader(packManager); final resourceLoader = ResourceLoader(packManager);
final accessService = AccessService( final accessService = AccessService(
PackAccessPolicy(resourceLoader), PackAccessPolicy(resourceLoader),
@ -87,10 +83,7 @@ void main() {
final discountsManager = const DiscountsManager(); final discountsManager = const DiscountsManager();
final productsPriceResolver = ProductsPriceResolver(discountsManager); final productsPriceResolver = ProductsPriceResolver(discountsManager);
final adsManager = AdsManager(); final adsManager = AdsManager();
packDtoConverter = PackDtoConverter( packDtoConverter = PackDtoConverter(productsPriceResolver, adsManager);
productsPriceResolver,
adsManager,
);
packManager = PackManager(packDtoConverter); packManager = PackManager(packDtoConverter);
testManager = TestManager(packDtoConverter); testManager = TestManager(packDtoConverter);
@ -148,10 +141,7 @@ void main() {
// Update cardsOrder // Update cardsOrder
await testIsar.cardPackModels.put( await testIsar.cardPackModels.put(
savedPack.copyWith( savedPack.copyWith(cardsOrder: [card1.id!, card2.id!], size: 2),
cardsOrder: [card1.id!, card2.id!],
size: 2,
),
); );
final privatePack = CardPackModel( final privatePack = CardPackModel(
@ -183,10 +173,7 @@ void main() {
await savedPrivatePack.cards.save(); await savedPrivatePack.cards.save();
await testIsar.cardPackModels.put( await testIsar.cardPackModels.put(
savedPrivatePack.copyWith( savedPrivatePack.copyWith(cardsOrder: [privateCard.id!], size: 1),
cardsOrder: [privateCard.id!],
size: 1,
),
); );
}); });
}); });
@ -213,8 +200,8 @@ void main() {
); );
final response = await packsApiV2.getPacks(request); final response = await packsApiV2.getPacks(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['items'], isA<List>()); expect(responseBody['items'], isA<List>());
@ -225,14 +212,11 @@ void main() {
}); });
test('should return packs with default pagination', () async { test('should return packs with default pagination', () async {
final request = buildRequest( final request = buildRequest('GET', 'http://localhost/api/v2/packs');
'GET',
'http://localhost/api/v2/packs',
);
final response = await packsApiV2.getPacks(request); final response = await packsApiV2.getPacks(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['page'], equals(1)); expect(responseBody['page'], equals(1));
@ -246,14 +230,15 @@ void main() {
); );
final response = await packsApiV2.getPacks(request); final response = await packsApiV2.getPacks(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final items = responseBody['items'] as List; final items = responseBody['items'] as List;
// Should find testPack with "Test" in title // Should find testPack with "Test" in title
final found = items.any((pack) => final found = items.any(
(pack['title'] as String).contains('Test')); (pack) => (pack['title'] as String).contains('Test'),
);
expect(found, isTrue); expect(found, isTrue);
}); });
@ -264,8 +249,8 @@ void main() {
); );
final response = await packsApiV2.getPacks(request); final response = await packsApiV2.getPacks(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('Bad Request')); expect(responseBody['error'], equals('Bad Request'));
@ -279,32 +264,34 @@ void main() {
); );
final response = await packsApiV2.getPacks(request); final response = await packsApiV2.getPacks(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('Bad Request')); expect(responseBody['error'], equals('Bad Request'));
expect(responseBody['message'], contains('Limit must be between 1 and 100')); expect(
responseBody['message'],
contains('Limit must be between 1 and 100'),
);
}); });
}); });
group('PacksApiV2 - Get Pack', () { group('PacksApiV2 - Get Pack', () {
test('should return pack details for public pack (id=10)', () async { test('should return pack details for public pack (id=10)', () async {
final request = buildRequest( final request = buildRequest('GET', 'http://localhost/api/v2/packs/10');
'GET',
'http://localhost/api/v2/packs/10',
);
final response = await packsApiV2.getPack(request, '10'); final response = await packsApiV2.getPack(request, '10');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['id'], equals('10')); expect(responseBody['id'], equals('10'));
expect(responseBody['title'], equals('Test Pack')); expect(responseBody['title'], equals('Test Pack'));
}); });
test('should return pack with purchase status for authenticated user', () async { test(
'should return pack with purchase status for authenticated user',
() async {
// First, add pack to user // First, add pack to user
await testIsar.writeTxn(() async { await testIsar.writeTxn(() async {
final user = await testIsar.userModels.get(1); final user = await testIsar.userModels.get(1);
@ -329,14 +316,15 @@ void main() {
); );
final response = await packsApiV2.getPack(request, '10'); final response = await packsApiV2.getPack(request, '10');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['id'], equals('10')); expect(responseBody['id'], equals('10'));
// isPurchased may be false since pack id=10 is public, so just check it exists // isPurchased may be false since pack id=10 is public, so just check it exists
expect(responseBody.containsKey('isPurchased'), isTrue); expect(responseBody.containsKey('isPurchased'), isTrue);
}); },
);
test('should return 404 for non-existent pack', () async { test('should return 404 for non-existent pack', () async {
final request = buildRequest( final request = buildRequest(
@ -346,30 +334,32 @@ void main() {
); );
final response = await packsApiV2.getPack(request, '999'); final response = await packsApiV2.getPack(request, '999');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(404)); expect(response.statusCode, equals(404));
expect(responseBody['error'], equals('Not Found')); expect(responseBody['error'], equals('Not Found'));
}); });
test('should return buy page for unauthenticated private pack access', () async { test(
final request = buildRequest( 'should return buy page for unauthenticated private pack access',
'GET', () async {
'http://localhost/api/v2/packs/11', final request = buildRequest('GET', 'http://localhost/api/v2/packs/11');
);
final response = await packsApiV2.getPack(request, '11'); final response = await packsApiV2.getPack(request, '11');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['id'], equals('11')); expect(responseBody['id'], equals('11'));
expect(responseBody['price'], equals('199')); expect(responseBody['price'], equals('199'));
expect(responseBody.containsKey('cards'), isTrue); expect(responseBody.containsKey('cards'), isTrue);
}); },
);
test('should return buy page for authenticated user without pack access', () async { test(
'should return buy page for authenticated user without pack access',
() async {
final request = buildRequest( final request = buildRequest(
'GET', 'GET',
'http://localhost/api/v2/packs/11', 'http://localhost/api/v2/packs/11',
@ -377,14 +367,15 @@ void main() {
); );
final response = await packsApiV2.getPack(request, '11'); final response = await packsApiV2.getPack(request, '11');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['id'], equals('11')); expect(responseBody['id'], equals('11'));
expect(responseBody['price'], equals('199')); expect(responseBody['price'], equals('199'));
expect(responseBody.containsKey('cards'), isTrue); expect(responseBody.containsKey('cards'), isTrue);
}); },
);
}); });
group('PacksApiV2 - Get Pack Cards', () { group('PacksApiV2 - Get Pack Cards', () {
@ -396,8 +387,8 @@ void main() {
); );
final response = await packsApiV2.getPackCards(request, '10'); final response = await packsApiV2.getPackCards(request, '10');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['items'], isA<List>()); expect(responseBody['items'], isA<List>());
@ -409,7 +400,6 @@ void main() {
expect(items.length, greaterThanOrEqualTo(0)); expect(items.length, greaterThanOrEqualTo(0));
}); });
test('should return 404 for non-existent pack', () async { test('should return 404 for non-existent pack', () async {
final request = buildRequest( final request = buildRequest(
'GET', 'GET',
@ -418,8 +408,8 @@ void main() {
); );
final response = await packsApiV2.getPackCards(request, '999'); final response = await packsApiV2.getPackCards(request, '999');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(404)); expect(response.statusCode, equals(404));
expect(responseBody['error'], equals('Not Found')); expect(responseBody['error'], equals('Not Found'));
@ -433,8 +423,8 @@ void main() {
); );
final response = await packsApiV2.getPackCards(request, '10'); final response = await packsApiV2.getPackCards(request, '10');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('Bad Request')); expect(responseBody['error'], equals('Bad Request'));
@ -450,8 +440,8 @@ void main() {
); );
final response = await packsApiV2.getCardImage(request, '10', '999'); final response = await packsApiV2.getCardImage(request, '10', '999');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(404)); expect(response.statusCode, equals(404));
expect(responseBody['error'], equals('Not Found')); expect(responseBody['error'], equals('Not Found'));
@ -465,8 +455,8 @@ void main() {
); );
final response = await packsApiV2.getCardImage(request, '999', '1'); final response = await packsApiV2.getCardImage(request, '999', '1');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(404)); expect(response.statusCode, equals(404));
expect(responseBody['error'], equals('Not Found')); expect(responseBody['error'], equals('Not Found'));
@ -480,8 +470,8 @@ void main() {
); );
final response = await packsApiV2.getCardImage(request, '10', 'invalid'); final response = await packsApiV2.getCardImage(request, '10', 'invalid');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('Bad Request')); expect(responseBody['error'], equals('Bad Request'));
@ -495,8 +485,8 @@ void main() {
); );
final response = await packsApiV2.getCardImage(request, 'invalid', '1'); final response = await packsApiV2.getCardImage(request, 'invalid', '1');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('Bad Request')); expect(responseBody['error'], equals('Bad Request'));
@ -524,14 +514,16 @@ void main() {
); );
final response = await packsApiV2.getCardImage(request, '99', '1'); final response = await packsApiV2.getCardImage(request, '99', '1');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(404)); expect(response.statusCode, equals(404));
expect(responseBody['error'], equals('Not Found')); expect(responseBody['error'], equals('Not Found'));
}); });
test('should allow access without authentication for enabled packs', () async { test(
'should allow access without authentication for enabled packs',
() async {
// Request without user context (public access) // Request without user context (public access)
final request = buildRequest( final request = buildRequest(
'GET', 'GET',
@ -545,7 +537,8 @@ void main() {
// Note: This might return 404 if image file doesn't exist, // Note: This might return 404 if image file doesn't exist,
// but it should not return 401 Unauthorized // but it should not return 401 Unauthorized
expect(response.statusCode, isNot(equals(401))); expect(response.statusCode, isNot(equals(401)));
}); },
);
test('should return 404 if card does not belong to pack', () async { test('should return 404 if card does not belong to pack', () async {
// Create another pack and card // Create another pack and card
@ -580,8 +573,8 @@ void main() {
); );
final response = await packsApiV2.getCardImage(request, '10', '3'); final response = await packsApiV2.getCardImage(request, '10', '3');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(404)); expect(response.statusCode, equals(404));
expect(responseBody['error'], equals('Not Found')); expect(responseBody['error'], equals('Not Found'));
@ -597,8 +590,8 @@ void main() {
); );
final response = await packsApiV2.getCardImageBack(request, '10', '1'); final response = await packsApiV2.getCardImageBack(request, '10', '1');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(404)); expect(response.statusCode, equals(404));
expect(responseBody['error'], equals('Not Found')); expect(responseBody['error'], equals('Not Found'));
@ -612,8 +605,8 @@ void main() {
); );
final response = await packsApiV2.getCardImageBack(request, '10', '999'); final response = await packsApiV2.getCardImageBack(request, '10', '999');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(404)); expect(response.statusCode, equals(404));
expect(responseBody['error'], equals('Not Found')); expect(responseBody['error'], equals('Not Found'));
@ -627,8 +620,8 @@ void main() {
); );
final response = await packsApiV2.getCardImageBack(request, '999', '1'); final response = await packsApiV2.getCardImageBack(request, '999', '1');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(404)); expect(response.statusCode, equals(404));
expect(responseBody['error'], equals('Not Found')); expect(responseBody['error'], equals('Not Found'));
@ -646,8 +639,8 @@ void main() {
'10', '10',
'invalid', 'invalid',
); );
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('Bad Request')); expect(responseBody['error'], equals('Bad Request'));
@ -665,8 +658,8 @@ void main() {
'invalid', 'invalid',
'1', '1',
); );
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('Bad Request')); expect(responseBody['error'], equals('Bad Request'));
@ -680,14 +673,16 @@ void main() {
); );
final response = await packsApiV2.getCardImageBack(request, '99', '1'); final response = await packsApiV2.getCardImageBack(request, '99', '1');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(404)); expect(response.statusCode, equals(404));
expect(responseBody['error'], equals('Not Found')); expect(responseBody['error'], equals('Not Found'));
}); });
test('should allow access without authentication for enabled packs', () async { test(
'should allow access without authentication for enabled packs',
() async {
// Request without user context (public access) // Request without user context (public access)
final request = buildRequest( final request = buildRequest(
'GET', 'GET',
@ -701,7 +696,8 @@ void main() {
// Note: This might return 404 if image file doesn't exist, // Note: This might return 404 if image file doesn't exist,
// but it should not return 401 Unauthorized // but it should not return 401 Unauthorized
expect(response.statusCode, isNot(equals(401))); expect(response.statusCode, isNot(equals(401)));
}); },
);
test('should return 404 if card does not belong to pack', () async { test('should return 404 if card does not belong to pack', () async {
// Try to get card 3 from pack 10 (card 3 belongs to pack 20) // Try to get card 3 from pack 10 (card 3 belongs to pack 20)
@ -711,8 +707,8 @@ void main() {
); );
final response = await packsApiV2.getCardImageBack(request, '10', '3'); final response = await packsApiV2.getCardImageBack(request, '10', '3');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(404)); expect(response.statusCode, equals(404));
expect(responseBody['error'], equals('Not Found')); expect(responseBody['error'], equals('Not Found'));
@ -727,14 +723,16 @@ void main() {
); );
final response = await packsApiV2.getPackTests(request, '10'); final response = await packsApiV2.getPackTests(request, '10');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(401)); expect(response.statusCode, equals(401));
expect(responseBody['error'], equals('Unauthorized')); expect(responseBody['error'], equals('Unauthorized'));
}); });
test('should return tests list for authenticated user with pack access', () async { test(
'should return tests list for authenticated user with pack access',
() async {
// First, add pack to user so they have access // First, add pack to user so they have access
await testIsar.writeTxn(() async { await testIsar.writeTxn(() async {
final user = await testIsar.userModels.get(1); final user = await testIsar.userModels.get(1);
@ -763,7 +761,8 @@ void main() {
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody, isA<List>()); expect(responseBody, isA<List>());
}); },
);
test('should return 404 for non-existent pack', () async { test('should return 404 for non-existent pack', () async {
final request = buildRequest( final request = buildRequest(
@ -773,8 +772,8 @@ void main() {
); );
final response = await packsApiV2.getPackTests(request, '999'); final response = await packsApiV2.getPackTests(request, '999');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(404)); expect(response.statusCode, equals(404));
expect(responseBody['error'], equals('Not Found')); expect(responseBody['error'], equals('Not Found'));
@ -789,8 +788,8 @@ void main() {
); );
final response = await packsApiV2.getPackBuyPage(request, '11'); final response = await packsApiV2.getPackBuyPage(request, '11');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['id'], equals('11')); expect(responseBody['id'], equals('11'));
@ -798,7 +797,9 @@ void main() {
expect(responseBody.containsKey('cards'), isTrue); expect(responseBody.containsKey('cards'), isTrue);
}); });
test('should return buy page for authenticated user without pack', () async { test(
'should return buy page for authenticated user without pack',
() async {
final request = buildRequest( final request = buildRequest(
'GET', 'GET',
'http://localhost/api/v2/packs/11/buy', 'http://localhost/api/v2/packs/11/buy',
@ -806,16 +807,19 @@ void main() {
); );
final response = await packsApiV2.getPackBuyPage(request, '11'); final response = await packsApiV2.getPackBuyPage(request, '11');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['id'], equals('11')); expect(responseBody['id'], equals('11'));
expect(responseBody['price'], equals('199')); expect(responseBody['price'], equals('199'));
expect(responseBody.containsKey('cards'), isTrue); expect(responseBody.containsKey('cards'), isTrue);
}); },
);
test('should return pack info with isPurchased=true for authenticated user who already owns pack', () async { test(
'should return pack info with isPurchased=true for authenticated user who already owns pack',
() async {
// First, add pack to user // First, add pack to user
await testIsar.writeTxn(() async { await testIsar.writeTxn(() async {
final user = await testIsar.userModels.get(1); final user = await testIsar.userModels.get(1);
@ -840,13 +844,14 @@ void main() {
); );
final response = await packsApiV2.getPackBuyPage(request, '11'); final response = await packsApiV2.getPackBuyPage(request, '11');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['id'], equals('11')); expect(responseBody['id'], equals('11'));
expect(responseBody['isPurchased'], equals(true)); expect(responseBody['isPurchased'], equals(true));
}); },
);
test('should return 404 for non-existent pack (unauthenticated)', () async { test('should return 404 for non-existent pack (unauthenticated)', () async {
final request = buildRequest( final request = buildRequest(
@ -855,8 +860,8 @@ void main() {
); );
final response = await packsApiV2.getPackBuyPage(request, '999'); final response = await packsApiV2.getPackBuyPage(request, '999');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(404)); expect(response.statusCode, equals(404));
expect(responseBody['error'], equals('Not Found')); expect(responseBody['error'], equals('Not Found'));
@ -870,8 +875,8 @@ void main() {
); );
final response = await packsApiV2.getPackBuyPage(request, '999'); final response = await packsApiV2.getPackBuyPage(request, '999');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(404)); expect(response.statusCode, equals(404));
expect(responseBody['error'], equals('Not Found')); expect(responseBody['error'], equals('Not Found'));
@ -884,12 +889,11 @@ void main() {
); );
final response = await packsApiV2.getPackBuyPage(request, 'invalid'); final response = await packsApiV2.getPackBuyPage(request, 'invalid');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('Bad Request')); expect(responseBody['error'], equals('Bad Request'));
}); });
}); });
} }

View file

@ -26,18 +26,16 @@ void main() {
late UserModel testUser; late UserModel testUser;
late UserDataModel testUserData; late UserDataModel testUserData;
Request buildRequest( Request buildRequest(String method, String url, {UserModel? user}) {
String method,
String url, {
UserModel? user,
}) {
final context = <String, Object?>{}; final context = <String, Object?>{};
if (user != null) { if (user != null) {
context['user'] = user; context['user'] = user;
} }
final uri = url.startsWith('http') ? Uri.parse(url) : Uri.parse('http://localhost$url'); final uri = url.startsWith('http')
? Uri.parse(url)
: Uri.parse('http://localhost$url');
return Request(method, uri).change(context: context); return Request(method, uri).change(context: context);
} }
@ -107,10 +105,7 @@ void main() {
await testIsar.userModels.put(user); await testIsar.userModels.put(user);
testUser = user; testUser = user;
final userData = UserDataModel( final userData = UserDataModel(tags: ['premium', 'beta'], words: []);
tags: ['premium', 'beta'],
words: [],
);
await testIsar.userDataModels.put(userData); await testIsar.userDataModels.put(userData);
userData.user.value = user; userData.user.value = user;
await userData.user.save(); await userData.user.save();
@ -137,7 +132,8 @@ void main() {
final response = await promocodesApiV2.listPromocodes(request); final response = await promocodesApiV2.listPromocodes(request);
expect(response.statusCode, equals(401)); expect(response.statusCode, equals(401));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(body['error'], equals('unauthorized')); expect(body['error'], equals('unauthorized'));
}); });
@ -146,7 +142,8 @@ void main() {
final response = await promocodesApiV2.listPromocodes(request); final response = await promocodesApiV2.listPromocodes(request);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(body['campaigns'], isA<List>()); expect(body['campaigns'], isA<List>());
expect((body['campaigns'] as List).isEmpty, isTrue); expect((body['campaigns'] as List).isEmpty, isTrue);
}); });
@ -184,11 +181,13 @@ void main() {
final response = await promocodesApiV2.listPromocodes(request); final response = await promocodesApiV2.listPromocodes(request);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(body['campaigns'], isA<List>()); expect(body['campaigns'], isA<List>());
expect((body['campaigns'] as List).length, equals(1)); expect((body['campaigns'] as List).length, equals(1));
final campaign = (body['campaigns'] as List).first as Map<String, dynamic>; final campaign =
(body['campaigns'] as List).first as Map<String, dynamic>;
expect(campaign['status'], equals('active')); expect(campaign['status'], equals('active'));
expect(campaign['promoCodes'], isA<List>()); expect(campaign['promoCodes'], isA<List>());
expect((campaign['promoCodes'] as List).length, equals(2)); expect((campaign['promoCodes'] as List).length, equals(2));
@ -238,10 +237,14 @@ void main() {
final response = await promocodesApiV2.listPromocodes(request); final response = await promocodesApiV2.listPromocodes(request);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
final campaigns = body['campaigns'] as List; final campaigns = body['campaigns'] as List;
expect(campaigns.length, equals(1)); expect(campaigns.length, equals(1));
expect((campaigns.first as Map<String, dynamic>)['template'], equals('ACTIVE')); expect(
(campaigns.first as Map<String, dynamic>)['template'],
equals('ACTIVE'),
);
}); });
test('filters out campaigns outside date range', () async { test('filters out campaigns outside date range', () async {
@ -300,10 +303,14 @@ void main() {
final response = await promocodesApiV2.listPromocodes(request); final response = await promocodesApiV2.listPromocodes(request);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
final campaigns = body['campaigns'] as List; final campaigns = body['campaigns'] as List;
expect(campaigns.length, equals(1)); expect(campaigns.length, equals(1));
expect((campaigns.first as Map<String, dynamic>)['template'], equals('ACTIVE')); expect(
(campaigns.first as Map<String, dynamic>)['template'],
equals('ACTIVE'),
);
}); });
test('filters campaigns by user tags', () async { test('filters campaigns by user tags', () async {
@ -368,10 +375,13 @@ void main() {
final response = await promocodesApiV2.listPromocodes(request); final response = await promocodesApiV2.listPromocodes(request);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
final campaigns = body['campaigns'] as List; final campaigns = body['campaigns'] as List;
expect(campaigns.length, equals(2)); expect(campaigns.length, equals(2));
final templates = campaigns.map((c) => (c as Map<String, dynamic>)['template']).toList(); final templates = campaigns
.map((c) => (c as Map<String, dynamic>)['template'])
.toList();
expect(templates, contains('MATCHING')); expect(templates, contains('MATCHING'));
expect(templates, contains('NOTAGS')); expect(templates, contains('NOTAGS'));
expect(templates, isNot(contains('NONMATCHING'))); expect(templates, isNot(contains('NONMATCHING')));
@ -411,7 +421,8 @@ void main() {
final response = await promocodesApiV2.listPromocodes(request); final response = await promocodesApiV2.listPromocodes(request);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
final campaigns = body['campaigns'] as List; final campaigns = body['campaigns'] as List;
expect(campaigns.isEmpty, isTrue); expect(campaigns.isEmpty, isTrue);
}); });
@ -435,8 +446,14 @@ void main() {
); );
await testIsar.promoCodesCampaignModels.put(campaign); await testIsar.promoCodesCampaignModels.put(campaign);
final promoCode1 = PromoCodeModel(code: 'LIMITED1', activations: 1); // Already used final promoCode1 = PromoCodeModel(
final promoCode2 = PromoCodeModel(code: 'LIMITED2', activations: 0); // Available code: 'LIMITED1',
activations: 1,
); // Already used
final promoCode2 = PromoCodeModel(
code: 'LIMITED2',
activations: 0,
); // Available
await testIsar.promoCodeModels.putAll([promoCode1, promoCode2]); await testIsar.promoCodeModels.putAll([promoCode1, promoCode2]);
campaign.promoCodes.addAll([promoCode1, promoCode2]); campaign.promoCodes.addAll([promoCode1, promoCode2]);
@ -447,7 +464,8 @@ void main() {
final response = await promocodesApiV2.listPromocodes(request); final response = await promocodesApiV2.listPromocodes(request);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
final campaigns = body['campaigns'] as List; final campaigns = body['campaigns'] as List;
expect(campaigns.length, equals(1)); expect(campaigns.length, equals(1));
@ -493,7 +511,8 @@ void main() {
final response = await promocodesApiV2.listPromocodes(request); final response = await promocodesApiV2.listPromocodes(request);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
final campaigns = body['campaigns'] as List; final campaigns = body['campaigns'] as List;
expect(campaigns.length, equals(1)); expect(campaigns.length, equals(1));
@ -503,7 +522,9 @@ void main() {
expect(promoCodes.first, equals('AVAILABLE1')); expect(promoCodes.first, equals('AVAILABLE1'));
}); });
test('returns proper JSON format with campaign and code information', () async { test(
'returns proper JSON format with campaign and code information',
() async {
final now = DateTime.now(); final now = DateTime.now();
final start = now.subtract(const Duration(days: 1)); final start = now.subtract(const Duration(days: 1));
final finish = now.add(const Duration(days: 1)); final finish = now.add(const Duration(days: 1));
@ -529,11 +550,16 @@ void main() {
await campaign.promoCodes.save(); await campaign.promoCodes.save();
}); });
final request = buildRequest('GET', '/api/v2/promocodes', user: testUser); final request = buildRequest(
'GET',
'/api/v2/promocodes',
user: testUser,
);
final response = await promocodesApiV2.listPromocodes(request); final response = await promocodesApiV2.listPromocodes(request);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(body.containsKey('campaigns'), isTrue); expect(body.containsKey('campaigns'), isTrue);
final campaigns = body['campaigns'] as List; final campaigns = body['campaigns'] as List;
@ -548,16 +574,24 @@ void main() {
expect(campaign.containsKey('promoCodes'), isTrue); expect(campaign.containsKey('promoCodes'), isTrue);
expect(campaign['status'], equals('active')); expect(campaign['status'], equals('active'));
expect(campaign['promoCodes'], isA<List>()); expect(campaign['promoCodes'], isA<List>());
}); },
);
}); });
group('GET /api/v2/promocodes/{code}/validate', () { group('GET /api/v2/promocodes/{code}/validate', () {
test('returns 401 for unauthenticated requests', () async { test('returns 401 for unauthenticated requests', () async {
final request = buildRequest('GET', '/api/v2/promocodes/TEST123/validate'); final request = buildRequest(
final response = await promocodesApiV2.validatePromocode(request, 'TEST123'); 'GET',
'/api/v2/promocodes/TEST123/validate',
);
final response = await promocodesApiV2.validatePromocode(
request,
'TEST123',
);
expect(response.statusCode, equals(401)); expect(response.statusCode, equals(401));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(body['error'], equals('unauthorized')); expect(body['error'], equals('unauthorized'));
}); });
@ -567,10 +601,14 @@ void main() {
'/api/v2/promocodes/NONEXISTENT/validate', '/api/v2/promocodes/NONEXISTENT/validate',
user: testUser, user: testUser,
); );
final response = await promocodesApiV2.validatePromocode(request, 'NONEXISTENT'); final response = await promocodesApiV2.validatePromocode(
request,
'NONEXISTENT',
);
expect(response.statusCode, equals(404)); expect(response.statusCode, equals(404));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(body['valid'], equals(false)); expect(body['valid'], equals(false));
expect(body['message'], equals('Промокод не найден')); expect(body['message'], equals('Промокод не найден'));
}); });
@ -584,7 +622,8 @@ void main() {
final response = await promocodesApiV2.validatePromocode(request, ''); final response = await promocodesApiV2.validatePromocode(request, '');
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(body['error'], equals('bad_request')); expect(body['error'], equals('bad_request'));
}); });
@ -618,10 +657,14 @@ void main() {
'/api/v2/promocodes/VALID123/validate', '/api/v2/promocodes/VALID123/validate',
user: testUser, user: testUser,
); );
final response = await promocodesApiV2.validatePromocode(request, 'VALID123'); final response = await promocodesApiV2.validatePromocode(
request,
'VALID123',
);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(body['valid'], equals(true)); expect(body['valid'], equals(true));
expect(body['message'], equals('Промокод действителен')); expect(body['message'], equals('Промокод действителен'));
}); });
@ -656,10 +699,14 @@ void main() {
'/api/v2/promocodes/INACTIVE123/validate', '/api/v2/promocodes/INACTIVE123/validate',
user: testUser, user: testUser,
); );
final response = await promocodesApiV2.validatePromocode(request, 'INACTIVE123'); final response = await promocodesApiV2.validatePromocode(
request,
'INACTIVE123',
);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(body['valid'], equals(false)); expect(body['valid'], equals(false));
expect(body['message'], equals('Промокод недействителен')); expect(body['message'], equals('Промокод недействителен'));
}); });
@ -694,10 +741,14 @@ void main() {
'/api/v2/promocodes/FUTURE123/validate', '/api/v2/promocodes/FUTURE123/validate',
user: testUser, user: testUser,
); );
final response = await promocodesApiV2.validatePromocode(request, 'FUTURE123'); final response = await promocodesApiV2.validatePromocode(
request,
'FUTURE123',
);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(body['valid'], equals(false)); expect(body['valid'], equals(false));
expect(body['message'], equals('Промокод еще не активен')); expect(body['message'], equals('Промокод еще не активен'));
}); });
@ -732,15 +783,21 @@ void main() {
'/api/v2/promocodes/EXPIRED123/validate', '/api/v2/promocodes/EXPIRED123/validate',
user: testUser, user: testUser,
); );
final response = await promocodesApiV2.validatePromocode(request, 'EXPIRED123'); final response = await promocodesApiV2.validatePromocode(
request,
'EXPIRED123',
);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(body['valid'], equals(false)); expect(body['valid'], equals(false));
expect(body['message'], equals('Промокод истек')); expect(body['message'], equals('Промокод истек'));
}); });
test('returns valid: false for promocode that reached activation limit', () async { test(
'returns valid: false for promocode that reached activation limit',
() async {
final now = DateTime.now(); final now = DateTime.now();
final start = now.subtract(const Duration(days: 1)); final start = now.subtract(const Duration(days: 1));
final finish = now.add(const Duration(days: 1)); final finish = now.add(const Duration(days: 1));
@ -759,7 +816,10 @@ void main() {
); );
await testIsar.promoCodesCampaignModels.put(campaign); await testIsar.promoCodesCampaignModels.put(campaign);
final promoCode = PromoCodeModel(code: 'EXHAUSTED123', activations: 1); final promoCode = PromoCodeModel(
code: 'EXHAUSTED123',
activations: 1,
);
await testIsar.promoCodeModels.put(promoCode); await testIsar.promoCodeModels.put(promoCode);
campaign.promoCodes.add(promoCode); campaign.promoCodes.add(promoCode);
await campaign.promoCodes.save(); await campaign.promoCodes.save();
@ -770,15 +830,22 @@ void main() {
'/api/v2/promocodes/EXHAUSTED123/validate', '/api/v2/promocodes/EXHAUSTED123/validate',
user: testUser, user: testUser,
); );
final response = await promocodesApiV2.validatePromocode(request, 'EXHAUSTED123'); final response = await promocodesApiV2.validatePromocode(
request,
'EXHAUSTED123',
);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(body['valid'], equals(false)); expect(body['valid'], equals(false));
expect(body['message'], equals('Промокод исчерпан')); expect(body['message'], equals('Промокод исчерпан'));
}); },
);
test('returns valid: false for promocode already activated by user', () async { test(
'returns valid: false for promocode already activated by user',
() async {
final now = DateTime.now(); final now = DateTime.now();
final start = now.subtract(const Duration(days: 1)); final start = now.subtract(const Duration(days: 1));
final finish = now.add(const Duration(days: 1)); final finish = now.add(const Duration(days: 1));
@ -813,15 +880,22 @@ void main() {
'/api/v2/promocodes/USED123/validate', '/api/v2/promocodes/USED123/validate',
user: testUser, user: testUser,
); );
final response = await promocodesApiV2.validatePromocode(request, 'USED123'); final response = await promocodesApiV2.validatePromocode(
request,
'USED123',
);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(body['valid'], equals(false)); expect(body['valid'], equals(false));
expect(body['message'], equals('Промокод уже был активирован')); expect(body['message'], equals('Промокод уже был активирован'));
}); },
);
test('returns valid: false when user reached campaign activation limit', () async { test(
'returns valid: false when user reached campaign activation limit',
() async {
final now = DateTime.now(); final now = DateTime.now();
final start = now.subtract(const Duration(days: 1)); final start = now.subtract(const Duration(days: 1));
final finish = now.add(const Duration(days: 1)); final finish = now.add(const Duration(days: 1));
@ -857,15 +931,22 @@ void main() {
'/api/v2/promocodes/LIMITED2/validate', '/api/v2/promocodes/LIMITED2/validate',
user: testUser, user: testUser,
); );
final response = await promocodesApiV2.validatePromocode(request, 'LIMITED2'); final response = await promocodesApiV2.validatePromocode(
request,
'LIMITED2',
);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(body['valid'], equals(false)); expect(body['valid'], equals(false));
expect(body['message'], equals('Вы уже участвовали в этой акции')); expect(body['message'], equals('Вы уже участвовали в этой акции'));
}); },
);
test('returns valid: false when user tags do not match campaign tags', () async { test(
'returns valid: false when user tags do not match campaign tags',
() async {
final now = DateTime.now(); final now = DateTime.now();
final start = now.subtract(const Duration(days: 1)); final start = now.subtract(const Duration(days: 1));
final finish = now.add(const Duration(days: 1)); final finish = now.add(const Duration(days: 1));
@ -895,13 +976,18 @@ void main() {
'/api/v2/promocodes/TAGGED123/validate', '/api/v2/promocodes/TAGGED123/validate',
user: testUser, user: testUser,
); );
final response = await promocodesApiV2.validatePromocode(request, 'TAGGED123'); final response = await promocodesApiV2.validatePromocode(
request,
'TAGGED123',
);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(body['valid'], equals(false)); expect(body['valid'], equals(false));
expect(body['message'], equals('Промокод недействителен')); expect(body['message'], equals('Промокод недействителен'));
}); },
);
test('returns valid: true when user tags match campaign tags', () async { test('returns valid: true when user tags match campaign tags', () async {
final now = DateTime.now(); final now = DateTime.now();
@ -933,15 +1019,21 @@ void main() {
'/api/v2/promocodes/MATCHING123/validate', '/api/v2/promocodes/MATCHING123/validate',
user: testUser, user: testUser,
); );
final response = await promocodesApiV2.validatePromocode(request, 'MATCHING123'); final response = await promocodesApiV2.validatePromocode(
request,
'MATCHING123',
);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(body['valid'], equals(true)); expect(body['valid'], equals(true));
expect(body['message'], equals('Промокод действителен')); expect(body['message'], equals('Промокод действителен'));
}); });
test('returns valid: false for individual promocode for another user', () async { test(
'returns valid: false for individual promocode for another user',
() async {
final now = DateTime.now(); final now = DateTime.now();
final start = now.subtract(const Duration(days: 1)); final start = now.subtract(const Duration(days: 1));
final finish = now.add(const Duration(days: 1)); final finish = now.add(const Duration(days: 1));
@ -958,10 +1050,7 @@ void main() {
); );
await testIsar.userModels.put(otherUser); await testIsar.userModels.put(otherUser);
otherUserData = UserDataModel( otherUserData = UserDataModel(tags: [], words: []);
tags: [],
words: [],
);
await testIsar.userDataModels.put(otherUserData); await testIsar.userDataModels.put(otherUserData);
otherUserData.user.value = otherUser; otherUserData.user.value = otherUser;
await otherUserData.user.save(); await otherUserData.user.save();
@ -981,7 +1070,10 @@ void main() {
); );
await testIsar.promoCodesCampaignModels.put(campaign); await testIsar.promoCodesCampaignModels.put(campaign);
final promoCode = PromoCodeModel(code: 'INDIVIDUAL123', activations: 0); final promoCode = PromoCodeModel(
code: 'INDIVIDUAL123',
activations: 0,
);
await testIsar.promoCodeModels.put(promoCode); await testIsar.promoCodeModels.put(promoCode);
campaign.promoCodes.add(promoCode); campaign.promoCodes.add(promoCode);
await campaign.promoCodes.save(); await campaign.promoCodes.save();
@ -996,15 +1088,25 @@ void main() {
'/api/v2/promocodes/INDIVIDUAL123/validate', '/api/v2/promocodes/INDIVIDUAL123/validate',
user: testUser, user: testUser,
); );
final response = await promocodesApiV2.validatePromocode(request, 'INDIVIDUAL123'); final response = await promocodesApiV2.validatePromocode(
request,
'INDIVIDUAL123',
);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(body['valid'], equals(false)); expect(body['valid'], equals(false));
expect(body['message'], equals('Это промокод для другого пользователя')); expect(
}); body['message'],
equals('Это промокод для другого пользователя'),
);
},
);
test('returns valid: true for individual promocode for current user', () async { test(
'returns valid: true for individual promocode for current user',
() async {
final now = DateTime.now(); final now = DateTime.now();
final start = now.subtract(const Duration(days: 1)); final start = now.subtract(const Duration(days: 1));
final finish = now.add(const Duration(days: 1)); final finish = now.add(const Duration(days: 1));
@ -1038,13 +1140,18 @@ void main() {
'/api/v2/promocodes/MYCODE123/validate', '/api/v2/promocodes/MYCODE123/validate',
user: testUser, user: testUser,
); );
final response = await promocodesApiV2.validatePromocode(request, 'MYCODE123'); final response = await promocodesApiV2.validatePromocode(
request,
'MYCODE123',
);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(body['valid'], equals(true)); expect(body['valid'], equals(true));
expect(body['message'], equals('Промокод действителен')); expect(body['message'], equals('Промокод действителен'));
}); },
);
test('handles case-insensitive promocode', () async { test('handles case-insensitive promocode', () async {
final now = DateTime.now(); final now = DateTime.now();
@ -1077,10 +1184,14 @@ void main() {
'/api/v2/promocodes/case123/validate', '/api/v2/promocodes/case123/validate',
user: testUser, user: testUser,
); );
final response = await promocodesApiV2.validatePromocode(request, 'case123'); final response = await promocodesApiV2.validatePromocode(
request,
'case123',
);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>; final body =
jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(body['valid'], equals(true)); expect(body['valid'], equals(true));
expect(body['message'], equals('Промокод действителен')); expect(body['message'], equals('Промокод действителен'));
}); });

View file

@ -64,10 +64,7 @@ void main() {
final discountsManager = const DiscountsManager(); final discountsManager = const DiscountsManager();
final productsPriceResolver = ProductsPriceResolver(discountsManager); final productsPriceResolver = ProductsPriceResolver(discountsManager);
final adsManager = AdsManager(); final adsManager = AdsManager();
packDtoConverter = PackDtoConverter( packDtoConverter = PackDtoConverter(productsPriceResolver, adsManager);
productsPriceResolver,
adsManager,
);
packManager = PackManager(packDtoConverter); packManager = PackManager(packDtoConverter);
@ -141,8 +138,8 @@ void main() {
); );
final response = await purchasesApiV2.createPackPurchase(request, '20'); final response = await purchasesApiV2.createPackPurchase(request, '20');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(401)); expect(response.statusCode, equals(401));
expect(responseBody['error'], equals('Unauthorized')); expect(responseBody['error'], equals('Unauthorized'));
@ -155,8 +152,8 @@ void main() {
).change(context: {'user': testUser}); ).change(context: {'user': testUser});
final response = await purchasesApiV2.createPackPurchase(request, '999'); final response = await purchasesApiV2.createPackPurchase(request, '999');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(404)); expect(response.statusCode, equals(404));
expect(responseBody['error'], equals('Not Found')); expect(responseBody['error'], equals('Not Found'));
@ -185,8 +182,8 @@ void main() {
).change(context: {'user': updatedUser!}); ).change(context: {'user': updatedUser!});
final response = await purchasesApiV2.createPackPurchase(request, '20'); final response = await purchasesApiV2.createPackPurchase(request, '20');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('Bad Request')); expect(responseBody['error'], equals('Bad Request'));
@ -205,8 +202,8 @@ void main() {
request, request,
'20', '20',
); );
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['packId'], equals('20')); expect(responseBody['packId'], equals('20'));
@ -241,8 +238,8 @@ void main() {
request, request,
'20', '20',
); );
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['isPurchased'], isTrue); expect(responseBody['isPurchased'], isTrue);
@ -259,8 +256,8 @@ void main() {
request, request,
'20', '20',
); );
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(401)); expect(response.statusCode, equals(401));
expect(responseBody['error'], equals('Unauthorized')); expect(responseBody['error'], equals('Unauthorized'));
@ -276,8 +273,8 @@ void main() {
request, request,
'invalid', 'invalid',
); );
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('Bad Request')); expect(responseBody['error'], equals('Bad Request'));
@ -289,15 +286,12 @@ void main() {
final request = Request( final request = Request(
'POST', 'POST',
Uri.parse('http://localhost/api/v2/purchases/payments'), Uri.parse('http://localhost/api/v2/purchases/payments'),
body: jsonEncode({ body: jsonEncode({'productId': '20', 'productType': 'pack'}),
'productId': '20',
'productType': 'pack',
}),
); );
final response = await purchasesApiV2.createPayment(request); final response = await purchasesApiV2.createPayment(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(401)); expect(response.statusCode, equals(401));
expect(responseBody['error'], equals('Unauthorized')); expect(responseBody['error'], equals('Unauthorized'));
@ -307,14 +301,12 @@ void main() {
final request = Request( final request = Request(
'POST', 'POST',
Uri.parse('http://localhost/api/v2/purchases/payments'), Uri.parse('http://localhost/api/v2/purchases/payments'),
body: jsonEncode({ body: jsonEncode({'productType': 'pack'}),
'productType': 'pack',
}),
).change(context: {'user': testUser}); ).change(context: {'user': testUser});
final response = await purchasesApiV2.createPayment(request); final response = await purchasesApiV2.createPayment(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('Bad Request')); expect(responseBody['error'], equals('Bad Request'));
@ -329,8 +321,8 @@ void main() {
).change(context: {'user': testUser}); ).change(context: {'user': testUser});
final response = await purchasesApiV2.createPayment(request); final response = await purchasesApiV2.createPayment(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('Bad Request')); expect(responseBody['error'], equals('Bad Request'));
@ -351,8 +343,8 @@ void main() {
request, request,
'payment123', 'payment123',
); );
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(401)); expect(response.statusCode, equals(401));
expect(responseBody['error'], equals('Unauthorized')); expect(responseBody['error'], equals('Unauthorized'));
@ -370,8 +362,8 @@ void main() {
request, request,
'payment123', 'payment123',
); );
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('Bad Request')); expect(responseBody['error'], equals('Bad Request'));
@ -397,8 +389,8 @@ void main() {
expect(response.statusCode, isIn([200, 500])); expect(response.statusCode, isIn([200, 500]));
if (response.statusCode == 200) { if (response.statusCode == 200) {
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(responseBody['paymentId'], equals('payment123')); expect(responseBody['paymentId'], equals('payment123'));
expect(responseBody['status'], isA<String>()); expect(responseBody['status'], isA<String>());
expect(responseBody['result'], isA<bool>()); expect(responseBody['result'], isA<bool>());
@ -406,4 +398,3 @@ void main() {
}); });
}); });
} }

View file

@ -87,20 +87,23 @@ void main() {
}); });
group('SubscriptionsApiV2 - Get Plans', () { group('SubscriptionsApiV2 - Get Plans', () {
test('should return 200 with empty array when no plans available', () async { test(
'should return 200 with empty array when no plans available',
() async {
final request = Request( final request = Request(
'GET', 'GET',
Uri.parse('http://localhost/api/v2/subscriptions/plans'), Uri.parse('http://localhost/api/v2/subscriptions/plans'),
); );
final response = await subscriptionsApiV2.getPlans(request); final response = await subscriptionsApiV2.getPlans(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['plans'], isA<List>()); expect(responseBody['plans'], isA<List>());
expect(responseBody['plans'], isEmpty); expect(responseBody['plans'], isEmpty);
}); },
);
test('should return 200 with list of plans when plans exist', () async { test('should return 200 with list of plans when plans exist', () async {
// Create test subscription plans // Create test subscription plans
@ -147,8 +150,8 @@ void main() {
); );
final response = await subscriptionsApiV2.getPlans(request); final response = await subscriptionsApiV2.getPlans(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['plans'], isA<List>()); expect(responseBody['plans'], isA<List>());
@ -194,8 +197,8 @@ void main() {
).change(context: {'user': testUser}); ).change(context: {'user': testUser});
final response = await subscriptionsApiV2.getPlans(request); final response = await subscriptionsApiV2.getPlans(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['plans'], isA<List>()); expect(responseBody['plans'], isA<List>());
@ -228,8 +231,8 @@ void main() {
); );
final response = await subscriptionsApiV2.getPlans(request); final response = await subscriptionsApiV2.getPlans(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(response.headers['content-type'], contains('application/json')); expect(response.headers['content-type'], contains('application/json'));
@ -249,7 +252,9 @@ void main() {
expect(plan.containsKey('ui'), isTrue); expect(plan.containsKey('ui'), isTrue);
}); });
test('should handle multiple plans with different payment systems', () async { test(
'should handle multiple plans with different payment systems',
() async {
// Create test subscription plans with different payment systems // Create test subscription plans with different payment systems
await testIsar.writeTxn(() async { await testIsar.writeTxn(() async {
final plan1 = SubscriptionPlanModel( final plan1 = SubscriptionPlanModel(
@ -260,9 +265,7 @@ void main() {
features: [SubscriptionFeatureEnum.packs], features: [SubscriptionFeatureEnum.packs],
paymentSystem: PaymentSystem.yookassa, paymentSystem: PaymentSystem.yookassa,
paymentId: 'plan_1', paymentId: 'plan_1',
ui: SubscriptionPlanUI( ui: SubscriptionPlanUI(title: 'YooKassa Plan'),
title: 'YooKassa Plan',
),
); );
await testIsar.subscriptionPlanModels.put(plan1); await testIsar.subscriptionPlanModels.put(plan1);
@ -274,9 +277,7 @@ void main() {
features: [SubscriptionFeatureEnum.packs], features: [SubscriptionFeatureEnum.packs],
paymentSystem: PaymentSystem.google, paymentSystem: PaymentSystem.google,
paymentId: 'plan_2', paymentId: 'plan_2',
ui: SubscriptionPlanUI( ui: SubscriptionPlanUI(title: 'Google Play Plan'),
title: 'Google Play Plan',
),
); );
await testIsar.subscriptionPlanModels.put(plan2); await testIsar.subscriptionPlanModels.put(plan2);
}); });
@ -287,8 +288,8 @@ void main() {
); );
final response = await subscriptionsApiV2.getPlans(request); final response = await subscriptionsApiV2.getPlans(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['plans'], hasLength(2)); expect(responseBody['plans'], hasLength(2));
@ -299,7 +300,8 @@ void main() {
.toList(); .toList();
expect(paymentSystems, contains('yookassa')); expect(paymentSystems, contains('yookassa'));
expect(paymentSystems, contains('google')); expect(paymentSystems, contains('google'));
}); },
);
}); });
group('SubscriptionsApiV2 - Get Status', () { group('SubscriptionsApiV2 - Get Status', () {
@ -310,8 +312,8 @@ void main() {
); );
final response = await subscriptionsApiV2.getStatus(request); final response = await subscriptionsApiV2.getStatus(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(401)); expect(response.statusCode, equals(401));
expect(responseBody['error'], equals('unauthorized')); expect(responseBody['error'], equals('unauthorized'));
@ -325,8 +327,8 @@ void main() {
).change(context: {'user': testUser}); ).change(context: {'user': testUser});
final response = await subscriptionsApiV2.getStatus(request); final response = await subscriptionsApiV2.getStatus(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['active'], isFalse); expect(responseBody['active'], isFalse);
@ -334,7 +336,9 @@ void main() {
expect(responseBody.containsKey('finish'), isFalse); expect(responseBody.containsKey('finish'), isFalse);
}); });
test('should return active: true with dates when subscription is active', () async { test(
'should return active: true with dates when subscription is active',
() async {
final now = DateTime.now(); final now = DateTime.now();
final startDate = now.subtract(const Duration(days: 5)); final startDate = now.subtract(const Duration(days: 5));
final finishDate = now.add(const Duration(days: 25)); final finishDate = now.add(const Duration(days: 25));
@ -366,8 +370,8 @@ void main() {
).change(context: {'user': userWithSubscription}); ).change(context: {'user': userWithSubscription});
final response = await subscriptionsApiV2.getStatus(request); final response = await subscriptionsApiV2.getStatus(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['active'], isTrue); expect(responseBody['active'], isTrue);
@ -379,7 +383,8 @@ void main() {
expect(start, equals(startDate)); expect(start, equals(startDate));
expect(finish, equals(finishDate)); expect(finish, equals(finishDate));
}); },
);
test('should return active: false when subscription is expired', () async { test('should return active: false when subscription is expired', () async {
final now = DateTime.now(); final now = DateTime.now();
@ -413,8 +418,8 @@ void main() {
).change(context: {'user': userWithSubscription}); ).change(context: {'user': userWithSubscription});
final response = await subscriptionsApiV2.getStatus(request); final response = await subscriptionsApiV2.getStatus(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['active'], isFalse); expect(responseBody['active'], isFalse);
@ -430,7 +435,10 @@ void main() {
final subscription = UserSubscriptionModel( final subscription = UserSubscriptionModel(
start: startDate, start: startDate,
finish: finishDate, finish: finishDate,
features: [SubscriptionFeatureEnum.packs, SubscriptionFeatureEnum.ads], features: [
SubscriptionFeatureEnum.packs,
SubscriptionFeatureEnum.ads,
],
); );
await testIsar.userSubscriptionModels.put(subscription); await testIsar.userSubscriptionModels.put(subscription);
@ -451,8 +459,8 @@ void main() {
).change(context: {'user': userWithSubscription}); ).change(context: {'user': userWithSubscription});
final response = await subscriptionsApiV2.getStatus(request); final response = await subscriptionsApiV2.getStatus(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(response.headers['content-type'], contains('application/json')); expect(response.headers['content-type'], contains('application/json'));

View file

@ -62,10 +62,7 @@ void main() {
final discountsManager = const DiscountsManager(); final discountsManager = const DiscountsManager();
final productsPriceResolver = ProductsPriceResolver(discountsManager); final productsPriceResolver = ProductsPriceResolver(discountsManager);
final adsManager = AdsManager(); final adsManager = AdsManager();
packDtoConverter = PackDtoConverter( packDtoConverter = PackDtoConverter(productsPriceResolver, adsManager);
productsPriceResolver,
adsManager,
);
testManager = TestManager(packDtoConverter); testManager = TestManager(packDtoConverter);
@ -93,11 +90,7 @@ void main() {
// Create test model // Create test model
await testIsar.writeTxn(() async { await testIsar.writeTxn(() async {
final test = TestModel( final test = TestModel(id: 100, name: 'Test Test', version: '1.0');
id: 100,
name: 'Test Test',
version: '1.0',
);
await testIsar.testModels.put(test); await testIsar.testModels.put(test);
}); });
}); });
@ -125,8 +118,8 @@ void main() {
).change(context: {'user': testUser}); ).change(context: {'user': testUser});
final response = await testsApiV2.getTest(request, '100'); final response = await testsApiV2.getTest(request, '100');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['id'], equals('100')); expect(responseBody['id'], equals('100'));
@ -140,8 +133,8 @@ void main() {
); );
final response = await testsApiV2.getTest(request, '100'); final response = await testsApiV2.getTest(request, '100');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(401)); expect(response.statusCode, equals(401));
expect(responseBody['error'], equals('Unauthorized')); expect(responseBody['error'], equals('Unauthorized'));
@ -154,8 +147,8 @@ void main() {
).change(context: {'user': testUser}); ).change(context: {'user': testUser});
final response = await testsApiV2.getTest(request, '999'); final response = await testsApiV2.getTest(request, '999');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(404)); expect(response.statusCode, equals(404));
expect(responseBody['error'], equals('Not Found')); expect(responseBody['error'], equals('Not Found'));
@ -168,8 +161,8 @@ void main() {
).change(context: {'user': testUser}); ).change(context: {'user': testUser});
final response = await testsApiV2.getTest(request, 'invalid'); final response = await testsApiV2.getTest(request, 'invalid');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('Bad Request')); expect(responseBody['error'], equals('Bad Request'));
@ -200,8 +193,8 @@ void main() {
).change(context: {'user': testUser}); ).change(context: {'user': testUser});
final response = await testsApiV2.submitTestResults(request, '100'); final response = await testsApiV2.submitTestResults(request, '100');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['success'], equals(true)); expect(responseBody['success'], equals(true));
@ -222,8 +215,8 @@ void main() {
); );
final response = await testsApiV2.submitTestResults(request, '100'); final response = await testsApiV2.submitTestResults(request, '100');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(401)); expect(response.statusCode, equals(401));
expect(responseBody['error'], equals('Unauthorized')); expect(responseBody['error'], equals('Unauthorized'));
@ -243,8 +236,8 @@ void main() {
).change(context: {'user': testUser}); ).change(context: {'user': testUser});
final response = await testsApiV2.submitTestResults(request, '100'); final response = await testsApiV2.submitTestResults(request, '100');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('Bad Request')); expect(responseBody['error'], equals('Bad Request'));
@ -259,12 +252,15 @@ void main() {
).change(context: {'user': testUser}); ).change(context: {'user': testUser});
final response = await testsApiV2.submitTestResults(request, '100'); final response = await testsApiV2.submitTestResults(request, '100');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('Bad Request')); expect(responseBody['error'], equals('Bad Request'));
expect(responseBody['message'], contains('Invalid test statistics format')); expect(
responseBody['message'],
contains('Invalid test statistics format'),
);
}); });
test('should return 400 for empty request body', () async { test('should return 400 for empty request body', () async {
@ -275,8 +271,8 @@ void main() {
).change(context: {'user': testUser}); ).change(context: {'user': testUser});
final response = await testsApiV2.submitTestResults(request, '100'); final response = await testsApiV2.submitTestResults(request, '100');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('Bad Request')); expect(responseBody['error'], equals('Bad Request'));
@ -297,8 +293,8 @@ void main() {
).change(context: {'user': testUser}); ).change(context: {'user': testUser});
final response = await testsApiV2.submitTestResults(request, 'invalid'); final response = await testsApiV2.submitTestResults(request, 'invalid');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('Bad Request')); expect(responseBody['error'], equals('Bad Request'));
@ -313,8 +309,8 @@ void main() {
).change(context: {'user': testUser}); ).change(context: {'user': testUser});
final response = await testsApiV2.getTestHistory(request, '100'); final response = await testsApiV2.getTestHistory(request, '100');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['items'], isA<List>()); expect(responseBody['items'], isA<List>());
@ -370,8 +366,8 @@ void main() {
).change(context: {'user': updatedUser!}); ).change(context: {'user': updatedUser!});
final response = await testsApiV2.getTestHistory(request, '100'); final response = await testsApiV2.getTestHistory(request, '100');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['items'], isA<List>()); expect(responseBody['items'], isA<List>());
@ -411,8 +407,8 @@ void main() {
).change(context: {'user': updatedUser!}); ).change(context: {'user': updatedUser!});
final response = await testsApiV2.getTestHistory(request, '100'); final response = await testsApiV2.getTestHistory(request, '100');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['items'].length, lessThanOrEqualTo(2)); expect(responseBody['items'].length, lessThanOrEqualTo(2));
@ -428,8 +424,8 @@ void main() {
); );
final response = await testsApiV2.getTestHistory(request, '100'); final response = await testsApiV2.getTestHistory(request, '100');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(401)); expect(response.statusCode, equals(401));
expect(responseBody['error'], equals('Unauthorized')); expect(responseBody['error'], equals('Unauthorized'));
@ -442,8 +438,8 @@ void main() {
).change(context: {'user': testUser}); ).change(context: {'user': testUser});
final response = await testsApiV2.getTestHistory(request, 'invalid'); final response = await testsApiV2.getTestHistory(request, 'invalid');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('Bad Request')); expect(responseBody['error'], equals('Bad Request'));
@ -456,8 +452,8 @@ void main() {
).change(context: {'user': testUser}); ).change(context: {'user': testUser});
final response = await testsApiV2.getTestHistory(request, '100'); final response = await testsApiV2.getTestHistory(request, '100');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('Bad Request')); expect(responseBody['error'], equals('Bad Request'));
@ -471,13 +467,15 @@ void main() {
).change(context: {'user': testUser}); ).change(context: {'user': testUser});
final response = await testsApiV2.getTestHistory(request, '100'); final response = await testsApiV2.getTestHistory(request, '100');
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('Bad Request')); expect(responseBody['error'], equals('Bad Request'));
expect(responseBody['message'], contains('Limit must be between 1 and 100')); expect(
responseBody['message'],
contains('Limit must be between 1 and 100'),
);
}); });
}); });
} }

View file

@ -40,15 +40,9 @@ void main() {
UserModel? user, UserModel? user,
String? body, String? body,
}) { }) {
var request = Request( var request = Request(method, Uri.parse(url), body: body);
method,
Uri.parse(url),
body: body,
);
if (user != null) { if (user != null) {
request = request.change( request = request.change(context: {'user': user});
context: {'user': user},
);
} }
return request; return request;
} }
@ -115,11 +109,7 @@ void main() {
statisticsCalculator, statisticsCalculator,
AchievementManager(testIsar), AchievementManager(testIsar),
); );
usersApiV2 = UsersApiV2( usersApiV2 = UsersApiV2(userManager, paymentManager, statisticsCalculator);
userManager,
paymentManager,
statisticsCalculator,
);
// Create test user with statistics data // Create test user with statistics data
await testIsar.writeTxn(() async { await testIsar.writeTxn(() async {
@ -243,8 +233,8 @@ void main() {
); );
final response = await usersApiV2.getDetailedStatistics(request); final response = await usersApiV2.getDetailedStatistics(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
// The endpoint should return 200 with UserDataDto or 404 if userData not found // The endpoint should return 200 with UserDataDto or 404 if userData not found
if (response.statusCode == 404) { if (response.statusCode == 404) {
@ -300,8 +290,8 @@ void main() {
); );
final response = await usersApiV2.getDetailedStatistics(request); final response = await usersApiV2.getDetailedStatistics(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(401)); expect(response.statusCode, equals(401));
expect(responseBody['error'], equals('unauthorized')); expect(responseBody['error'], equals('unauthorized'));
@ -315,8 +305,8 @@ void main() {
); );
final response = await usersApiV2.getDetailedStatistics(request); final response = await usersApiV2.getDetailedStatistics(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(404)); expect(response.statusCode, equals(404));
expect(responseBody['error'], equals('user_data_not_found')); expect(responseBody['error'], equals('user_data_not_found'));
@ -394,8 +384,8 @@ void main() {
); );
final response = await usersApiV2.getPacksStatistics(request); final response = await usersApiV2.getPacksStatistics(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(401)); expect(response.statusCode, equals(401));
expect(responseBody['error'], equals('unauthorized')); expect(responseBody['error'], equals('unauthorized'));
@ -411,8 +401,8 @@ void main() {
); );
final response = await usersApiV2.getWordsStatistics(request); final response = await usersApiV2.getWordsStatistics(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['words'], isA<List>()); expect(responseBody['words'], isA<List>());
@ -431,8 +421,8 @@ void main() {
); );
final response = await usersApiV2.getWordsStatistics(request); final response = await usersApiV2.getWordsStatistics(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect((responseBody['words'] as List).length, equals(2)); expect((responseBody['words'] as List).length, equals(2));
@ -446,8 +436,8 @@ void main() {
); );
final response2 = await usersApiV2.getWordsStatistics(request2); final response2 = await usersApiV2.getWordsStatistics(request2);
final responseBody2 = jsonDecode(await response2.readAsString()) final responseBody2 =
as Map<String, dynamic>; jsonDecode(await response2.readAsString()) as Map<String, dynamic>;
expect(responseBody2['totalCount'], equals(3)); expect(responseBody2['totalCount'], equals(3));
expect((responseBody2['words'] as List).length, equals(1)); expect((responseBody2['words'] as List).length, equals(1));
@ -462,8 +452,8 @@ void main() {
); );
final response = await usersApiV2.getWordsStatistics(request); final response = await usersApiV2.getWordsStatistics(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final words = responseBody['words'] as List; final words = responseBody['words'] as List;
@ -476,10 +466,15 @@ void main() {
expect(word['incorrect'], isA<double>()); expect(word['incorrect'], isA<double>());
} }
// Verify sorting: check that difficulty scores are in descending order // Verify sorting: check that difficulty scores are in descending order
final difficulties = words.map((w) => w['difficultyScore'] as double).toList(); final difficulties = words
.map((w) => w['difficultyScore'] as double)
.toList();
for (var i = 0; i < difficulties.length - 1; i++) { for (var i = 0; i < difficulties.length - 1; i++) {
expect(difficulties[i], greaterThanOrEqualTo(difficulties[i + 1]), expect(
reason: 'Words should be sorted by difficulty descending'); difficulties[i],
greaterThanOrEqualTo(difficulties[i + 1]),
reason: 'Words should be sorted by difficulty descending',
);
} }
}); });
@ -491,8 +486,8 @@ void main() {
); );
final response = await usersApiV2.getWordsStatistics(request); final response = await usersApiV2.getWordsStatistics(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final words = responseBody['words'] as List; final words = responseBody['words'] as List;
@ -508,8 +503,8 @@ void main() {
); );
final response = await usersApiV2.getWordsStatistics(request); final response = await usersApiV2.getWordsStatistics(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['words'], isA<List>()); expect(responseBody['words'], isA<List>());
@ -523,8 +518,8 @@ void main() {
); );
final response = await usersApiV2.getWordsStatistics(request); final response = await usersApiV2.getWordsStatistics(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
final words = responseBody['words'] as List; final words = responseBody['words'] as List;
@ -541,8 +536,8 @@ void main() {
); );
final response = await usersApiV2.getWordsStatistics(request); final response = await usersApiV2.getWordsStatistics(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['pageSize'], equals(50)); // Default limit expect(responseBody['pageSize'], equals(50)); // Default limit
@ -557,8 +552,8 @@ void main() {
); );
final response = await usersApiV2.getWordsStatistics(request); final response = await usersApiV2.getWordsStatistics(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['pageSize'], lessThanOrEqualTo(100)); expect(responseBody['pageSize'], lessThanOrEqualTo(100));
@ -572,8 +567,8 @@ void main() {
); );
final response = await usersApiV2.getWordsStatistics(request); final response = await usersApiV2.getWordsStatistics(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['page'], equals(0)); // Should clamp to 0 expect(responseBody['page'], equals(0)); // Should clamp to 0
@ -587,8 +582,8 @@ void main() {
); );
final response = await usersApiV2.getWordsStatistics(request); final response = await usersApiV2.getWordsStatistics(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['words'], isA<List>()); expect(responseBody['words'], isA<List>());
@ -604,8 +599,8 @@ void main() {
); );
final response = await usersApiV2.getWordsStatistics(request); final response = await usersApiV2.getWordsStatistics(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(401)); expect(response.statusCode, equals(401));
expect(responseBody['error'], equals('unauthorized')); expect(responseBody['error'], equals('unauthorized'));
@ -621,8 +616,8 @@ void main() {
); );
final response = await usersApiV2.getTimelineStatistics(request); final response = await usersApiV2.getTimelineStatistics(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['period'], equals('month')); expect(responseBody['period'], equals('month'));
@ -643,8 +638,8 @@ void main() {
); );
final response = await usersApiV2.getTimelineStatistics(request); final response = await usersApiV2.getTimelineStatistics(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['period'], equals('week')); expect(responseBody['period'], equals('week'));
@ -660,8 +655,8 @@ void main() {
); );
final response = await usersApiV2.getTimelineStatistics(request); final response = await usersApiV2.getTimelineStatistics(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['period'], isA<String>()); expect(responseBody['period'], isA<String>());
@ -675,8 +670,8 @@ void main() {
); );
final response = await usersApiV2.getTimelineStatistics(request); final response = await usersApiV2.getTimelineStatistics(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
// Should still return valid response, ignoring invalid date // Should still return valid response, ignoring invalid date
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
@ -691,8 +686,8 @@ void main() {
); );
final response = await usersApiV2.getTimelineStatistics(request); final response = await usersApiV2.getTimelineStatistics(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['totalDays'], equals(0)); expect(responseBody['totalDays'], equals(0));
@ -713,8 +708,8 @@ void main() {
); );
final response = await usersApiV2.getTimelineStatistics(request); final response = await usersApiV2.getTimelineStatistics(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(401)); expect(response.statusCode, equals(401));
expect(responseBody['error'], equals('unauthorized')); expect(responseBody['error'], equals('unauthorized'));
@ -735,8 +730,8 @@ void main() {
); );
final response = await usersApiV2.recordStudySession(request); final response = await usersApiV2.recordStudySession(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
expect(responseBody['result'], equals(true)); expect(responseBody['result'], equals(true));
@ -752,8 +747,8 @@ void main() {
); );
final response = await usersApiV2.recordStudySession(request); final response = await usersApiV2.recordStudySession(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('bad_request')); expect(responseBody['error'], equals('bad_request'));
@ -769,8 +764,8 @@ void main() {
); );
final response = await usersApiV2.recordStudySession(request); final response = await usersApiV2.recordStudySession(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('bad_request')); expect(responseBody['error'], equals('bad_request'));
@ -786,8 +781,8 @@ void main() {
); );
final response = await usersApiV2.recordStudySession(request); final response = await usersApiV2.recordStudySession(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(400)); expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('bad_request')); expect(responseBody['error'], equals('bad_request'));
@ -805,8 +800,8 @@ void main() {
); );
final response = await usersApiV2.recordStudySession(request); final response = await usersApiV2.recordStudySession(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(401)); expect(response.statusCode, equals(401));
expect(responseBody['error'], equals('unauthorized')); expect(responseBody['error'], equals('unauthorized'));
@ -854,8 +849,8 @@ void main() {
); );
final response = await usersApiV2.getAchievements(request); final response = await usersApiV2.getAchievements(request);
final responseBody = jsonDecode(await response.readAsString()) final responseBody =
as Map<String, dynamic>; jsonDecode(await response.readAsString()) as Map<String, dynamic>;
expect(response.statusCode, equals(401)); expect(response.statusCode, equals(401));
expect(responseBody['error'], equals('unauthorized')); expect(responseBody['error'], equals('unauthorized'));

View file

@ -19,7 +19,8 @@ void main() {
setUpAll(() async { setUpAll(() async {
// Подключение к тестовой БД // Подключение к тестовой БД
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost'; final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
final port = int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432; final port =
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test'; final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
final username = Platform.environment['TEST_DB_USER'] ?? 'postgres'; final username = Platform.environment['TEST_DB_USER'] ?? 'postgres';
final password = Platform.environment['TEST_DB_PASSWORD'] ?? 'postgres'; final password = Platform.environment['TEST_DB_PASSWORD'] ?? 'postgres';
@ -114,9 +115,9 @@ void main() {
expect(activeStats.length, equals(3)); expect(activeStats.length, equals(3));
// Удалить одну запись (soft delete) // Удалить одну запись (soft delete)
await (db.update(db.wordStatistics) await (db.update(
..where((w) => w.id.equals(stats2.id))) db.wordStatistics,
.write( )..where((w) => w.id.equals(stats2.id))).write(
WordStatisticsCompanion( WordStatisticsCompanion(
isDeleted: const Value(true), isDeleted: const Value(true),
deletedAt: Value(PgDateTime(DateTime.now())), deletedAt: Value(PgDateTime(DateTime.now())),
@ -145,9 +146,9 @@ void main() {
); );
// Удалить запись // Удалить запись
await (db.update(db.wordStatistics) await (db.update(
..where((w) => w.id.equals(stats.id))) db.wordStatistics,
.write( )..where((w) => w.id.equals(stats.id))).write(
WordStatisticsCompanion( WordStatisticsCompanion(
isDeleted: const Value(true), isDeleted: const Value(true),
deletedAt: Value(PgDateTime(DateTime.now())), deletedAt: Value(PgDateTime(DateTime.now())),
@ -162,7 +163,8 @@ void main() {
// Создать записи для разных пользователей // Создать записи для разных пользователей
final user2 = await db.userDao.createUser( final user2 = await db.userDao.createUser(
UsersCompanion.insert( UsersCompanion.insert(
externalUserId: 'test_user_2_${DateTime.now().millisecondsSinceEpoch}', externalUserId:
'test_user_2_${DateTime.now().millisecondsSinceEpoch}',
name: Value('Test User 2'), name: Value('Test User 2'),
), ),
); );
@ -185,16 +187,17 @@ void main() {
); );
// selectActive с where должен фильтровать и по isDeleted, и по условию // selectActive с where должен фильтровать и по isDeleted, и по условию
final user1Stats = await (db.wordStatisticsDao.selectActive() final user1Stats =
await (db.wordStatisticsDao.selectActive()
..where((w) => w.userId.equals(testUserId))) ..where((w) => w.userId.equals(testUserId)))
.get(); .get();
expect(user1Stats.length, equals(1)); expect(user1Stats.length, equals(1));
expect(user1Stats.first.id, equals(stats1.id)); expect(user1Stats.first.id, equals(stats1.id));
// Удалить запись пользователя 1 // Удалить запись пользователя 1
await (db.update(db.wordStatistics) await (db.update(
..where((w) => w.id.equals(stats1.id))) db.wordStatistics,
.write( )..where((w) => w.id.equals(stats1.id))).write(
WordStatisticsCompanion( WordStatisticsCompanion(
isDeleted: const Value(true), isDeleted: const Value(true),
deletedAt: Value(PgDateTime(DateTime.now())), deletedAt: Value(PgDateTime(DateTime.now())),
@ -202,13 +205,15 @@ void main() {
); );
// Теперь selectActive для пользователя 1 должен вернуть пустой список // Теперь selectActive для пользователя 1 должен вернуть пустой список
final user1StatsAfterDelete = await (db.wordStatisticsDao.selectActive() final user1StatsAfterDelete =
await (db.wordStatisticsDao.selectActive()
..where((w) => w.userId.equals(testUserId))) ..where((w) => w.userId.equals(testUserId)))
.get(); .get();
expect(user1StatsAfterDelete, isEmpty); expect(user1StatsAfterDelete, isEmpty);
// Но запись пользователя 2 все еще активна // Но запись пользователя 2 все еще активна
final user2Stats = await (db.wordStatisticsDao.selectActive() final user2Stats =
await (db.wordStatisticsDao.selectActive()
..where((w) => w.userId.equals(user2.id))) ..where((w) => w.userId.equals(user2.id)))
.get(); .get();
expect(user2Stats.length, equals(1)); expect(user2Stats.length, equals(1));
@ -233,7 +238,9 @@ void main() {
}); });
test('возвращает null если запись не существует', () async { test('возвращает null если запись не существует', () async {
final result = await db.wordStatisticsDao.getActiveById('non_existent_id'); final result = await db.wordStatisticsDao.getActiveById(
'non_existent_id',
);
expect(result, isNull); expect(result, isNull);
}); });
@ -246,9 +253,9 @@ void main() {
); );
// Удалить запись // Удалить запись
await (db.update(db.wordStatistics) await (db.update(
..where((w) => w.id.equals(stats.id))) db.wordStatistics,
.write( )..where((w) => w.id.equals(stats.id))).write(
WordStatisticsCompanion( WordStatisticsCompanion(
isDeleted: const Value(true), isDeleted: const Value(true),
deletedAt: Value(PgDateTime(DateTime.now())), deletedAt: Value(PgDateTime(DateTime.now())),
@ -268,9 +275,9 @@ void main() {
); );
// Удалить запись // Удалить запись
await (db.update(db.wordStatistics) await (db.update(
..where((w) => w.id.equals(stats.id))) db.wordStatistics,
.write( )..where((w) => w.id.equals(stats.id))).write(
WordStatisticsCompanion( WordStatisticsCompanion(
isDeleted: const Value(true), isDeleted: const Value(true),
deletedAt: Value(PgDateTime(DateTime.now())), deletedAt: Value(PgDateTime(DateTime.now())),

View file

@ -12,12 +12,13 @@ void main() {
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost'; final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
final port = final port =
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432; int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
final database = final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test'; final username =
final username = Platform.environment['TEST_DB_USER'] ?? Platform.environment['TEST_DB_USER'] ??
Platform.environment['DB_USER'] ?? Platform.environment['DB_USER'] ??
'mnemo_user'; 'mnemo_user';
final password = Platform.environment['TEST_DB_PASSWORD'] ?? final password =
Platform.environment['TEST_DB_PASSWORD'] ??
Platform.environment['DB_PASSWORD'] ?? Platform.environment['DB_PASSWORD'] ??
''; '';
@ -44,9 +45,9 @@ void main() {
tearDown(() async { tearDown(() async {
// Keep cleanup scoped to generated tests only to avoid touching other data // Keep cleanup scoped to generated tests only to avoid touching other data
// that might exist in the shared test DB. // that might exist in the shared test DB.
final allGenerated = await (db.select(db.tests) final allGenerated = await (db.select(
..where((t) => t.version.equals('generated'))) db.tests,
.get(); )..where((t) => t.version.equals('generated'))).get();
for (final t in allGenerated) { for (final t in allGenerated) {
await (db.delete(db.tests)..where((x) => x.id.equals(t.id))).go(); await (db.delete(db.tests)..where((x) => x.id.equals(t.id))).go();
@ -84,7 +85,8 @@ void main() {
expect(stillThere, isNull); expect(stillThere, isNull);
}); });
test('hardDeleteOldSoftDeletedGeneratedTests removes old soft-deleted tests', test(
'hardDeleteOldSoftDeletedGeneratedTests removes old soft-deleted tests',
() async { () async {
final now = DateTime.now(); final now = DateTime.now();
@ -102,8 +104,12 @@ void main() {
TestsCompanion.insert( TestsCompanion.insert(
name: 'linked generated test', name: 'linked generated test',
version: const Value('generated'), version: const Value('generated'),
createdAt: Value(PgDateTime(now.subtract(const Duration(days: 10)))), createdAt: Value(
updatedAt: Value(PgDateTime(now.subtract(const Duration(days: 10)))), PgDateTime(now.subtract(const Duration(days: 10))),
),
updatedAt: Value(
PgDateTime(now.subtract(const Duration(days: 10))),
),
), ),
); );
await db.testDao.linkTestToPack(testId, packId); await db.testDao.linkTestToPack(testId, packId);
@ -126,11 +132,11 @@ void main() {
expect(stillThere, isNull); expect(stillThere, isNull);
// Cleanup the pack relation/pack. // Cleanup the pack relation/pack.
await (db.delete(db.testPackRelations) await (db.delete(
..where((r) => r.packId.equals(packId))) db.testPackRelations,
.go(); )..where((r) => r.packId.equals(packId))).go();
await (db.delete(db.cardPacks)..where((p) => p.id.equals(packId))).go(); await (db.delete(db.cardPacks)..where((p) => p.id.equals(packId))).go();
}); },
);
}); });
} }

View file

@ -16,12 +16,13 @@ void main() {
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost'; final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
final port = final port =
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432; int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
final database = final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test'; final username =
final username = Platform.environment['TEST_DB_USER'] ?? Platform.environment['TEST_DB_USER'] ??
Platform.environment['DB_USER'] ?? Platform.environment['DB_USER'] ??
'mnemo_user'; 'mnemo_user';
final password = Platform.environment['TEST_DB_PASSWORD'] ?? final password =
Platform.environment['TEST_DB_PASSWORD'] ??
Platform.environment['DB_PASSWORD'] ?? Platform.environment['DB_PASSWORD'] ??
''; '';
@ -40,12 +41,12 @@ void main() {
await Migrator(db).createAll(); await Migrator(db).createAll();
// Initialize MinioService // Initialize MinioService
final minioEndpoint = Platform.environment['MINIO_ENDPOINT'] ?? final minioEndpoint =
'localhost:9000'; Platform.environment['MINIO_ENDPOINT'] ?? 'localhost:9000';
final minioAccessKey = Platform.environment['MINIO_ACCESS_KEY'] ?? final minioAccessKey =
'minioadmin'; Platform.environment['MINIO_ACCESS_KEY'] ?? 'minioadmin';
final minioSecretKey = Platform.environment['MINIO_SECRET_KEY'] ?? final minioSecretKey =
'minioadmin'; Platform.environment['MINIO_SECRET_KEY'] ?? 'minioadmin';
minioService = MinioService( minioService = MinioService(
endpoint: minioEndpoint, endpoint: minioEndpoint,
@ -68,20 +69,22 @@ void main() {
tearDown(() async { tearDown(() async {
// Cleanup is scoped to the entities created by this test. // Cleanup is scoped to the entities created by this test.
// Delete generated tests to avoid leaving orphans if pack is removed. // Delete generated tests to avoid leaving orphans if pack is removed.
final generatedTests = await (db.select(db.tests) final generatedTests = await (db.select(
..where((t) => t.version.equals('generated'))) db.tests,
.get(); )..where((t) => t.version.equals('generated'))).get();
for (final t in generatedTests) { for (final t in generatedTests) {
await (db.delete(db.tests)..where((x) => x.id.equals(t.id))).go(); await (db.delete(db.tests)..where((x) => x.id.equals(t.id))).go();
} }
if (packId != null) { if (packId != null) {
await (db.delete(db.cardPacks)..where((p) => p.id.equals(packId!))) await (db.delete(
.go(); db.cardPacks,
)..where((p) => p.id.equals(packId!))).go();
} }
if (cardId != null) { if (cardId != null) {
await (db.delete(db.gameCards)..where((c) => c.id.equals(cardId!))) await (db.delete(
.go(); db.gameCards,
)..where((c) => c.id.equals(cardId!))).go();
} }
packId = null; packId = null;
@ -121,18 +124,20 @@ void main() {
await testManager.updateGeneratedTests(packModel); await testManager.updateGeneratedTests(packModel);
final packTests = await db.testDao.getTestsByPackId(packId!); final packTests = await db.testDao.getTestsByPackId(packId!);
final generated = final generated = packTests
packTests.where((t) => t.version == 'generated').toList(); .where((t) => t.version == 'generated')
.toList();
expect(generated, hasLength(1)); expect(generated, hasLength(1));
final linkedPackId = final linkedPackId = await db.testDao.getPackIdForTest(
await db.testDao.getPackIdForTest(generated.single.id); generated.single.id,
);
expect(linkedPackId, equals(packId)); expect(linkedPackId, equals(packId));
final relations = await (db.select(db.testPackRelations) final relations = await (db.select(
..where((r) => r.packId.equals(packId!))) db.testPackRelations,
.get(); )..where((r) => r.packId.equals(packId!))).get();
expect(relations, hasLength(1)); expect(relations, hasLength(1));
expect(relations.single.testId, equals(generated.single.id)); expect(relations.single.testId, equals(generated.single.id));
}); });

View file

@ -22,7 +22,8 @@ void main() {
// Подключение к тестовой БД // Подключение к тестовой БД
// Можно использовать переменные окружения для настройки // Можно использовать переменные окружения для настройки
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost'; final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
final port = int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432; final port =
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test'; final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
final username = Platform.environment['TEST_DB_USER'] ?? 'postgres'; final username = Platform.environment['TEST_DB_USER'] ?? 'postgres';
final password = Platform.environment['TEST_DB_PASSWORD'] ?? 'postgres'; final password = Platform.environment['TEST_DB_PASSWORD'] ?? 'postgres';
@ -51,9 +52,7 @@ void main() {
// 2. Создать UserData // 2. Создать UserData
await db.userDao.createUserData( await db.userDao.createUserData(
UserDatasCompanion.insert( UserDatasCompanion.insert(userId: testUserId),
userId: testUserId,
),
); );
// 3. Создать пак // 3. Создать пак
@ -194,9 +193,9 @@ void main() {
); );
// Soft delete // Soft delete
await (db.update(db.wordStatistics) await (db.update(
..where((w) => w.id.equals(stats.id))) db.wordStatistics,
.write( )..where((w) => w.id.equals(stats.id))).write(
WordStatisticsCompanion( WordStatisticsCompanion(
isDeleted: const Value(true), isDeleted: const Value(true),
deletedAt: Value(PgDateTime(DateTime.now())), deletedAt: Value(PgDateTime(DateTime.now())),
@ -389,7 +388,8 @@ void main() {
// Создать другого пользователя // Создать другого пользователя
final user2 = await db.userDao.createUser( final user2 = await db.userDao.createUser(
UsersCompanion.insert( UsersCompanion.insert(
externalUserId: 'test_user_2_${DateTime.now().millisecondsSinceEpoch}', externalUserId:
'test_user_2_${DateTime.now().millisecondsSinceEpoch}',
name: Value('Test User 2'), name: Value('Test User 2'),
), ),
); );
@ -444,9 +444,9 @@ void main() {
); );
// Удалить одну запись // Удалить одну запись
await (db.update(db.wordStatistics) await (db.update(
..where((w) => w.id.equals(stats1.id))) db.wordStatistics,
.write( )..where((w) => w.id.equals(stats1.id))).write(
WordStatisticsCompanion( WordStatisticsCompanion(
isDeleted: const Value(true), isDeleted: const Value(true),
deletedAt: Value(PgDateTime(DateTime.now())), deletedAt: Value(PgDateTime(DateTime.now())),
@ -468,9 +468,9 @@ void main() {
); );
// Удалить запись // Удалить запись
await (db.update(db.wordStatistics) await (db.update(
..where((w) => w.id.equals(stats.id))) db.wordStatistics,
.write( )..where((w) => w.id.equals(stats.id))).write(
WordStatisticsCompanion( WordStatisticsCompanion(
isDeleted: const Value(true), isDeleted: const Value(true),
deletedAt: Value(PgDateTime(DateTime.now())), deletedAt: Value(PgDateTime(DateTime.now())),

View file

@ -6,13 +6,16 @@ void main() {
test('AppDatabase can be instantiated', () { test('AppDatabase can be instantiated', () {
// This test verifies that the database class can be created // This test verifies that the database class can be created
// Actual connection will be tested with real PostgreSQL instance // Actual connection will be tested with real PostgreSQL instance
expect(() => AppDatabase.connect( expect(
() => AppDatabase.connect(
host: 'localhost', host: 'localhost',
port: 5432, port: 5432,
database: 'test_db', database: 'test_db',
username: 'test_user', username: 'test_user',
password: 'test_pass', password: 'test_pass',
), returnsNormally); ),
returnsNormally,
);
}); });
test('All tables are registered', () { test('All tables are registered', () {

View file

@ -256,11 +256,15 @@ void main() {
expect(unlocked.unlockedAt, isNotNull); expect(unlocked.unlockedAt, isNotNull);
expect( expect(
unlocked.unlockedAt!.isAfter(beforeUnlock.subtract(const Duration(seconds: 1))), unlocked.unlockedAt!.isAfter(
beforeUnlock.subtract(const Duration(seconds: 1)),
),
isTrue, isTrue,
); );
expect( expect(
unlocked.unlockedAt!.isBefore(afterUnlock.add(const Duration(seconds: 1))), unlocked.unlockedAt!.isBefore(
afterUnlock.add(const Duration(seconds: 1)),
),
isTrue, isTrue,
); );
}); });
@ -526,11 +530,7 @@ void main() {
group('edge cases', () { group('edge cases', () {
test('handles empty strings', () { test('handles empty strings', () {
final model = AchievementModel( final model = AchievementModel(id: '', title: '', description: '');
id: '',
title: '',
description: '',
);
expect(model.id, ''); expect(model.id, '');
expect(model.title, ''); expect(model.title, '');

View file

@ -83,10 +83,7 @@ void main() {
}); });
test('converts model with null dates to DTO', () { test('converts model with null dates to DTO', () {
final model = PackProgressModel( final model = PackProgressModel(packId: 'test_pack', totalCards: 10);
packId: 'test_pack',
totalCards: 10,
);
final dto = model.toDto(); final dto = model.toDto();
@ -134,10 +131,7 @@ void main() {
}); });
test('creates model from DTO with null dates', () { test('creates model from DTO with null dates', () {
final dto = PackProgressDto( final dto = PackProgressDto(packId: 'test_pack', totalCards: 10);
packId: 'test_pack',
totalCards: 10,
);
final model = PackProgressModel.fromDto(dto); final model = PackProgressModel.fromDto(dto);
@ -208,10 +202,7 @@ void main() {
}); });
test('adds new card to attempts map', () { test('adds new card to attempts map', () {
final model = PackProgressModel( final model = PackProgressModel(packId: 'test_pack', totalCards: 10);
packId: 'test_pack',
totalCards: 10,
);
final updated = model.updateCardProgress( final updated = model.updateCardProgress(
cardId: 'new_card', cardId: 'new_card',
@ -238,9 +229,7 @@ void main() {
}); });
test('sets lastStudyDate to current time', () { test('sets lastStudyDate to current time', () {
final model = PackProgressModel( final model = PackProgressModel(packId: 'test_pack');
packId: 'test_pack',
);
final beforeUpdate = DateTime.now(); final beforeUpdate = DateTime.now();
final updated = model.updateCardProgress( final updated = model.updateCardProgress(
@ -252,19 +241,21 @@ void main() {
expect(updated.lastStudyDate, isNotNull); expect(updated.lastStudyDate, isNotNull);
expect( expect(
updated.lastStudyDate!.isAfter(beforeUpdate.subtract(const Duration(seconds: 1))), updated.lastStudyDate!.isAfter(
beforeUpdate.subtract(const Duration(seconds: 1)),
),
isTrue, isTrue,
); );
expect( expect(
updated.lastStudyDate!.isBefore(afterUpdate.add(const Duration(seconds: 1))), updated.lastStudyDate!.isBefore(
afterUpdate.add(const Duration(seconds: 1)),
),
isTrue, isTrue,
); );
}); });
test('sets firstStudyDate if null', () { test('sets firstStudyDate if null', () {
final model = PackProgressModel( final model = PackProgressModel(packId: 'test_pack');
packId: 'test_pack',
);
final updated = model.updateCardProgress( final updated = model.updateCardProgress(
cardId: 'card1', cardId: 'card1',
@ -312,10 +303,7 @@ void main() {
}); });
test('returns true when both learnedCards and studyTimeMinutes > 0', () { test('returns true when both learnedCards and studyTimeMinutes > 0', () {
final model = PackProgressModel( final model = PackProgressModel(learnedCards: 5, studyTimeMinutes: 10);
learnedCards: 5,
studyTimeMinutes: 10,
);
expect(model.hasStarted, isTrue); expect(model.hasStarted, isTrue);
}); });
@ -335,37 +323,25 @@ void main() {
}); });
test('calculates progress correctly', () { test('calculates progress correctly', () {
final model = PackProgressModel( final model = PackProgressModel(totalCards: 10, learnedCards: 5);
totalCards: 10,
learnedCards: 5,
);
expect(model.progress, 0.5); expect(model.progress, 0.5);
}); });
test('calculates progress for partial completion', () { test('calculates progress for partial completion', () {
final model = PackProgressModel( final model = PackProgressModel(totalCards: 100, learnedCards: 33);
totalCards: 100,
learnedCards: 33,
);
expect(model.progress, 0.33); expect(model.progress, 0.33);
}); });
test('returns 1.0 when all cards learned', () { test('returns 1.0 when all cards learned', () {
final model = PackProgressModel( final model = PackProgressModel(totalCards: 10, learnedCards: 10);
totalCards: 10,
learnedCards: 10,
);
expect(model.progress, 1.0); expect(model.progress, 1.0);
}); });
test('handles learnedCards exceeding totalCards', () { test('handles learnedCards exceeding totalCards', () {
final model = PackProgressModel( final model = PackProgressModel(totalCards: 10, learnedCards: 15);
totalCards: 10,
learnedCards: 15,
);
expect(model.progress, 1.5); expect(model.progress, 1.5);
}); });

View file

@ -8,10 +8,7 @@ void main() {
test('creates model with required parameters', () { test('creates model with required parameters', () {
final startTime = DateTime.now(); final startTime = DateTime.now();
final model = StudySessionModel( final model = StudySessionModel(userId: 1, startTime: startTime);
userId: 1,
startTime: startTime,
);
expect(model.userId, 1); expect(model.userId, 1);
expect(model.startTime, startTime); expect(model.startTime, startTime);
@ -54,10 +51,7 @@ void main() {
test('creates model with default values', () { test('creates model with default values', () {
final startTime = DateTime.now(); final startTime = DateTime.now();
final model = StudySessionModel( final model = StudySessionModel(userId: 1, startTime: startTime);
userId: 1,
startTime: startTime,
);
expect(model.wordsLearned, 0); expect(model.wordsLearned, 0);
expect(model.testsCompleted, 0); expect(model.testsCompleted, 0);
@ -97,10 +91,7 @@ void main() {
test('converts model with null values to DTO', () { test('converts model with null values to DTO', () {
final startTime = DateTime.now(); final startTime = DateTime.now();
final model = StudySessionModel( final model = StudySessionModel(userId: 1, startTime: startTime);
userId: 1,
startTime: startTime,
);
final dto = model.toDto(); final dto = model.toDto();
@ -143,9 +134,7 @@ void main() {
test('creates model from DTO with null values', () { test('creates model from DTO with null values', () {
final startTime = DateTime.now(); final startTime = DateTime.now();
final dto = StudySessionDto( final dto = StudySessionDto(startTime: startTime);
startTime: startTime,
);
final model = StudySessionModel.fromDto(dto, 1); final model = StudySessionModel.fromDto(dto, 1);
@ -190,10 +179,7 @@ void main() {
group('isActive', () { group('isActive', () {
test('returns true when endTime is null', () { test('returns true when endTime is null', () {
final model = StudySessionModel( final model = StudySessionModel(userId: 1, startTime: DateTime.now());
userId: 1,
startTime: DateTime.now(),
);
expect(model.isActive, isTrue); expect(model.isActive, isTrue);
}); });
@ -227,10 +213,7 @@ void main() {
test('calculates duration using current time when active', () { test('calculates duration using current time when active', () {
final startTime = DateTime.now().subtract(const Duration(minutes: 15)); final startTime = DateTime.now().subtract(const Duration(minutes: 15));
final model = StudySessionModel( final model = StudySessionModel(userId: 1, startTime: startTime);
userId: 1,
startTime: startTime,
);
final duration = model.duration; final duration = model.duration;
expect(duration.inMinutes, greaterThanOrEqualTo(14)); expect(duration.inMinutes, greaterThanOrEqualTo(14));
@ -287,10 +270,7 @@ void main() {
}); });
test('returns General Study when neither is set', () { test('returns General Study when neither is set', () {
final model = StudySessionModel( final model = StudySessionModel(userId: 1, startTime: DateTime.now());
userId: 1,
startTime: DateTime.now(),
);
expect(model.sessionType, 'General Study'); expect(model.sessionType, 'General Study');
}); });
@ -416,10 +396,7 @@ void main() {
test('sets endTime to current time', () { test('sets endTime to current time', () {
final startTime = DateTime.now().subtract(const Duration(minutes: 10)); final startTime = DateTime.now().subtract(const Duration(minutes: 10));
final model = StudySessionModel( final model = StudySessionModel(userId: 1, startTime: startTime);
userId: 1,
startTime: startTime,
);
final beforeEnd = DateTime.now(); final beforeEnd = DateTime.now();
final ended = model.end( final ended = model.end(
@ -431,7 +408,9 @@ void main() {
expect(ended.endTime, isNotNull); expect(ended.endTime, isNotNull);
expect( expect(
ended.endTime!.isAfter(beforeEnd.subtract(const Duration(seconds: 1))), ended.endTime!.isAfter(
beforeEnd.subtract(const Duration(seconds: 1)),
),
isTrue, isTrue,
); );
expect( expect(
@ -441,10 +420,7 @@ void main() {
}); });
test('updates statistics correctly', () { test('updates statistics correctly', () {
final model = StudySessionModel( final model = StudySessionModel(userId: 1, startTime: DateTime.now());
userId: 1,
startTime: DateTime.now(),
);
final ended = model.end( final ended = model.end(
wordsLearned: 15, wordsLearned: 15,

View file

@ -8,15 +8,21 @@ void main() {
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO5n5p0AAAAASUVORK5CYII='; 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO5n5p0AAAAASUVORK5CYII=';
group('CardImageStorage', () { group('CardImageStorage', () {
test('sanitizeCardsFileName strips cards/ prefix and rejects traversal', () { 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('/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'), 'a.png');
expect(CardImageStorage.sanitizeCardsFileName('../a.png'), isNull); expect(CardImageStorage.sanitizeCardsFileName('../a.png'), isNull);
expect(CardImageStorage.sanitizeCardsFileName('cards/../a.png'), isNull); expect(
CardImageStorage.sanitizeCardsFileName('cards/../a.png'),
isNull,
);
expect(CardImageStorage.sanitizeCardsFileName('cards/a/b.png'), isNull); expect(CardImageStorage.sanitizeCardsFileName('cards/a/b.png'), isNull);
}); },
);
test('tryParseBase64Image parses data: url and detects png', () { test('tryParseBase64Image parses data: url and detects png', () {
final parsed = CardImageStorage.tryParseBase64Image( final parsed = CardImageStorage.tryParseBase64Image(
@ -64,4 +70,3 @@ void main() {
}); });
}); });
} }

View file

@ -7,7 +7,9 @@ import 'package:test/test.dart';
void main() { void main() {
group('VoiceStorage', () { group('VoiceStorage', () {
test('sanitizeVoiceFileName strips voice/ prefix and rejects traversal', () { 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('/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'), 'a.mp3');
@ -15,7 +17,8 @@ void main() {
expect(VoiceStorage.sanitizeVoiceFileName('../a.mp3'), isNull); expect(VoiceStorage.sanitizeVoiceFileName('../a.mp3'), isNull);
expect(VoiceStorage.sanitizeVoiceFileName('voice/../a.mp3'), isNull); expect(VoiceStorage.sanitizeVoiceFileName('voice/../a.mp3'), isNull);
expect(VoiceStorage.sanitizeVoiceFileName('voice/a/b.mp3'), isNull); expect(VoiceStorage.sanitizeVoiceFileName('voice/a/b.mp3'), isNull);
}); },
);
test('tryParseBase64Audio parses data: url and detects mp3', () { test('tryParseBase64Audio parses data: url and detects mp3', () {
final bytes = Uint8List.fromList([0x49, 0x44, 0x33, 0x03, 0x00, 0x00]); final bytes = Uint8List.fromList([0x49, 0x44, 0x33, 0x03, 0x00, 0x00]);
@ -64,4 +67,3 @@ void main() {
}); });
}); });
} }

View file

@ -28,7 +28,8 @@ void main() {
setUpAll(() async { setUpAll(() async {
// Подключение к тестовой БД // Подключение к тестовой БД
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost'; final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
final port = int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432; final port =
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test'; final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
final username = Platform.environment['TEST_DB_USER'] ?? 'postgres'; final username = Platform.environment['TEST_DB_USER'] ?? 'postgres';
final password = Platform.environment['TEST_DB_PASSWORD'] ?? 'postgres'; final password = Platform.environment['TEST_DB_PASSWORD'] ?? 'postgres';
@ -47,7 +48,8 @@ void main() {
// Создать тестовые данные // Создать тестовые данные
final user = await db.userDao.createUser( final user = await db.userDao.createUser(
UsersCompanion.insert( UsersCompanion.insert(
externalUserId: 'smoke_test_user_${DateTime.now().millisecondsSinceEpoch}', externalUserId:
'smoke_test_user_${DateTime.now().millisecondsSinceEpoch}',
name: Value('Smoke Test User'), name: Value('Smoke Test User'),
email: Value('smoke@test.com'), email: Value('smoke@test.com'),
), ),
@ -156,7 +158,10 @@ void main() {
expect(packProgress.packId, equals(testPackId)); expect(packProgress.packId, equals(testPackId));
expect(packProgress.totalCards, equals(10)); expect(packProgress.totalCards, equals(10));
expect(packProgress.learnedCards, equals(2)); // 2 карточки с ответами expect(packProgress.learnedCards, equals(2)); // 2 карточки с ответами
expect(packProgress.averageAccuracy, closeTo(0.5, 0.01)); // 1 правильный, 1 неправильный expect(
packProgress.averageAccuracy,
closeTo(0.5, 0.01),
); // 1 правильный, 1 неправильный
}); });
test('studyDates рассчитываются из StudySessions', () async { test('studyDates рассчитываются из StudySessions', () async {
@ -172,16 +177,20 @@ void main() {
), ),
); );
final studyDates = await statisticsCalculator.calculateStudyDates(testUserId); final studyDates = await statisticsCalculator.calculateStudyDates(
testUserId,
);
expect(studyDates, isNotEmpty); expect(studyDates, isNotEmpty);
// Проверить что дата сегодняшнего дня присутствует // Проверить что дата сегодняшнего дня присутствует
final today = DateTime(now.year, now.month, now.day); final today = DateTime(now.year, now.month, now.day);
expect( expect(
studyDates.any((d) => studyDates.any(
(d) =>
d.year == today.year && d.year == today.year &&
d.month == today.month && d.month == today.month &&
d.day == today.day), d.day == today.day,
),
isTrue, isTrue,
); );
}); });
@ -204,18 +213,22 @@ void main() {
userId: testUserId, userId: testUserId,
packId: testPackId, packId: testPackId,
startTime: PgDateTime(now.add(const Duration(hours: 1))), startTime: PgDateTime(now.add(const Duration(hours: 1))),
endTime: Value(PgDateTime(now.add(const Duration(hours: 1, minutes: 20)))), endTime: Value(
PgDateTime(now.add(const Duration(hours: 1, minutes: 20))),
),
durationMinutes: 20, durationMinutes: 20,
), ),
); );
final categoryMinutes = await statisticsCalculator.calculateCategoryMinutes( final categoryMinutes = await statisticsCalculator
testUserId, .calculateCategoryMinutes(testUserId);
);
expect(categoryMinutes, isNotEmpty); expect(categoryMinutes, isNotEmpty);
// Должно быть минимум 35 минут (15 + 20) // Должно быть минимум 35 минут (15 + 20)
final totalMinutes = categoryMinutes.values.fold<int>(0, (sum, minutes) => sum + minutes); final totalMinutes = categoryMinutes.values.fold<int>(
0,
(sum, minutes) => sum + minutes,
);
expect(totalMinutes, greaterThanOrEqualTo(35)); expect(totalMinutes, greaterThanOrEqualTo(35));
}); });
@ -229,9 +242,9 @@ void main() {
); );
// Soft delete // Soft delete
await (db.update(db.wordStatistics) await (db.update(
..where((w) => w.id.equals(stats.id))) db.wordStatistics,
.write( )..where((w) => w.id.equals(stats.id))).write(
WordStatisticsCompanion( WordStatisticsCompanion(
isDeleted: const Value(true), isDeleted: const Value(true),
deletedAt: Value(PgDateTime(DateTime.now())), deletedAt: Value(PgDateTime(DateTime.now())),

View file

@ -34,7 +34,10 @@ void main() {
expect(sampleAchievementIds.length, equals(19)); expect(sampleAchievementIds.length, equals(19));
expect(sampleAchievementIds.every((id) => id.isNotEmpty), isTrue); expect(sampleAchievementIds.every((id) => id.isNotEmpty), isTrue);
expect(sampleAchievementIds.toSet().length, equals(sampleAchievementIds.length)); // All unique expect(
sampleAchievementIds.toSet().length,
equals(sampleAchievementIds.length),
); // All unique
}); });
test('Achievement categories are properly defined', () { test('Achievement categories are properly defined', () {
@ -96,8 +99,19 @@ void main() {
test('Progressive achievements have increasing difficulty', () { test('Progressive achievements have increasing difficulty', () {
// Test that later achievements require more progress // Test that later achievements require more progress
const wordAchievements = ['words_10', 'words_50', 'words_100', 'words_500', 'words_1000']; const wordAchievements = [
const streakAchievements = ['streak_3', 'streak_7', 'streak_30', 'streak_100']; 'words_10',
'words_50',
'words_100',
'words_500',
'words_1000',
];
const streakAchievements = [
'streak_3',
'streak_7',
'streak_30',
'streak_100',
];
expect(wordAchievements.length, equals(5)); expect(wordAchievements.length, equals(5));
expect(streakAchievements.length, equals(4)); expect(streakAchievements.length, equals(4));
@ -136,11 +150,11 @@ void main() {
final progress1 = calculateProgressMap(5, 2); final progress1 = calculateProgressMap(5, 2);
expect(progress1['words_10'], equals(0.5)); expect(progress1['words_10'], equals(0.5));
expect(progress1['streak_3'], equals(2/3)); expect(progress1['streak_3'], equals(2 / 3));
final progress2 = calculateProgressMap(25, 5); final progress2 = calculateProgressMap(25, 5);
expect(progress2['words_50'], equals(0.5)); expect(progress2['words_50'], equals(0.5));
expect(progress2['streak_7'], equals(5/7)); expect(progress2['streak_7'], equals(5 / 7));
}); });
test('Achievement progress is properly bounded', () { test('Achievement progress is properly bounded', () {

View file

@ -34,12 +34,14 @@ class MockIsar extends Fake implements Isar {
} }
@override @override
QueryBuilder<StudySessionModel, StudySessionModel, QWhereClause> get studySessionModels { QueryBuilder<StudySessionModel, StudySessionModel, QWhereClause>
get studySessionModels {
return MockQueryBuilder(_sessions); return MockQueryBuilder(_sessions);
} }
@override @override
QueryBuilder<AchievementModel, AchievementModel, QWhereClause> get achievementModels { QueryBuilder<AchievementModel, AchievementModel, QWhereClause>
get achievementModels {
return MockQueryBuilder([]); return MockQueryBuilder([]);
} }
@ -133,18 +135,12 @@ void main() {
testId: 'integration_test', testId: 'integration_test',
sessionToken: 'test_session_${DateTime.now().millisecondsSinceEpoch}', sessionToken: 'test_session_${DateTime.now().millisecondsSinceEpoch}',
packId: 'test_pack', packId: 'test_pack',
words: TestWordsDto(words: [ words: TestWordsDto(
TestWordDto( words: [
word: 'hello', TestWordDto(word: 'hello', correct: 1, incorrect: 0),
correct: 1, TestWordDto(word: 'world', correct: 1, incorrect: 0),
incorrect: 0, ],
), ),
TestWordDto(
word: 'world',
correct: 1,
incorrect: 0,
),
]),
); );
// Simulate the addTestStatistics call (without the actual database save) // Simulate the addTestStatistics call (without the actual database save)
@ -208,21 +204,25 @@ void main() {
testId: 'test1', testId: 'test1',
sessionToken: 'session1', sessionToken: 'session1',
packId: 'pack1', packId: 'pack1',
words: TestWordsDto(words: [ words: TestWordsDto(
words: [
TestWordDto(word: 'word1', correct: 1, incorrect: 0), TestWordDto(word: 'word1', correct: 1, incorrect: 0),
TestWordDto(word: 'word2', correct: 0, incorrect: 1), TestWordDto(word: 'word2', correct: 0, incorrect: 1),
]), ],
),
); );
final testStats2 = TestStatisticsDto( final testStats2 = TestStatisticsDto(
testId: 'test2', testId: 'test2',
sessionToken: 'session2', sessionToken: 'session2',
packId: 'pack1', packId: 'pack1',
words: TestWordsDto(words: [ words: TestWordsDto(
words: [
TestWordDto(word: 'word3', correct: 1, incorrect: 0), TestWordDto(word: 'word3', correct: 1, incorrect: 0),
TestWordDto(word: 'word4', correct: 1, incorrect: 0), TestWordDto(word: 'word4', correct: 1, incorrect: 0),
TestWordDto(word: 'word5', correct: 0, incorrect: 1), TestWordDto(word: 'word5', correct: 0, incorrect: 1),
]), ],
),
); );
// Test session tracking across multiple tests // Test session tracking across multiple tests
@ -233,7 +233,9 @@ void main() {
testsCompleted: 1, testsCompleted: 1,
); );
final sessionId2 = await sessionTracker.getOrCreateSession(testUser.id!); // Should reuse session final sessionId2 = await sessionTracker.getOrCreateSession(
testUser.id!,
); // Should reuse session
expect(sessionId2, equals(sessionId1)); expect(sessionId2, equals(sessionId1));
await sessionTracker.updateSessionProgress( await sessionTracker.updateSessionProgress(
@ -256,7 +258,10 @@ void main() {
]; ];
final updatedUserData = testUser.userData.value!.copyWith(words: words); final updatedUserData = testUser.userData.value!.copyWith(words: words);
final progress = await achievementManager.getAchievementProgress(testUser.id!, updatedUserData); final progress = await achievementManager.getAchievementProgress(
testUser.id!,
updatedUserData,
);
// Should show progress towards word achievements // Should show progress towards word achievements
expect(progress['words_10'], equals(0.5)); // 5/10 = 0.5 expect(progress['words_10'], equals(0.5)); // 5/10 = 0.5
@ -290,7 +295,10 @@ void main() {
testUser.userData.value!, testUser.userData.value!,
); );
expect(newlyUnlocked, isEmpty); // No achievements should unlock with empty data expect(
newlyUnlocked,
isEmpty,
); // No achievements should unlock with empty data
// Test session tracking with non-existent user // Test session tracking with non-existent user
final sessionId = await sessionTracker.getOrCreateSession(999); final sessionId = await sessionTracker.getOrCreateSession(999);
@ -365,11 +373,11 @@ void main() {
// Add some words // Add some words
final updatedUserData1 = testUser.userData.value!.copyWith( final updatedUserData1 = testUser.userData.value!.copyWith(
words: List.generate(6, (i) => WordStatisticsModel( words: List.generate(
word: 'word$i', 6,
correct: 1.0, (i) =>
incorrect: 0.0, WordStatisticsModel(word: 'word$i', correct: 1.0, incorrect: 0.0),
)), ),
currentStreak: 2, currentStreak: 2,
); );
@ -379,15 +387,15 @@ void main() {
); );
expect(progress1['words_10'], equals(0.6)); // 6/10 = 0.6 expect(progress1['words_10'], equals(0.6)); // 6/10 = 0.6
expect(progress1['streak_3'], equals(2/3)); // 2/3 0.67 expect(progress1['streak_3'], equals(2 / 3)); // 2/3 0.67
// Add more progress // Add more progress
final updatedUserData2 = updatedUserData1.copyWith( final updatedUserData2 = updatedUserData1.copyWith(
words: List.generate(8, (i) => WordStatisticsModel( words: List.generate(
word: 'word$i', 8,
correct: 1.0, (i) =>
incorrect: 0.0, WordStatisticsModel(word: 'word$i', correct: 1.0, incorrect: 0.0),
)), ),
currentStreak: 3, currentStreak: 3,
); );
@ -397,7 +405,10 @@ void main() {
); );
expect(progress2['words_10'], equals(0.8)); // 8/10 = 0.8 expect(progress2['words_10'], equals(0.8)); // 8/10 = 0.8
expect(progress2['streak_3'], equals(1.0)); // 3/3 = 1.0 (achievement unlocked) expect(
progress2['streak_3'],
equals(1.0),
); // 3/3 = 1.0 (achievement unlocked)
}); });
}); });
} }

View file

@ -79,7 +79,10 @@ void main() {
// 3. Session is tracked // 3. Session is tracked
// 4. Achievements are checked and unlocked // 4. Achievements are checked and unlocked
expect(true, isTrue); // Integration tests would be implemented with proper mocking expect(
true,
isTrue,
); // Integration tests would be implemented with proper mocking
}); });
test('Error handling prevents system crashes', () { test('Error handling prevents system crashes', () {

View file

@ -50,7 +50,11 @@ void main() {
final yesterday = today.subtract(const Duration(days: 1)); final yesterday = today.subtract(const Duration(days: 1));
final threeDaysAgo = today.subtract(const Duration(days: 3)); final threeDaysAgo = today.subtract(const Duration(days: 3));
final result = calculator.calculateStreak([today, yesterday, threeDaysAgo]); final result = calculator.calculateStreak([
today,
yesterday,
threeDaysAgo,
]);
expect(result, 2); // Today and yesterday, gap on day 3 expect(result, 2); // Today and yesterday, gap on day 3
}); });
@ -63,9 +67,15 @@ void main() {
test('handles dates with different times', () { test('handles dates with different times', () {
final todayMorning = DateTime.now().copyWith(hour: 9); final todayMorning = DateTime.now().copyWith(hour: 9);
final todayEvening = DateTime.now().copyWith(hour: 20); final todayEvening = DateTime.now().copyWith(hour: 20);
final yesterday = DateTime.now().subtract(const Duration(days: 1, hours: 10)); final yesterday = DateTime.now().subtract(
const Duration(days: 1, hours: 10),
);
final result = calculator.calculateStreak([todayMorning, todayEvening, yesterday]); final result = calculator.calculateStreak([
todayMorning,
todayEvening,
yesterday,
]);
expect(result, 2); expect(result, 2);
}); });
}); });
@ -122,9 +132,7 @@ void main() {
studyTimeMinutes: 45, studyTimeMinutes: 45,
); );
final userData = UserDataModel( final userData = UserDataModel(packProgress: [pack1, pack2]);
packProgress: [pack1, pack2],
);
final result = calculator.calculateTotalStudyTime(userData); final result = calculator.calculateTotalStudyTime(userData);
expect(result, 75); // 30 + 45 expect(result, 75); // 30 + 45
@ -224,10 +232,7 @@ void main() {
], ],
); );
final result = calculator.getTimelineStatistics( final result = calculator.getTimelineStatistics(userData, period: 'week');
userData,
period: 'week',
);
expect(result['period'], 'week'); expect(result['period'], 'week');
expect(result.containsKey('totalDays'), true); expect(result.containsKey('totalDays'), true);
@ -247,10 +252,17 @@ void main() {
test('calculates level based on activity', () { test('calculates level based on activity', () {
final userData = UserDataModel( final userData = UserDataModel(
words: List.generate(20, (i) => WordStatisticsModel(word: 'word$i')), // 20 words words: List.generate(
20,
(i) => WordStatisticsModel(word: 'word$i'),
), // 20 words
totalStudyTimeMinutes: 60 * 10, // 10 hours = 600 minutes totalStudyTimeMinutes: 60 * 10, // 10 hours = 600 minutes
packProgress: [ packProgress: [
PackProgressModel(packId: 'pack1', totalCards: 10, learnedCards: 10), // 1 completed pack PackProgressModel(
packId: 'pack1',
totalCards: 10,
learnedCards: 10,
), // 1 completed pack
], ],
); );

View file

@ -20,7 +20,8 @@ void main() {
setUpAll(() async { setUpAll(() async {
// Подключение к тестовой БД // Подключение к тестовой БД
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost'; final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
final port = int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432; final port =
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test'; final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
final username = Platform.environment['TEST_DB_USER'] ?? 'postgres'; final username = Platform.environment['TEST_DB_USER'] ?? 'postgres';
final password = Platform.environment['TEST_DB_PASSWORD'] ?? 'postgres'; final password = Platform.environment['TEST_DB_PASSWORD'] ?? 'postgres';
@ -242,10 +243,7 @@ void main() {
isCorrect: true, isCorrect: true,
); );
final packStats = await manager.getPackStatistics( final packStats = await manager.getPackStatistics(testUserId, pack.id);
testUserId,
pack.id,
);
expect(packStats.length, equals(2)); expect(packStats.length, equals(2));
expect( expect(
@ -263,10 +261,7 @@ void main() {
), ),
); );
final packStats = await manager.getPackStatistics( final packStats = await manager.getPackStatistics(testUserId, pack.id);
testUserId,
pack.id,
);
expect(packStats, isEmpty); expect(packStats, isEmpty);
}); });

View file

@ -4,14 +4,8 @@ import 'package:mnemo_cards_backend/storage/minio_config.dart';
void main() { void main() {
group('MinioConfig', () { group('MinioConfig', () {
test('should have correct bucket names', () { test('should have correct bucket names', () {
expect( expect(MinioConfig.cardImagesBucket, equals('card-images'));
MinioConfig.cardImagesBucket, expect(MinioConfig.voiceAudioBucket, equals('voice-audio'));
equals('card-images'),
);
expect(
MinioConfig.voiceAudioBucket,
equals('voice-audio'),
);
}); });
test('should have correct presigned URL expiry', () { test('should have correct presigned URL expiry', () {
@ -44,14 +38,8 @@ void main() {
test('should have reasonable presigned URL expiry', () { test('should have reasonable presigned URL expiry', () {
// Presigned URLs should expire after 4 hours (14400 seconds) // Presigned URLs should expire after 4 hours (14400 seconds)
expect( expect(MinioConfig.presignedUrlExpirySeconds, equals(14400));
MinioConfig.presignedUrlExpirySeconds, expect(MinioConfig.presignedUrlExpirySeconds, greaterThan(0));
equals(14400),
);
expect(
MinioConfig.presignedUrlExpirySeconds,
greaterThan(0),
);
}); });
}); });
} }

View file

@ -79,4 +79,3 @@ void main() {
}); });
}); });
} }

View file

@ -46,9 +46,7 @@ void main() {
final generator = SimpleQuestionGenerator( final generator = SimpleQuestionGenerator(
data, data,
seed: 1, seed: 1,
possibleTypes: { possibleTypes: {SimpleQuestionType.audio_translation},
SimpleQuestionType.audio_translation,
},
); );
final answerCard = data.items.first; // card_1 with UUID audio final answerCard = data.items.first; // card_1 with UUID audio
@ -82,9 +80,7 @@ void main() {
final generator = SimpleQuestionGenerator( final generator = SimpleQuestionGenerator(
data, data,
seed: 1, seed: 1,
possibleTypes: { possibleTypes: {SimpleQuestionType.audio_translation},
SimpleQuestionType.audio_translation,
},
); );
final answerCard = data.items.first; final answerCard = data.items.first;
@ -116,9 +112,7 @@ void main() {
final generator = SimpleQuestionGenerator( final generator = SimpleQuestionGenerator(
data, data,
seed: 1, seed: 1,
possibleTypes: { possibleTypes: {SimpleQuestionType.audio_translation},
SimpleQuestionType.audio_translation,
},
); );
final answerCard = data.items.first; final answerCard = data.items.first;
@ -151,9 +145,7 @@ void main() {
final generator = SimpleQuestionGenerator( final generator = SimpleQuestionGenerator(
data, data,
seed: 1, seed: 1,
possibleTypes: { possibleTypes: {SimpleQuestionType.audio_images},
SimpleQuestionType.audio_images,
},
); );
final answerCard = data.items.first; final answerCard = data.items.first;

View file

@ -17,12 +17,13 @@ void main() {
final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost'; final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost';
final port = final port =
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432; int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
final database = final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test';
Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test'; final username =
final username = Platform.environment['TEST_DB_USER'] ?? Platform.environment['TEST_DB_USER'] ??
Platform.environment['DB_USER'] ?? Platform.environment['DB_USER'] ??
'mnemo_user'; 'mnemo_user';
final password = Platform.environment['TEST_DB_PASSWORD'] ?? final password =
Platform.environment['TEST_DB_PASSWORD'] ??
Platform.environment['DB_PASSWORD'] ?? Platform.environment['DB_PASSWORD'] ??
''; '';
@ -38,12 +39,12 @@ void main() {
await Migrator(db).createAll(); await Migrator(db).createAll();
// Initialize MinioService // Initialize MinioService
final minioEndpoint = Platform.environment['MINIO_ENDPOINT'] ?? final minioEndpoint =
'localhost:9000'; Platform.environment['MINIO_ENDPOINT'] ?? 'localhost:9000';
final minioAccessKey = Platform.environment['MINIO_ACCESS_KEY'] ?? final minioAccessKey =
'minioadmin'; Platform.environment['MINIO_ACCESS_KEY'] ?? 'minioadmin';
final minioSecretKey = Platform.environment['MINIO_SECRET_KEY'] ?? final minioSecretKey =
'minioadmin'; Platform.environment['MINIO_SECRET_KEY'] ?? 'minioadmin';
minioService = MinioService( minioService = MinioService(
endpoint: minioEndpoint, endpoint: minioEndpoint,
@ -71,18 +72,21 @@ void main() {
testId = null; testId = null;
} }
if (packId != null) { if (packId != null) {
await (db.delete(db.cardPacks)..where((p) => p.id.equals(packId!))) await (db.delete(
.go(); db.cardPacks,
)..where((p) => p.id.equals(packId!))).go();
packId = null; packId = null;
} }
if (cardId != null) { if (cardId != null) {
await (db.delete(db.gameCards)..where((c) => c.id.equals(cardId!))) await (db.delete(
.go(); db.gameCards,
)..where((c) => c.id.equals(cardId!))).go();
cardId = null; cardId = null;
} }
}); });
test('matrix question buttons should have imageUrl from card images', test(
'matrix question buttons should have imageUrl from card images',
() async { () async {
// Create pack // Create pack
packId = await db.packDao.createPack( packId = await db.packDao.createPack(
@ -155,15 +159,13 @@ void main() {
// imageUrl should be a presigned URL or API endpoint // imageUrl should be a presigned URL or API endpoint
expect( expect(
button.imageUrl, button.imageUrl,
anyOf( anyOf(startsWith('http'), startsWith('/api/v2/packs')),
startsWith('http'),
startsWith('/api/v2/packs'),
),
); );
} else { } else {
fail('Expected MatrixTestQuestion'); fail('Expected MatrixTestQuestion');
} }
}); },
);
test('test question with image should have imageUrl', () async { test('test question with image should have imageUrl', () async {
// Create pack // Create pack

View file

@ -13,9 +13,7 @@ void main() async {
username: 'mnemo_user', username: 'mnemo_user',
password: 'dev_password_change_me', password: 'dev_password_change_me',
), ),
settings: pg.ConnectionSettings( settings: pg.ConnectionSettings(sslMode: pg.SslMode.disable),
sslMode: pg.SslMode.disable,
),
); );
print('✅ PostgreSQL connection successful'); print('✅ PostgreSQL connection successful');
@ -27,7 +25,6 @@ void main() async {
await connection.close(); await connection.close();
print('✅ Database connection closed'); print('✅ Database connection closed');
print('🎉 PostgreSQL backend can connect to database!'); print('🎉 PostgreSQL backend can connect to database!');
} catch (e, s) { } catch (e, s) {
print('❌ Error: $e'); print('❌ Error: $e');
print('Stack trace: $s'); print('Stack trace: $s');

View file

@ -55,3 +55,4 @@ echo " - favicon.png (32x32)"