diff --git a/test/data/repositories/pet_repository_test.dart b/test/data/repositories/pet_repository_test.dart new file mode 100644 index 0000000..3cb025a --- /dev/null +++ b/test/data/repositories/pet_repository_test.dart @@ -0,0 +1,153 @@ +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:kittie_mobile/data/db/app_database.dart'; +import 'package:kittie_mobile/data/repositories/pet_repository.dart'; +import 'package:kittie_mobile/data/repositories/user_repository.dart'; +import 'package:kittie_mobile/domain/entities/pet_state_entity.dart'; +import 'package:kittie_mobile/domain/entities/pet_type.dart'; + +AppDatabase _openMemory() => AppDatabase(NativeDatabase.memory()); + +PetStateEntity _makePetState({ + String id = 'pet-1', + String userId = 'user-1', + int streakDays = 0, + int totalTasksDone = 0, + int? energyToday, +}) { + return PetStateEntity( + id: id, + userId: userId, + streakDays: streakDays, + totalTasksDone: totalTasksDone, + energyToday: energyToday, + updatedAt: DateTime(2024, 1, 1), + ); +} + +void main() { + late AppDatabase db; + late PetRepository repo; + + setUp(() async { + db = _openMemory(); + repo = PetRepository(db); + await UserRepository(db).createUser( + id: 'user-1', + deviceId: 'dev-1', + name: 'Test', + petType: PetType.cat, + petName: 'Yoshi', + ); + }); + + tearDown(() async { + await db.close(); + }); + + group('PetRepository', () { + test('should return null when no pet state exists', () async { + final state = await repo.getPetState('user-1'); + expect(state, isNull); + }); + + test('should return inserted pet state when getPetState is called', () async { + await repo.insertPetState(_makePetState()); + + final state = await repo.getPetState('user-1'); + + expect(state, isNotNull); + expect(state!.id, 'pet-1'); + expect(state.userId, 'user-1'); + expect(state.streakDays, 0); + expect(state.totalTasksDone, 0); + }); + + test('should persist totalTasksDone and streakDays when updatePetState is called', () async { + await repo.insertPetState(_makePetState()); + final state = (await repo.getPetState('user-1'))!; + final updated = state.copyWith(totalTasksDone: 5, streakDays: 3); + + await repo.updatePetState(updated); + + final fetched = await repo.getPetState('user-1'); + expect(fetched!.totalTasksDone, 5); + expect(fetched.streakDays, 3); + }); + + test('should set energyToday and energyDate when setEnergyToday is called', () async { + await repo.insertPetState(_makePetState()); + + await repo.setEnergyToday('user-1', 3); + + final state = await repo.getPetState('user-1'); + expect(state!.energyToday, 3); + expect(state.energyDate, isNotNull); + }); + + test('should update energyToday value when setEnergyToday is called again', () async { + await repo.insertPetState(_makePetState(energyToday: 1)); + + await repo.setEnergyToday('user-1', 2); + + final state = await repo.getPetState('user-1'); + expect(state!.energyToday, 2); + }); + + test('should increment streakDays by 1 when incrementStreak is called', () async { + await repo.insertPetState(_makePetState(streakDays: 4)); + + await repo.incrementStreak('user-1'); + + final state = await repo.getPetState('user-1'); + expect(state!.streakDays, 5); + }); + + test('should accumulate streakDays correctly after multiple incrementStreak calls', () async { + await repo.insertPetState(_makePetState()); + + await repo.incrementStreak('user-1'); + await repo.incrementStreak('user-1'); + await repo.incrementStreak('user-1'); + + final state = await repo.getPetState('user-1'); + expect(state!.streakDays, 3); + }); + + test('should emit null when watchPetState is called and no state exists', () async { + final stream = repo.watchPetState('user-1'); + await expectLater(stream, emits(isNull)); + }); + + test('should emit state after insert when watchPetState is used', () async { + final stream = repo.watchPetState('user-1'); + + await repo.insertPetState(_makePetState(streakDays: 7)); + + await expectLater( + stream, + emitsThrough( + predicate( + (s) => s != null && s.streakDays == 7, + ), + ), + ); + }); + + test('should emit updated state after setEnergyToday when watchPetState is used', () async { + await repo.insertPetState(_makePetState()); + final stream = repo.watchPetState('user-1'); + + await repo.setEnergyToday('user-1', 2); + + await expectLater( + stream, + emitsThrough( + predicate( + (s) => s != null && s.energyToday == 2, + ), + ), + ); + }); + }); +} diff --git a/test/data/repositories/task_repository_test.dart b/test/data/repositories/task_repository_test.dart new file mode 100644 index 0000000..ab7012a --- /dev/null +++ b/test/data/repositories/task_repository_test.dart @@ -0,0 +1,198 @@ +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:kittie_mobile/data/db/app_database.dart'; +import 'package:kittie_mobile/data/repositories/task_repository.dart'; +import 'package:kittie_mobile/data/repositories/user_repository.dart'; +import 'package:kittie_mobile/domain/entities/pet_type.dart'; +import 'package:kittie_mobile/domain/entities/task_entity.dart'; +import 'package:kittie_mobile/domain/entities/task_status.dart'; + +AppDatabase _openMemory() => AppDatabase(NativeDatabase.memory()); + +TaskEntity _makeTask({ + String id = 'task-1', + String userId = 'user-1', + String title = 'Test Task', + TaskStatus status = TaskStatus.pending, + int energyLevel = 2, + int sortOrder = 0, +}) { + final now = DateTime(2024, 1, 1); + return TaskEntity( + id: id, + userId: userId, + title: title, + status: status, + energyLevel: energyLevel, + sortOrder: sortOrder, + createdAt: now, + updatedAt: now, + ); +} + +void main() { + late AppDatabase db; + late TaskRepository repo; + + setUp(() async { + db = _openMemory(); + repo = TaskRepository(db); + await UserRepository(db).createUser( + id: 'user-1', + deviceId: 'dev-1', + name: 'Test', + petType: PetType.cat, + petName: 'Yoshi', + ); + }); + + tearDown(() async { + await db.close(); + }); + + group('TaskRepository', () { + test('should return empty list when no tasks exist', () async { + final tasks = await repo.getAllTasks('user-1'); + expect(tasks, isEmpty); + }); + + test('should return inserted task when getAllTasks is called', () async { + await repo.insertTask(_makeTask()); + + final tasks = await repo.getAllTasks('user-1'); + + expect(tasks.length, 1); + expect(tasks.first.id, 'task-1'); + expect(tasks.first.title, 'Test Task'); + }); + + test('should order tasks by sortOrder ascending', () async { + await repo.insertTask(_makeTask(id: 'task-3', sortOrder: 3)); + await repo.insertTask(_makeTask(id: 'task-1', sortOrder: 1)); + await repo.insertTask(_makeTask(id: 'task-2', sortOrder: 2)); + + final tasks = await repo.getAllTasks('user-1'); + + expect(tasks.map((t) => t.id).toList(), ['task-1', 'task-2', 'task-3']); + }); + + test('should return only pending tasks when getTodayTasks is called', () async { + await repo.insertTask(_makeTask(id: 'p1', status: TaskStatus.pending)); + await repo.insertTask(_makeTask(id: 'd1', status: TaskStatus.done)); + await repo.insertTask(_makeTask(id: 'p2', status: TaskStatus.pending)); + await repo.insertTask(_makeTask(id: 'a1', status: TaskStatus.archived)); + + final tasks = await repo.getTodayTasks('user-1'); + + expect(tasks.length, 2); + expect(tasks.every((t) => t.status == TaskStatus.pending), isTrue); + }); + + test('should return task for known id', () async { + await repo.insertTask(_makeTask()); + + final task = await repo.getTaskById('task-1'); + + expect(task, isNotNull); + expect(task!.id, 'task-1'); + }); + + test('should return null for unknown id', () async { + final task = await repo.getTaskById('nonexistent'); + expect(task, isNull); + }); + + test('should persist title and energyLevel when updateTask is called', () async { + await repo.insertTask(_makeTask()); + final task = (await repo.getTaskById('task-1'))!; + final updated = task.copyWith(title: 'Updated Title', energyLevel: 3); + + await repo.updateTask(updated); + + final fetched = await repo.getTaskById('task-1'); + expect(fetched!.title, 'Updated Title'); + expect(fetched.energyLevel, 3); + }); + + test('should remove task when deleteTask is called', () async { + await repo.insertTask(_makeTask()); + + await repo.deleteTask('task-1'); + + final tasks = await repo.getAllTasks('user-1'); + expect(tasks, isEmpty); + }); + + test('should set status to done when completeTask is called', () async { + await repo.insertTask(_makeTask()); + + await repo.completeTask('task-1'); + + final task = await repo.getTaskById('task-1'); + expect(task!.status, TaskStatus.done); + }); + + test('should create TaskCompletion record when completeTask is called', () async { + await repo.insertTask(_makeTask(title: 'Buy milk', energyLevel: 1)); + + await repo.completeTask('task-1'); + + final completions = await db.select(db.taskCompletions).get(); + expect(completions.length, 1); + expect(completions.first.taskId, 'task-1'); + expect(completions.first.title, 'Buy milk'); + expect(completions.first.energyLevel, 1); + }); + + test('should throw ArgumentError when completeTask is called with unknown id', () async { + expect( + () => repo.completeTask('nonexistent'), + throwsA(isA()), + ); + }); + + test('should not include other users tasks in getAllTasks', () async { + await UserRepository(db).createUser( + id: 'user-2', + deviceId: 'dev-2', + name: 'Other', + petType: PetType.dog, + petName: 'Rex', + ); + await repo.insertTask(_makeTask(id: 'task-1', userId: 'user-1')); + await repo.insertTask(_makeTask(id: 'task-2', userId: 'user-2')); + + final tasks = await repo.getAllTasks('user-1'); + + expect(tasks.length, 1); + expect(tasks.first.userId, 'user-1'); + }); + + test('should emit updated list after insert when watchTasks is used', () async { + final stream = repo.watchTasks('user-1'); + + await repo.insertTask(_makeTask()); + + await expectLater( + stream, + emitsThrough( + predicate>( + (list) => list.length == 1 && list.first.id == 'task-1', + ), + ), + ); + }); + + test('should emit empty list after deleteTask when watchTasks is used', () async { + await repo.insertTask(_makeTask()); + final stream = repo.watchTasks('user-1'); + + await repo.deleteTask('task-1'); + + await expectLater( + stream, + emitsThrough(isEmpty), + ); + }); + }); +} diff --git a/test/data/repositories/user_repository_test.dart b/test/data/repositories/user_repository_test.dart new file mode 100644 index 0000000..7e8cf77 --- /dev/null +++ b/test/data/repositories/user_repository_test.dart @@ -0,0 +1,108 @@ +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:kittie_mobile/data/db/app_database.dart'; +import 'package:kittie_mobile/data/repositories/user_repository.dart'; +import 'package:kittie_mobile/domain/entities/pet_type.dart'; + +AppDatabase _openMemory() => AppDatabase(NativeDatabase.memory()); + +void main() { + late AppDatabase db; + late UserRepository repo; + + setUp(() { + db = _openMemory(); + repo = UserRepository(db); + }); + + tearDown(() async { + await db.close(); + }); + + group('UserRepository', () { + test('should return null when no user exists', () async { + final user = await repo.getCurrentUser(); + expect(user, isNull); + }); + + test('should return created user when getCurrentUser is called', () async { + await repo.createUser( + id: 'user-1', + deviceId: 'device-abc', + name: 'Alice', + petType: PetType.cat, + petName: 'Yoshi', + ); + + final user = await repo.getCurrentUser(); + + expect(user, isNotNull); + expect(user!.id, 'user-1'); + expect(user.deviceId, 'device-abc'); + expect(user.name, 'Alice'); + expect(user.petType, PetType.cat); + expect(user.petName, 'Yoshi'); + }); + + test('should map dog petType correctly when creating user', () async { + await repo.createUser( + id: 'u1', + deviceId: 'dev-1', + name: 'Bob', + petType: PetType.dog, + petName: 'Rex', + ); + + final user = await repo.getCurrentUser(); + expect(user!.petType, PetType.dog); + }); + + test('should map capybara petType correctly when creating user', () async { + await repo.createUser( + id: 'u1', + deviceId: 'dev-1', + name: 'Carol', + petType: PetType.capybara, + petName: 'Cappi', + ); + + final user = await repo.getCurrentUser(); + expect(user!.petType, PetType.capybara); + }); + + test('should persist name and petName changes when updateUser is called', () async { + await repo.createUser( + id: 'user-1', + deviceId: 'device-abc', + name: 'Alice', + petType: PetType.dog, + petName: 'Rex', + ); + final created = await repo.getCurrentUser(); + final updated = created!.copyWith(name: 'Bob', petName: 'Buddy'); + + await repo.updateUser(updated); + + final fetched = await repo.getCurrentUser(); + expect(fetched!.name, 'Bob'); + expect(fetched.petName, 'Buddy'); + }); + + test('should persist petType change when updateUser is called', () async { + await repo.createUser( + id: 'user-1', + deviceId: 'device-abc', + name: 'Alice', + petType: PetType.cat, + petName: 'Yoshi', + ); + final created = await repo.getCurrentUser(); + final updated = created!.copyWith(petType: PetType.capybara); + + await repo.updateUser(updated); + + final fetched = await repo.getCurrentUser(); + expect(fetched!.petType, PetType.capybara); + }); + }); +}