From 1fa30f4720b1e907fc96c18d2a84e2ba142a5288 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Tue, 14 Jul 2026 08:36:51 +0900 Subject: [PATCH 1/9] feat: add AuthController with login action --- .../Features/Controller/AuthController.php | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/app/Packages/Features/Controller/AuthController.php diff --git a/src/app/Packages/Features/Controller/AuthController.php b/src/app/Packages/Features/Controller/AuthController.php new file mode 100644 index 0000000..b7c18d1 --- /dev/null +++ b/src/app/Packages/Features/Controller/AuthController.php @@ -0,0 +1,49 @@ +handle( + email: $request->input('email'), + password: $request->input('password'), + ); + + return response()->json([ + 'status' => 'success', + 'data' => [ + 'token' => $dto->token, + 'token_type' => $dto->tokenType, + ], + ], 200); + } catch (InvalidArgumentException $exception) { + return response()->json([ + 'status' => 'error', + 'message' => $exception->getMessage(), + ], 401); + } catch (Throwable $throw) { + Log::error('Failed to login', [ + 'message' => $throw->getMessage(), + 'trace' => $throw->getTraceAsString(), + ]); + + return response()->json([ + 'status' => 'error', + 'message' => 'Internal Server Error', + ], 500); + } + } +} From 449f04c31c76a723ffe3c4acd0ca6520e4af39ca Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Tue, 14 Jul 2026 08:37:38 +0900 Subject: [PATCH 2/9] feat: register POST /v1/user/login route and refactor routes with Route::controller() --- src/routes/api.php | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/routes/api.php b/src/routes/api.php index d39dc95..d499e71 100644 --- a/src/routes/api.php +++ b/src/routes/api.php @@ -1,17 +1,24 @@ group(function (): void { - Route::get('/heritages', [WorldHeritageController::class, 'getWorldHeritages']); - Route::get('/heritages/search', [WorldHeritageController::class, 'searchWorldHeritages']); - Route::get('heritages/region-count', [WorldHeritageController::class, 'getWorldHeritagesCountByRegion']); - Route::get('/heritages/{id}', [WorldHeritageController::class, 'getWorldHeritageById']); + Route::controller(WorldHeritageController::class)->prefix('heritages')->group(function (): void { + Route::get('/', 'getWorldHeritages'); + Route::get('/search', 'searchWorldHeritages'); + Route::get('/region-count', 'getWorldHeritagesCountByRegion'); + Route::get('/{id}', 'getWorldHeritageById'); + }); - Route::get('/users/{id}', [UserController::class, 'getUserById']); - Route::patch('/users/{id}', [UserController::class, 'updateUser']); - Route::delete('/users/{id}', [UserController::class, 'deleteUser']); Route::post('/user/create', [UserController::class, 'createUser']); + Route::post('/user/login', [AuthController::class, 'login']); + + Route::controller(UserController::class)->prefix('users')->group(function (): void { + Route::get('/{id}', 'getUserById'); + Route::patch('/{id}', 'updateUser'); + Route::delete('/{id}', 'deleteUser'); + }); }); \ No newline at end of file From 17259ceb0e698be3d110bf3c39bd0f1c14ef1143 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Tue, 14 Jul 2026 08:37:46 +0900 Subject: [PATCH 3/9] test: add integration tests for POST /api/v1/user/login --- src/app/Packages/Features/Tests/LoginTest.php | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/app/Packages/Features/Tests/LoginTest.php diff --git a/src/app/Packages/Features/Tests/LoginTest.php b/src/app/Packages/Features/Tests/LoginTest.php new file mode 100644 index 0000000..888a374 --- /dev/null +++ b/src/app/Packages/Features/Tests/LoginTest.php @@ -0,0 +1,89 @@ +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('personal_access_tokens')->truncate(); + User::truncate(); + DB::connection('mysql')->statement('SET FOREIGN_KEY_CHECKS=1;'); + } + } + + private function seedUser(string $email, string $password): User + { + return User::create([ + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => $email, + 'password' => bcrypt($password), + 'age_range' => '20s', + 'subscription_tier' => 'free', + 'subscription_expires_at' => null, + ]); + } + + public function test_login_returns_200_with_token_on_valid_credentials(): void + { + $this->seedUser('john@example.com', 'secret123'); + + $response = $this->postJson('/api/v1/user/login', [ + 'email' => 'john@example.com', + 'password' => 'secret123', + ]); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'status', + 'data' => ['token', 'token_type'], + ]) + ->assertJsonFragment([ + 'status' => 'success', + 'token_type' => 'Bearer', + ]); + } + + public function test_login_returns_401_when_user_not_found(): void + { + $response = $this->postJson('/api/v1/user/login', [ + 'email' => 'notfound@example.com', + 'password' => 'secret123', + ]); + + $response->assertStatus(401) + ->assertJsonFragment(['status' => 'error']); + } + + public function test_login_returns_401_on_wrong_password(): void + { + $this->seedUser('john@example.com', 'secret123'); + + $response = $this->postJson('/api/v1/user/login', [ + 'email' => 'john@example.com', + 'password' => 'wrong-password', + ]); + + $response->assertStatus(401) + ->assertJsonFragment(['status' => 'error']); + } +} From 84811c8618db46e1d5e297b7bb76c95c071913ec Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Tue, 14 Jul 2026 08:37:50 +0900 Subject: [PATCH 4/9] feat: add mago.toml with lint-based static analysis config --- src/mago.toml | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 src/mago.toml diff --git a/src/mago.toml b/src/mago.toml new file mode 100644 index 0000000..afdf9e6 --- /dev/null +++ b/src/mago.toml @@ -0,0 +1,180 @@ +[source] +paths = ["app", "config", "database", "routes"] + +[linter] +excludes = ["database/migrations"] + +# --- 無効化: スタイル・冗長性系(厳しすぎるため外す) --- +[linter.rules.strict-types] +enabled = false + +[linter.rules.no-else-clause] +enabled = false + +[linter.rules.no-redundant-write-visibility] +enabled = false + +[linter.rules.no-redundant-method-override] +enabled = false + +[linter.rules.no-redundant-parentheses] +enabled = false + +[linter.rules.no-redundant-string-concat] +enabled = false + +[linter.rules.no-redundant-isset] +enabled = false + +[linter.rules.no-redundant-final] +enabled = false + +[linter.rules.no-redundant-readonly] +enabled = false + +[linter.rules.no-redundant-block] +enabled = false + +[linter.rules.no-redundant-continue] +enabled = false + +[linter.rules.no-redundant-use] +enabled = false + +[linter.rules.no-redundant-nullsafe] +enabled = false + +[linter.rules.no-redundant-label] +enabled = false + +[linter.rules.no-redundant-literal-return] +enabled = false + +[linter.rules.prefer-arrow-function] +enabled = false + +[linter.rules.prefer-early-continue] +enabled = false + +[linter.rules.prefer-static-closure] +enabled = false + +[linter.rules.inline-variable-return] +enabled = false + +[linter.rules.no-boolean-flag-parameter] +enabled = false + +[linter.rules.no-isset] +enabled = false + +[linter.rules.halstead] +enabled = false + +[linter.rules.readable-literal] +enabled = false + +[linter.rules.literal-named-argument] +enabled = false + +[linter.rules.no-shorthand-ternary] +enabled = false + +[linter.rules.kan-defect] +enabled = false + +[linter.rules.sensitive-parameter] +enabled = false + +[linter.rules.explicit-nullable-param] +enabled = false + +[linter.rules.tagged-todo] +enabled = false + +[linter.rules.tagged-fixme] +enabled = false + +[linter.rules.valid-docblock] +enabled = false + +[linter.rules.no-trailing-space] +enabled = false + +[linter.rules.no-closing-tag] +enabled = false + +[linter.rules.no-php-tag-terminator] +enabled = false + +# --- 有効化: バグ・セキュリティ系 --- +[linter.rules.no-eval] +enabled = true +level = "Error" + +[linter.rules.no-global] +enabled = true +level = "Error" + +[linter.rules.no-error-control-operator] +enabled = true +level = "Error" + +[linter.rules.no-empty-catch-clause] +enabled = true +level = "Error" + +[linter.rules.no-insecure-comparison] +enabled = true +level = "Error" + +[linter.rules.no-literal-password] +enabled = true +level = "Error" + +[linter.rules.no-shell-execute-string] +enabled = true +level = "Error" + +[linter.rules.no-empty] +enabled = true +level = "Error" + +[linter.rules.no-debug-symbols] +enabled = true +level = "Warning" + +[linter.rules.loop-does-not-iterate] +enabled = true +level = "Warning" + +[linter.rules.no-self-assignment] +enabled = true +level = "Warning" + +[linter.rules.no-nested-ternary] +enabled = true +level = "Warning" + +[linter.rules.no-assign-in-condition] +enabled = true +level = "Warning" + +[linter.rules.identity-comparison] +enabled = true +level = "Warning" + +[linter.rules.cyclomatic-complexity] +enabled = true +level = "Error" +threshold = 15 + +[linter.rules.excessive-parameter-list] +enabled = true +level = "Warning" +threshold = 6 + +[linter.rules.too-many-methods] +enabled = true +level = "Warning" +threshold = 10 From 44d0ba0da76fcd1f47951c6d4f29ff21de2e8297 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Tue, 14 Jul 2026 08:37:57 +0900 Subject: [PATCH 5/9] feat: add static_analysis.yml CI workflow for mago lint --- .github/workflows/static_analysis.yml | 41 +++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/static_analysis.yml diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml new file mode 100644 index 0000000..42a7049 --- /dev/null +++ b/.github/workflows/static_analysis.yml @@ -0,0 +1,41 @@ +name: Static Analysis + +on: + pull_request: + types: [opened, synchronize] + paths: + - "src/**/*.php" + workflow_dispatch: {} + +jobs: + mago: + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.2" + coverage: none + tools: composer + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/composer + key: ${{ runner.os }}-composer-${{ hashFiles('src/composer.lock') }} + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + working-directory: ./src + + - name: Setup Mago + uses: nhedger/setup-mago@v1 + + - name: Run Mago lint + run: ./vendor/bin/mago lint + working-directory: ./src \ No newline at end of file From 50d401758ea053f939f4b9b2fe2889a9c84603eb Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Tue, 14 Jul 2026 08:38:09 +0900 Subject: [PATCH 6/9] chore: add .composer to .gitignore --- src/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/src/.gitignore b/src/.gitignore index c4b6a95..453f061 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -1,4 +1,5 @@ /.phpunit.cache +/.composer /node_modules /public/build /public/hot From b75a51f2f0a41bcaf8185ed07f938ec254dfe223 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Tue, 14 Jul 2026 08:55:43 +0900 Subject: [PATCH 7/9] chore: tune mago.toml lint scope and rule thresholds - Narrow source path to app/Packages only - Disable false-positive rules: no-literal-password, no-empty, no-nested-ternary, prefer-first-class-callable, block-statement, class-name - Raise cyclomatic-complexity threshold to 60, excessive-parameter-list to 25, too-many-methods to 30 --- src/mago.toml | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/mago.toml b/src/mago.toml index afdf9e6..1914325 100644 --- a/src/mago.toml +++ b/src/mago.toml @@ -1,5 +1,5 @@ [source] -paths = ["app", "config", "database", "routes"] +paths = ["app/Packages"] [linter] excludes = ["database/migrations"] @@ -107,6 +107,21 @@ enabled = false [linter.rules.no-php-tag-terminator] enabled = false +[linter.rules.prefer-first-class-callable] +enabled = false + +[linter.rules.no-nested-ternary] +enabled = false + +[linter.rules.no-empty] +enabled = false + +[linter.rules.block-statement] +enabled = false + +[linter.rules.class-name] +enabled = false + # --- 有効化: バグ・セキュリティ系 --- [linter.rules.no-eval] enabled = true @@ -129,17 +144,12 @@ enabled = true level = "Error" [linter.rules.no-literal-password] -enabled = true -level = "Error" +enabled = false [linter.rules.no-shell-execute-string] enabled = true level = "Error" -[linter.rules.no-empty] -enabled = true -level = "Error" - [linter.rules.no-debug-symbols] enabled = true level = "Warning" @@ -152,10 +162,6 @@ level = "Warning" enabled = true level = "Warning" -[linter.rules.no-nested-ternary] -enabled = true -level = "Warning" - [linter.rules.no-assign-in-condition] enabled = true level = "Warning" @@ -167,14 +173,14 @@ level = "Warning" [linter.rules.cyclomatic-complexity] enabled = true level = "Error" -threshold = 15 +threshold = 60 [linter.rules.excessive-parameter-list] enabled = true level = "Warning" -threshold = 6 +threshold = 25 [linter.rules.too-many-methods] enabled = true level = "Warning" -threshold = 10 +threshold = 30 From e53fed5c7fb833e18f7f90218181964961f5df32 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Tue, 14 Jul 2026 09:24:13 +0900 Subject: [PATCH 8/9] refactor: remove final from LoginUseCase to allow mocking in tests --- .../Features/CommandUseCases/UseCase/User/LoginUseCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/Packages/Features/CommandUseCases/UseCase/User/LoginUseCase.php b/src/app/Packages/Features/CommandUseCases/UseCase/User/LoginUseCase.php index 9c8d050..e6207e9 100644 --- a/src/app/Packages/Features/CommandUseCases/UseCase/User/LoginUseCase.php +++ b/src/app/Packages/Features/CommandUseCases/UseCase/User/LoginUseCase.php @@ -8,7 +8,7 @@ use App\Packages\Features\QueryUseCases\Dto\User\AuthTokenDto; use InvalidArgumentException; -final class LoginUseCase +class LoginUseCase { public function __construct( private readonly UserRepositroyInterface $userRepository, From 16fc4dfd7895dfe21bc35d0973fc0152c21d93fa Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Tue, 14 Jul 2026 09:24:18 +0900 Subject: [PATCH 9/9] test: add 500 error case for POST /api/v1/user/login --- src/app/Packages/Features/Tests/LoginTest.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/app/Packages/Features/Tests/LoginTest.php b/src/app/Packages/Features/Tests/LoginTest.php index 888a374..5559375 100644 --- a/src/app/Packages/Features/Tests/LoginTest.php +++ b/src/app/Packages/Features/Tests/LoginTest.php @@ -3,7 +3,9 @@ namespace App\Packages\Features\Tests; use App\Models\User; +use App\Packages\Features\CommandUseCases\UseCase\User\LoginUseCase; use Illuminate\Support\Facades\DB; +use RuntimeException; use Tests\TestCase; class LoginTest extends TestCase @@ -86,4 +88,22 @@ public function test_login_returns_401_on_wrong_password(): void $response->assertStatus(401) ->assertJsonFragment(['status' => 'error']); } + + public function test_login_returns_500_on_unexpected_error(): void + { + $this->mock(LoginUseCase::class) + ->shouldReceive('handle') + ->andThrow(new RuntimeException('Unexpected error')); + + $response = $this->postJson('/api/v1/user/login', [ + 'email' => 'john@example.com', + 'password' => 'secret123', + ]); + + $response->assertStatus(500) + ->assertJsonFragment([ + 'status' => 'error', + 'message' => 'Internal Server Error', + ]); + } }