import 'dart:io'; import 'package:test/test.dart'; import 'package:mnemo_cards_backend/database/database.dart'; import 'package:mnemo_cards_backend/database/tables/users.dart'; import 'package:mnemo_cards_backend/database/tables/packs.dart'; import 'package:mnemo_cards_backend/statistics/word_statistics_manager.dart'; import 'package:mnemo_cards_backend/statistics/statistics_calculator.dart'; /// Smoke тесты для проверки базовой функциональности после деплоя /// /// Эти тесты проверяют: /// - Создание БД и таблиц /// - Основные CRUD операции /// - Интеграцию WordStatisticsManager с TestManager /// - Расчет статистики через StatisticsCalculator /// /// Требования: /// - PostgreSQL должен быть запущен /// - Тестовая БД: mnemo_cards_test void main() { late AppDatabase db; late WordStatisticsManager wordStatsManager; late StatisticsCalculator statisticsCalculator; late String testUserId; late String testCardId; late String testPackId; setUpAll(() async { // Подключение к тестовой БД final host = Platform.environment['TEST_DB_HOST'] ?? 'localhost'; final port = int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432; final database = Platform.environment['TEST_DB_NAME'] ?? 'mnemo_cards_test'; final username = Platform.environment['TEST_DB_USER'] ?? 'postgres'; final password = Platform.environment['TEST_DB_PASSWORD'] ?? 'postgres'; db = AppDatabase.connect( host: host, port: port, database: database, username: username, password: password, ); // Создать схему БД await db.migrator.createAll(); // Создать тестовые данные final user = await db.userDao.createUser( UsersCompanion.insert( externalUserId: 'smoke_test_user_${DateTime.now().millisecondsSinceEpoch}', name: Value('Smoke Test User'), email: Value('smoke@test.com'), ), ); testUserId = user.id; await db.userDao.createUserData( UserDatasCompanion.insert(userId: testUserId), ); final pack = await db.packDao.createPack( CardPacksCompanion.insert( title: 'Smoke Test Pack', subtitle: 'Test Subtitle', size: 10, ), ); testPackId = pack.id; final card = await db.packDao.createCard( GameCardsCompanion.insert( original: 'test', translation: 'тест', image: 'test.png', ), ); testCardId = card.id; await db.packDao.linkCardToPack(testCardId, testPackId); await db.packDao.linkUserToPack(testUserId, testPackId); wordStatsManager = WordStatisticsManager(db); statisticsCalculator = StatisticsCalculator(db); }); tearDownAll(() async { // Очистить тестовые данные await db.delete(db.wordStatistics).go(); await db.delete(db.cardPackCards).go(); await db.delete(db.userPacks).go(); await db.delete(db.gameCards).go(); await db.delete(db.cardPacks).go(); await db.delete(db.userDatas).go(); await db.delete(db.users).go(); await db.close(); }); group('Smoke Tests', () { test('БД создается без ошибок', () { expect(db, isNotNull); expect(db.userDao, isNotNull); expect(db.packDao, isNotNull); expect(db.wordStatisticsDao, isNotNull); expect(db.statisticsDao, isNotNull); }); test('WordStatistics записываются после ответа', () async { // Имитация ответа на карточку (как в TestManager.submitTest) await wordStatsManager.recordAnswer( userId: testUserId, cardId: testCardId, isCorrect: true, ); final stats = await db.wordStatisticsDao.getByUserAndCard( testUserId, testCardId, ); expect(stats, isNotNull); expect(stats!.correctAnswers, equals(1)); expect(stats.incorrectAnswers, equals(0)); expect(stats.mastery, equals(1.0)); }); test('packProgress рассчитывается корректно', () async { // Записать несколько ответов await wordStatsManager.recordAnswer( userId: testUserId, cardId: testCardId, isCorrect: true, ); // Создать еще одну карточку в паке final card2 = await db.packDao.createCard( GameCardsCompanion.insert( original: 'test2', translation: 'тест2', image: 'test2.png', ), ); await db.packDao.linkCardToPack(card2.id, testPackId); await wordStatsManager.recordAnswer( userId: testUserId, cardId: card2.id, isCorrect: false, ); // Рассчитать packProgress final packProgress = await statisticsCalculator.calculatePackProgress( testUserId, testPackId, ); expect(packProgress.packId, equals(testPackId)); expect(packProgress.totalCards, equals(10)); expect(packProgress.learnedCards, equals(2)); // 2 карточки с ответами expect( packProgress.averageAccuracy, closeTo(0.5, 0.01), ); // 1 правильный, 1 неправильный }); test('studyDates рассчитываются из StudySessions', () async { // Создать сессию изучения final now = DateTime.now(); await db.statisticsDao.createSession( StudySessionsCompanion.insert( userId: testUserId, packId: testPackId, startTime: PgDateTime(now), endTime: Value(PgDateTime(now.add(const Duration(minutes: 10)))), durationMinutes: 10, ), ); final studyDates = await statisticsCalculator.calculateStudyDates( testUserId, ); expect(studyDates, isNotEmpty); // Проверить что дата сегодняшнего дня присутствует final today = DateTime(now.year, now.month, now.day); expect( studyDates.any( (d) => d.year == today.year && d.month == today.month && d.day == today.day, ), isTrue, ); }); test('categoryMinutes рассчитываются из StudySessions', () async { // Создать несколько сессий final now = DateTime.now(); await db.statisticsDao.createSession( StudySessionsCompanion.insert( userId: testUserId, packId: testPackId, startTime: PgDateTime(now), endTime: Value(PgDateTime(now.add(const Duration(minutes: 15)))), durationMinutes: 15, ), ); await db.statisticsDao.createSession( StudySessionsCompanion.insert( userId: testUserId, packId: testPackId, startTime: PgDateTime(now.add(const Duration(hours: 1))), endTime: Value( PgDateTime(now.add(const Duration(hours: 1, minutes: 20))), ), durationMinutes: 20, ), ); final categoryMinutes = await statisticsCalculator .calculateCategoryMinutes(testUserId); expect(categoryMinutes, isNotEmpty); // Должно быть минимум 35 минут (15 + 20) final totalMinutes = categoryMinutes.values.fold( 0, (sum, minutes) => sum + minutes, ); expect(totalMinutes, greaterThanOrEqualTo(35)); }); test('soft delete работает корректно', () async { // Создать запись статистики final stats = await db.wordStatisticsDao.create( userId: testUserId, cardId: testCardId, correctAnswers: 5, incorrectAnswers: 2, ); // Soft delete await (db.update( db.wordStatistics, )..where((w) => w.id.equals(stats.id))).write( WordStatisticsCompanion( isDeleted: const Value(true), deletedAt: Value(PgDateTime(DateTime.now())), ), ); // Проверить что запись не возвращается через getActiveById final activeRecord = await db.wordStatisticsDao.getActiveById(stats.id); expect(activeRecord, isNull); // Проверить что запись физически существует в БД final allRecords = await db.select(db.wordStatistics).get(); expect(allRecords.length, greaterThan(0)); expect(allRecords.any((r) => r.id == stats.id && r.isDeleted), isTrue); }); test('WordStatisticsManager обновляет существующую статистику', () async { // Первый ответ await wordStatsManager.recordAnswer( userId: testUserId, cardId: testCardId, isCorrect: true, ); // Второй ответ await wordStatsManager.recordAnswer( userId: testUserId, cardId: testCardId, isCorrect: true, ); // Третий ответ (неправильный) await wordStatsManager.recordAnswer( userId: testUserId, cardId: testCardId, isCorrect: false, ); final stats = await db.wordStatisticsDao.getByUserAndCard( testUserId, testCardId, ); expect(stats, isNotNull); expect(stats!.correctAnswers, equals(2)); expect(stats.incorrectAnswers, equals(1)); expect(stats.mastery, closeTo(2.0 / 3.0, 0.001)); }); test('getPackStatistics возвращает статистику по карточкам пака', () async { // Создать еще одну карточку в паке final card2 = await db.packDao.createCard( GameCardsCompanion.insert( original: 'test2', translation: 'тест2', image: 'test2.png', ), ); await db.packDao.linkCardToPack(card2.id, testPackId); // Записать ответы для обеих карточек await wordStatsManager.recordAnswer( userId: testUserId, cardId: testCardId, isCorrect: true, ); await wordStatsManager.recordAnswer( userId: testUserId, cardId: card2.id, isCorrect: false, ); final packStats = await wordStatsManager.getPackStatistics( testUserId, testPackId, ); expect(packStats.length, equals(2)); expect( packStats.map((s) => s.cardId).toSet(), containsAll([testCardId, card2.id]), ); }); }); }