From d4bf238f501bf47aa5db1cbeed0c88c2d3afc725 Mon Sep 17 00:00:00 2001 From: Jean Jacques Avril Date: Sat, 4 Jan 2025 16:25:00 +0000 Subject: [PATCH] tests: tests for dart repositories --- .../repository/project_repository_impl.dart | 5 + backend-dart/lib/domain/entities/user.dart | 4 +- .../lib/domain/entities/user.freezed.dart | 52 ++-- .../domain/repository/project_repository.dart | 3 + .../data/mapper/user_dbo_mapper.dart | 4 +- backend-dart/pubspec.lock | 12 +- backend-dart/pubspec.yaml | 2 +- backend-dart/test/mocks/mock_database.dart | 45 ++++ .../test/mocks/mock_project_data_source.dart | 137 ++++++++++ .../mocks/mock_project_task_data_source.dart | 135 ++++++++++ .../mocks/mock_time_entry_data_source.dart | 156 ++++++++++++ .../test/mocks/mock_user_data_source.dart | 144 +++++++++++ .../test/project_repository_tests.dart | 199 +++++++++++++++ .../test/project_task_repository_tests.dart | 191 ++++++++++++++ .../test/time_entries_repository_tests.dart | 239 ++++++++++++++++++ backend-dart/test/user_repository_tests.dart | 202 +++++++++++++++ 16 files changed, 1493 insertions(+), 37 deletions(-) create mode 100644 backend-dart/test/mocks/mock_database.dart create mode 100644 backend-dart/test/mocks/mock_project_data_source.dart create mode 100644 backend-dart/test/mocks/mock_project_task_data_source.dart create mode 100644 backend-dart/test/mocks/mock_time_entry_data_source.dart create mode 100644 backend-dart/test/mocks/mock_user_data_source.dart create mode 100644 backend-dart/test/project_repository_tests.dart create mode 100644 backend-dart/test/project_task_repository_tests.dart create mode 100644 backend-dart/test/time_entries_repository_tests.dart create mode 100644 backend-dart/test/user_repository_tests.dart diff --git a/backend-dart/lib/application/repository/project_repository_impl.dart b/backend-dart/lib/application/repository/project_repository_impl.dart index 9e49e2b..40bf4fd 100644 --- a/backend-dart/lib/application/repository/project_repository_impl.dart +++ b/backend-dart/lib/application/repository/project_repository_impl.dart @@ -35,4 +35,9 @@ class ProjectRepositoryImpl implements ProjectRepository { TaskEither> findAll() { return database.projects.findAll(); } + + @override + TaskEither> findByUserId(String userId) { + return database.projects.findByUserId(userId); + } } diff --git a/backend-dart/lib/domain/entities/user.dart b/backend-dart/lib/domain/entities/user.dart index 2dd0537..fb37ce5 100755 --- a/backend-dart/lib/domain/entities/user.dart +++ b/backend-dart/lib/domain/entities/user.dart @@ -9,8 +9,8 @@ class User with _$User { required String name, required String email, String? passwordHash, - DateTime? createdAt, - DateTime? updatedAt, + required DateTime createdAt, + required DateTime updatedAt, }) = _User; } diff --git a/backend-dart/lib/domain/entities/user.freezed.dart b/backend-dart/lib/domain/entities/user.freezed.dart index 80e141f..c13340b 100644 --- a/backend-dart/lib/domain/entities/user.freezed.dart +++ b/backend-dart/lib/domain/entities/user.freezed.dart @@ -20,8 +20,8 @@ mixin _$User { String get name => throw _privateConstructorUsedError; String get email => throw _privateConstructorUsedError; String? get passwordHash => throw _privateConstructorUsedError; - DateTime? get createdAt => throw _privateConstructorUsedError; - DateTime? get updatedAt => throw _privateConstructorUsedError; + DateTime get createdAt => throw _privateConstructorUsedError; + DateTime get updatedAt => throw _privateConstructorUsedError; /// Create a copy of User /// with the given fields replaced by the non-null parameter values. @@ -39,8 +39,8 @@ abstract class $UserCopyWith<$Res> { String name, String email, String? passwordHash, - DateTime? createdAt, - DateTime? updatedAt}); + DateTime createdAt, + DateTime updatedAt}); } /// @nodoc @@ -62,8 +62,8 @@ class _$UserCopyWithImpl<$Res, $Val extends User> Object? name = null, Object? email = null, Object? passwordHash = freezed, - Object? createdAt = freezed, - Object? updatedAt = freezed, + Object? createdAt = null, + Object? updatedAt = null, }) { return _then(_value.copyWith( id: null == id @@ -82,14 +82,14 @@ class _$UserCopyWithImpl<$Res, $Val extends User> ? _value.passwordHash : passwordHash // ignore: cast_nullable_to_non_nullable as String?, - createdAt: freezed == createdAt + createdAt: null == createdAt ? _value.createdAt : createdAt // ignore: cast_nullable_to_non_nullable - as DateTime?, - updatedAt: freezed == updatedAt + as DateTime, + updatedAt: null == updatedAt ? _value.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable - as DateTime?, + as DateTime, ) as $Val); } } @@ -106,8 +106,8 @@ abstract class _$$UserImplCopyWith<$Res> implements $UserCopyWith<$Res> { String name, String email, String? passwordHash, - DateTime? createdAt, - DateTime? updatedAt}); + DateTime createdAt, + DateTime updatedAt}); } /// @nodoc @@ -126,8 +126,8 @@ class __$$UserImplCopyWithImpl<$Res> Object? name = null, Object? email = null, Object? passwordHash = freezed, - Object? createdAt = freezed, - Object? updatedAt = freezed, + Object? createdAt = null, + Object? updatedAt = null, }) { return _then(_$UserImpl( id: null == id @@ -146,14 +146,14 @@ class __$$UserImplCopyWithImpl<$Res> ? _value.passwordHash : passwordHash // ignore: cast_nullable_to_non_nullable as String?, - createdAt: freezed == createdAt + createdAt: null == createdAt ? _value.createdAt : createdAt // ignore: cast_nullable_to_non_nullable - as DateTime?, - updatedAt: freezed == updatedAt + as DateTime, + updatedAt: null == updatedAt ? _value.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable - as DateTime?, + as DateTime, )); } } @@ -166,8 +166,8 @@ class _$UserImpl implements _User { required this.name, required this.email, this.passwordHash, - this.createdAt, - this.updatedAt}); + required this.createdAt, + required this.updatedAt}); @override final String id; @@ -178,9 +178,9 @@ class _$UserImpl implements _User { @override final String? passwordHash; @override - final DateTime? createdAt; + final DateTime createdAt; @override - final DateTime? updatedAt; + final DateTime updatedAt; @override String toString() { @@ -222,8 +222,8 @@ abstract class _User implements User { required final String name, required final String email, final String? passwordHash, - final DateTime? createdAt, - final DateTime? updatedAt}) = _$UserImpl; + required final DateTime createdAt, + required final DateTime updatedAt}) = _$UserImpl; @override String get id; @@ -234,9 +234,9 @@ abstract class _User implements User { @override String? get passwordHash; @override - DateTime? get createdAt; + DateTime get createdAt; @override - DateTime? get updatedAt; + DateTime get updatedAt; /// Create a copy of User /// with the given fields replaced by the non-null parameter values. diff --git a/backend-dart/lib/domain/repository/project_repository.dart b/backend-dart/lib/domain/repository/project_repository.dart index 22e6236..a1149e9 100644 --- a/backend-dart/lib/domain/repository/project_repository.dart +++ b/backend-dart/lib/domain/repository/project_repository.dart @@ -9,6 +9,9 @@ abstract class ProjectRepository { /// Finds a project by its unique ID. TaskEither findById(String id); + // Finds all projects by a user's unique ID. + TaskEither> findByUserId(String userId); + /// Updates an existing project. TaskEither update(ProjectUpdate project); diff --git a/backend-dart/lib/infrastructure/data/mapper/user_dbo_mapper.dart b/backend-dart/lib/infrastructure/data/mapper/user_dbo_mapper.dart index 482817b..a68e717 100644 --- a/backend-dart/lib/infrastructure/data/mapper/user_dbo_mapper.dart +++ b/backend-dart/lib/infrastructure/data/mapper/user_dbo_mapper.dart @@ -13,8 +13,8 @@ class UserDboMapper { name: target.name!, email: target.email!, passwordHash: target.password, - createdAt: target.createdAt, - updatedAt: target.updatedAt, + createdAt: target.createdAt!, + updatedAt: target.updatedAt!, )); TaskEither> listFrom(Iterable targets) { diff --git a/backend-dart/pubspec.lock b/backend-dart/pubspec.lock index b5b2fc5..ad7d6de 100755 --- a/backend-dart/pubspec.lock +++ b/backend-dart/pubspec.lock @@ -786,26 +786,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" + sha256: "8391fbe68d520daf2314121764d38e37f934c02fd7301ad18307bd93bd6b725d" url: "https://pub.dev" source: hosted - version: "1.25.8" + version: "1.25.14" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.4" test_core: dependency: transitive description: name: test_core - sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" + sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.8" timing: dependency: transitive description: diff --git a/backend-dart/pubspec.yaml b/backend-dart/pubspec.yaml index 973101c..d8e3036 100755 --- a/backend-dart/pubspec.yaml +++ b/backend-dart/pubspec.yaml @@ -26,7 +26,7 @@ dependencies: dev_dependencies: lints: ^3.0.0 - test: ^1.24.0 + test: ^1.25.14 shelf_router_generator: ^1.1.0 build_runner: ^2.4.14 riverpod_generator: ^2.6.3 diff --git a/backend-dart/test/mocks/mock_database.dart b/backend-dart/test/mocks/mock_database.dart new file mode 100644 index 0000000..fcf8675 --- /dev/null +++ b/backend-dart/test/mocks/mock_database.dart @@ -0,0 +1,45 @@ +import 'package:backend_dart/domain/data/database.dart'; +import 'package:backend_dart/domain/data/project_data_source.dart'; +import 'package:backend_dart/domain/data/project_task_data_source.dart'; +import 'package:backend_dart/domain/data/time_entry_data_source.dart'; +import 'package:backend_dart/domain/data/user_data_source.dart'; +import 'package:backend_dart/domain/entities/project.dart'; +import 'package:backend_dart/domain/entities/project_task.dart'; +import 'package:backend_dart/domain/entities/time_entry.dart'; +import 'package:backend_dart/domain/entities/user.dart'; + +import 'mock_project_data_source.dart'; +import 'mock_project_task_data_source.dart'; +import 'mock_time_entry_data_source.dart'; +import 'mock_user_data_source.dart'; + +class MockDatabase implements IDatabase { + final Map _usersStore = {}; + final Map _tasksStore = {}; + final Map _projectsStore = {}; + final Map _timeEntriesStore = {}; + @override + Future close() { + throw UnimplementedError(); + } + + @override + ProjectDataSource get projects => MockProjectDataSource(_projectsStore); + + @override + ProjectTaskDataSource get tasks => MockProjectTaskDataSource(_tasksStore); + + @override + TimeEntryDataSource get timeEntries => + MockTimeEntryDataSource(_timeEntriesStore); + + @override + UserDataSource get users => MockUserDataSource(_usersStore); + + void clear() { + _usersStore.clear(); + _tasksStore.clear(); + _projectsStore.clear(); + _timeEntriesStore.clear(); + } +} diff --git a/backend-dart/test/mocks/mock_project_data_source.dart b/backend-dart/test/mocks/mock_project_data_source.dart new file mode 100644 index 0000000..5ac55ca --- /dev/null +++ b/backend-dart/test/mocks/mock_project_data_source.dart @@ -0,0 +1,137 @@ +import 'package:backend_dart/domain/data/project_data_source.dart'; +import 'package:backend_dart/domain/entities/project.dart'; +import 'package:backend_dart/domain/errors/app_error.dart'; +import 'package:backend_dart/domain/errors/error.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:uuid/uuid.dart'; + +class MockProjectDataSource implements ProjectDataSource { + MockProjectDataSource(this._store); + final Map _store; + final Uuid _uuid = Uuid(); + + @override + TaskEither create(ProjectCreate project) { + return TaskEither.tryCatch( + () async { + final id = _uuid.v4(); + final newProject = Project( + id: id, + userId: project.userId, + name: project.name, + description: project.description, + clientId: project.clientId, + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + ); + _store[id] = newProject; + return newProject; + }, + (error, _) => AppError.databaseError( + message: 'Failed to create project: ${error.toString()}', + ), + ); + } + + @override + TaskEither findById(String id) { + return TaskEither.tryCatch( + () async { + final project = _store[id]; + if (project == null) { + throw AppError.notFound('Project with ID $id not found'); + } + return project; + }, + (error, _) => error is AppError + ? error + : AppError.databaseError( + message: 'Failed to find project by ID: ${error.toString()}', + ), + ); + } + + @override + TaskEither> findByUserId(String userId) { + return TaskEither.tryCatch( + () async { + final projects = + _store.values.where((project) => project.userId == userId).toList(); + return projects; + }, + (error, _) => AppError.databaseError( + message: + 'Failed to fetch projects for user $userId: ${error.toString()}', + ), + ); + } + + @override + TaskEither update(ProjectUpdate project) { + return TaskEither.tryCatch( + () async { + final existingProject = _store[project.id]; + if (existingProject == null) { + throw AppError.notFound('Project with ID ${project.id} not found'); + } + final updatedProject = existingProject.copyWith( + name: project.name ?? existingProject.name, + description: project.description ?? existingProject.description, + clientId: project.clientId ?? existingProject.clientId, + updatedAt: DateTime.now(), + ); + _store[project.id] = updatedProject; + return updatedProject; + }, + (error, _) => error is AppError + ? error + : AppError.databaseError( + message: 'Failed to update project: ${error.toString()}', + ), + ); + } + + @override + TaskEither delete(String id) { + return TaskEither.tryCatch( + () async { + final project = _store.remove(id); + if (project == null) { + throw AppError.notFound('Project with ID $id not found'); + } + return project; + }, + (error, _) => error is AppError + ? error + : AppError.databaseError( + message: 'Failed to delete project: ${error.toString()}', + ), + ); + } + + @override + TaskEither> findAll() { + return TaskEither.tryCatch( + () async => _store.values.toList(), + (error, _) => AppError.databaseError( + message: 'Failed to fetch all projects: ${error.toString()}', + ), + ); + } + + @override + TaskEither generateId() { + return TaskEither.tryCatch( + () async { + String id; + do { + id = _uuid.v4(); + } while (_store.containsKey(id)); + return id; + }, + (error, _) => AppError.databaseError( + message: 'Failed to generate ID: ${error.toString()}', + ), + ); + } +} diff --git a/backend-dart/test/mocks/mock_project_task_data_source.dart b/backend-dart/test/mocks/mock_project_task_data_source.dart new file mode 100644 index 0000000..acb45a7 --- /dev/null +++ b/backend-dart/test/mocks/mock_project_task_data_source.dart @@ -0,0 +1,135 @@ +import 'package:backend_dart/domain/data/project_task_data_source.dart'; +import 'package:backend_dart/domain/entities/project_task.dart'; +import 'package:backend_dart/domain/errors/app_error.dart'; +import 'package:backend_dart/domain/errors/error.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:uuid/uuid.dart'; + +class MockProjectTaskDataSource implements ProjectTaskDataSource { + MockProjectTaskDataSource(this._store); + final Map _store; + final Uuid _uuid = Uuid(); + + @override + TaskEither create(ProjectTaskCreate task) { + return TaskEither.tryCatch( + () async { + final id = _uuid.v4(); + final newTask = ProjectTask( + id: id, + projectId: task.projectId, + name: task.name, + description: task.description, + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + ); + _store[id] = newTask; + return newTask; + }, + (error, _) => AppError.databaseError( + message: 'Failed to create project task: ${error.toString()}', + ), + ); + } + + @override + TaskEither findById(String id) { + return TaskEither.tryCatch( + () async { + final task = _store[id]; + if (task == null) { + throw AppError.notFound('Project task with ID $id not found'); + } + return task; + }, + (error, _) => error is AppError + ? error + : AppError.databaseError( + message: 'Failed to find project task by ID: ${error.toString()}', + ), + ); + } + + @override + TaskEither> findByProjectId(String projectId) { + return TaskEither.tryCatch( + () async { + final tasks = + _store.values.where((task) => task.projectId == projectId).toList(); + return tasks; + }, + (error, _) => AppError.databaseError( + message: + 'Failed to fetch tasks for project $projectId: ${error.toString()}', + ), + ); + } + + @override + TaskEither update(ProjectTaskUpdate task) { + return TaskEither.tryCatch( + () async { + final existingTask = _store[task.id]; + if (existingTask == null) { + throw AppError.notFound('Project task with ID ${task.id} not found'); + } + final updatedTask = existingTask.copyWith( + name: task.name ?? existingTask.name, + description: task.description ?? existingTask.description, + updatedAt: DateTime.now(), + ); + _store[task.id] = updatedTask; + return updatedTask; + }, + (error, _) => error is AppError + ? error + : AppError.databaseError( + message: 'Failed to update project task: ${error.toString()}', + ), + ); + } + + @override + TaskEither delete(String id) { + return TaskEither.tryCatch( + () async { + final task = _store.remove(id); + if (task == null) { + throw AppError.notFound('Project task with ID $id not found'); + } + return task; + }, + (error, _) => error is AppError + ? error + : AppError.databaseError( + message: 'Failed to delete project task: ${error.toString()}', + ), + ); + } + + @override + TaskEither> findAll() { + return TaskEither.tryCatch( + () async => _store.values.toList(), + (error, _) => AppError.databaseError( + message: 'Failed to fetch all project tasks: ${error.toString()}', + ), + ); + } + + @override + TaskEither generateId() { + return TaskEither.tryCatch( + () async { + String id; + do { + id = _uuid.v4(); + } while (_store.containsKey(id)); + return id; + }, + (error, _) => AppError.databaseError( + message: 'Failed to generate ID: ${error.toString()}', + ), + ); + } +} diff --git a/backend-dart/test/mocks/mock_time_entry_data_source.dart b/backend-dart/test/mocks/mock_time_entry_data_source.dart new file mode 100644 index 0000000..a016ac0 --- /dev/null +++ b/backend-dart/test/mocks/mock_time_entry_data_source.dart @@ -0,0 +1,156 @@ +import 'package:backend_dart/domain/data/time_entry_data_source.dart'; +import 'package:backend_dart/domain/entities/time_entry.dart'; +import 'package:backend_dart/domain/errors/app_error.dart'; +import 'package:backend_dart/domain/errors/error.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:uuid/uuid.dart'; + +class MockTimeEntryDataSource implements TimeEntryDataSource { + MockTimeEntryDataSource(this._store); + final Map _store; + final Uuid _uuid = Uuid(); + + @override + TaskEither create(TimeEntryCreate timeEntry) { + return TaskEither.tryCatch( + () async { + final id = _uuid.v4(); + final newEntry = TimeEntry( + id: id, + userId: timeEntry.userId, + projectId: timeEntry.projectId, + startTime: timeEntry.startTime, + endTime: timeEntry.endTime, + description: timeEntry.description, + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + ); + _store[id] = newEntry; + return newEntry; + }, + (error, _) => AppError.databaseError( + message: 'Failed to create time entry: ${error.toString()}', + ), + ); + } + + @override + TaskEither findById(String id) { + return TaskEither.tryCatch( + () async { + final entry = _store[id]; + if (entry == null) { + throw AppError.notFound('Time entry with ID $id not found'); + } + return entry; + }, + (error, _) => error is AppError + ? error + : AppError.databaseError( + message: 'Failed to find time entry by ID: ${error.toString()}', + ), + ); + } + + @override + TaskEither> findByUserId(String userId) { + return TaskEither.tryCatch( + () async { + final entries = + _store.values.where((entry) => entry.userId == userId).toList(); + return entries; + }, + (error, _) => AppError.databaseError( + message: + 'Failed to fetch time entries for user $userId: ${error.toString()}', + ), + ); + } + + @override + TaskEither> findByProjectId(String projectId) { + return TaskEither.tryCatch( + () async { + final entries = _store.values + .where((entry) => entry.projectId == projectId) + .toList(); + return entries; + }, + (error, _) => AppError.databaseError( + message: + 'Failed to fetch time entries for project $projectId: ${error.toString()}', + ), + ); + } + + @override + TaskEither update(TimeEntryUpdate timeEntry) { + return TaskEither.tryCatch( + () async { + final existingEntry = _store[timeEntry.id]; + if (existingEntry == null) { + throw AppError.notFound( + 'Time entry with ID ${timeEntry.id} not found'); + } + final updatedEntry = existingEntry.copyWith( + startTime: timeEntry.startTime ?? existingEntry.startTime, + endTime: timeEntry.endTime ?? existingEntry.endTime, + description: timeEntry.description ?? existingEntry.description, + projectId: timeEntry.projectId ?? existingEntry.projectId, + updatedAt: DateTime.now(), + ); + _store[timeEntry.id] = updatedEntry; + return updatedEntry; + }, + (error, _) => error is AppError + ? error + : AppError.databaseError( + message: 'Failed to update time entry: ${error.toString()}', + ), + ); + } + + @override + TaskEither delete(String id) { + return TaskEither.tryCatch( + () async { + final entry = _store.remove(id); + if (entry == null) { + throw AppError.notFound('Time entry with ID $id not found'); + } + return entry; + }, + (error, _) => error is AppError + ? error + : AppError.databaseError( + message: 'Failed to delete time entry: ${error.toString()}', + ), + ); + } + + @override + TaskEither> findAll() { + return TaskEither.tryCatch( + () async => _store.values.toList(), + (error, _) => AppError.databaseError( + message: 'Failed to fetch all time entries: ${error.toString()}', + ), + ); + } + + @override + TaskEither generateId() { + return TaskEither.tryCatch( + () async { + String id; + do { + id = _uuid.v4(); + } while (_store.containsKey(id)); + return id; + }, + (error, _) => AppError.databaseError( + message: 'Failed to generate ID: ${error.toString()}', + ), + ); + } +} diff --git a/backend-dart/test/mocks/mock_user_data_source.dart b/backend-dart/test/mocks/mock_user_data_source.dart new file mode 100644 index 0000000..d39a464 --- /dev/null +++ b/backend-dart/test/mocks/mock_user_data_source.dart @@ -0,0 +1,144 @@ +import 'package:backend_dart/common/extensions.dart'; +import 'package:backend_dart/common/secure_hash.dart'; +import 'package:backend_dart/domain/data/user_data_source.dart'; +import 'package:backend_dart/domain/entities/user.dart'; +import 'package:backend_dart/domain/errors/app_error.dart'; +import 'package:backend_dart/domain/errors/error.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:uuid/uuid.dart'; + +class MockUserDataSource implements UserDataSource { + MockUserDataSource(this.store); + final Map store; + final Uuid _uuid = Uuid(); + + @override + TaskEither create(UserCreate user) { + return TaskEither.tryCatch( + () async { + final id = _uuid.v4(); + final newUser = User( + id: id, + email: user.email, + passwordHash: generateSecureHash(user.password), + name: user.name, + createdAt: DateTime.now(), + updatedAt: DateTime.now()); + store[id] = newUser; + return newUser; + }, + (error, _) => AppError.databaseError( + message: 'Failed to create user: ${error.toString()}', + ), + ); + } + + @override + TaskEither findByEmail(String email) { + return TaskEither.tryCatch( + () async { + final user = store.values + .where( + (user) => user.email == email, + ) + .firstOrNull; + if (user == null) { + throw AppError.notFound('User with email $email not found'); + } + return user; + }, + (error, _) => error is AppError + ? error + : AppError.databaseError( + message: 'Failed to find user by email: ${error.toString()}', + ), + ); + } + + @override + TaskEither findById(String id) { + return TaskEither.tryCatch( + () async { + final user = store[id]; + if (user == null) { + throw AppError.notFound('User with ID $id not found'); + } + return user; + }, + (error, _) => error is AppError + ? error + : AppError.databaseError( + message: 'Failed to find user by ID: ${error.toString()}', + ), + ); + } + + @override + TaskEither update(UserUpdate user) { + return TaskEither.tryCatch( + () async { + final existingUser = store[user.id]; + if (existingUser == null) { + throw AppError.notFound('User with ID ${user.id} not found'); + } + final updatedUser = existingUser.copyWith( + email: user.email ?? existingUser.email, + passwordHash: user.password.let(generateSecureHash) ?? + existingUser.passwordHash, + name: user.name ?? existingUser.name, + ); + store[user.id] = updatedUser; + return updatedUser; + }, + (error, _) => error is AppError + ? error + : AppError.databaseError( + message: 'Failed to update user: ${error.toString()}', + ), + ); + } + + @override + TaskEither delete(String id) { + return TaskEither.tryCatch( + () async { + final user = store.remove(id); + if (user == null) { + throw AppError.notFound('User with ID $id not found'); + } + return user; + }, + (error, _) => error is AppError + ? error + : AppError.databaseError( + message: 'Failed to delete user: ${error.toString()}', + ), + ); + } + + @override + TaskEither> findAll() { + return TaskEither.tryCatch( + () async => store.values.toList(), + (error, _) => AppError.databaseError( + message: 'Failed to fetch all users: ${error.toString()}', + ), + ); + } + + @override + TaskEither generateId() { + return TaskEither.tryCatch( + () async { + String id; + do { + id = _uuid.v4(); + } while (store.containsKey(id)); + return id; + }, + (error, _) => AppError.databaseError( + message: 'Failed to generate ID: ${error.toString()}', + ), + ); + } +} diff --git a/backend-dart/test/project_repository_tests.dart b/backend-dart/test/project_repository_tests.dart new file mode 100644 index 0000000..fd77f44 --- /dev/null +++ b/backend-dart/test/project_repository_tests.dart @@ -0,0 +1,199 @@ +import 'package:backend_dart/application/repository/project_repository_impl.dart'; +import 'package:backend_dart/domain/entities/project.dart'; +import 'package:backend_dart/domain/repository/project_repository.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:test/test.dart'; + +import 'mocks/mock_database.dart'; + +void main() { + late MockDatabase database; + late ProjectRepository projectRepository; + + setUpAll(() { + database = MockDatabase(); + projectRepository = ProjectRepositoryImpl(database); + }); + + setUp(() { + database.clear(); // Clear the database before each test + }); + + test('create project', () async { + final projectCreate = ProjectCreate( + userId: 'user123', + name: 'Test Project', + description: 'This is a test project', + clientId: 'client123', + ); + + final project = await projectRepository.create(projectCreate).run(); + expect(project.isRight(), true, reason: 'Result should be right'); + + project.match( + (_) => fail('Result should be right'), + (project) { + expect(project.userId, projectCreate.userId, + reason: 'User ID should be ${projectCreate.userId}'); + expect(project.name, projectCreate.name, + reason: 'Name should be ${projectCreate.name}'); + expect(project.description, projectCreate.description, + reason: 'Description should be ${projectCreate.description}'); + expect(project.clientId, projectCreate.clientId, + reason: 'Client ID should be ${projectCreate.clientId}'); + }, + ); + }); + + test('find project by id', () async { + final projectCreate = ProjectCreate( + userId: 'user123', + name: 'Test Project', + description: 'This is a test project', + clientId: 'client123', + ); + + final createdProject = await projectRepository.create(projectCreate).run(); + + createdProject.match( + (_) => fail('Result should be right'), + (project) async { + final foundProject = await projectRepository.findById(project.id).run(); + + foundProject.match( + (_) => fail('Result should be right'), + (project) { + expect(project.name, projectCreate.name, + reason: 'Name should be ${projectCreate.name}'); + }, + ); + }, + ); + }); + + test('find projects by user id', () async { + final project1 = ProjectCreate( + userId: 'user123', + name: 'Project 1', + description: 'Description 1', + clientId: 'client123', + ); + final project2 = ProjectCreate( + userId: 'user123', + name: 'Project 2', + description: 'Description 2', + clientId: 'client123', + ); + + await projectRepository.create(project1).run(); + await projectRepository.create(project2).run(); + + final projects = await projectRepository.findByUserId('user123').run(); + + projects.match( + (_) => fail('Result should be right'), + (projects) { + expect(projects.length, 2, reason: 'Should return two projects'); + expect( + projects.map((p) => p.name).toList(), + containsAll([project1.name, project2.name]), + ); + }, + ); + }); + + test('update project', () async { + final projectCreate = ProjectCreate( + userId: 'user123', + name: 'Old Name', + description: 'Old Description', + clientId: 'client123', + ); + + final createdProject = await projectRepository.create(projectCreate).run(); + + createdProject.match( + (_) => fail('Result should be right'), + (project) async { + final projectUpdate = ProjectUpdate( + id: project.id, + name: 'Updated Name', + description: 'Updated Description', + ); + + final updatedProject = + await projectRepository.update(projectUpdate).run(); + + updatedProject.match( + (_) => fail('Result should be right'), + (project) { + expect(project.name, 'Updated Name', + reason: 'Name should be Updated Name'); + expect(project.description, 'Updated Description', + reason: 'Description should be Updated Description'); + }, + ); + }, + ); + }); + + test('delete project', () async { + final projectCreate = ProjectCreate( + userId: 'user123', + name: 'Test Project', + description: 'This is a test project', + clientId: 'client123', + ); + + final createdProject = await projectRepository.create(projectCreate).run(); + + createdProject.match( + (_) => fail('Result should be right'), + (project) async { + final deletedProject = await projectRepository.delete(project.id).run(); + + deletedProject.match( + (_) => fail('Result should be right'), + (project) { + expect(project.id, createdProject.match((e) => null, identity)?.id, + reason: 'Deleted project ID should match created project ID'); + }, + ); + + final result = await projectRepository.findById(project.id).run(); + expect(result.isLeft(), true, reason: 'Project should no longer exist'); + }, + ); + }); + + test('find all projects', () async { + final project1 = ProjectCreate( + userId: 'user123', + name: 'Project 1', + description: 'Description 1', + clientId: 'client123', + ); + final project2 = ProjectCreate( + userId: 'user456', + name: 'Project 2', + description: 'Description 2', + clientId: 'client123', + ); + + await projectRepository.create(project1).run(); + await projectRepository.create(project2).run(); + + final projects = await projectRepository.findAll().run(); + + projects.match( + (_) => fail('Result should be right'), + (projects) { + expect(projects.length, 2, reason: 'Should return all projects'); + expect( + projects.map((p) => p.name).toList(), + containsAll([project1.name, project2.name]), + ); + }, + ); + }); +} diff --git a/backend-dart/test/project_task_repository_tests.dart b/backend-dart/test/project_task_repository_tests.dart new file mode 100644 index 0000000..3f655b9 --- /dev/null +++ b/backend-dart/test/project_task_repository_tests.dart @@ -0,0 +1,191 @@ +import 'package:backend_dart/application/repository/project_task_repository_impl.dart'; +import 'package:backend_dart/domain/entities/project_task.dart'; +import 'package:backend_dart/domain/repository/project_task_repository.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:test/test.dart'; + +import 'mocks/mock_database.dart'; + +void main() { + late MockDatabase database; + late ProjectTaskRepository projectTaskRepository; + + setUpAll(() { + database = MockDatabase(); + projectTaskRepository = ProjectTaskRepositoryImpl(database); + }); + + setUp(() { + database.clear(); // Clear the database before each test + }); + + test('create project task', () async { + final projectTaskCreate = ProjectTaskCreate( + projectId: 'project123', + name: 'Test Task', + description: 'This is a test task', + ); + + final projectTask = + await projectTaskRepository.create(projectTaskCreate).run(); + expect(projectTask.isRight(), true, reason: 'Result should be right'); + + projectTask.match( + (_) => fail('Result should be right'), + (task) { + expect(task.projectId, projectTaskCreate.projectId, + reason: 'Project ID should match'); + expect(task.name, projectTaskCreate.name, + reason: 'Task name should match'); + expect(task.description, projectTaskCreate.description, + reason: 'Description should match'); + }, + ); + }); + + test('find project task by id', () async { + final projectTaskCreate = ProjectTaskCreate( + projectId: 'project123', + name: 'Test Task', + description: 'This is a test task', + ); + + final createdTask = + await projectTaskRepository.create(projectTaskCreate).run(); + + createdTask.match( + (_) => fail('Result should be right'), + (task) async { + final foundTask = await projectTaskRepository.findById(task.id).run(); + + foundTask.match( + (_) => fail('Result should be right'), + (task) { + expect(task.id, createdTask.match((e) => null, identity)?.id, + reason: 'Task ID should match'); + }, + ); + }, + ); + }); + + test('find tasks by project id', () async { + final task1 = ProjectTaskCreate( + projectId: 'project123', + name: 'Task 1', + description: 'First task', + ); + final task2 = ProjectTaskCreate( + projectId: 'project123', + name: 'Task 2', + description: 'Second task', + ); + + await projectTaskRepository.create(task1).run(); + await projectTaskRepository.create(task2).run(); + + final tasks = + await projectTaskRepository.findByProjectId('project123').run(); + + tasks.match( + (_) => fail('Result should be right'), + (tasks) { + expect(tasks.length, 2, reason: 'Should return two tasks'); + expect( + tasks.map((t) => t.name).toList(), + containsAll([task1.name, task2.name]), + ); + }, + ); + }); + + test('update project task', () async { + final projectTaskCreate = ProjectTaskCreate( + projectId: 'project123', + name: 'Initial Task', + description: 'Initial Description', + ); + + final createdTask = + await projectTaskRepository.create(projectTaskCreate).run(); + + createdTask.match( + (_) => fail('Result should be right'), + (task) async { + final taskUpdate = ProjectTaskUpdate( + id: task.id, + name: 'Updated Task', + ); + + final updatedTask = + await projectTaskRepository.update(taskUpdate).run(); + + updatedTask.match( + (_) => fail('Result should be right'), + (task) { + expect(task.name, 'Updated Task', + reason: 'Task name should be updated'); + }, + ); + }, + ); + }); + + test('delete project task', () async { + final projectTaskCreate = ProjectTaskCreate( + projectId: 'project123', + name: 'Task to delete', + description: 'Task description', + ); + + final createdTask = + await projectTaskRepository.create(projectTaskCreate).run(); + + createdTask.match( + (_) => fail('Result should be right'), + (task) async { + final deletedTask = await projectTaskRepository.delete(task.id).run(); + + deletedTask.match( + (_) => fail('Result should be right'), + (task) { + expect(task.id, createdTask.match((e) => null, identity)?.id, + reason: 'Deleted task ID should match created task ID'); + }, + ); + + final result = await projectTaskRepository.findById(task.id).run(); + expect(result.isLeft(), true, reason: 'Task should no longer exist'); + }, + ); + }); + + test('find all project tasks', () async { + final task1 = ProjectTaskCreate( + projectId: 'project123', + name: 'Task 1', + description: 'Description 1', + ); + final task2 = ProjectTaskCreate( + projectId: 'project456', + name: 'Task 2', + description: 'Description 2', + ); + + await projectTaskRepository.create(task1).run(); + await projectTaskRepository.create(task2).run(); + + final tasks = await projectTaskRepository.findAll().run(); + + tasks.match( + (_) => fail('Result should be right'), + (tasks) { + expect(tasks.length, 2, reason: 'Should return all tasks'); + expect( + tasks.map((t) => t.name), + containsAll([task1.name, task2.name]), + ); + }, + ); + }); +} diff --git a/backend-dart/test/time_entries_repository_tests.dart b/backend-dart/test/time_entries_repository_tests.dart new file mode 100644 index 0000000..65d663f --- /dev/null +++ b/backend-dart/test/time_entries_repository_tests.dart @@ -0,0 +1,239 @@ +import 'package:backend_dart/application/repository/time_entry_repository_impl.dart'; +import 'package:backend_dart/domain/entities/time_entry.dart'; +import 'package:backend_dart/domain/repository/time_entry_repository.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:test/test.dart'; + +import 'mocks/mock_database.dart'; + +void main() { + late MockDatabase database; + late TimeEntryRepository timeEntryRepository; + + setUpAll(() { + database = MockDatabase(); + timeEntryRepository = TimeEntryRepositoryImpl(database); + }); + + setUp(() { + database.clear(); // Clear the database before each test + }); + + test('create time entry', () async { + final timeEntryCreate = TimeEntryCreate( + userId: 'user123', + projectId: 'project123', + startTime: DateTime.now(), + endTime: DateTime.now().add(Duration(hours: 1)), + description: 'Working on project', + ); + + final timeEntry = await timeEntryRepository.create(timeEntryCreate).run(); + expect(timeEntry.isRight(), true, reason: 'Result should be right'); + + timeEntry.match( + (_) => fail('Result should be right'), + (timeEntry) { + expect(timeEntry.userId, timeEntryCreate.userId, + reason: 'User ID should match'); + expect(timeEntry.projectId, timeEntryCreate.projectId, + reason: 'Project ID should match'); + expect(timeEntry.startTime, timeEntryCreate.startTime, + reason: 'Start time should match'); + expect(timeEntry.endTime, timeEntryCreate.endTime, + reason: 'End time should match'); + expect(timeEntry.description, timeEntryCreate.description, + reason: 'Description should match'); + }, + ); + }); + + test('find time entry by id', () async { + final timeEntryCreate = TimeEntryCreate( + userId: 'user123', + projectId: 'project123', + startTime: DateTime.now(), + endTime: DateTime.now().add(Duration(hours: 1)), + description: 'Working on project', + ); + + final createdEntry = + await timeEntryRepository.create(timeEntryCreate).run(); + + createdEntry.match( + (_) => fail('Result should be right'), + (entry) async { + final foundEntry = await timeEntryRepository.findById(entry.id).run(); + + foundEntry.match( + (_) => fail('Result should be right'), + (entry) { + expect(entry.id, createdEntry.match((e) => null, identity)?.id, + reason: 'ID should match the created entry'); + }, + ); + }, + ); + }); + + test('find time entries by user id', () async { + final timeEntry1 = TimeEntryCreate( + userId: 'user123', + projectId: 'project123', + startTime: DateTime.now(), + endTime: DateTime.now().add(Duration(hours: 1)), + description: 'Task 1', + ); + final timeEntry2 = TimeEntryCreate( + userId: 'user123', + projectId: 'project456', + startTime: DateTime.now().add(Duration(hours: 2)), + endTime: DateTime.now().add(Duration(hours: 3)), + description: 'Task 2', + ); + + await timeEntryRepository.create(timeEntry1).run(); + await timeEntryRepository.create(timeEntry2).run(); + + final entries = await timeEntryRepository.findByUserId('user123').run(); + + entries.match( + (_) => fail('Result should be right'), + (entries) { + expect(entries.length, 2, reason: 'Should return two entries'); + expect(entries.map((e) => e.description), + containsAll([timeEntry1.description, timeEntry2.description])); + }, + ); + }); + + test('find time entries by project id', () async { + final timeEntry1 = TimeEntryCreate( + userId: 'user123', + projectId: 'project123', + startTime: DateTime.now(), + endTime: DateTime.now().add(Duration(hours: 1)), + description: 'Task 1', + ); + final timeEntry2 = TimeEntryCreate( + userId: 'user456', + projectId: 'project123', + startTime: DateTime.now().add(Duration(hours: 2)), + endTime: DateTime.now().add(Duration(hours: 3)), + description: 'Task 2', + ); + + await timeEntryRepository.create(timeEntry1).run(); + await timeEntryRepository.create(timeEntry2).run(); + + final entries = + await timeEntryRepository.findByProjectId('project123').run(); + + entries.match( + (_) => fail('Result should be right'), + (entries) { + expect(entries.length, 2, reason: 'Should return two entries'); + expect(entries.map((e) => e.description), + containsAll([timeEntry1.description, timeEntry2.description])); + }, + ); + }); + + test('update time entry', () async { + final timeEntryCreate = TimeEntryCreate( + userId: 'user123', + projectId: 'project123', + startTime: DateTime.now(), + endTime: DateTime.now().add(Duration(hours: 1)), + description: 'Initial Task', + ); + + final createdEntry = + await timeEntryRepository.create(timeEntryCreate).run(); + + createdEntry.match( + (_) => fail('Result should be right'), + (entry) async { + final timeEntryUpdate = TimeEntryUpdate( + id: entry.id, + description: 'Updated Task', + ); + + final updatedEntry = + await timeEntryRepository.update(timeEntryUpdate).run(); + + updatedEntry.match( + (_) => fail('Result should be right'), + (entry) { + expect(entry.description, 'Updated Task', + reason: 'Description should be updated'); + }, + ); + }, + ); + }); + + test('delete time entry', () async { + final timeEntryCreate = TimeEntryCreate( + userId: 'user123', + projectId: 'project123', + startTime: DateTime.now(), + endTime: DateTime.now().add(Duration(hours: 1)), + description: 'Task to delete', + ); + + final createdEntry = + await timeEntryRepository.create(timeEntryCreate).run(); + + createdEntry.match( + (_) => fail('Result should be right'), + (entry) async { + final deletedEntry = await timeEntryRepository.delete(entry.id).run(); + + deletedEntry.match( + (_) => fail('Result should be right'), + (entry) { + expect(entry.id, createdEntry.match((e) => null, identity)?.id, + reason: 'Deleted entry ID should match created entry ID'); + }, + ); + + final result = await timeEntryRepository.findById(entry.id).run(); + expect(result.isLeft(), true, reason: 'Entry should no longer exist'); + }, + ); + }); + + test('find all time entries', () async { + final timeEntry1 = TimeEntryCreate( + userId: 'user123', + projectId: 'project123', + startTime: DateTime.now(), + endTime: DateTime.now().add(Duration(hours: 1)), + description: 'Task 1', + ); + final timeEntry2 = TimeEntryCreate( + userId: 'user456', + projectId: 'project456', + startTime: DateTime.now().add(Duration(hours: 2)), + endTime: DateTime.now().add(Duration(hours: 3)), + description: 'Task 2', + ); + + await timeEntryRepository.create(timeEntry1).run(); + await timeEntryRepository.create(timeEntry2).run(); + + final entries = await timeEntryRepository.findAll().run(); + + entries.match( + (_) => fail('Result should be right'), + (entries) { + expect(entries.length, 2, reason: 'Should return all entries'); + expect( + entries.map((e) => e.description), + containsAll([timeEntry1.description, timeEntry2.description]), + ); + }, + ); + }); +} diff --git a/backend-dart/test/user_repository_tests.dart b/backend-dart/test/user_repository_tests.dart new file mode 100644 index 0000000..c9a30c9 --- /dev/null +++ b/backend-dart/test/user_repository_tests.dart @@ -0,0 +1,202 @@ +import 'package:backend_dart/application/repository/user_repository_impl.dart'; +import 'package:backend_dart/common/secure_hash.dart'; +import 'package:backend_dart/domain/entities/user.dart'; +import 'package:backend_dart/domain/repository/user_repository.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:test/test.dart'; + +import 'mocks/mock_database.dart'; + +void main() { + late MockDatabase database; + late UserRepository userRepository; + + setUpAll(() { + database = MockDatabase(); + userRepository = UserRepositoryImpl(database); + }); + + setUp(() { + database.clear(); // Für jeden Test die Datenbank zurücksetzen + }); + + test('generateSecureHash', () { + final password = 'password'; + final hash = generateSecureHash(password); + final expected = + '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8'; + expect(hash, expected, reason: 'Hash should be $expected'); + }); + + test('create user', () async { + database.clear(); + + final userCreate = UserCreate( + email: 'user@example.com', + password: 'password', + name: 'John Doe1', + ); + final user = await userRepository.create(userCreate).run(); + expect(user.isRight(), true, reason: 'Result should be right'); + + user.match( + (_) => fail('Result should be right'), + (user) { + final hashedPassword = generateSecureHash(userCreate.password); + expect(user.email, userCreate.email, + reason: 'Email should be ${userCreate.email}'); + expect(user.name, userCreate.name, + reason: 'Name should be ${userCreate.name}'); + expect(user.passwordHash, hashedPassword, + reason: 'Password hash should be $hashedPassword'); + }, + ); + }); + + test('find user by email', () async { + database.clear(); + + final userCreate = UserCreate( + email: 'user@example.com', + password: 'password', + name: 'John Doe2', + ); + await userRepository.create(userCreate).run(); + + final foundUser = await userRepository.findByEmail(userCreate.email).run(); + + foundUser.match( + (_) => fail('Result should be right'), + (user) { + final hashedPassword = generateSecureHash(userCreate.password); + expect(user.email, userCreate.email, + reason: 'Email should be ${userCreate.email}'); + expect(user.name, userCreate.name, + reason: 'Name should be ${userCreate.name}'); + expect(user.passwordHash, hashedPassword, + reason: 'Password hash should be $hashedPassword'); + }, + ); + }); + + test('find user by id', () async { + database.clear(); + + final userCreate = UserCreate( + email: 'user@example.com', + password: 'password', + name: 'John Doe3', + ); + final createdUser = await userRepository.create(userCreate).run(); + + createdUser.match( + (_) => fail('Result should be right'), + (user) async { + final foundUser = await userRepository.findById(user.id).run(); + + foundUser.match( + (_) => fail('Result should be right'), + (user) { + final hashedPassword = generateSecureHash(userCreate.password); + expect(user.email, userCreate.email, + reason: 'Email should be ${userCreate.email}'); + expect(user.name, userCreate.name, + reason: 'Name should be ${userCreate.name}'); + expect(user.passwordHash, hashedPassword, + reason: 'Password hash should be $hashedPassword'); + }, + ); + }, + ); + }); + + test('update user', () async { + database.clear(); + + final userCreate = UserCreate( + email: 'user@example.com', + password: 'password', + name: 'John Doe4', + ); + final createdUser = await userRepository.create(userCreate).run(); + + createdUser.match( + (_) => fail('Result should be right'), + (user) async { + final userUpdate = UserUpdate( + id: user.id, + name: 'Jane Doe', + ); + + final updatedUser = await userRepository.update(userUpdate).run(); + + updatedUser.match( + (_) => fail('Result should be right'), + (user) { + expect(user.name, 'Jane Doe', + reason: 'Updated name should be Jane Doe'); + }, + ); + }, + ); + }); + + test('delete user', () async { + database.clear(); + + final userCreate = UserCreate( + email: 'user@example.com', + password: 'password', + name: 'John Doe5', + ); + final createdUser = await userRepository.create(userCreate).run(); + + createdUser.match( + (_) => fail('Result should be right'), + (user) async { + final deletedUser = await userRepository.delete(user.id).run(); + + deletedUser.match( + (_) => fail('Result should be right'), + (user) { + expect(user.id, createdUser.match((e) => null, identity)?.id, + reason: 'Deleted user ID should match created user ID'); + }, + ); + + final result = await userRepository.findById(user.id).run(); + expect(result.isLeft(), true, reason: 'User should no longer exist'); + }, + ); + }); + + test('find all users', () async { + database.clear(); + + final user1 = UserCreate( + email: 'user1@example.com', + password: 'password1', + name: 'User One', + ); + final user2 = UserCreate( + email: 'user2@example.com', + password: 'password2', + name: 'User Two', + ); + + await userRepository.create(user1).run(); + await userRepository.create(user2).run(); + + final result = await userRepository.findAll().run(); + + result.match( + (_) => fail('Result should be right'), + (users) { + print(users); + expect(users.length, 2, reason: 'There should be two users'); + expect(users.map((u) => u.email).toList(), + containsAll([user1.email, user2.email])); + }, + ); + }); +}