Improve coverage

This commit is contained in:
myrmidex 2025-08-15 22:09:55 +02:00
parent e986f7871b
commit cb276cf81d
6 changed files with 581 additions and 123 deletions

View file

@ -1,112 +0,0 @@
<?php
namespace App\Http\Controllers\Api\V1;
use Domains\User\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends BaseController
{
/**
* Login user and create token
*/
public function login(Request $request): JsonResponse
{
try {
$request->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');
}
}

View file

@ -1,7 +1,6 @@
<?php <?php
use App\Http\Controllers\Api\V1\ArticlesController; use App\Http\Controllers\Api\V1\ArticlesController;
use App\Http\Controllers\Api\V1\AuthController;
use App\Http\Controllers\Api\V1\DashboardController; use App\Http\Controllers\Api\V1\DashboardController;
use App\Http\Controllers\Api\V1\FeedsController; use App\Http\Controllers\Api\V1\FeedsController;
use App\Http\Controllers\Api\V1\LogsController; use App\Http\Controllers\Api\V1\LogsController;
@ -25,16 +24,6 @@
*/ */
Route::prefix('v1')->group(function () { Route::prefix('v1')->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 // Onboarding
Route::get('/onboarding/status', [OnboardingController::class, 'status'])->name('api.onboarding.status'); Route::get('/onboarding/status', [OnboardingController::class, 'status'])->name('api.onboarding.status');
Route::get('/onboarding/options', [OnboardingController::class, 'options'])->name('api.onboarding.options'); Route::get('/onboarding/options', [OnboardingController::class, 'options'])->name('api.onboarding.options');

View file

@ -0,0 +1,210 @@
<?php
namespace Tests\Unit\Http\Controllers\Api\V1;
use App\Http\Controllers\Api\V1\BaseController;
use Illuminate\Http\JsonResponse;
use Tests\TestCase;
class BaseControllerTest extends TestCase
{
protected BaseController $controller;
protected function setUp(): void
{
parent::setUp();
$this->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']);
}
}
}

View file

@ -0,0 +1,105 @@
<?php
namespace Tests\Unit\Http\Controllers;
use App\Http\Controllers\Controller;
use Tests\TestCase;
class ControllerTest extends TestCase
{
public function test_controller_is_abstract(): void
{
$reflection = new \ReflectionClass(Controller::class);
$this->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));
}
}

View file

@ -0,0 +1,150 @@
<?php
namespace Tests\Unit\Http\Middleware;
use App\Http\Middleware\HandleAppearance;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\View;
use Tests\TestCase;
class HandleAppearanceTest extends TestCase
{
protected HandleAppearance $middleware;
protected function setUp(): void
{
parent::setUp();
$this->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'));
}
}

View file

@ -0,0 +1,116 @@
<?php
namespace Tests\Unit\Http\Middleware;
use App\Http\Middleware\HandleInertiaRequests;
use Illuminate\Http\Request;
use Tests\TestCase;
class HandleInertiaRequestsTest extends TestCase
{
protected HandleInertiaRequests $middleware;
protected function setUp(): void
{
parent::setUp();
$this->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
}
}