diff --git a/backend/app/Http/Controllers/Api/V1/AuthController.php b/backend/app/Http/Controllers/Api/V1/AuthController.php deleted file mode 100644 index a5e4e9e..0000000 --- a/backend/app/Http/Controllers/Api/V1/AuthController.php +++ /dev/null @@ -1,112 +0,0 @@ -validate([ - 'email' => 'required|email', - 'password' => 'required', - ]); - - $user = User::where('email', $request->email)->first(); - - if (!$user || !Hash::check($request->password, $user->password)) { - return $this->sendError('Invalid credentials', [], 401); - } - - $token = $user->createToken('api-token')->plainTextToken; - - return $this->sendResponse([ - 'user' => [ - 'id' => $user->id, - 'name' => $user->name, - 'email' => $user->email, - ], - 'token' => $token, - 'token_type' => 'Bearer', - ], 'Login successful'); - } catch (ValidationException $e) { - return $this->sendValidationError($e->errors()); - } catch (\Exception $e) { - return $this->sendError('Login failed: ' . $e->getMessage(), [], 500); - } - } - - /** - * Register a new user - */ - public function register(Request $request): JsonResponse - { - try { - $validated = $request->validate([ - 'name' => 'required|string|max:255', - 'email' => 'required|string|email|max:255|unique:users', - 'password' => 'required|string|min:8|confirmed', - ]); - - $user = User::create([ - 'name' => $validated['name'], - 'email' => $validated['email'], - 'password' => Hash::make($validated['password']), - ]); - - $token = $user->createToken('api-token')->plainTextToken; - - return $this->sendResponse([ - 'user' => [ - 'id' => $user->id, - 'name' => $user->name, - 'email' => $user->email, - ], - 'token' => $token, - 'token_type' => 'Bearer', - ], 'Registration successful', 201); - } catch (ValidationException $e) { - return $this->sendValidationError($e->errors()); - } catch (\Exception $e) { - return $this->sendError('Registration failed: ' . $e->getMessage(), [], 500); - } - } - - /** - * Logout user (revoke token) - */ - public function logout(Request $request): JsonResponse - { - try { - $request->user()->currentAccessToken()->delete(); - - return $this->sendResponse(null, 'Logged out successfully'); - } catch (\Exception $e) { - return $this->sendError('Logout failed: ' . $e->getMessage(), [], 500); - } - } - - /** - * Get current authenticated user - */ - public function me(Request $request): JsonResponse - { - return $this->sendResponse([ - 'user' => [ - 'id' => $request->user()->id, - 'name' => $request->user()->name, - 'email' => $request->user()->email, - ], - ], 'User retrieved successfully'); - } -} \ No newline at end of file diff --git a/backend/routes/api.php b/backend/routes/api.php index b54f639..15c5da0 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -1,7 +1,6 @@ group(function () { - // Public authentication routes - Route::post('/auth/login', [AuthController::class, 'login'])->name('api.auth.login'); - Route::post('/auth/register', [AuthController::class, 'register'])->name('api.auth.register'); - - // Protected authentication routes - Route::middleware('auth:sanctum')->group(function () { - Route::post('/auth/logout', [AuthController::class, 'logout'])->name('api.auth.logout'); - Route::get('/auth/me', [AuthController::class, 'me'])->name('api.auth.me'); - }); - // Onboarding Route::get('/onboarding/status', [OnboardingController::class, 'status'])->name('api.onboarding.status'); Route::get('/onboarding/options', [OnboardingController::class, 'options'])->name('api.onboarding.options'); diff --git a/backend/tests/Unit/Http/Controllers/Api/V1/BaseControllerTest.php b/backend/tests/Unit/Http/Controllers/Api/V1/BaseControllerTest.php new file mode 100644 index 0000000..4a95207 --- /dev/null +++ b/backend/tests/Unit/Http/Controllers/Api/V1/BaseControllerTest.php @@ -0,0 +1,210 @@ +controller = new BaseController(); + } + + public function test_send_response_returns_success_json_response(): void + { + $data = ['test' => 'data']; + $message = 'Test message'; + $code = 200; + + $response = $this->controller->sendResponse($data, $message, $code); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals($code, $response->getStatusCode()); + + $responseData = json_decode($response->getContent(), true); + $this->assertTrue($responseData['success']); + $this->assertEquals($data, $responseData['data']); + $this->assertEquals($message, $responseData['message']); + } + + public function test_send_response_with_default_parameters(): void + { + $data = ['test' => 'data']; + + $response = $this->controller->sendResponse($data); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals(200, $response->getStatusCode()); + + $responseData = json_decode($response->getContent(), true); + $this->assertTrue($responseData['success']); + $this->assertEquals($data, $responseData['data']); + $this->assertEquals('Success', $responseData['message']); + } + + public function test_send_response_with_null_data(): void + { + $response = $this->controller->sendResponse(null, 'Test message'); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals(200, $response->getStatusCode()); + + $responseData = json_decode($response->getContent(), true); + $this->assertTrue($responseData['success']); + $this->assertNull($responseData['data']); + $this->assertEquals('Test message', $responseData['message']); + } + + public function test_send_error_returns_error_json_response(): void + { + $error = 'Test error'; + $errorMessages = ['field' => ['error message']]; + $code = 400; + + $response = $this->controller->sendError($error, $errorMessages, $code); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals($code, $response->getStatusCode()); + + $responseData = json_decode($response->getContent(), true); + $this->assertFalse($responseData['success']); + $this->assertEquals($error, $responseData['message']); + $this->assertEquals($errorMessages, $responseData['errors']); + } + + public function test_send_error_without_error_messages(): void + { + $error = 'Test error'; + + $response = $this->controller->sendError($error); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals(400, $response->getStatusCode()); + + $responseData = json_decode($response->getContent(), true); + $this->assertFalse($responseData['success']); + $this->assertEquals($error, $responseData['message']); + $this->assertArrayNotHasKey('errors', $responseData); + } + + public function test_send_error_with_empty_error_messages(): void + { + $error = 'Test error'; + $errorMessages = []; + + $response = $this->controller->sendError($error, $errorMessages); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals(400, $response->getStatusCode()); + + $responseData = json_decode($response->getContent(), true); + $this->assertFalse($responseData['success']); + $this->assertEquals($error, $responseData['message']); + $this->assertArrayNotHasKey('errors', $responseData); + } + + public function test_send_validation_error_returns_422_status(): void + { + $errors = [ + 'email' => ['Email is required'], + 'password' => ['Password must be at least 8 characters'] + ]; + + $response = $this->controller->sendValidationError($errors); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals(422, $response->getStatusCode()); + + $responseData = json_decode($response->getContent(), true); + $this->assertFalse($responseData['success']); + $this->assertEquals('Validation failed', $responseData['message']); + $this->assertEquals($errors, $responseData['errors']); + } + + public function test_send_not_found_returns_404_status(): void + { + $message = 'Custom not found message'; + + $response = $this->controller->sendNotFound($message); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals(404, $response->getStatusCode()); + + $responseData = json_decode($response->getContent(), true); + $this->assertFalse($responseData['success']); + $this->assertEquals($message, $responseData['message']); + $this->assertArrayNotHasKey('errors', $responseData); + } + + public function test_send_not_found_with_default_message(): void + { + $response = $this->controller->sendNotFound(); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals(404, $response->getStatusCode()); + + $responseData = json_decode($response->getContent(), true); + $this->assertFalse($responseData['success']); + $this->assertEquals('Resource not found', $responseData['message']); + $this->assertArrayNotHasKey('errors', $responseData); + } + + public function test_send_unauthorized_returns_401_status(): void + { + $message = 'Custom unauthorized message'; + + $response = $this->controller->sendUnauthorized($message); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals(401, $response->getStatusCode()); + + $responseData = json_decode($response->getContent(), true); + $this->assertFalse($responseData['success']); + $this->assertEquals($message, $responseData['message']); + $this->assertArrayNotHasKey('errors', $responseData); + } + + public function test_send_unauthorized_with_default_message(): void + { + $response = $this->controller->sendUnauthorized(); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals(401, $response->getStatusCode()); + + $responseData = json_decode($response->getContent(), true); + $this->assertFalse($responseData['success']); + $this->assertEquals('Unauthorized', $responseData['message']); + $this->assertArrayNotHasKey('errors', $responseData); + } + + public function test_response_structure_consistency(): void + { + // Test that all response methods return consistent JSON structure + $successResponse = $this->controller->sendResponse(['data'], 'Success'); + $errorResponse = $this->controller->sendError('Error'); + $validationResponse = $this->controller->sendValidationError(['field' => ['error']]); + $notFoundResponse = $this->controller->sendNotFound(); + $unauthorizedResponse = $this->controller->sendUnauthorized(); + + $responses = [ + json_decode($successResponse->getContent(), true), + json_decode($errorResponse->getContent(), true), + json_decode($validationResponse->getContent(), true), + json_decode($notFoundResponse->getContent(), true), + json_decode($unauthorizedResponse->getContent(), true), + ]; + + foreach ($responses as $response) { + $this->assertArrayHasKey('success', $response); + $this->assertArrayHasKey('message', $response); + $this->assertIsBool($response['success']); + $this->assertIsString($response['message']); + } + } +} \ No newline at end of file diff --git a/backend/tests/Unit/Http/Controllers/ControllerTest.php b/backend/tests/Unit/Http/Controllers/ControllerTest.php new file mode 100644 index 0000000..7a06ba5 --- /dev/null +++ b/backend/tests/Unit/Http/Controllers/ControllerTest.php @@ -0,0 +1,105 @@ +assertTrue($reflection->isAbstract()); + } + + public function test_controller_can_be_extended(): void + { + $testController = new class extends Controller { + public function testMethod(): string + { + return 'test'; + } + }; + + $this->assertInstanceOf(Controller::class, $testController); + $this->assertEquals('test', $testController->testMethod()); + } + + public function test_controller_can_be_used_as_base_class(): void + { + $reflection = new \ReflectionClass(Controller::class); + + // Check that it's an abstract class that can be extended + $this->assertTrue($reflection->isAbstract()); + $this->assertNotNull($reflection->getName()); + } + + public function test_controller_namespace_is_correct(): void + { + $reflection = new \ReflectionClass(Controller::class); + + $this->assertEquals('App\Http\Controllers', $reflection->getNamespaceName()); + } + + public function test_controller_has_no_methods(): void + { + $reflection = new \ReflectionClass(Controller::class); + $methods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC); + + // Filter out methods inherited from parent classes + $ownMethods = array_filter($methods, function ($method) { + return $method->getDeclaringClass()->getName() === Controller::class; + }); + + $this->assertEmpty($ownMethods, 'Controller should not define any methods of its own'); + } + + public function test_controller_has_no_properties(): void + { + $reflection = new \ReflectionClass(Controller::class); + $properties = $reflection->getProperties(); + + // Filter out properties inherited from parent classes + $ownProperties = array_filter($properties, function ($property) { + return $property->getDeclaringClass()->getName() === Controller::class; + }); + + $this->assertEmpty($ownProperties, 'Controller should not define any properties of its own'); + } + + public function test_multiple_inheritance_works(): void + { + $firstController = new class extends Controller { + public function method1(): string + { + return 'first'; + } + }; + + $secondController = new class extends Controller { + public function method2(): string + { + return 'second'; + } + }; + + $this->assertInstanceOf(Controller::class, $firstController); + $this->assertInstanceOf(Controller::class, $secondController); + $this->assertEquals('first', $firstController->method1()); + $this->assertEquals('second', $secondController->method2()); + } + + public function test_controller_can_be_used_as_type_hint(): void + { + $testFunction = function (Controller $controller): bool { + return $controller instanceof Controller; + }; + + $testController = new class extends Controller { + }; + + $this->assertTrue($testFunction($testController)); + } +} \ No newline at end of file diff --git a/backend/tests/Unit/Http/Middleware/HandleAppearanceTest.php b/backend/tests/Unit/Http/Middleware/HandleAppearanceTest.php new file mode 100644 index 0000000..3b04ecb --- /dev/null +++ b/backend/tests/Unit/Http/Middleware/HandleAppearanceTest.php @@ -0,0 +1,150 @@ +middleware = new HandleAppearance(); + } + + public function test_handle_shares_appearance_from_cookie(): void + { + View::shouldReceive('share') + ->once() + ->with('appearance', 'dark'); + + $request = Request::create('/test'); + $request->cookies->set('appearance', 'dark'); + + $next = function ($request) { + return new Response(); + }; + + $response = $this->middleware->handle($request, $next); + + $this->assertInstanceOf(Response::class, $response); + } + + public function test_handle_uses_system_as_default_when_no_cookie(): void + { + View::shouldReceive('share') + ->once() + ->with('appearance', 'system'); + + $request = Request::create('/test'); + + $next = function ($request) { + return new Response(); + }; + + $response = $this->middleware->handle($request, $next); + + $this->assertInstanceOf(Response::class, $response); + } + + public function test_handle_shares_light_appearance(): void + { + View::shouldReceive('share') + ->once() + ->with('appearance', 'light'); + + $request = Request::create('/test'); + $request->cookies->set('appearance', 'light'); + + $next = function ($request) { + return new Response(); + }; + + $response = $this->middleware->handle($request, $next); + + $this->assertInstanceOf(Response::class, $response); + } + + public function test_handle_passes_request_to_next_middleware(): void + { + View::shouldReceive('share') + ->once() + ->with('appearance', 'system'); + + $request = Request::create('/test'); + $expectedResponse = new Response('Test content', 200); + + $next = function ($passedRequest) use ($request, $expectedResponse) { + $this->assertSame($request, $passedRequest); + return $expectedResponse; + }; + + $response = $this->middleware->handle($request, $next); + + $this->assertSame($expectedResponse, $response); + } + + public function test_handle_with_custom_appearance_value(): void + { + View::shouldReceive('share') + ->once() + ->with('appearance', 'custom-theme'); + + $request = Request::create('/test'); + $request->cookies->set('appearance', 'custom-theme'); + + $next = function ($request) { + return new Response(); + }; + + $response = $this->middleware->handle($request, $next); + + $this->assertInstanceOf(Response::class, $response); + } + + public function test_handle_with_empty_cookie_value(): void + { + View::shouldReceive('share') + ->once() + ->with('appearance', ''); + + $request = Request::create('/test'); + $request->cookies->set('appearance', ''); + + $next = function ($request) { + return new Response(); + }; + + $response = $this->middleware->handle($request, $next); + + $this->assertInstanceOf(Response::class, $response); + } + + public function test_handle_preserves_response_status_and_headers(): void + { + View::shouldReceive('share') + ->once() + ->with('appearance', 'system'); + + $request = Request::create('/test'); + $expectedHeaders = ['X-Test-Header' => 'test-value']; + + $next = function ($request) use ($expectedHeaders) { + $response = new Response('Test content', 201); + $response->headers->add($expectedHeaders); + return $response; + }; + + $response = $this->middleware->handle($request, $next); + + $this->assertEquals(201, $response->getStatusCode()); + $this->assertEquals('Test content', $response->getContent()); + $this->assertEquals('test-value', $response->headers->get('X-Test-Header')); + } +} \ No newline at end of file diff --git a/backend/tests/Unit/Http/Middleware/HandleInertiaRequestsTest.php b/backend/tests/Unit/Http/Middleware/HandleInertiaRequestsTest.php new file mode 100644 index 0000000..5e60226 --- /dev/null +++ b/backend/tests/Unit/Http/Middleware/HandleInertiaRequestsTest.php @@ -0,0 +1,116 @@ +middleware = new HandleInertiaRequests(); + } + + public function test_root_view_is_set_to_app(): void + { + $reflection = new \ReflectionClass($this->middleware); + $property = $reflection->getProperty('rootView'); + $property->setAccessible(true); + + $this->assertEquals('app', $property->getValue($this->middleware)); + } + + public function test_version_calls_parent_version(): void + { + $request = Request::create('/test'); + + // Since this method calls parent::version(), we're testing that it doesn't throw an exception + // and returns the expected type (string or null) + $version = $this->middleware->version($request); + + $this->assertTrue(is_string($version) || is_null($version)); + } + + public function test_share_returns_array_with_parent_data(): void + { + $request = Request::create('/test'); + + $shared = $this->middleware->share($request); + + $this->assertIsArray($shared); + // Since we're merging with parent::share(), we expect at least some basic Inertia data + // The exact keys depend on the parent implementation, but it should be an array + } + + public function test_share_merges_parent_data_correctly(): void + { + $request = Request::create('/test'); + + $shared = $this->middleware->share($request); + + // Test that the method returns an array and doesn't throw exceptions + $this->assertIsArray($shared); + + // Since the implementation currently only returns parent data merged with empty array, + // we can test that the structure is maintained + $this->assertNotNull($shared); + } + + public function test_middleware_can_be_instantiated(): void + { + $this->assertInstanceOf(HandleInertiaRequests::class, $this->middleware); + $this->assertInstanceOf(\Inertia\Middleware::class, $this->middleware); + } + + public function test_share_method_is_callable(): void + { + $request = Request::create('/test'); + + $this->assertTrue(method_exists($this->middleware, 'share')); + $this->assertTrue(is_callable([$this->middleware, 'share'])); + + // Ensure the method can be called without throwing exceptions + $result = $this->middleware->share($request); + $this->assertIsArray($result); + } + + public function test_version_method_is_callable(): void + { + $request = Request::create('/test'); + + $this->assertTrue(method_exists($this->middleware, 'version')); + $this->assertTrue(is_callable([$this->middleware, 'version'])); + + // Ensure the method can be called without throwing exceptions + $result = $this->middleware->version($request); + $this->assertTrue(is_string($result) || is_null($result)); + } + + public function test_share_with_different_request_methods(): void + { + $getMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']; + + foreach ($getMethods as $method) { + $request = Request::create('/test', $method); + $shared = $this->middleware->share($request); + + $this->assertIsArray($shared, "Failed for {$method} method"); + } + } + + public function test_share_with_request_containing_data(): void + { + $request = Request::create('/test', 'POST', ['key' => 'value']); + $request->headers->set('X-Custom-Header', 'test-value'); + + $shared = $this->middleware->share($request); + + $this->assertIsArray($shared); + // The middleware should handle requests with data without issues + } +} \ No newline at end of file