From ee398da2cfd3159af2f9643ba0f958e7beac45c8 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sat, 1 Aug 2026 21:50:23 +0900 Subject: [PATCH 01/25] feat: add user_favorite migration for favorite heritages join table --- ...8_01_000000_create_user_favorite_table.php | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/database/migrations/2026_08_01_000000_create_user_favorite_table.php diff --git a/src/database/migrations/2026_08_01_000000_create_user_favorite_table.php b/src/database/migrations/2026_08_01_000000_create_user_favorite_table.php new file mode 100644 index 00000000..0376384c --- /dev/null +++ b/src/database/migrations/2026_08_01_000000_create_user_favorite_table.php @@ -0,0 +1,35 @@ +bigIncrements('id'); + $table->unsignedBigInteger('user_id'); + $table->unsignedInteger('world_heritage_site_id'); + $table->timestamps(); + + $table->foreign('user_id') + ->references('id') + ->on('users') + ->cascadeOnDelete(); + + $table->foreign('world_heritage_site_id') + ->references('id') + ->on('world_heritage_sites') + ->cascadeOnDelete(); + + $table->unique(['user_id', 'world_heritage_site_id'], 'user_favorite_unique'); + }); + } + + public function down(): void + { + Schema::dropIfExists('user_favorite'); + } +}; \ No newline at end of file From 66cd28991750227fbd639b16e8025dd07d31298e Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sun, 2 Aug 2026 18:12:50 +0900 Subject: [PATCH 02/25] chore: ignore src/.config/ (psysh cache) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9f11b755..c0b6dde2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .idea/ +src/.config/ From 241471becea14949ce1258ed739036eecf640745 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Thu, 6 Aug 2026 23:32:41 +0900 Subject: [PATCH 03/25] feat: add favorites relation to User model --- src/app/Models/User.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/app/Models/User.php b/src/app/Models/User.php index 741315d8..62c8754c 100644 --- a/src/app/Models/User.php +++ b/src/app/Models/User.php @@ -2,6 +2,7 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Laravel\Sanctum\HasApiTokens; @@ -33,4 +34,14 @@ protected function casts(): array 'email_verified_at' => 'datetime', ]; } + + public function favorites(): BelongsToMany + { + return $this->belongsToMany( + WorldHeritage::class, + 'user_favorite', + 'user_id', + 'world_heritage_site_id' + ); + } } \ No newline at end of file From f2b497d57f2c6899736a3ce5cbe767dc453bc423 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Thu, 6 Aug 2026 23:33:54 +0900 Subject: [PATCH 04/25] feat: add FavoriteRepositoryInterface and FavoriteRepository --- .../Domains/Favorite/FavoriteRepository.php | 25 +++++++++++++++++++ .../Interface/FavoriteRepositoryInterface.php | 8 ++++++ 2 files changed, 33 insertions(+) create mode 100644 src/app/Packages/Domains/Favorite/FavoriteRepository.php create mode 100644 src/app/Packages/Domains/Favorite/Interface/FavoriteRepositoryInterface.php diff --git a/src/app/Packages/Domains/Favorite/FavoriteRepository.php b/src/app/Packages/Domains/Favorite/FavoriteRepository.php new file mode 100644 index 00000000..6fa2205d --- /dev/null +++ b/src/app/Packages/Domains/Favorite/FavoriteRepository.php @@ -0,0 +1,25 @@ +userModel->find($userId); + + if ($user === null) { + throw new Exception('User not found.'); + } + + $user->favorites()->attach($worldHeritageSiteId); + } +} diff --git a/src/app/Packages/Domains/Favorite/Interface/FavoriteRepositoryInterface.php b/src/app/Packages/Domains/Favorite/Interface/FavoriteRepositoryInterface.php new file mode 100644 index 00000000..7b1291ac --- /dev/null +++ b/src/app/Packages/Domains/Favorite/Interface/FavoriteRepositoryInterface.php @@ -0,0 +1,8 @@ + Date: Thu, 6 Aug 2026 23:36:38 +0900 Subject: [PATCH 05/25] feat: bind FavoriteRepositoryInterface in RepositoryProvider --- src/app/Providers/RepositoryProvider.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/app/Providers/RepositoryProvider.php b/src/app/Providers/RepositoryProvider.php index cbba05bf..50348344 100644 --- a/src/app/Providers/RepositoryProvider.php +++ b/src/app/Providers/RepositoryProvider.php @@ -2,6 +2,8 @@ namespace App\Providers; +use App\Packages\Domains\Favorite\FavoriteRepository; +use App\Packages\Domains\Favorite\Interface\FavoriteRepositoryInterface; use App\Packages\Domains\User\Interface\UserRepositroyInterface; use App\Packages\Domains\User\UserRepository; use App\Packages\Domains\WorldHeritageRepository; @@ -21,6 +23,11 @@ public function register(): void UserRepositroyInterface::class, UserRepository::class ); + + $this->app->bind( + FavoriteRepositoryInterface::class, + FavoriteRepository::class + ); } public function boot(): void From 7719b6a7f7d054d25b43c58b80d7006caab24ef5 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Thu, 6 Aug 2026 23:56:20 +0900 Subject: [PATCH 06/25] test: cover FavoriteRepository addFavorite in integration test --- .../Favorite/Tests/FavoriteRepositoryTest.php | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 src/app/Packages/Domains/Favorite/Tests/FavoriteRepositoryTest.php diff --git a/src/app/Packages/Domains/Favorite/Tests/FavoriteRepositoryTest.php b/src/app/Packages/Domains/Favorite/Tests/FavoriteRepositoryTest.php new file mode 100644 index 00000000..7b71a092 --- /dev/null +++ b/src/app/Packages/Domains/Favorite/Tests/FavoriteRepositoryTest.php @@ -0,0 +1,93 @@ +truncate(); + } + + protected function tearDown(): void + { + $this->truncate(); + parent::tearDown(); + } + + private function truncate(): void + { + if (env('APP_ENV') === 'testing') { + DB::connection('mysql')->statement('SET FOREIGN_KEY_CHECKS=0;'); + DB::connection('mysql')->table('user_favorite')->truncate(); + WorldHeritage::truncate(); + User::truncate(); + DB::connection('mysql')->statement('SET FOREIGN_KEY_CHECKS=1;'); + } + } + + private function repository(): FavoriteRepository + { + return new FavoriteRepository(new User()); + } + + private function seedUser(): User + { + $faker = FakerFactory::create(); + + return User::create([ + 'first_name' => $faker->firstName(), + 'last_name' => $faker->lastName(), + 'email' => $faker->unique()->safeEmail(), + 'password' => bcrypt('password'), + 'age_range' => 'teens', + 'subscription_tier' => 'free', + 'subscription_expires_at' => null, + ]); + } + + private function seedWorldHeritage(int $id): WorldHeritage + { + return WorldHeritage::create([ + 'id' => $id, + 'official_name' => 'Example Site', + 'name' => 'Example Site', + 'region' => 'Asia', + 'category' => 'Cultural', + 'criteria' => ['i'], + 'year_inscribed' => 2000, + ]); + } + + public function test_addFavorite_attaches_world_heritage_site_to_user(): void + { + $user = $this->seedUser(); + $worldHeritage = $this->seedWorldHeritage(1); + + $this->repository()->addFavorite($user->id, $worldHeritage->id); + + $this->assertDatabaseHas('user_favorite', [ + 'user_id' => $user->id, + 'world_heritage_site_id' => $worldHeritage->id, + ]); + } + + public function test_addFavorite_throws_exception_when_user_not_found(): void + { + $worldHeritage = $this->seedWorldHeritage(1); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('User not found.'); + + $this->repository()->addFavorite(999999, $worldHeritage->id); + } +} From 86e46e6e83e13f0053764e7548d6eddef1ad826f Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Fri, 7 Aug 2026 08:29:54 +0900 Subject: [PATCH 07/25] feat: add AddFavoriteUseCase --- .../UseCase/Favorite/AddFavoriteUseCase.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/app/Packages/Features/CommandUseCases/UseCase/Favorite/AddFavoriteUseCase.php diff --git a/src/app/Packages/Features/CommandUseCases/UseCase/Favorite/AddFavoriteUseCase.php b/src/app/Packages/Features/CommandUseCases/UseCase/Favorite/AddFavoriteUseCase.php new file mode 100644 index 00000000..91789323 --- /dev/null +++ b/src/app/Packages/Features/CommandUseCases/UseCase/Favorite/AddFavoriteUseCase.php @@ -0,0 +1,17 @@ +favoriteRepository->addFavorite($userId, $worldHeritageSiteId); + } +} From 8755795d1fcf8a676ba674a2af35a3ff7b5b0a71 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Fri, 7 Aug 2026 08:31:30 +0900 Subject: [PATCH 08/25] test: cover AddFavoriteUseCase --- .../AddFavoriteUseCaseTest.php | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/app/Packages/Features/Tests/CommandUseCases/AddFavoriteUseCaseTest.php diff --git a/src/app/Packages/Features/Tests/CommandUseCases/AddFavoriteUseCaseTest.php b/src/app/Packages/Features/Tests/CommandUseCases/AddFavoriteUseCaseTest.php new file mode 100644 index 00000000..50426fca --- /dev/null +++ b/src/app/Packages/Features/Tests/CommandUseCases/AddFavoriteUseCaseTest.php @@ -0,0 +1,46 @@ +shouldReceive('addFavorite') + ->once() + ->with(1, 2); + + (new AddFavoriteUseCase($repository))->handle(1, 2); + } + + public function test_handle_propagates_exception_when_user_not_found(): void + { + /** @var FavoriteRepositoryInterface|MockInterface $repository */ + $repository = Mockery::mock(FavoriteRepositoryInterface::class); + $repository->shouldReceive('addFavorite') + ->once() + ->with(999999, 2) + ->andThrow(new Exception('User not found.')); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('User not found.'); + + (new AddFavoriteUseCase($repository))->handle(999999, 2); + } +} From b0b354ad093807892f926123e35a180f9e498fa9 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Fri, 7 Aug 2026 09:23:09 +0900 Subject: [PATCH 09/25] feat: add FavoriteController and route --- .../Controller/FavoriteController.php | 46 +++++++++++++++++++ src/routes/api.php | 5 ++ 2 files changed, 51 insertions(+) create mode 100644 src/app/Packages/Features/Controller/FavoriteController.php diff --git a/src/app/Packages/Features/Controller/FavoriteController.php b/src/app/Packages/Features/Controller/FavoriteController.php new file mode 100644 index 00000000..34561f30 --- /dev/null +++ b/src/app/Packages/Features/Controller/FavoriteController.php @@ -0,0 +1,46 @@ +handle( + $request->user()->id, + (int) $request->input('world_heritage_id'), + ); + + return response()->json([ + 'status' => 'success', + ], 201); + } catch (Throwable $throwable) { + if ($throwable->getMessage() === 'User not found.') { + return response()->json([ + 'status' => 'error', + 'message' => 'User not found.', + ], 404); + } + + Log::error('Failed to add favorite', [ + 'message' => $throwable->getMessage(), + 'trace' => $throwable->getTraceAsString(), + ]); + + return response()->json([ + 'status' => 'error', + 'message' => 'Internal Server Error', + ], 500); + } + } +} diff --git a/src/routes/api.php b/src/routes/api.php index 3d84d52c..73b27956 100644 --- a/src/routes/api.php +++ b/src/routes/api.php @@ -1,6 +1,7 @@ prefix('favorites')->middleware('auth:sanctum')->group(function (): void { + Route::post('/', 'addFavorite'); + }); }); \ No newline at end of file From 2f70034c0414b7afd185d4b1108441f1ed52f965 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Fri, 7 Aug 2026 09:24:45 +0900 Subject: [PATCH 10/25] test: cover FavoriteController integration --- .../Features/Tests/FavoriteControllerTest.php | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/app/Packages/Features/Tests/FavoriteControllerTest.php diff --git a/src/app/Packages/Features/Tests/FavoriteControllerTest.php b/src/app/Packages/Features/Tests/FavoriteControllerTest.php new file mode 100644 index 00000000..f734f4ba --- /dev/null +++ b/src/app/Packages/Features/Tests/FavoriteControllerTest.php @@ -0,0 +1,113 @@ +truncate(); + } + + protected function tearDown(): void + { + $this->truncate(); + parent::tearDown(); + } + + private function truncate(): void + { + if (env('APP_ENV') === 'testing') { + DB::connection('mysql')->statement('SET FOREIGN_KEY_CHECKS=0;'); + DB::connection('mysql')->table('user_favorite')->truncate(); + DB::connection('mysql')->table('personal_access_tokens')->truncate(); + WorldHeritage::truncate(); + User::truncate(); + DB::connection('mysql')->statement('SET FOREIGN_KEY_CHECKS=1;'); + } + } + + private function seedUser(): User + { + return User::create([ + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'john@example.com', + 'password' => bcrypt('secret123'), + 'age_range' => '20s', + 'subscription_tier' => 'free', + 'subscription_expires_at' => null, + ]); + } + + private function seedWorldHeritage(int $id): WorldHeritage + { + return WorldHeritage::create([ + 'id' => $id, + 'official_name' => 'Example Site', + 'name' => 'Example Site', + 'region' => 'Asia', + 'category' => 'Cultural', + 'criteria' => ['i'], + 'year_inscribed' => 2000, + ]); + } + + public function test_addFavorite_returns_201_and_persists_favorite_when_authenticated(): void + { + $user = $this->seedUser(); + $worldHeritage = $this->seedWorldHeritage(1); + $token = $user->createToken('auth-token')->plainTextToken; + + $response = $this->withToken($token) + ->postJson('/api/v1/favorites', ['world_heritage_id' => $worldHeritage->id]); + + $response->assertStatus(201) + ->assertJsonFragment(['status' => 'success']); + + $this->assertDatabaseHas('user_favorite', [ + 'user_id' => $user->id, + 'world_heritage_site_id' => $worldHeritage->id, + ]); + } + + public function test_addFavorite_returns_401_when_unauthenticated(): void + { + $worldHeritage = $this->seedWorldHeritage(1); + + $response = $this->postJson('/api/v1/favorites', ['world_heritage_id' => $worldHeritage->id]); + + $response->assertStatus(401); + } + + public function test_addFavorite_returns_500_on_unexpected_error(): void + { + $user = $this->seedUser(); + $token = $user->createToken('auth-token')->plainTextToken; + + $repository = Mockery::mock(FavoriteRepositoryInterface::class); + $repository->shouldReceive('addFavorite') + ->andThrow(new RuntimeException('Unexpected error')); + + $this->app->instance(AddFavoriteUseCase::class, new AddFavoriteUseCase($repository)); + + $response = $this->withToken($token) + ->postJson('/api/v1/favorites', ['world_heritage_id' => 1]); + + $response->assertStatus(500) + ->assertJsonFragment([ + 'status' => 'error', + 'message' => 'Internal Server Error', + ]); + } +} From ad058bff10d532338e49e4acdb9197d040e96b93 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sat, 8 Aug 2026 16:47:37 +0900 Subject: [PATCH 11/25] feat: add getFavoriteWorldHeritageIds to FavoriteRepository --- src/app/Models/User.php | 2 +- .../Domains/Favorite/FavoriteRepository.php | 14 ++++++++++++++ .../Interface/FavoriteRepositoryInterface.php | 2 ++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/app/Models/User.php b/src/app/Models/User.php index 62c8754c..af253d1f 100644 --- a/src/app/Models/User.php +++ b/src/app/Models/User.php @@ -42,6 +42,6 @@ public function favorites(): BelongsToMany 'user_favorite', 'user_id', 'world_heritage_site_id' - ); + )->withTimestamps(); } } \ No newline at end of file diff --git a/src/app/Packages/Domains/Favorite/FavoriteRepository.php b/src/app/Packages/Domains/Favorite/FavoriteRepository.php index 6fa2205d..586538f0 100644 --- a/src/app/Packages/Domains/Favorite/FavoriteRepository.php +++ b/src/app/Packages/Domains/Favorite/FavoriteRepository.php @@ -22,4 +22,18 @@ public function addFavorite(int $userId, int $worldHeritageSiteId): void $user->favorites()->attach($worldHeritageSiteId); } + + public function getFavoriteWorldHeritageIds(int $userId): array + { + $user = $this->userModel->find($userId); + + if ($user === null) { + throw new Exception('User not found.'); + } + + return $user->favorites() + ->orderBy('user_favorite.created_at', 'desc') + ->pluck('world_heritage_sites.id') + ->all(); + } } diff --git a/src/app/Packages/Domains/Favorite/Interface/FavoriteRepositoryInterface.php b/src/app/Packages/Domains/Favorite/Interface/FavoriteRepositoryInterface.php index 7b1291ac..164187e6 100644 --- a/src/app/Packages/Domains/Favorite/Interface/FavoriteRepositoryInterface.php +++ b/src/app/Packages/Domains/Favorite/Interface/FavoriteRepositoryInterface.php @@ -5,4 +5,6 @@ interface FavoriteRepositoryInterface { public function addFavorite(int $userId, int $worldHeritageSiteId): void; + + public function getFavoriteWorldHeritageIds(int $userId): array; } From b4acd3480137b6a68f8b3ad799f18d29f71a92ba Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sat, 8 Aug 2026 16:47:41 +0900 Subject: [PATCH 12/25] test: cover getFavoriteWorldHeritageIds in FavoriteRepositoryTest --- .../Favorite/Tests/FavoriteRepositoryTest.php | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/app/Packages/Domains/Favorite/Tests/FavoriteRepositoryTest.php b/src/app/Packages/Domains/Favorite/Tests/FavoriteRepositoryTest.php index 7b71a092..7264d602 100644 --- a/src/app/Packages/Domains/Favorite/Tests/FavoriteRepositoryTest.php +++ b/src/app/Packages/Domains/Favorite/Tests/FavoriteRepositoryTest.php @@ -90,4 +90,35 @@ public function test_addFavorite_throws_exception_when_user_not_found(): void $this->repository()->addFavorite(999999, $worldHeritage->id); } + + public function test_getFavoriteWorldHeritageIds_returns_ids_ordered_by_most_recently_favorited(): void + { + $user = $this->seedUser(); + $worldHeritage1 = $this->seedWorldHeritage(1); + $worldHeritage2 = $this->seedWorldHeritage(2); + + $user->favorites()->attach($worldHeritage1->id, ['created_at' => now()->subMinute()]); + $user->favorites()->attach($worldHeritage2->id, ['created_at' => now()]); + + $result = $this->repository()->getFavoriteWorldHeritageIds($user->id); + + $this->assertSame([$worldHeritage2->id, $worldHeritage1->id], $result); + } + + public function test_getFavoriteWorldHeritageIds_returns_empty_array_when_no_favorites(): void + { + $user = $this->seedUser(); + + $result = $this->repository()->getFavoriteWorldHeritageIds($user->id); + + $this->assertSame([], $result); + } + + public function test_getFavoriteWorldHeritageIds_throws_exception_when_user_not_found(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('User not found.'); + + $this->repository()->getFavoriteWorldHeritageIds(999999); + } } From b62ec92a51fcbaf6188a97a809cd1c0dc6be315c Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sat, 8 Aug 2026 17:51:53 +0900 Subject: [PATCH 13/25] feat: add getHeritagesByIds to WorldHeritageQueryService --- .../WorldHeritage/WorldHeritageQueryService.php | 11 +++++++++++ .../WorldHeritageQueryServiceInterface.php | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/src/app/Packages/Domains/WorldHeritage/WorldHeritageQueryService.php b/src/app/Packages/Domains/WorldHeritage/WorldHeritageQueryService.php index ca7a96c9..f9dd0683 100644 --- a/src/app/Packages/Domains/WorldHeritage/WorldHeritageQueryService.php +++ b/src/app/Packages/Domains/WorldHeritage/WorldHeritageQueryService.php @@ -246,6 +246,17 @@ public function searchHeritages(AlgoliaSearchListQuery $query): PaginationDto ); } + public function getHeritagesByIds(array $ids): WorldHeritageDtoCollection + { + $models = $this->readQueryService->findByIdsPreserveOrder($ids); + + $payloads = $models + ->map(fn ($m) => $this->buildWorldHeritagePayload($m)) + ->all(); + + return $this->buildDtoFromCollection($payloads); + } + public function getEachRegionsHeritagesCount(): array { $counts = $this->model diff --git a/src/app/Packages/Features/QueryUseCases/QueryServiceInterface/WorldHeritageQueryServiceInterface.php b/src/app/Packages/Features/QueryUseCases/QueryServiceInterface/WorldHeritageQueryServiceInterface.php index e7919e72..63539cb9 100644 --- a/src/app/Packages/Features/QueryUseCases/QueryServiceInterface/WorldHeritageQueryServiceInterface.php +++ b/src/app/Packages/Features/QueryUseCases/QueryServiceInterface/WorldHeritageQueryServiceInterface.php @@ -5,6 +5,7 @@ use App\Common\Pagination\PaginationDto; use App\Packages\Features\QueryUseCases\Dto\WorldHeritageDto; +use App\Packages\Features\QueryUseCases\Dto\WorldHeritageDtoCollection; use App\Packages\Features\QueryUseCases\ListQuery\AlgoliaSearchListQuery; @@ -25,5 +26,9 @@ public function searchHeritages( AlgoliaSearchListQuery $query ): PaginationDto; + public function getHeritagesByIds( + array $ids + ): WorldHeritageDtoCollection; + public function getEachRegionsHeritagesCount(): array; } \ No newline at end of file From 38553e5c3a73a7c16f6cc033aeb96d37a866c84e Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sat, 8 Aug 2026 17:51:53 +0900 Subject: [PATCH 14/25] test: cover getHeritagesByIds in WorldHeritageQueryService --- ...tageQueryService_getHeritagesByIdsTest.php | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src/app/Packages/Domains/WorldHeritage/Tests/QueryService/WorldHeritageQueryService_getHeritagesByIdsTest.php diff --git a/src/app/Packages/Domains/WorldHeritage/Tests/QueryService/WorldHeritageQueryService_getHeritagesByIdsTest.php b/src/app/Packages/Domains/WorldHeritage/Tests/QueryService/WorldHeritageQueryService_getHeritagesByIdsTest.php new file mode 100644 index 00000000..112d5d30 --- /dev/null +++ b/src/app/Packages/Domains/WorldHeritage/Tests/QueryService/WorldHeritageQueryService_getHeritagesByIdsTest.php @@ -0,0 +1,98 @@ +refresh(); + + $seeder = new DatabaseSeeder(); + $seeder->run(); + + $this->app->bind(WorldHeritageSearchPort::class, static function () { + return new class implements WorldHeritageSearchPort { + public function search($query, int $currentPage, int $perPage): HeritageSearchResult { + return new HeritageSearchResult(ids: [], total: 0, currentPage: 1, perPage: $perPage, lastPage: 0); + } + }; + }); + + $this->queryService = app(WorldHeritageQueryService::class); + } + + protected function tearDown(): void + { + $this->refresh(); + parent::tearDown(); + } + + private function refresh(): void + { + if (env('APP_ENV') === 'testing') { + DB::connection('mysql')->statement('SET FOREIGN_KEY_CHECKS=0;'); + WorldHeritage::truncate(); + Country::truncate(); + DB::table('site_state_parties')->truncate(); + Image::truncate(); + WorldHeritageDescription::truncate(); + DB::connection('mysql')->statement('SET FOREIGN_KEY_CHECKS=1;'); + } + } + + public function test_returns_world_heritage_dto_collection(): void + { + $ids = DB::table('world_heritage_sites')->orderBy('id')->limit(2)->pluck('id')->all(); + + $result = $this->queryService->getHeritagesByIds($ids); + + $this->assertInstanceOf(WorldHeritageDtoCollection::class, $result); + } + + public function test_preserves_order_of_given_ids(): void + { + $ids = DB::table('world_heritage_sites')->orderBy('id')->limit(3)->pluck('id')->all(); + $this->assertCount(3, $ids, 'Seeder must insert at least 3 world heritages.'); + + $requested = [$ids[2], $ids[0], $ids[1]]; + + $result = $this->queryService->getHeritagesByIds($requested); + + $resultIds = array_map(fn ($dto) => $dto->getId(), $result->getHeritages()); + $this->assertSame($requested, $resultIds); + } + + public function test_returns_empty_collection_when_no_ids_given(): void + { + $result = $this->queryService->getHeritagesByIds([]); + + $this->assertSame([], $result->getHeritages()); + } + + public function test_skips_missing_ids_without_failing(): void + { + $existingId = (int) DB::table('world_heritage_sites')->orderBy('id')->value('id'); + $this->assertNotNull($existingId, 'Seeder must insert at least 1 world heritage.'); + + $result = $this->queryService->getHeritagesByIds([$existingId, 999_999_999]); + + $resultIds = array_map(fn ($dto) => $dto->getId(), $result->getHeritages()); + $this->assertSame([$existingId], $resultIds); + } +} From 8315955230064b358f86175a976c07b51cc589cc Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sat, 8 Aug 2026 17:51:53 +0900 Subject: [PATCH 15/25] feat: add GetFavoriteHeritagesUseCase for favorite heritages list --- .../Favorite/GetFavoriteHeritagesUseCase.php | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/app/Packages/Features/QueryUseCases/UseCase/Favorite/GetFavoriteHeritagesUseCase.php diff --git a/src/app/Packages/Features/QueryUseCases/UseCase/Favorite/GetFavoriteHeritagesUseCase.php b/src/app/Packages/Features/QueryUseCases/UseCase/Favorite/GetFavoriteHeritagesUseCase.php new file mode 100644 index 00000000..857e68f2 --- /dev/null +++ b/src/app/Packages/Features/QueryUseCases/UseCase/Favorite/GetFavoriteHeritagesUseCase.php @@ -0,0 +1,22 @@ +favoriteRepository->getFavoriteWorldHeritageIds($userId); + + return $this->worldHeritageQueryService->getHeritagesByIds($ids); + } +} From 74a285736970f8bf0e49842505d447dfa9445a7b Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sat, 8 Aug 2026 17:51:53 +0900 Subject: [PATCH 16/25] test: cover GetFavoriteHeritagesUseCase --- .../GetFavoriteHeritagesUseCaseTest.php | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/app/Packages/Features/QueryUseCases/Tests/UseCase/GetFavoriteHeritagesUseCaseTest.php diff --git a/src/app/Packages/Features/QueryUseCases/Tests/UseCase/GetFavoriteHeritagesUseCaseTest.php b/src/app/Packages/Features/QueryUseCases/Tests/UseCase/GetFavoriteHeritagesUseCaseTest.php new file mode 100644 index 00000000..d777492d --- /dev/null +++ b/src/app/Packages/Features/QueryUseCases/Tests/UseCase/GetFavoriteHeritagesUseCaseTest.php @@ -0,0 +1,70 @@ +shouldReceive('getFavoriteWorldHeritageIds') + ->once() + ->with(1) + ->andReturn([3, 2]); + + $dtoCollection = new WorldHeritageDtoCollection(); + + /** @var WorldHeritageQueryServiceInterface|MockInterface $worldHeritageQueryService */ + $worldHeritageQueryService = Mockery::mock(WorldHeritageQueryServiceInterface::class); + $worldHeritageQueryService->shouldReceive('getHeritagesByIds') + ->once() + ->with([3, 2]) + ->andReturn($dtoCollection); + + $useCase = new GetFavoriteHeritagesUseCase($favoriteRepository, $worldHeritageQueryService); + + $result = $useCase->handle(1); + + $this->assertSame($dtoCollection, $result); + } + + public function test_handle_returns_empty_collection_when_user_has_no_favorites(): void + { + /** @var FavoriteRepositoryInterface|MockInterface $favoriteRepository */ + $favoriteRepository = Mockery::mock(FavoriteRepositoryInterface::class); + $favoriteRepository->shouldReceive('getFavoriteWorldHeritageIds') + ->once() + ->with(1) + ->andReturn([]); + + $dtoCollection = new WorldHeritageDtoCollection(); + + /** @var WorldHeritageQueryServiceInterface|MockInterface $worldHeritageQueryService */ + $worldHeritageQueryService = Mockery::mock(WorldHeritageQueryServiceInterface::class); + $worldHeritageQueryService->shouldReceive('getHeritagesByIds') + ->once() + ->with([]) + ->andReturn($dtoCollection); + + $useCase = new GetFavoriteHeritagesUseCase($favoriteRepository, $worldHeritageQueryService); + + $result = $useCase->handle(1); + + $this->assertSame([], $result->getHeritages()); + } +} From 0dd7eba81474d2ef26bf2d983852bfe965edef47 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sat, 8 Aug 2026 18:34:47 +0900 Subject: [PATCH 17/25] feat: add getFavorites to FavoriteController and route --- .../Controller/FavoriteController.php | 25 +++++++++++++++++++ src/routes/api.php | 1 + 2 files changed, 26 insertions(+) diff --git a/src/app/Packages/Features/Controller/FavoriteController.php b/src/app/Packages/Features/Controller/FavoriteController.php index 34561f30..10004a59 100644 --- a/src/app/Packages/Features/Controller/FavoriteController.php +++ b/src/app/Packages/Features/Controller/FavoriteController.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Controller; use App\Packages\Features\CommandUseCases\UseCase\Favorite\AddFavoriteUseCase; +use App\Packages\Features\QueryUseCases\UseCase\Favorite\GetFavoriteHeritagesUseCase; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; @@ -11,6 +12,30 @@ class FavoriteController extends Controller { + public function getFavorites( + Request $request, + GetFavoriteHeritagesUseCase $useCase, + ): JsonResponse { + try { + $dtoCollection = $useCase->handle($request->user()->id); + + return response()->json([ + 'status' => 'success', + 'data' => $dtoCollection->toSummaryArray(), + ], 200); + } catch (Throwable $throwable) { + Log::error('Failed to get favorites', [ + 'message' => $throwable->getMessage(), + 'trace' => $throwable->getTraceAsString(), + ]); + + return response()->json([ + 'status' => 'error', + 'message' => 'Internal Server Error', + ], 500); + } + } + public function addFavorite( Request $request, AddFavoriteUseCase $useCase, diff --git a/src/routes/api.php b/src/routes/api.php index 73b27956..4c58d279 100644 --- a/src/routes/api.php +++ b/src/routes/api.php @@ -29,6 +29,7 @@ }); Route::controller(FavoriteController::class)->prefix('favorites')->middleware('auth:sanctum')->group(function (): void { + Route::get('/', 'getFavorites'); Route::post('/', 'addFavorite'); }); }); \ No newline at end of file From 206baef635a015ee1e2463190349f2681c8a3efe Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sat, 8 Aug 2026 18:34:47 +0900 Subject: [PATCH 18/25] test: cover getFavorites in FavoriteControllerTest --- .../Features/Tests/FavoriteControllerTest.php | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/app/Packages/Features/Tests/FavoriteControllerTest.php b/src/app/Packages/Features/Tests/FavoriteControllerTest.php index f734f4ba..99f360c1 100644 --- a/src/app/Packages/Features/Tests/FavoriteControllerTest.php +++ b/src/app/Packages/Features/Tests/FavoriteControllerTest.php @@ -6,6 +6,8 @@ use App\Models\WorldHeritage; use App\Packages\Domains\Favorite\Interface\FavoriteRepositoryInterface; use App\Packages\Features\CommandUseCases\UseCase\Favorite\AddFavoriteUseCase; +use App\Packages\Features\QueryUseCases\QueryServiceInterface\WorldHeritageQueryServiceInterface; +use App\Packages\Features\QueryUseCases\UseCase\Favorite\GetFavoriteHeritagesUseCase; use Illuminate\Support\Facades\DB; use Mockery; use RuntimeException; @@ -110,4 +112,68 @@ public function test_addFavorite_returns_500_on_unexpected_error(): void 'message' => 'Internal Server Error', ]); } + + public function test_getFavorites_returns_200_with_favorite_heritages_when_authenticated(): void + { + $user = $this->seedUser(); + $worldHeritage1 = $this->seedWorldHeritage(1); + $worldHeritage2 = $this->seedWorldHeritage(2); + $token = $user->createToken('auth-token')->plainTextToken; + + $user->favorites()->attach($worldHeritage1->id); + $user->favorites()->attach($worldHeritage2->id); + + $response = $this->withToken($token)->getJson('/api/v1/favorites'); + + $response->assertStatus(200) + ->assertJsonFragment(['status' => 'success']) + ->assertJsonCount(2, 'data'); + + $ids = array_column($response->json('data'), 'id'); + $this->assertEqualsCanonicalizing([$worldHeritage1->id, $worldHeritage2->id], $ids); + } + + public function test_getFavorites_returns_200_with_empty_array_when_user_has_no_favorites(): void + { + $user = $this->seedUser(); + $token = $user->createToken('auth-token')->plainTextToken; + + $response = $this->withToken($token)->getJson('/api/v1/favorites'); + + $response->assertStatus(200) + ->assertJsonFragment(['status' => 'success']) + ->assertJsonCount(0, 'data'); + } + + public function test_getFavorites_returns_401_when_unauthenticated(): void + { + $response = $this->getJson('/api/v1/favorites'); + + $response->assertStatus(401); + } + + public function test_getFavorites_returns_500_on_unexpected_error(): void + { + $user = $this->seedUser(); + $token = $user->createToken('auth-token')->plainTextToken; + + $favoriteRepository = Mockery::mock(FavoriteRepositoryInterface::class); + $favoriteRepository->shouldReceive('getFavoriteWorldHeritageIds') + ->andThrow(new RuntimeException('Unexpected error')); + + $worldHeritageQueryService = Mockery::mock(WorldHeritageQueryServiceInterface::class); + + $this->app->instance( + GetFavoriteHeritagesUseCase::class, + new GetFavoriteHeritagesUseCase($favoriteRepository, $worldHeritageQueryService), + ); + + $response = $this->withToken($token)->getJson('/api/v1/favorites'); + + $response->assertStatus(500) + ->assertJsonFragment([ + 'status' => 'error', + 'message' => 'Internal Server Error', + ]); + } } From 0c4e887991255f74e0d2062be4b8f82f3831d432 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sat, 8 Aug 2026 18:53:34 +0900 Subject: [PATCH 19/25] test: stub WorldHeritageSearchPort in FavoriteControllerTest to avoid real Algolia client resolution --- .../Features/Tests/FavoriteControllerTest.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/app/Packages/Features/Tests/FavoriteControllerTest.php b/src/app/Packages/Features/Tests/FavoriteControllerTest.php index 99f360c1..b9f6a768 100644 --- a/src/app/Packages/Features/Tests/FavoriteControllerTest.php +++ b/src/app/Packages/Features/Tests/FavoriteControllerTest.php @@ -5,6 +5,8 @@ use App\Models\User; use App\Models\WorldHeritage; use App\Packages\Domains\Favorite\Interface\FavoriteRepositoryInterface; +use App\Packages\Domains\WorldHeritage\Ports\Dto\HeritageSearchResult; +use App\Packages\Domains\WorldHeritage\Ports\WorldHeritageSearchPort; use App\Packages\Features\CommandUseCases\UseCase\Favorite\AddFavoriteUseCase; use App\Packages\Features\QueryUseCases\QueryServiceInterface\WorldHeritageQueryServiceInterface; use App\Packages\Features\QueryUseCases\UseCase\Favorite\GetFavoriteHeritagesUseCase; @@ -19,6 +21,15 @@ protected function setUp(): void { parent::setUp(); $this->truncate(); + + $this->app->bind(WorldHeritageSearchPort::class, static function () { + return new class implements WorldHeritageSearchPort { + public function search($query, int $currentPage, int $perPage): HeritageSearchResult + { + return new HeritageSearchResult(ids: [], total: 0, currentPage: 1, perPage: $perPage, lastPage: 0); + } + }; + }); } protected function tearDown(): void From eb154e0bd479958acf2f682b837c3112dc97e462 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sun, 16 Aug 2026 18:00:05 +0900 Subject: [PATCH 20/25] feat: add removeFavorite to FavoriteRepository --- .../Packages/Domains/Favorite/FavoriteRepository.php | 11 +++++++++++ .../Interface/FavoriteRepositoryInterface.php | 2 ++ 2 files changed, 13 insertions(+) diff --git a/src/app/Packages/Domains/Favorite/FavoriteRepository.php b/src/app/Packages/Domains/Favorite/FavoriteRepository.php index 586538f0..6a98f5fe 100644 --- a/src/app/Packages/Domains/Favorite/FavoriteRepository.php +++ b/src/app/Packages/Domains/Favorite/FavoriteRepository.php @@ -23,6 +23,17 @@ public function addFavorite(int $userId, int $worldHeritageSiteId): void $user->favorites()->attach($worldHeritageSiteId); } + public function removeFavorite(int $userId, int $worldHeritageSiteId): void + { + $user = $this->userModel->find($userId); + + if ($user === null) { + throw new Exception('User not found.'); + } + + $user->favorites()->detach($worldHeritageSiteId); + } + public function getFavoriteWorldHeritageIds(int $userId): array { $user = $this->userModel->find($userId); diff --git a/src/app/Packages/Domains/Favorite/Interface/FavoriteRepositoryInterface.php b/src/app/Packages/Domains/Favorite/Interface/FavoriteRepositoryInterface.php index 164187e6..1c507374 100644 --- a/src/app/Packages/Domains/Favorite/Interface/FavoriteRepositoryInterface.php +++ b/src/app/Packages/Domains/Favorite/Interface/FavoriteRepositoryInterface.php @@ -6,5 +6,7 @@ interface FavoriteRepositoryInterface { public function addFavorite(int $userId, int $worldHeritageSiteId): void; + public function removeFavorite(int $userId, int $worldHeritageSiteId): void; + public function getFavoriteWorldHeritageIds(int $userId): array; } From 63bdf4947eb98f2a07e3c193e0dda14ccae09272 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sun, 16 Aug 2026 18:00:06 +0900 Subject: [PATCH 21/25] test: cover removeFavorite in FavoriteRepositoryTest --- .../Favorite/Tests/FavoriteRepositoryTest.php | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/app/Packages/Domains/Favorite/Tests/FavoriteRepositoryTest.php b/src/app/Packages/Domains/Favorite/Tests/FavoriteRepositoryTest.php index 7264d602..bd001ace 100644 --- a/src/app/Packages/Domains/Favorite/Tests/FavoriteRepositoryTest.php +++ b/src/app/Packages/Domains/Favorite/Tests/FavoriteRepositoryTest.php @@ -91,6 +91,39 @@ public function test_addFavorite_throws_exception_when_user_not_found(): void $this->repository()->addFavorite(999999, $worldHeritage->id); } + public function test_removeFavorite_detaches_world_heritage_site_from_user(): void + { + $user = $this->seedUser(); + $worldHeritage = $this->seedWorldHeritage(1); + $user->favorites()->attach($worldHeritage->id); + + $this->repository()->removeFavorite($user->id, $worldHeritage->id); + + $this->assertDatabaseMissing('user_favorite', [ + 'user_id' => $user->id, + 'world_heritage_site_id' => $worldHeritage->id, + ]); + } + + #[\PHPUnit\Framework\Attributes\DoesNotPerformAssertions] + public function test_removeFavorite_does_not_throw_when_not_favorited(): void + { + $user = $this->seedUser(); + $worldHeritage = $this->seedWorldHeritage(1); + + $this->repository()->removeFavorite($user->id, $worldHeritage->id); + } + + public function test_removeFavorite_throws_exception_when_user_not_found(): void + { + $worldHeritage = $this->seedWorldHeritage(1); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('User not found.'); + + $this->repository()->removeFavorite(999999, $worldHeritage->id); + } + public function test_getFavoriteWorldHeritageIds_returns_ids_ordered_by_most_recently_favorited(): void { $user = $this->seedUser(); From 7839d413c247a396c221b96571b43921332caac4 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sun, 16 Aug 2026 20:38:09 +0900 Subject: [PATCH 22/25] feat: add RemoveFavoriteUseCase --- .../UseCase/Favorite/RemoveFavoriteUseCase.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/app/Packages/Features/CommandUseCases/UseCase/Favorite/RemoveFavoriteUseCase.php diff --git a/src/app/Packages/Features/CommandUseCases/UseCase/Favorite/RemoveFavoriteUseCase.php b/src/app/Packages/Features/CommandUseCases/UseCase/Favorite/RemoveFavoriteUseCase.php new file mode 100644 index 00000000..dee77098 --- /dev/null +++ b/src/app/Packages/Features/CommandUseCases/UseCase/Favorite/RemoveFavoriteUseCase.php @@ -0,0 +1,17 @@ +favoriteRepository->removeFavorite($userId, $worldHeritageSiteId); + } +} From 7f1caa62d6ca6d9f51a3eee15de5f6480288e648 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sun, 16 Aug 2026 20:38:09 +0900 Subject: [PATCH 23/25] test: cover RemoveFavoriteUseCase --- .../RemoveFavoriteUseCaseTest.php | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/app/Packages/Features/Tests/CommandUseCases/RemoveFavoriteUseCaseTest.php diff --git a/src/app/Packages/Features/Tests/CommandUseCases/RemoveFavoriteUseCaseTest.php b/src/app/Packages/Features/Tests/CommandUseCases/RemoveFavoriteUseCaseTest.php new file mode 100644 index 00000000..037c4e27 --- /dev/null +++ b/src/app/Packages/Features/Tests/CommandUseCases/RemoveFavoriteUseCaseTest.php @@ -0,0 +1,46 @@ +shouldReceive('removeFavorite') + ->once() + ->with(1, 2); + + (new RemoveFavoriteUseCase($repository))->handle(1, 2); + } + + public function test_handle_propagates_exception_when_user_not_found(): void + { + /** @var FavoriteRepositoryInterface|MockInterface $repository */ + $repository = Mockery::mock(FavoriteRepositoryInterface::class); + $repository->shouldReceive('removeFavorite') + ->once() + ->with(999999, 2) + ->andThrow(new Exception('User not found.')); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('User not found.'); + + (new RemoveFavoriteUseCase($repository))->handle(999999, 2); + } +} From 76bbd1afeff769b56c45b119b9ae1c5abcfc541b Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sun, 16 Aug 2026 22:16:14 +0900 Subject: [PATCH 24/25] feat: add removeFavorite to FavoriteController and route --- .../Controller/FavoriteController.php | 34 +++++++++++++++++++ src/routes/api.php | 1 + 2 files changed, 35 insertions(+) diff --git a/src/app/Packages/Features/Controller/FavoriteController.php b/src/app/Packages/Features/Controller/FavoriteController.php index 10004a59..7a7ce9ed 100644 --- a/src/app/Packages/Features/Controller/FavoriteController.php +++ b/src/app/Packages/Features/Controller/FavoriteController.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Controller; use App\Packages\Features\CommandUseCases\UseCase\Favorite\AddFavoriteUseCase; +use App\Packages\Features\CommandUseCases\UseCase\Favorite\RemoveFavoriteUseCase; use App\Packages\Features\QueryUseCases\UseCase\Favorite\GetFavoriteHeritagesUseCase; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -68,4 +69,37 @@ public function addFavorite( ], 500); } } + + public function removeFavorite( + Request $request, + RemoveFavoriteUseCase $useCase, + ): JsonResponse { + try { + $useCase->handle( + $request->user()->id, + (int) $request->route('world_heritage_id'), + ); + + return response()->json([ + 'status' => 'success', + ], 200); + } catch (Throwable $throwable) { + if ($throwable->getMessage() === 'User not found.') { + return response()->json([ + 'status' => 'error', + 'message' => 'User not found.', + ], 404); + } + + Log::error('Failed to remove favorite', [ + 'message' => $throwable->getMessage(), + 'trace' => $throwable->getTraceAsString(), + ]); + + return response()->json([ + 'status' => 'error', + 'message' => 'Internal Server Error', + ], 500); + } + } } diff --git a/src/routes/api.php b/src/routes/api.php index 4c58d279..83bd0a17 100644 --- a/src/routes/api.php +++ b/src/routes/api.php @@ -31,5 +31,6 @@ Route::controller(FavoriteController::class)->prefix('favorites')->middleware('auth:sanctum')->group(function (): void { Route::get('/', 'getFavorites'); Route::post('/', 'addFavorite'); + Route::delete('/{world_heritage_id}', 'removeFavorite'); }); }); \ No newline at end of file From 4527c4ee472475b45d4a1c67eca233cc3754fdef Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sun, 16 Aug 2026 22:16:14 +0900 Subject: [PATCH 25/25] test: cover removeFavorite in FavoriteControllerTest --- .../Features/Tests/FavoriteControllerTest.php | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/app/Packages/Features/Tests/FavoriteControllerTest.php b/src/app/Packages/Features/Tests/FavoriteControllerTest.php index b9f6a768..1f4229d9 100644 --- a/src/app/Packages/Features/Tests/FavoriteControllerTest.php +++ b/src/app/Packages/Features/Tests/FavoriteControllerTest.php @@ -8,6 +8,7 @@ use App\Packages\Domains\WorldHeritage\Ports\Dto\HeritageSearchResult; use App\Packages\Domains\WorldHeritage\Ports\WorldHeritageSearchPort; use App\Packages\Features\CommandUseCases\UseCase\Favorite\AddFavoriteUseCase; +use App\Packages\Features\CommandUseCases\UseCase\Favorite\RemoveFavoriteUseCase; use App\Packages\Features\QueryUseCases\QueryServiceInterface\WorldHeritageQueryServiceInterface; use App\Packages\Features\QueryUseCases\UseCase\Favorite\GetFavoriteHeritagesUseCase; use Illuminate\Support\Facades\DB; @@ -124,6 +125,68 @@ public function test_addFavorite_returns_500_on_unexpected_error(): void ]); } + public function test_removeFavorite_returns_200_and_removes_favorite_when_authenticated(): void + { + $user = $this->seedUser(); + $worldHeritage = $this->seedWorldHeritage(1); + $token = $user->createToken('auth-token')->plainTextToken; + $user->favorites()->attach($worldHeritage->id); + + $response = $this->withToken($token) + ->deleteJson("/api/v1/favorites/{$worldHeritage->id}"); + + $response->assertStatus(200) + ->assertJsonFragment(['status' => 'success']); + + $this->assertDatabaseMissing('user_favorite', [ + 'user_id' => $user->id, + 'world_heritage_site_id' => $worldHeritage->id, + ]); + } + + public function test_removeFavorite_returns_200_when_not_favorited(): void + { + $user = $this->seedUser(); + $worldHeritage = $this->seedWorldHeritage(1); + $token = $user->createToken('auth-token')->plainTextToken; + + $response = $this->withToken($token) + ->deleteJson("/api/v1/favorites/{$worldHeritage->id}"); + + $response->assertStatus(200) + ->assertJsonFragment(['status' => 'success']); + } + + public function test_removeFavorite_returns_401_when_unauthenticated(): void + { + $worldHeritage = $this->seedWorldHeritage(1); + + $response = $this->deleteJson("/api/v1/favorites/{$worldHeritage->id}"); + + $response->assertStatus(401); + } + + public function test_removeFavorite_returns_500_on_unexpected_error(): void + { + $user = $this->seedUser(); + $token = $user->createToken('auth-token')->plainTextToken; + + $repository = Mockery::mock(FavoriteRepositoryInterface::class); + $repository->shouldReceive('removeFavorite') + ->andThrow(new RuntimeException('Unexpected error')); + + $this->app->instance(RemoveFavoriteUseCase::class, new RemoveFavoriteUseCase($repository)); + + $response = $this->withToken($token) + ->deleteJson('/api/v1/favorites/1'); + + $response->assertStatus(500) + ->assertJsonFragment([ + 'status' => 'error', + 'message' => 'Internal Server Error', + ]); + } + public function test_getFavorites_returns_200_with_favorite_heritages_when_authenticated(): void { $user = $this->seedUser();