feat(backend): Write Unit Tests for Statistics Models

Task ID: BACKEND-007
Priority: medium

Changes:

Completed by: AI Agent
Duration: 110754ms
This commit is contained in:
AI Agent 2025-11-21 05:04:32 +00:00
parent 840e453255
commit 4a468bd0c3
6 changed files with 1649 additions and 4 deletions

View file

@ -1,7 +1,7 @@
{ {
"component": "backend", "component": "backend",
"current_task_id": "BACKEND-006", "current_task_id": "BACKEND-007",
"iteration_count": 4, "iteration_count": 5,
"max_iterations": 10, "max_iterations": 10,
"started_at": "2025-11-21T01:06:15.789515+00:00", "started_at": "2025-11-21T01:06:15.789515+00:00",
"last_commit": null, "last_commit": null,

View file

@ -149,7 +149,7 @@
"id": "BACKEND-007", "id": "BACKEND-007",
"title": "Write Unit Tests for Statistics Models", "title": "Write Unit Tests for Statistics Models",
"priority": "medium", "priority": "medium",
"status": "pending", "status": "in_progress",
"estimated_hours": 3.0, "estimated_hours": 3.0,
"description": "Write comprehensive unit tests for all statistics-related models (PackProgressModel, AchievementModel, StudySessionModel) including toDto/fromDto conversions, serialization, and edge cases.", "description": "Write comprehensive unit tests for all statistics-related models (PackProgressModel, AchievementModel, StudySessionModel) including toDto/fromDto conversions, serialization, and edge cases.",
"acceptance_criteria": [ "acceptance_criteria": [

View file

@ -0,0 +1,590 @@
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'package:test/test.dart';
void main() {
group('AchievementModel', () {
group('constructor', () {
test('creates model with default values', () {
final model = AchievementModel();
expect(model.id, '');
expect(model.title, '');
expect(model.description, '');
expect(model.iconUrl, isNull);
expect(model.unlockedAt, isNull);
expect(model.type, AchievementType.firstWordLearned);
expect(model.progress, 0.0);
});
test('creates model with all parameters', () {
final now = DateTime.now();
final model = AchievementModel(
id: 'test_achievement',
title: 'Test Achievement',
description: 'Test Description',
iconUrl: 'https://example.com/icon.png',
unlockedAt: now,
type: AchievementType.streak7Days,
progress: 0.75,
);
expect(model.id, 'test_achievement');
expect(model.title, 'Test Achievement');
expect(model.description, 'Test Description');
expect(model.iconUrl, 'https://example.com/icon.png');
expect(model.unlockedAt, now);
expect(model.type, AchievementType.streak7Days);
expect(model.progress, 0.75);
});
test('creates model with null iconUrl', () {
final model = AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
);
expect(model.iconUrl, isNull);
});
test('creates model with null unlockedAt', () {
final model = AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
);
expect(model.unlockedAt, isNull);
});
});
group('toDto', () {
test('converts model to DTO correctly', () {
final now = DateTime.now();
final model = AchievementModel(
id: 'test_achievement',
title: 'Test Achievement',
description: 'Test Description',
iconUrl: 'https://example.com/icon.png',
unlockedAt: now,
type: AchievementType.words100Learned,
progress: 0.85,
);
final dto = model.toDto();
expect(dto.id, 'test_achievement');
expect(dto.title, 'Test Achievement');
expect(dto.description, 'Test Description');
expect(dto.iconUrl, 'https://example.com/icon.png');
expect(dto.unlockedAt, now);
expect(dto.type, AchievementType.words100Learned);
expect(dto.progress, 0.85);
});
test('converts model with null values to DTO', () {
final model = AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
);
final dto = model.toDto();
expect(dto.iconUrl, isNull);
expect(dto.unlockedAt, isNull);
});
test('converts all achievement types correctly', () {
for (final type in AchievementType.values) {
final model = AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
type: type,
);
final dto = model.toDto();
expect(dto.type, type);
}
});
});
group('fromDto', () {
test('creates model from DTO correctly', () {
final now = DateTime.now();
final dto = AchievementDto(
id: 'test_achievement',
title: 'Test Achievement',
description: 'Test Description',
iconUrl: 'https://example.com/icon.png',
unlockedAt: now,
type: AchievementType.streak30Days,
progress: 0.9,
);
final model = AchievementModel.fromDto(dto);
expect(model.id, 'test_achievement');
expect(model.title, 'Test Achievement');
expect(model.description, 'Test Description');
expect(model.iconUrl, 'https://example.com/icon.png');
expect(model.unlockedAt, now);
expect(model.type, AchievementType.streak30Days);
expect(model.progress, 0.9);
});
test('creates model from DTO with null values', () {
final dto = AchievementDto(
id: 'test',
title: 'Test',
description: 'Test',
type: AchievementType.firstWordLearned,
);
final model = AchievementModel.fromDto(dto);
expect(model.iconUrl, isNull);
expect(model.unlockedAt, isNull);
});
test('creates model from DTO for all achievement types', () {
for (final type in AchievementType.values) {
final dto = AchievementDto(
id: 'test',
title: 'Test',
description: 'Test',
type: type,
);
final model = AchievementModel.fromDto(dto);
expect(model.type, type);
}
});
});
group('toDto and fromDto roundtrip', () {
test('preserves all data through conversion', () {
final now = DateTime.now();
final original = AchievementModel(
id: 'test_achievement',
title: 'Test Achievement',
description: 'Test Description',
iconUrl: 'https://example.com/icon.png',
unlockedAt: now,
type: AchievementType.perfectTestScore,
progress: 0.95,
);
final dto = original.toDto();
final restored = AchievementModel.fromDto(dto);
expect(restored.id, original.id);
expect(restored.title, original.title);
expect(restored.description, original.description);
expect(restored.iconUrl, original.iconUrl);
expect(restored.unlockedAt, original.unlockedAt);
expect(restored.type, original.type);
expect(restored.progress, original.progress);
});
});
group('isUnlocked', () {
test('returns false when unlockedAt is null', () {
final model = AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
);
expect(model.isUnlocked, isFalse);
});
test('returns true when unlockedAt is set', () {
final model = AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
unlockedAt: DateTime.now(),
);
expect(model.isUnlocked, isTrue);
});
});
group('isLocked', () {
test('returns true when unlockedAt is null', () {
final model = AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
);
expect(model.isLocked, isTrue);
});
test('returns false when unlockedAt is set', () {
final model = AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
unlockedAt: DateTime.now(),
);
expect(model.isLocked, isFalse);
});
});
group('unlock', () {
test('sets unlockedAt to current time', () {
final model = AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
);
final beforeUnlock = DateTime.now();
final unlocked = model.unlock();
final afterUnlock = DateTime.now();
expect(unlocked.unlockedAt, isNotNull);
expect(
unlocked.unlockedAt!.isAfter(beforeUnlock.subtract(const Duration(seconds: 1))),
isTrue,
);
expect(
unlocked.unlockedAt!.isBefore(afterUnlock.add(const Duration(seconds: 1))),
isTrue,
);
});
test('sets progress to 1.0', () {
final model = AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
progress: 0.5,
);
final unlocked = model.unlock();
expect(unlocked.progress, 1.0);
});
test('preserves other fields', () {
final model = AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
iconUrl: 'https://example.com/icon.png',
type: AchievementType.streak7Days,
);
final unlocked = model.unlock();
expect(unlocked.id, model.id);
expect(unlocked.title, model.title);
expect(unlocked.description, model.description);
expect(unlocked.iconUrl, model.iconUrl);
expect(unlocked.type, model.type);
});
});
group('updateProgress', () {
test('updates progress correctly', () {
final model = AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
progress: 0.3,
);
final updated = model.updateProgress(0.7);
expect(updated.progress, 0.7);
});
test('clamps progress to 0.0 minimum', () {
final model = AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
);
final updated = model.updateProgress(-0.5);
expect(updated.progress, 0.0);
});
test('clamps progress to 1.0 maximum', () {
final model = AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
);
final updated = model.updateProgress(1.5);
expect(updated.progress, 1.0);
});
test('preserves unlockedAt when updating progress', () {
final now = DateTime.now();
final model = AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
unlockedAt: now,
);
final updated = model.updateProgress(0.5);
expect(updated.unlockedAt, now);
});
test('preserves other fields', () {
final model = AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
iconUrl: 'https://example.com/icon.png',
type: AchievementType.words50Learned,
);
final updated = model.updateProgress(0.6);
expect(updated.id, model.id);
expect(updated.title, model.title);
expect(updated.description, model.description);
expect(updated.iconUrl, model.iconUrl);
expect(updated.type, model.type);
});
});
group('category', () {
test('returns correct category for first_steps achievements', () {
expect(
AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
type: AchievementType.firstWordLearned,
).category,
'first_steps',
);
expect(
AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
type: AchievementType.firstTestCompleted,
).category,
'first_steps',
);
expect(
AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
type: AchievementType.firstPackCompleted,
).category,
'first_steps',
);
});
test('returns correct category for streaks achievements', () {
expect(
AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
type: AchievementType.streak3Days,
).category,
'streaks',
);
expect(
AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
type: AchievementType.streak100Days,
).category,
'streaks',
);
});
test('returns correct category for words_mastery achievements', () {
expect(
AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
type: AchievementType.words10Learned,
).category,
'words_mastery',
);
expect(
AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
type: AchievementType.words1000Learned,
).category,
'words_mastery',
);
});
test('returns correct category for performance achievements', () {
expect(
AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
type: AchievementType.perfectTestScore,
).category,
'performance',
);
expect(
AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
type: AchievementType.speedLearner,
).category,
'performance',
);
});
test('returns correct category for dedication achievements', () {
expect(
AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
type: AchievementType.dedicatedLearner,
).category,
'dedication',
);
});
test('returns correct category for time_based achievements', () {
expect(
AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
type: AchievementType.nightOwl,
).category,
'time_based',
);
expect(
AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
type: AchievementType.earlyBird,
).category,
'time_based',
);
});
test('returns correct category for special achievements', () {
expect(
AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
type: AchievementType.consistentLearner,
).category,
'special',
);
expect(
AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
type: AchievementType.languageMaster,
).category,
'special',
);
});
});
group('edge cases', () {
test('handles empty strings', () {
final model = AchievementModel(
id: '',
title: '',
description: '',
);
expect(model.id, '');
expect(model.title, '');
expect(model.description, '');
});
test('handles very long strings', () {
final longString = 'a' * 1000;
final model = AchievementModel(
id: longString,
title: longString,
description: longString,
);
expect(model.id, longString);
expect(model.title, longString);
expect(model.description, longString);
});
test('handles progress at boundaries', () {
expect(
AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
progress: 0.0,
).progress,
0.0,
);
expect(
AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
progress: 1.0,
).progress,
1.0,
);
});
test('handles all achievement types', () {
for (final type in AchievementType.values) {
final model = AchievementModel(
id: 'test',
title: 'Test',
description: 'Test',
type: type,
);
expect(model.type, type);
expect(model.category, isNotEmpty);
}
});
});
});
}

View file

@ -0,0 +1,415 @@
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'package:test/test.dart';
void main() {
group('PackProgressModel', () {
group('constructor', () {
test('creates model with default values', () {
final model = PackProgressModel();
expect(model.packId, '');
expect(model.totalCards, 0);
expect(model.learnedCards, 0);
expect(model.studyTimeMinutes, 0);
expect(model.lastStudyDate, isNull);
expect(model.firstStudyDate, isNull);
expect(model.cardAttempts, isEmpty);
expect(model.averageAccuracy, 0.0);
});
test('creates model with all parameters', () {
final now = DateTime.now();
final yesterday = now.subtract(const Duration(days: 1));
final cardAttempts = {'card1': 3, 'card2': 5};
final model = PackProgressModel(
packId: 'test_pack',
totalCards: 20,
learnedCards: 15,
studyTimeMinutes: 120,
lastStudyDate: now,
firstStudyDate: yesterday,
cardAttempts: cardAttempts,
averageAccuracy: 0.85,
);
expect(model.packId, 'test_pack');
expect(model.totalCards, 20);
expect(model.learnedCards, 15);
expect(model.studyTimeMinutes, 120);
expect(model.lastStudyDate, now);
expect(model.firstStudyDate, yesterday);
expect(model.cardAttempts, cardAttempts);
expect(model.averageAccuracy, 0.85);
});
test('creates model with empty cardAttempts map', () {
final model = PackProgressModel(
packId: 'test_pack',
cardAttempts: const {},
);
expect(model.cardAttempts, isEmpty);
});
});
group('toDto', () {
test('converts model to DTO correctly', () {
final now = DateTime.now();
final cardAttempts = {'card1': 2, 'card2': 4};
final model = PackProgressModel(
packId: 'test_pack',
totalCards: 10,
learnedCards: 7,
studyTimeMinutes: 60,
lastStudyDate: now,
firstStudyDate: now,
cardAttempts: cardAttempts,
averageAccuracy: 0.75,
);
final dto = model.toDto();
expect(dto.packId, 'test_pack');
expect(dto.totalCards, 10);
expect(dto.learnedCards, 7);
expect(dto.studyTimeMinutes, 60);
expect(dto.lastStudyDate, now);
expect(dto.firstStudyDate, now);
expect(dto.cardAttempts, cardAttempts);
expect(dto.averageAccuracy, 0.75);
});
test('converts model with null dates to DTO', () {
final model = PackProgressModel(
packId: 'test_pack',
totalCards: 10,
);
final dto = model.toDto();
expect(dto.lastStudyDate, isNull);
expect(dto.firstStudyDate, isNull);
});
test('creates new map instance in DTO', () {
final cardAttempts = {'card1': 1};
final model = PackProgressModel(cardAttempts: cardAttempts);
final dto = model.toDto();
expect(dto.cardAttempts, equals(cardAttempts));
expect(dto.cardAttempts, isNot(same(cardAttempts)));
});
});
group('fromDto', () {
test('creates model from DTO correctly', () {
final now = DateTime.now();
final cardAttempts = {'card1': 3, 'card2': 5};
final dto = PackProgressDto(
packId: 'test_pack',
totalCards: 20,
learnedCards: 15,
studyTimeMinutes: 120,
lastStudyDate: now,
firstStudyDate: now,
cardAttempts: cardAttempts,
averageAccuracy: 0.85,
);
final model = PackProgressModel.fromDto(dto);
expect(model.packId, 'test_pack');
expect(model.totalCards, 20);
expect(model.learnedCards, 15);
expect(model.studyTimeMinutes, 120);
expect(model.lastStudyDate, now);
expect(model.firstStudyDate, now);
expect(model.cardAttempts, cardAttempts);
expect(model.averageAccuracy, 0.85);
});
test('creates model from DTO with null dates', () {
final dto = PackProgressDto(
packId: 'test_pack',
totalCards: 10,
);
final model = PackProgressModel.fromDto(dto);
expect(model.lastStudyDate, isNull);
expect(model.firstStudyDate, isNull);
});
test('creates new map instance from DTO', () {
final cardAttempts = {'card1': 1};
final dto = PackProgressDto(
packId: 'test_pack',
totalCards: 10,
cardAttempts: cardAttempts,
);
final model = PackProgressModel.fromDto(dto);
expect(model.cardAttempts, equals(cardAttempts));
expect(model.cardAttempts, isNot(same(cardAttempts)));
});
});
group('toDto and fromDto roundtrip', () {
test('preserves all data through conversion', () {
final now = DateTime.now();
final cardAttempts = {'card1': 2, 'card2': 4, 'card3': 1};
final original = PackProgressModel(
packId: 'test_pack',
totalCards: 30,
learnedCards: 20,
studyTimeMinutes: 180,
lastStudyDate: now,
firstStudyDate: now.subtract(const Duration(days: 5)),
cardAttempts: cardAttempts,
averageAccuracy: 0.92,
);
final dto = original.toDto();
final restored = PackProgressModel.fromDto(dto);
expect(restored.packId, original.packId);
expect(restored.totalCards, original.totalCards);
expect(restored.learnedCards, original.learnedCards);
expect(restored.studyTimeMinutes, original.studyTimeMinutes);
expect(restored.lastStudyDate, original.lastStudyDate);
expect(restored.firstStudyDate, original.firstStudyDate);
expect(restored.cardAttempts, original.cardAttempts);
expect(restored.averageAccuracy, original.averageAccuracy);
});
});
group('updateCardProgress', () {
test('updates card attempts correctly', () {
final model = PackProgressModel(
packId: 'test_pack',
totalCards: 10,
cardAttempts: {'card1': 2},
);
final updated = model.updateCardProgress(
cardId: 'card1',
wasCorrect: true,
additionalTimeMinutes: 5,
);
expect(updated.cardAttempts['card1'], 3);
});
test('adds new card to attempts map', () {
final model = PackProgressModel(
packId: 'test_pack',
totalCards: 10,
);
final updated = model.updateCardProgress(
cardId: 'new_card',
wasCorrect: true,
additionalTimeMinutes: 5,
);
expect(updated.cardAttempts['new_card'], 1);
});
test('updates study time correctly', () {
final model = PackProgressModel(
packId: 'test_pack',
studyTimeMinutes: 10,
);
final updated = model.updateCardProgress(
cardId: 'card1',
wasCorrect: true,
additionalTimeMinutes: 5,
);
expect(updated.studyTimeMinutes, 15);
});
test('sets lastStudyDate to current time', () {
final model = PackProgressModel(
packId: 'test_pack',
);
final beforeUpdate = DateTime.now();
final updated = model.updateCardProgress(
cardId: 'card1',
wasCorrect: true,
additionalTimeMinutes: 5,
);
final afterUpdate = DateTime.now();
expect(updated.lastStudyDate, isNotNull);
expect(
updated.lastStudyDate!.isAfter(beforeUpdate.subtract(const Duration(seconds: 1))),
isTrue,
);
expect(
updated.lastStudyDate!.isBefore(afterUpdate.add(const Duration(seconds: 1))),
isTrue,
);
});
test('sets firstStudyDate if null', () {
final model = PackProgressModel(
packId: 'test_pack',
);
final updated = model.updateCardProgress(
cardId: 'card1',
wasCorrect: true,
additionalTimeMinutes: 5,
);
expect(updated.firstStudyDate, isNotNull);
});
test('preserves firstStudyDate if already set', () {
final originalDate = DateTime(2024, 1, 1);
final model = PackProgressModel(
packId: 'test_pack',
firstStudyDate: originalDate,
);
final updated = model.updateCardProgress(
cardId: 'card1',
wasCorrect: true,
additionalTimeMinutes: 5,
);
expect(updated.firstStudyDate, originalDate);
});
});
group('hasStarted', () {
test('returns false for new pack', () {
final model = PackProgressModel();
expect(model.hasStarted, isFalse);
});
test('returns true when learnedCards > 0', () {
final model = PackProgressModel(learnedCards: 1);
expect(model.hasStarted, isTrue);
});
test('returns true when studyTimeMinutes > 0', () {
final model = PackProgressModel(studyTimeMinutes: 1);
expect(model.hasStarted, isTrue);
});
test('returns true when both learnedCards and studyTimeMinutes > 0', () {
final model = PackProgressModel(
learnedCards: 5,
studyTimeMinutes: 10,
);
expect(model.hasStarted, isTrue);
});
});
group('progress', () {
test('returns 0.0 when totalCards is 0', () {
final model = PackProgressModel();
expect(model.progress, 0.0);
});
test('returns 0.0 when learnedCards is 0', () {
final model = PackProgressModel(totalCards: 10);
expect(model.progress, 0.0);
});
test('calculates progress correctly', () {
final model = PackProgressModel(
totalCards: 10,
learnedCards: 5,
);
expect(model.progress, 0.5);
});
test('calculates progress for partial completion', () {
final model = PackProgressModel(
totalCards: 100,
learnedCards: 33,
);
expect(model.progress, 0.33);
});
test('returns 1.0 when all cards learned', () {
final model = PackProgressModel(
totalCards: 10,
learnedCards: 10,
);
expect(model.progress, 1.0);
});
test('handles learnedCards exceeding totalCards', () {
final model = PackProgressModel(
totalCards: 10,
learnedCards: 15,
);
expect(model.progress, 1.5);
});
});
group('edge cases', () {
test('handles empty cardAttempts map', () {
final model = PackProgressModel(cardAttempts: const {});
expect(model.cardAttempts, isEmpty);
expect(model.toDto().cardAttempts, isEmpty);
});
test('handles large cardAttempts map', () {
final cardAttempts = Map.fromEntries(
List.generate(100, (i) => MapEntry('card$i', i)),
);
final model = PackProgressModel(cardAttempts: cardAttempts);
expect(model.cardAttempts.length, 100);
expect(model.toDto().cardAttempts.length, 100);
});
test('handles zero averageAccuracy', () {
final model = PackProgressModel(averageAccuracy: 0.0);
expect(model.averageAccuracy, 0.0);
expect(model.toDto().averageAccuracy, 0.0);
});
test('handles maximum averageAccuracy', () {
final model = PackProgressModel(averageAccuracy: 1.0);
expect(model.averageAccuracy, 1.0);
expect(model.toDto().averageAccuracy, 1.0);
});
test('handles very large studyTimeMinutes', () {
final model = PackProgressModel(studyTimeMinutes: 999999);
expect(model.studyTimeMinutes, 999999);
expect(model.toDto().studyTimeMinutes, 999999);
});
});
});
}

View file

@ -0,0 +1,640 @@
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'package:test/test.dart';
void main() {
group('StudySessionModel', () {
group('constructor', () {
test('creates model with required parameters', () {
final startTime = DateTime.now();
final model = StudySessionModel(
userId: 1,
startTime: startTime,
);
expect(model.userId, 1);
expect(model.startTime, startTime);
expect(model.sessionId, isNull);
expect(model.endTime, isNull);
expect(model.wordsLearned, 0);
expect(model.testsCompleted, 0);
expect(model.accuracy, 0.0);
expect(model.packId, isNull);
expect(model.testId, isNull);
});
test('creates model with all parameters', () {
final startTime = DateTime.now();
final endTime = startTime.add(const Duration(minutes: 30));
final model = StudySessionModel(
sessionId: 'session_123',
userId: 42,
startTime: startTime,
endTime: endTime,
wordsLearned: 10,
testsCompleted: 2,
accuracy: 0.85,
packId: 'pack_1',
testId: 'test_1',
);
expect(model.sessionId, 'session_123');
expect(model.userId, 42);
expect(model.startTime, startTime);
expect(model.endTime, endTime);
expect(model.wordsLearned, 10);
expect(model.testsCompleted, 2);
expect(model.accuracy, 0.85);
expect(model.packId, 'pack_1');
expect(model.testId, 'test_1');
});
test('creates model with default values', () {
final startTime = DateTime.now();
final model = StudySessionModel(
userId: 1,
startTime: startTime,
);
expect(model.wordsLearned, 0);
expect(model.testsCompleted, 0);
expect(model.accuracy, 0.0);
});
});
group('toDto', () {
test('converts model to DTO correctly', () {
final startTime = DateTime.now();
final endTime = startTime.add(const Duration(minutes: 30));
final model = StudySessionModel(
sessionId: 'session_123',
userId: 42,
startTime: startTime,
endTime: endTime,
wordsLearned: 10,
testsCompleted: 2,
accuracy: 0.85,
packId: 'pack_1',
testId: 'test_1',
);
final dto = model.toDto();
expect(dto.sessionId, 'session_123');
expect(dto.startTime, startTime);
expect(dto.endTime, endTime);
expect(dto.wordsLearned, 10);
expect(dto.testsCompleted, 2);
expect(dto.accuracy, 0.85);
expect(dto.packId, 'pack_1');
expect(dto.testId, 'test_1');
});
test('converts model with null values to DTO', () {
final startTime = DateTime.now();
final model = StudySessionModel(
userId: 1,
startTime: startTime,
);
final dto = model.toDto();
expect(dto.sessionId, isNull);
expect(dto.endTime, isNull);
expect(dto.packId, isNull);
expect(dto.testId, isNull);
});
});
group('fromDto', () {
test('creates model from DTO correctly', () {
final startTime = DateTime.now();
final endTime = startTime.add(const Duration(minutes: 30));
final dto = StudySessionDto(
sessionId: 'session_123',
startTime: startTime,
endTime: endTime,
wordsLearned: 10,
testsCompleted: 2,
accuracy: 0.85,
packId: 'pack_1',
testId: 'test_1',
);
final model = StudySessionModel.fromDto(dto, 42);
expect(model.sessionId, 'session_123');
expect(model.userId, 42);
expect(model.startTime, startTime);
expect(model.endTime, endTime);
expect(model.wordsLearned, 10);
expect(model.testsCompleted, 2);
expect(model.accuracy, 0.85);
expect(model.packId, 'pack_1');
expect(model.testId, 'test_1');
});
test('creates model from DTO with null values', () {
final startTime = DateTime.now();
final dto = StudySessionDto(
startTime: startTime,
);
final model = StudySessionModel.fromDto(dto, 1);
expect(model.sessionId, isNull);
expect(model.endTime, isNull);
expect(model.packId, isNull);
expect(model.testId, isNull);
});
});
group('toDto and fromDto roundtrip', () {
test('preserves all data through conversion', () {
final startTime = DateTime.now();
final endTime = startTime.add(const Duration(minutes: 45));
final original = StudySessionModel(
sessionId: 'session_456',
userId: 100,
startTime: startTime,
endTime: endTime,
wordsLearned: 25,
testsCompleted: 3,
accuracy: 0.92,
packId: 'pack_2',
testId: 'test_2',
);
final dto = original.toDto();
final restored = StudySessionModel.fromDto(dto, original.userId);
expect(restored.sessionId, original.sessionId);
expect(restored.userId, original.userId);
expect(restored.startTime, original.startTime);
expect(restored.endTime, original.endTime);
expect(restored.wordsLearned, original.wordsLearned);
expect(restored.testsCompleted, original.testsCompleted);
expect(restored.accuracy, original.accuracy);
expect(restored.packId, original.packId);
expect(restored.testId, original.testId);
});
});
group('isActive', () {
test('returns true when endTime is null', () {
final model = StudySessionModel(
userId: 1,
startTime: DateTime.now(),
);
expect(model.isActive, isTrue);
});
test('returns false when endTime is set', () {
final startTime = DateTime.now();
final model = StudySessionModel(
userId: 1,
startTime: startTime,
endTime: startTime.add(const Duration(minutes: 30)),
);
expect(model.isActive, isFalse);
});
});
group('duration', () {
test('calculates duration correctly when ended', () {
final startTime = DateTime.now();
final endTime = startTime.add(const Duration(minutes: 30));
final model = StudySessionModel(
userId: 1,
startTime: startTime,
endTime: endTime,
);
expect(model.duration, const Duration(minutes: 30));
});
test('calculates duration using current time when active', () {
final startTime = DateTime.now().subtract(const Duration(minutes: 15));
final model = StudySessionModel(
userId: 1,
startTime: startTime,
);
final duration = model.duration;
expect(duration.inMinutes, greaterThanOrEqualTo(14));
expect(duration.inMinutes, lessThanOrEqualTo(16));
});
});
group('durationMinutes', () {
test('returns duration in minutes correctly', () {
final startTime = DateTime.now();
final endTime = startTime.add(const Duration(minutes: 45));
final model = StudySessionModel(
userId: 1,
startTime: startTime,
endTime: endTime,
);
expect(model.durationMinutes, 45);
});
test('handles zero duration', () {
final startTime = DateTime.now();
final model = StudySessionModel(
userId: 1,
startTime: startTime,
endTime: startTime,
);
expect(model.durationMinutes, 0);
});
});
group('sessionType', () {
test('returns Test Session when testId is set', () {
final model = StudySessionModel(
userId: 1,
startTime: DateTime.now(),
testId: 'test_1',
);
expect(model.sessionType, 'Test Session');
});
test('returns Pack Study when packId is set', () {
final model = StudySessionModel(
userId: 1,
startTime: DateTime.now(),
packId: 'pack_1',
);
expect(model.sessionType, 'Pack Study');
});
test('returns General Study when neither is set', () {
final model = StudySessionModel(
userId: 1,
startTime: DateTime.now(),
);
expect(model.sessionType, 'General Study');
});
test('prioritizes testId over packId', () {
final model = StudySessionModel(
userId: 1,
startTime: DateTime.now(),
packId: 'pack_1',
testId: 'test_1',
);
expect(model.sessionType, 'Test Session');
});
});
group('productivityScore', () {
test('calculates productivity score correctly', () {
final startTime = DateTime.now();
final endTime = startTime.add(const Duration(minutes: 30));
final model = StudySessionModel(
userId: 1,
startTime: startTime,
endTime: endTime,
wordsLearned: 15,
);
expect(model.productivityScore, 0.5); // 15 words / 30 minutes
});
test('returns 0.0 when duration is 0', () {
final startTime = DateTime.now();
final model = StudySessionModel(
userId: 1,
startTime: startTime,
endTime: startTime,
wordsLearned: 10,
);
expect(model.productivityScore, 0.0);
});
test('handles active session', () {
final startTime = DateTime.now().subtract(const Duration(minutes: 10));
final model = StudySessionModel(
userId: 1,
startTime: startTime,
wordsLearned: 5,
);
final score = model.productivityScore;
expect(score, greaterThan(0));
expect(score, lessThanOrEqualTo(1.0));
});
});
group('isProductive', () {
test('returns true for productive session', () {
final startTime = DateTime.now();
final endTime = startTime.add(const Duration(minutes: 10));
final model = StudySessionModel(
userId: 1,
startTime: startTime,
endTime: endTime,
wordsLearned: 10,
accuracy: 0.8,
);
expect(model.isProductive, isTrue);
});
test('returns false when wordsLearned < 5', () {
final startTime = DateTime.now();
final endTime = startTime.add(const Duration(minutes: 10));
final model = StudySessionModel(
userId: 1,
startTime: startTime,
endTime: endTime,
wordsLearned: 3,
accuracy: 0.8,
);
expect(model.isProductive, isFalse);
});
test('returns false when accuracy < 0.7', () {
final startTime = DateTime.now();
final endTime = startTime.add(const Duration(minutes: 10));
final model = StudySessionModel(
userId: 1,
startTime: startTime,
endTime: endTime,
wordsLearned: 10,
accuracy: 0.6,
);
expect(model.isProductive, isFalse);
});
test('returns false when duration < 5 minutes', () {
final startTime = DateTime.now();
final endTime = startTime.add(const Duration(minutes: 3));
final model = StudySessionModel(
userId: 1,
startTime: startTime,
endTime: endTime,
wordsLearned: 10,
accuracy: 0.8,
);
expect(model.isProductive, isFalse);
});
});
group('end', () {
test('sets endTime to current time', () {
final startTime = DateTime.now().subtract(const Duration(minutes: 10));
final model = StudySessionModel(
userId: 1,
startTime: startTime,
);
final beforeEnd = DateTime.now();
final ended = model.end(
wordsLearned: 10,
testsCompleted: 2,
accuracy: 0.85,
);
final afterEnd = DateTime.now();
expect(ended.endTime, isNotNull);
expect(
ended.endTime!.isAfter(beforeEnd.subtract(const Duration(seconds: 1))),
isTrue,
);
expect(
ended.endTime!.isBefore(afterEnd.add(const Duration(seconds: 1))),
isTrue,
);
});
test('updates statistics correctly', () {
final model = StudySessionModel(
userId: 1,
startTime: DateTime.now(),
);
final ended = model.end(
wordsLearned: 15,
testsCompleted: 3,
accuracy: 0.9,
);
expect(ended.wordsLearned, 15);
expect(ended.testsCompleted, 3);
expect(ended.accuracy, 0.9);
});
test('preserves other fields', () {
final model = StudySessionModel(
sessionId: 'session_123',
userId: 1,
startTime: DateTime.now(),
packId: 'pack_1',
testId: 'test_1',
);
final ended = model.end();
expect(ended.sessionId, model.sessionId);
expect(ended.userId, model.userId);
expect(ended.startTime, model.startTime);
expect(ended.packId, model.packId);
expect(ended.testId, model.testId);
});
});
group('addProgress', () {
test('adds wordsLearned correctly', () {
final model = StudySessionModel(
userId: 1,
startTime: DateTime.now(),
wordsLearned: 5,
);
final updated = model.addProgress(wordsLearned: 3);
expect(updated.wordsLearned, 8);
});
test('adds testsCompleted correctly', () {
final model = StudySessionModel(
userId: 1,
startTime: DateTime.now(),
testsCompleted: 1,
);
final updated = model.addProgress(testsCompleted: 2);
expect(updated.testsCompleted, 3);
});
test('updates accuracy when provided', () {
final model = StudySessionModel(
userId: 1,
startTime: DateTime.now(),
accuracy: 0.5,
);
final updated = model.addProgress(accuracy: 0.8);
expect(updated.accuracy, 0.8);
});
test('preserves accuracy when not provided', () {
final model = StudySessionModel(
userId: 1,
startTime: DateTime.now(),
accuracy: 0.7,
);
final updated = model.addProgress(wordsLearned: 5);
expect(updated.accuracy, 0.7);
});
test('preserves endTime', () {
final startTime = DateTime.now();
final endTime = startTime.add(const Duration(minutes: 30));
final model = StudySessionModel(
userId: 1,
startTime: startTime,
endTime: endTime,
);
final updated = model.addProgress(wordsLearned: 5);
expect(updated.endTime, endTime);
});
test('preserves other fields', () {
final model = StudySessionModel(
sessionId: 'session_123',
userId: 1,
startTime: DateTime.now(),
packId: 'pack_1',
testId: 'test_1',
);
final updated = model.addProgress(wordsLearned: 5);
expect(updated.sessionId, model.sessionId);
expect(updated.userId, model.userId);
expect(updated.startTime, model.startTime);
expect(updated.packId, model.packId);
expect(updated.testId, model.testId);
});
});
group('edge cases', () {
test('handles zero wordsLearned', () {
final model = StudySessionModel(
userId: 1,
startTime: DateTime.now(),
wordsLearned: 0,
);
expect(model.wordsLearned, 0);
expect(model.toDto().wordsLearned, 0);
});
test('handles zero testsCompleted', () {
final model = StudySessionModel(
userId: 1,
startTime: DateTime.now(),
testsCompleted: 0,
);
expect(model.testsCompleted, 0);
expect(model.toDto().testsCompleted, 0);
});
test('handles zero accuracy', () {
final model = StudySessionModel(
userId: 1,
startTime: DateTime.now(),
accuracy: 0.0,
);
expect(model.accuracy, 0.0);
expect(model.toDto().accuracy, 0.0);
});
test('handles maximum accuracy', () {
final model = StudySessionModel(
userId: 1,
startTime: DateTime.now(),
accuracy: 1.0,
);
expect(model.accuracy, 1.0);
expect(model.toDto().accuracy, 1.0);
});
test('handles very long session', () {
final startTime = DateTime.now();
final endTime = startTime.add(const Duration(hours: 5));
final model = StudySessionModel(
userId: 1,
startTime: startTime,
endTime: endTime,
);
expect(model.durationMinutes, 300);
});
test('handles empty sessionId', () {
final model = StudySessionModel(
sessionId: '',
userId: 1,
startTime: DateTime.now(),
);
expect(model.sessionId, '');
});
test('handles very large userId', () {
final model = StudySessionModel(
userId: 999999999,
startTime: DateTime.now(),
);
expect(model.userId, 999999999);
});
});
});
}

View file

@ -75,7 +75,7 @@ class PackProgressModel {
required int additionalTimeMinutes, required int additionalTimeMinutes,
}) { }) {
final currentAttempts = cardAttempts[cardId] ?? 0; final currentAttempts = cardAttempts[cardId] ?? 0;
final newAttempts = {cardId: currentAttempts + 1, ...cardAttempts}; final newAttempts = {...cardAttempts, cardId: currentAttempts + 1};
// Simple accuracy calculation - could be more sophisticated // Simple accuracy calculation - could be more sophisticated
final totalAttempts = newAttempts.values.fold<int>(0, (sum, attempts) => sum + attempts); final totalAttempts = newAttempts.values.fold<int>(0, (sum, attempts) => sum + attempts);