2025-12-17 00:41:47 +00:00
|
|
|
|
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 тесты для проверки базовой функциональности после деплоя
|
2026-01-08 13:02:47 +00:00
|
|
|
|
///
|
2025-12-17 00:41:47 +00:00
|
|
|
|
/// Эти тесты проверяют:
|
|
|
|
|
|
/// - Создание БД и таблиц
|
|
|
|
|
|
/// - Основные CRUD операции
|
|
|
|
|
|
/// - Интеграцию WordStatisticsManager с TestManager
|
|
|
|
|
|
/// - Расчет статистики через StatisticsCalculator
|
2026-01-08 13:02:47 +00:00
|
|
|
|
///
|
2025-12-17 00:41:47 +00:00
|
|
|
|
/// Требования:
|
|
|
|
|
|
/// - 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';
|
2026-01-08 13:02:47 +00:00
|
|
|
|
final port =
|
|
|
|
|
|
int.tryParse(Platform.environment['TEST_DB_PORT'] ?? '5432') ?? 5432;
|
2025-12-17 00:41:47 +00:00
|
|
|
|
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(
|
2026-01-08 13:02:47 +00:00
|
|
|
|
externalUserId:
|
|
|
|
|
|
'smoke_test_user_${DateTime.now().millisecondsSinceEpoch}',
|
2025-12-17 00:41:47 +00:00
|
|
|
|
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 карточки с ответами
|
2026-01-08 13:02:47 +00:00
|
|
|
|
expect(
|
|
|
|
|
|
packProgress.averageAccuracy,
|
|
|
|
|
|
closeTo(0.5, 0.01),
|
|
|
|
|
|
); // 1 правильный, 1 неправильный
|
2025-12-17 00:41:47 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-01-08 13:02:47 +00:00
|
|
|
|
final studyDates = await statisticsCalculator.calculateStudyDates(
|
|
|
|
|
|
testUserId,
|
|
|
|
|
|
);
|
2025-12-17 00:41:47 +00:00
|
|
|
|
|
|
|
|
|
|
expect(studyDates, isNotEmpty);
|
|
|
|
|
|
// Проверить что дата сегодняшнего дня присутствует
|
|
|
|
|
|
final today = DateTime(now.year, now.month, now.day);
|
|
|
|
|
|
expect(
|
2026-01-08 13:02:47 +00:00
|
|
|
|
studyDates.any(
|
|
|
|
|
|
(d) =>
|
|
|
|
|
|
d.year == today.year &&
|
|
|
|
|
|
d.month == today.month &&
|
|
|
|
|
|
d.day == today.day,
|
|
|
|
|
|
),
|
2025-12-17 00:41:47 +00:00
|
|
|
|
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))),
|
2026-01-08 13:02:47 +00:00
|
|
|
|
endTime: Value(
|
|
|
|
|
|
PgDateTime(now.add(const Duration(hours: 1, minutes: 20))),
|
|
|
|
|
|
),
|
2025-12-17 00:41:47 +00:00
|
|
|
|
durationMinutes: 20,
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-01-08 13:02:47 +00:00
|
|
|
|
final categoryMinutes = await statisticsCalculator
|
|
|
|
|
|
.calculateCategoryMinutes(testUserId);
|
2025-12-17 00:41:47 +00:00
|
|
|
|
|
|
|
|
|
|
expect(categoryMinutes, isNotEmpty);
|
|
|
|
|
|
// Должно быть минимум 35 минут (15 + 20)
|
2026-01-08 13:02:47 +00:00
|
|
|
|
final totalMinutes = categoryMinutes.values.fold<int>(
|
|
|
|
|
|
0,
|
|
|
|
|
|
(sum, minutes) => sum + minutes,
|
|
|
|
|
|
);
|
2025-12-17 00:41:47 +00:00
|
|
|
|
expect(totalMinutes, greaterThanOrEqualTo(35));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('soft delete работает корректно', () async {
|
|
|
|
|
|
// Создать запись статистики
|
|
|
|
|
|
final stats = await db.wordStatisticsDao.create(
|
|
|
|
|
|
userId: testUserId,
|
|
|
|
|
|
cardId: testCardId,
|
|
|
|
|
|
correctAnswers: 5,
|
|
|
|
|
|
incorrectAnswers: 2,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Soft delete
|
2026-01-08 13:02:47 +00:00
|
|
|
|
await (db.update(
|
|
|
|
|
|
db.wordStatistics,
|
|
|
|
|
|
)..where((w) => w.id.equals(stats.id))).write(
|
|
|
|
|
|
WordStatisticsCompanion(
|
|
|
|
|
|
isDeleted: const Value(true),
|
|
|
|
|
|
deletedAt: Value(PgDateTime(DateTime.now())),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
2025-12-17 00:41:47 +00:00
|
|
|
|
|
|
|
|
|
|
// Проверить что запись не возвращается через 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]),
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|