Initial commit

This commit is contained in:
2026-02-06 23:26:56 +07:00
commit d6022b9bca
92 changed files with 41386 additions and 0 deletions

View File

@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace Tests\Feature;
use App\Contracts\AuthServiceContract;
use App\Data\Auth\LoginData;
use App\Data\Auth\LoginResult;
use App\Enums\LoginError;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Laravel\Sanctum\PersonalAccessToken;
use Mockery;
use Tests\TestCase;
class AuthControllerTest extends TestCase
{
private const string LOGIN_URL = '/api/login';
private const string LOGOUT_URL = '/api/logout';
public function test_it_returns_correct_error_for_invalid_credentials(): void
{
User::factory()->create([
'email' => 'test@example.com',
'password' => Hash::make('password123'),
]);
$response = $this->postJson(self::LOGIN_URL, [
'email' => 'test@example.com',
'password' => 'ne_password123',
]);
$response->assertStatus(401)
->assertJson([
'message' => 'Invalid credentials',
]);
}
public function test_it_returns_success_response(): void
{
User::factory()->create([
'email' => 'test@example.com',
'password' => Hash::make('password123'),
]);
$response = $this->postJson(self::LOGIN_URL, [
'email' => 'test@example.com',
'password' => 'password123',
]);
$response->assertStatus(200)
->assertJsonStructure([
'token',
]);
}
public function test_it_handles_server_error_from_service(): void
{
$mock = $this->mock(AuthServiceContract::class);
$mock->shouldReceive('attemptLogin')
->once()
->with(Mockery::type(LoginData::class))
->andReturn(LoginResult::error(LoginError::SERVER_ERROR));
$response = $this->postJson(self::LOGIN_URL, [
'email' => 'test@example.com',
'password' => 'password123',
]);
$response->assertStatus(500)
->assertJson([
'message' => 'Authentication failed',
]);
}
public function test_user_can_logout_successfully(): void
{
User::factory()->create([
'email' => 'test@example.com',
'password' => Hash::make('password123'),
]);
$loginResponse = $this->postJson(self::LOGIN_URL, [
'email' => 'test@example.com',
'password' => 'password123',
]);
$token = $loginResponse->json('token');
$tokenBefore = PersonalAccessToken::findToken($token);
$this->assertNotNull($tokenBefore, 'Token should exist before logout');
$logoutResponse = $this->postJson(self::LOGOUT_URL, [], [
'Authorization' => 'Bearer ' . $token,
]);
$logoutResponse->assertStatus(200)
->assertJson([
'message' => 'Logged out',
]);
$tokenAfter = PersonalAccessToken::findToken($token);
$this->assertNull($tokenAfter);
$this->refreshApplication();
$tasksResponse = $this->getJson('/api/tasks', [
'Authorization' => 'Bearer ' . $token,
]);
$tasksResponse->assertStatus(401);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Tests\Feature;
// use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*/
public function test_the_application_returns_a_successful_response(): void
{
$response = $this->get('/');
$response->assertStatus(200);
}
}

View File

@@ -0,0 +1,203 @@
<?php
namespace Tests\Feature;
use App\Enums\TaskStatus;
use App\Models\Task;
use App\Models\User;
use App\Notifications\TaskCreatedNotification;
use App\Notifications\TaskOverdueNotification;
use Laravel\Sanctum\Sanctum;
use Notification;
use Tests\TestCase;
use Throwable;
class TaskApiTest extends TestCase
{
public function test_guest_cannot_access_tasks_endpoints(): void
{
$this->getJson('/api/tasks')
->assertUnauthorized();
$this->postJson('/api/tasks')
->assertUnauthorized();
}
public function test_authenticated_user_can_create_task(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$payload = [
'title' => 'Новая задача',
'description' => 'Описание задачи',
'status' => TaskStatus::PENDING,
'due_date' => now()->addDay()->toDateString(),
];
$response = $this->postJson('/api/tasks', $payload);
$response
->assertCreated()
->assertJsonFragment([
'title' => 'Новая задача',
'status' => TaskStatus::PENDING,
]);
$this->assertDatabaseHas('tasks', [
'title' => 'Новая задача',
'user_id' => $user->id,
]);
}
/**
* @throws Throwable
*/
public function test_authenticated_user_can_create_task_and_notification_is_queued(): void
{
Notification::fake();
$user = User::factory()->create();
Sanctum::actingAs($user);
$payload = [
'title' => 'Новая задача',
'description' => 'Описание задачи',
'status' => TaskStatus::PENDING,
'due_date' => now()->addDay()->toDateString(),
];
$response = $this->postJson('/api/tasks', $payload);
$response
->assertCreated()
->assertHeader('Location')
->assertJsonFragment([
'title' => 'Новая задача',
'status' => TaskStatus::PENDING,
]);
$task = Task::where('title', 'Новая задача')->firstOrFail();
Notification::assertSentTo(
$user,
TaskCreatedNotification::class,
function (TaskCreatedNotification $notification) use ($task) {
return $notification->task->id === $task->id;
}
);
}
public function test_user_cannot_access_task_of_another_user(): void
{
$owner = User::factory()->create();
$wrong = User::factory()->create();
/**
* @var Task $task
*/
$task = Task::factory()->for($owner)->create();
Sanctum::actingAs($wrong);
$this->getJson("/api/tasks/$task->id")
->assertForbidden();
$this->putJson("/api/tasks/$task->id", [
'title' => 'йоу',
])->assertForbidden();
$this->deleteJson("/api/tasks/$task->id")
->assertForbidden();
}
/**
* @throws Throwable
*/
public function test_overdue_task_sends_notification(): void
{
Notification::fake();
$user = User::factory()->create();
/**
* @var Task $task
*/
$task = Task::factory()->for($user)->create([
'status' => TaskStatus::PENDING,
'due_date' => now()->subDay(),
'notified_at' => null,
]);
$this->artisan('tasks:notify-overdue')
->assertExitCode(0);
Notification::assertSentTo(
$user,
TaskOverdueNotification::class,
fn($notification) => $notification->task->id === $task->id
);
}
public function test_real_user_can_login_and_create_task(): void
{
$user = User::where('email', 'yo_yo@example.com')->first();
$loginResponse = $this->postJson('/api/login', [
'email' => 'yo_yo@example.com',
'password' => '123',
]);
$loginResponse->assertOk();
$token = $loginResponse->json('token');
$this->assertNotEmpty($token);
$payload = [
'title' => '123 задача',
'description' => 'Описание реальной задачи',
'status' => TaskStatus::PENDING,
'due_date' => now()->addDay()->toDateString(),
];
$taskResponse = $this->withHeader('Authorization', "Bearer $token")
->postJson('/api/tasks', $payload);
$taskResponse
->assertCreated()
->assertJsonFragment([
'title' => '123 задача',
'status' => TaskStatus::PENDING,
]);
$this->assertDatabaseHas('tasks', [
'title' => '123 задача',
'user_id' => $user->id,
]);
}
public function test_authenticated_user_can_list_tasks(): void
{
$user = User::factory()->create();
Task::factory(3)->for($user)->create();
Sanctum::actingAs($user);
$response = $this->getJson('/api/tasks');
$response->assertOk()
->assertJsonStructure([
'data' => [
'*' => [
'id',
'title',
'description',
'status',
'due_date',
'created_at',
'updated_at',
]
],
'links',
'meta',
]);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Tests;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
use DatabaseTransactions;
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*/
public function test_that_true_is_true(): void
{
$this->assertTrue(true);
}
}

View File

@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace Tests\Unit;
use App\Http\Requests\LoginRequest;
use Illuminate\Support\Facades\Validator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
class LoginRequestTest extends TestCase
{
private LoginRequest $request;
protected function setUp(): void
{
parent::setUp();
$this->request = new LoginRequest();
}
#[Test]
public function it_validates_correctly(): void
{
$data = [
'email' => 'test@example.com',
'password' => 'password123',
];
$validator = Validator::make($data, $this->request->rules());
$this->assertTrue($validator->passes());
}
#[Test]
#[DataProvider('invalidEmailProvider')]
public function it_rejects_invalid_emails(string $invalidEmail): void
{
$data = [
'email' => $invalidEmail,
'password' => 'password123',
];
$validator = Validator::make($data, $this->request->rules());
$this->assertFalse($validator->passes());
$this->assertTrue($validator->errors()->has('email'));
}
public static function invalidEmailProvider(): array
{
return [
['invalid-email'],
['invalid@'],
['@example.com'],
['invalid@.com'],
[''],
];
}
#[Test]
#[DataProvider('validEmailProvider')]
public function it_accepts_valid_emails(string $validEmail): void
{
$data = [
'email' => $validEmail,
'password' => 'password123',
];
$validator = Validator::make($data, $this->request->rules());
$this->assertTrue(
$validator->passes(),
"Email '$validEmail' should be valid"
);
}
public static function validEmailProvider(): array
{
return [
['user@example.com'],
['user.name@example.com'],
['user+tag@example.com'],
['user@sub.example.com'],
['user@example.co.uk'],
['user123@example.io'],
];
}
#[Test]
public function it_requires_email_and_password(): void
{
$validator = Validator::make([], $this->request->rules(), $this->request->messages());
$this->assertFalse($validator->passes());
$this->assertTrue($validator->errors()->has('email'));
$this->assertTrue($validator->errors()->has('password'));
$this->assertEquals('Email is required', $validator->errors()->first('email'));
$this->assertEquals('Password is required', $validator->errors()->first('password'));
}
#[Test]
public function it_enforces_email_max_length(): void
{
$longEmail = str_repeat('a', 256).'@example.com';
$data = [
'email' => $longEmail,
'password' => 'password123',
];
$validator = Validator::make($data, $this->request->rules());
$this->assertFalse($validator->passes());
$this->assertTrue($validator->errors()->has('email'));
}
#[Test]
public function it_allows_email_at_max_length(): void
{
$maxEmail = str_repeat('a', 240).'@example.com';
$data = [
'email' => $maxEmail,
'password' => 'password123',
];
$validator = Validator::make($data, $this->request->rules());
$this->assertTrue(
$validator->passes(),
'Email at max length should be valid'
);
}
}