Add PHPUnit tests + add code coverage
This commit is contained in:
parent
70440e03d5
commit
3ad4994856
9 changed files with 978 additions and 36 deletions
1
backend/.gitignore
vendored
1
backend/.gitignore
vendored
|
|
@ -12,6 +12,7 @@
|
||||||
/.vscode
|
/.vscode
|
||||||
/.zed
|
/.zed
|
||||||
/auth.json
|
/auth.json
|
||||||
|
/coverage
|
||||||
/node_modules
|
/node_modules
|
||||||
/public/build
|
/public/build
|
||||||
/public/hot
|
/public/hot
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,13 @@
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
class Trip extends Model
|
class Trip extends Model
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name',
|
'name',
|
||||||
'description',
|
'description',
|
||||||
|
|
|
||||||
72
backend/database/factories/TripFactory.php
Normal file
72
backend/database/factories/TripFactory.php
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Trip;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Trip>
|
||||||
|
*/
|
||||||
|
class TripFactory extends Factory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name of the factory's corresponding model.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $model = Trip::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
$startDate = $this->faker->dateTimeBetween('now', '+1 year');
|
||||||
|
$endDate = $this->faker->dateTimeBetween($startDate, '+1 year');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => $this->faker->sentence(3),
|
||||||
|
'description' => $this->faker->optional()->paragraph(),
|
||||||
|
'start_date' => $this->faker->optional()->date('Y-m-d', $startDate),
|
||||||
|
'end_date' => $this->faker->optional()->date('Y-m-d', $endDate),
|
||||||
|
'created_by_user_id' => User::factory(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the trip has no dates.
|
||||||
|
*/
|
||||||
|
public function withoutDates(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'start_date' => null,
|
||||||
|
'end_date' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the trip is upcoming.
|
||||||
|
*/
|
||||||
|
public function upcoming(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'start_date' => now()->addDays(30)->format('Y-m-d'),
|
||||||
|
'end_date' => now()->addDays(37)->format('Y-m-d'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the trip is past.
|
||||||
|
*/
|
||||||
|
public function past(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'start_date' => now()->subDays(37)->format('Y-m-d'),
|
||||||
|
'end_date' => now()->subDays(30)->format('Y-m-d'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,20 @@
|
||||||
<include>
|
<include>
|
||||||
<directory>app</directory>
|
<directory>app</directory>
|
||||||
</include>
|
</include>
|
||||||
|
<exclude>
|
||||||
|
<directory>app/Console</directory>
|
||||||
|
<directory>app/Exceptions</directory>
|
||||||
|
<file>app/Http/Kernel.php</file>
|
||||||
|
<file>app/Providers/BroadcastServiceProvider.php</file>
|
||||||
|
</exclude>
|
||||||
</source>
|
</source>
|
||||||
|
<coverage>
|
||||||
|
<report>
|
||||||
|
<html outputDirectory="coverage/html"/>
|
||||||
|
<text outputFile="coverage/coverage.txt"/>
|
||||||
|
<clover outputFile="coverage/clover.xml"/>
|
||||||
|
</report>
|
||||||
|
</coverage>
|
||||||
<php>
|
<php>
|
||||||
<env name="APP_ENV" value="testing"/>
|
<env name="APP_ENV" value="testing"/>
|
||||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||||
|
|
|
||||||
457
backend/tests/Feature/AuthTest.php
Normal file
457
backend/tests/Feature/AuthTest.php
Normal file
|
|
@ -0,0 +1,457 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Foundation\Testing\WithFaker;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class AuthTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase, WithFaker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test user registration with valid data.
|
||||||
|
*/
|
||||||
|
public function test_user_can_register_with_valid_data()
|
||||||
|
{
|
||||||
|
$userData = [
|
||||||
|
'name' => 'John Doe',
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
'password' => 'password123',
|
||||||
|
'password_confirmation' => 'password123',
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/register', $userData);
|
||||||
|
|
||||||
|
$response->assertStatus(201)
|
||||||
|
->assertJsonStructure([
|
||||||
|
'success',
|
||||||
|
'message',
|
||||||
|
'data' => [
|
||||||
|
'user' => [
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'email',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
],
|
||||||
|
'access_token',
|
||||||
|
'token_type',
|
||||||
|
]
|
||||||
|
])
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'User registered successfully',
|
||||||
|
'data' => [
|
||||||
|
'user' => [
|
||||||
|
'name' => 'John Doe',
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
],
|
||||||
|
'token_type' => 'Bearer',
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify user was created in database
|
||||||
|
$this->assertDatabaseHas('users', [
|
||||||
|
'name' => 'John Doe',
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify password was hashed
|
||||||
|
$user = User::where('email', 'john@example.com')->first();
|
||||||
|
$this->assertTrue(Hash::check('password123', $user->password));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test registration validation errors.
|
||||||
|
*/
|
||||||
|
public function test_registration_validates_required_fields()
|
||||||
|
{
|
||||||
|
// Test without any data
|
||||||
|
$response = $this->postJson('/api/register', []);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJson([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Validation errors',
|
||||||
|
])
|
||||||
|
->assertJsonPath('data.name', fn($value) => !empty($value))
|
||||||
|
->assertJsonPath('data.email', fn($value) => !empty($value))
|
||||||
|
->assertJsonPath('data.password', fn($value) => !empty($value));
|
||||||
|
|
||||||
|
// Test with invalid email
|
||||||
|
$response = $this->postJson('/api/register', [
|
||||||
|
'name' => 'John Doe',
|
||||||
|
'email' => 'invalid-email',
|
||||||
|
'password' => 'password123',
|
||||||
|
'password_confirmation' => 'password123',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonPath('data.email', fn($value) => !empty($value));
|
||||||
|
|
||||||
|
// Test with password too short
|
||||||
|
$response = $this->postJson('/api/register', [
|
||||||
|
'name' => 'John Doe',
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
'password' => '123',
|
||||||
|
'password_confirmation' => '123',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonPath('data.password', fn($value) => !empty($value));
|
||||||
|
|
||||||
|
// Test with password confirmation mismatch
|
||||||
|
$response = $this->postJson('/api/register', [
|
||||||
|
'name' => 'John Doe',
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
'password' => 'password123',
|
||||||
|
'password_confirmation' => 'different123',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonPath('data.password', fn($value) => !empty($value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test registration with duplicate email.
|
||||||
|
*/
|
||||||
|
public function test_registration_prevents_duplicate_email()
|
||||||
|
{
|
||||||
|
// Create an existing user
|
||||||
|
User::factory()->create([
|
||||||
|
'email' => 'existing@example.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$userData = [
|
||||||
|
'name' => 'John Doe',
|
||||||
|
'email' => 'existing@example.com',
|
||||||
|
'password' => 'password123',
|
||||||
|
'password_confirmation' => 'password123',
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/register', $userData);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonPath('data.email', fn($value) => !empty($value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test user login with valid credentials.
|
||||||
|
*/
|
||||||
|
public function test_user_can_login_with_valid_credentials()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create([
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
'password' => Hash::make('password123'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$loginData = [
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
'password' => 'password123',
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/login', $loginData);
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJsonStructure([
|
||||||
|
'success',
|
||||||
|
'message',
|
||||||
|
'data' => [
|
||||||
|
'user' => [
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'email',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
],
|
||||||
|
'access_token',
|
||||||
|
'token_type',
|
||||||
|
]
|
||||||
|
])
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Login successful',
|
||||||
|
'data' => [
|
||||||
|
'user' => [
|
||||||
|
'id' => $user->id,
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
],
|
||||||
|
'token_type' => 'Bearer',
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify token is valid
|
||||||
|
$this->assertNotEmpty($response->json('data.access_token'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test login with invalid credentials.
|
||||||
|
*/
|
||||||
|
public function test_login_fails_with_invalid_credentials()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create([
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
'password' => Hash::make('correct-password'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Test with wrong password - This should return 422 with validation exception
|
||||||
|
$response = $this->postJson('/api/login', [
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
'password' => 'wrong-password',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422);
|
||||||
|
|
||||||
|
// Test with non-existent email
|
||||||
|
$response = $this->postJson('/api/login', [
|
||||||
|
'email' => 'nonexistent@example.com',
|
||||||
|
'password' => 'password123',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test login validation errors.
|
||||||
|
*/
|
||||||
|
public function test_login_validates_required_fields()
|
||||||
|
{
|
||||||
|
// Test without any data
|
||||||
|
$response = $this->postJson('/api/login', []);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJson([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Validation errors',
|
||||||
|
])
|
||||||
|
->assertJsonPath('data.email', fn($value) => !empty($value))
|
||||||
|
->assertJsonPath('data.password', fn($value) => !empty($value));
|
||||||
|
|
||||||
|
// Test with invalid email format
|
||||||
|
$response = $this->postJson('/api/login', [
|
||||||
|
'email' => 'invalid-email',
|
||||||
|
'password' => 'password123',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonPath('data.email', fn($value) => !empty($value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test authenticated user can access profile.
|
||||||
|
*/
|
||||||
|
public function test_authenticated_user_can_access_profile()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create([
|
||||||
|
'name' => 'John Doe',
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Sanctum::actingAs($user);
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/profile');
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Profile retrieved successfully',
|
||||||
|
'data' => [
|
||||||
|
'id' => $user->id,
|
||||||
|
'name' => 'John Doe',
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test unauthenticated user cannot access profile.
|
||||||
|
*/
|
||||||
|
public function test_unauthenticated_user_cannot_access_profile()
|
||||||
|
{
|
||||||
|
$response = $this->getJson('/api/profile');
|
||||||
|
|
||||||
|
$response->assertStatus(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test authenticated user can logout.
|
||||||
|
*/
|
||||||
|
public function test_authenticated_user_can_logout()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$token = $user->createToken('test-token');
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $token->plainTextToken,
|
||||||
|
])->postJson('/api/logout');
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Logout successful',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify token was deleted
|
||||||
|
$this->assertDatabaseMissing('personal_access_tokens', [
|
||||||
|
'id' => $token->accessToken->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test unauthenticated user cannot logout.
|
||||||
|
*/
|
||||||
|
public function test_unauthenticated_user_cannot_logout()
|
||||||
|
{
|
||||||
|
$response = $this->postJson('/api/logout');
|
||||||
|
|
||||||
|
$response->assertStatus(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test token authentication works.
|
||||||
|
*/
|
||||||
|
public function test_token_authentication_works()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$token = $user->createToken('test-token');
|
||||||
|
|
||||||
|
// Test that the token can be used to access protected routes
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $token->plainTextToken,
|
||||||
|
])->getJson('/api/user');
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
|
'id' => $user->id,
|
||||||
|
'email' => $user->email,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test invalid token is rejected.
|
||||||
|
*/
|
||||||
|
public function test_invalid_token_is_rejected()
|
||||||
|
{
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer invalid-token',
|
||||||
|
])->getJson('/api/user');
|
||||||
|
|
||||||
|
$response->assertStatus(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test registration creates valid tokens.
|
||||||
|
*/
|
||||||
|
public function test_registration_creates_valid_token()
|
||||||
|
{
|
||||||
|
$userData = [
|
||||||
|
'name' => 'John Doe',
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
'password' => 'password123',
|
||||||
|
'password_confirmation' => 'password123',
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/register', $userData);
|
||||||
|
$token = $response->json('data.access_token');
|
||||||
|
|
||||||
|
// Use the token to access a protected route
|
||||||
|
$profileResponse = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $token,
|
||||||
|
])->getJson('/api/profile');
|
||||||
|
|
||||||
|
$profileResponse->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test login creates valid tokens.
|
||||||
|
*/
|
||||||
|
public function test_login_creates_valid_token()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create([
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
'password' => Hash::make('password123'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$loginResponse = $this->postJson('/api/login', [
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
'password' => 'password123',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$token = $loginResponse->json('data.access_token');
|
||||||
|
|
||||||
|
// Use the token to access a protected route
|
||||||
|
$profileResponse = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $token,
|
||||||
|
])->getJson('/api/profile');
|
||||||
|
|
||||||
|
$profileResponse->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'id' => $user->id,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test registration returns user without password.
|
||||||
|
*/
|
||||||
|
public function test_registration_does_not_return_password()
|
||||||
|
{
|
||||||
|
$userData = [
|
||||||
|
'name' => 'John Doe',
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
'password' => 'password123',
|
||||||
|
'password_confirmation' => 'password123',
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/register', $userData);
|
||||||
|
|
||||||
|
$response->assertStatus(201)
|
||||||
|
->assertJsonMissing(['data.user.password']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test login returns user without password.
|
||||||
|
*/
|
||||||
|
public function test_login_does_not_return_password()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create([
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
'password' => Hash::make('password123'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/login', [
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
'password' => 'password123',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJsonMissing(['data.user.password']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test profile returns user without password.
|
||||||
|
*/
|
||||||
|
public function test_profile_does_not_return_password()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
Sanctum::actingAs($user);
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/profile');
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJsonMissing(['data.password']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<?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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
422
backend/tests/Feature/TripTest.php
Normal file
422
backend/tests/Feature/TripTest.php
Normal file
|
|
@ -0,0 +1,422 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Trip;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Foundation\Testing\WithFaker;
|
||||||
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class TripTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase, WithFaker;
|
||||||
|
|
||||||
|
protected $user;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->user = User::factory()->create();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that unauthenticated users cannot access trip endpoints.
|
||||||
|
*/
|
||||||
|
public function test_unauthenticated_user_cannot_access_trips()
|
||||||
|
{
|
||||||
|
$response = $this->getJson('/api/trips');
|
||||||
|
$response->assertStatus(401);
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/trips', []);
|
||||||
|
$response->assertStatus(401);
|
||||||
|
|
||||||
|
$response = $this->putJson('/api/trips/1', []);
|
||||||
|
$response->assertStatus(401);
|
||||||
|
|
||||||
|
$response = $this->deleteJson('/api/trips/1');
|
||||||
|
$response->assertStatus(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test user can create a trip.
|
||||||
|
*/
|
||||||
|
public function test_user_can_create_trip()
|
||||||
|
{
|
||||||
|
Sanctum::actingAs($this->user);
|
||||||
|
|
||||||
|
$tripData = [
|
||||||
|
'name' => 'Summer Vacation 2025',
|
||||||
|
'description' => 'A wonderful trip to Europe',
|
||||||
|
'start_date' => '2025-06-01',
|
||||||
|
'end_date' => '2025-06-15',
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/trips', $tripData);
|
||||||
|
|
||||||
|
$response->assertStatus(201)
|
||||||
|
->assertJsonStructure([
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'start_date',
|
||||||
|
'end_date',
|
||||||
|
'created_by_user_id',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
])
|
||||||
|
->assertJson([
|
||||||
|
'name' => 'Summer Vacation 2025',
|
||||||
|
'description' => 'A wonderful trip to Europe',
|
||||||
|
'created_by_user_id' => $this->user->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('trips', [
|
||||||
|
'name' => 'Summer Vacation 2025',
|
||||||
|
'created_by_user_id' => $this->user->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test trip creation validation.
|
||||||
|
*/
|
||||||
|
public function test_trip_creation_validates_required_fields()
|
||||||
|
{
|
||||||
|
Sanctum::actingAs($this->user);
|
||||||
|
|
||||||
|
// Test without name
|
||||||
|
$response = $this->postJson('/api/trips', [
|
||||||
|
'description' => 'A trip without a name',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['name']);
|
||||||
|
|
||||||
|
// Test with empty name
|
||||||
|
$response = $this->postJson('/api/trips', [
|
||||||
|
'name' => '',
|
||||||
|
'description' => 'A trip with empty name',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['name']);
|
||||||
|
|
||||||
|
// Test with invalid dates
|
||||||
|
$response = $this->postJson('/api/trips', [
|
||||||
|
'name' => 'Trip with invalid dates',
|
||||||
|
'start_date' => 'not-a-date',
|
||||||
|
'end_date' => '2025-13-45',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['start_date', 'end_date']);
|
||||||
|
|
||||||
|
// Test with end date before start date
|
||||||
|
$response = $this->postJson('/api/trips', [
|
||||||
|
'name' => 'Trip with reversed dates',
|
||||||
|
'start_date' => '2025-06-15',
|
||||||
|
'end_date' => '2025-06-01',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['end_date']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test user can list their trips.
|
||||||
|
*/
|
||||||
|
public function test_user_can_list_their_own_trips()
|
||||||
|
{
|
||||||
|
Sanctum::actingAs($this->user);
|
||||||
|
|
||||||
|
// Create some trips for this user
|
||||||
|
$trips = Trip::factory()->count(3)->create([
|
||||||
|
'created_by_user_id' => $this->user->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create trips for another user (should not be visible)
|
||||||
|
$otherUser = User::factory()->create();
|
||||||
|
Trip::factory()->count(2)->create([
|
||||||
|
'created_by_user_id' => $otherUser->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/trips');
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJsonCount(3)
|
||||||
|
->assertJsonStructure([
|
||||||
|
'*' => [
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'start_date',
|
||||||
|
'end_date',
|
||||||
|
'created_by_user_id',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify all returned trips belong to the authenticated user
|
||||||
|
foreach ($response->json() as $trip) {
|
||||||
|
$this->assertEquals($this->user->id, $trip['created_by_user_id']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test user can view a specific trip.
|
||||||
|
*/
|
||||||
|
public function test_user_can_view_their_own_trip()
|
||||||
|
{
|
||||||
|
Sanctum::actingAs($this->user);
|
||||||
|
|
||||||
|
$trip = Trip::factory()->create([
|
||||||
|
'created_by_user_id' => $this->user->id,
|
||||||
|
'name' => 'My Special Trip',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->getJson("/api/trips/{$trip->id}");
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
|
'id' => $trip->id,
|
||||||
|
'name' => 'My Special Trip',
|
||||||
|
'created_by_user_id' => $this->user->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test user cannot view another user's trip.
|
||||||
|
*/
|
||||||
|
public function test_user_cannot_view_another_users_trip()
|
||||||
|
{
|
||||||
|
Sanctum::actingAs($this->user);
|
||||||
|
|
||||||
|
$otherUser = User::factory()->create();
|
||||||
|
$otherTrip = Trip::factory()->create([
|
||||||
|
'created_by_user_id' => $otherUser->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->getJson("/api/trips/{$otherTrip->id}");
|
||||||
|
|
||||||
|
// Controller returns 404 when trip doesn't belong to user
|
||||||
|
$response->assertStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test user can update their trip.
|
||||||
|
*/
|
||||||
|
public function test_user_can_update_their_own_trip()
|
||||||
|
{
|
||||||
|
Sanctum::actingAs($this->user);
|
||||||
|
|
||||||
|
$trip = Trip::factory()->create([
|
||||||
|
'created_by_user_id' => $this->user->id,
|
||||||
|
'name' => 'Original Name',
|
||||||
|
'description' => 'Original Description',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$updateData = [
|
||||||
|
'name' => 'Updated Trip Name',
|
||||||
|
'description' => 'Updated Description',
|
||||||
|
'start_date' => '2025-07-01',
|
||||||
|
'end_date' => '2025-07-15',
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->putJson("/api/trips/{$trip->id}", $updateData);
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
|
'id' => $trip->id,
|
||||||
|
'name' => 'Updated Trip Name',
|
||||||
|
'description' => 'Updated Description',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('trips', [
|
||||||
|
'id' => $trip->id,
|
||||||
|
'name' => 'Updated Trip Name',
|
||||||
|
'description' => 'Updated Description',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test user cannot update another user's trip.
|
||||||
|
*/
|
||||||
|
public function test_user_cannot_update_another_users_trip()
|
||||||
|
{
|
||||||
|
Sanctum::actingAs($this->user);
|
||||||
|
|
||||||
|
$otherUser = User::factory()->create();
|
||||||
|
$otherTrip = Trip::factory()->create([
|
||||||
|
'created_by_user_id' => $otherUser->id,
|
||||||
|
'name' => 'Other User Trip',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->putJson("/api/trips/{$otherTrip->id}", [
|
||||||
|
'name' => 'Trying to Update',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Controller returns 404 when trip doesn't belong to user
|
||||||
|
$response->assertStatus(404);
|
||||||
|
|
||||||
|
// Verify the trip wasn't updated
|
||||||
|
$this->assertDatabaseHas('trips', [
|
||||||
|
'id' => $otherTrip->id,
|
||||||
|
'name' => 'Other User Trip',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test user can delete their trip.
|
||||||
|
*/
|
||||||
|
public function test_user_can_delete_their_own_trip()
|
||||||
|
{
|
||||||
|
Sanctum::actingAs($this->user);
|
||||||
|
|
||||||
|
$trip = Trip::factory()->create([
|
||||||
|
'created_by_user_id' => $this->user->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->deleteJson("/api/trips/{$trip->id}");
|
||||||
|
|
||||||
|
// Controller returns 200 with a message
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
|
'message' => 'Trip deleted successfully'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseMissing('trips', [
|
||||||
|
'id' => $trip->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test user cannot delete another user's trip.
|
||||||
|
*/
|
||||||
|
public function test_user_cannot_delete_another_users_trip()
|
||||||
|
{
|
||||||
|
Sanctum::actingAs($this->user);
|
||||||
|
|
||||||
|
$otherUser = User::factory()->create();
|
||||||
|
$otherTrip = Trip::factory()->create([
|
||||||
|
'created_by_user_id' => $otherUser->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->deleteJson("/api/trips/{$otherTrip->id}");
|
||||||
|
|
||||||
|
// Controller returns 404 when trip doesn't belong to user
|
||||||
|
$response->assertStatus(404);
|
||||||
|
|
||||||
|
// Verify the trip still exists
|
||||||
|
$this->assertDatabaseHas('trips', [
|
||||||
|
'id' => $otherTrip->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test handling non-existent trip.
|
||||||
|
*/
|
||||||
|
public function test_returns_404_for_non_existent_trip()
|
||||||
|
{
|
||||||
|
Sanctum::actingAs($this->user);
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/trips/99999');
|
||||||
|
$response->assertStatus(404);
|
||||||
|
|
||||||
|
$response = $this->putJson('/api/trips/99999', ['name' => 'Updated']);
|
||||||
|
$response->assertStatus(404);
|
||||||
|
|
||||||
|
$response = $this->deleteJson('/api/trips/99999');
|
||||||
|
$response->assertStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test trip creation with minimal data.
|
||||||
|
*/
|
||||||
|
public function test_user_can_create_trip_with_minimal_data()
|
||||||
|
{
|
||||||
|
Sanctum::actingAs($this->user);
|
||||||
|
|
||||||
|
$tripData = [
|
||||||
|
'name' => 'Minimal Trip',
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/trips', $tripData);
|
||||||
|
|
||||||
|
$response->assertStatus(201)
|
||||||
|
->assertJson([
|
||||||
|
'name' => 'Minimal Trip',
|
||||||
|
'created_by_user_id' => $this->user->id,
|
||||||
|
])
|
||||||
|
->assertJsonStructure([
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'created_by_user_id',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('trips', [
|
||||||
|
'name' => 'Minimal Trip',
|
||||||
|
'created_by_user_id' => $this->user->id,
|
||||||
|
'description' => null,
|
||||||
|
'start_date' => null,
|
||||||
|
'end_date' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test trip name length validation.
|
||||||
|
*/
|
||||||
|
public function test_trip_name_length_validation()
|
||||||
|
{
|
||||||
|
Sanctum::actingAs($this->user);
|
||||||
|
|
||||||
|
// Test with too long name (assuming max is 255)
|
||||||
|
$response = $this->postJson('/api/trips', [
|
||||||
|
'name' => str_repeat('a', 256),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test trips are returned in correct order.
|
||||||
|
*/
|
||||||
|
public function test_trips_are_returned_in_descending_order()
|
||||||
|
{
|
||||||
|
Sanctum::actingAs($this->user);
|
||||||
|
|
||||||
|
// Create trips with specific timestamps
|
||||||
|
$oldTrip = Trip::factory()->create([
|
||||||
|
'created_by_user_id' => $this->user->id,
|
||||||
|
'name' => 'Old Trip',
|
||||||
|
'created_at' => now()->subDays(2),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$newTrip = Trip::factory()->create([
|
||||||
|
'created_by_user_id' => $this->user->id,
|
||||||
|
'name' => 'New Trip',
|
||||||
|
'created_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$middleTrip = Trip::factory()->create([
|
||||||
|
'created_by_user_id' => $this->user->id,
|
||||||
|
'name' => 'Middle Trip',
|
||||||
|
'created_at' => now()->subDay(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/trips');
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
|
||||||
|
$trips = $response->json();
|
||||||
|
|
||||||
|
// Verify trips are in descending order (newest first)
|
||||||
|
$this->assertEquals('New Trip', $trips[0]['name']);
|
||||||
|
$this->assertEquals('Middle Trip', $trips[1]['name']);
|
||||||
|
$this->assertEquals('Old Trip', $trips[2]['name']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<?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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -11,11 +11,21 @@ RUN apk add --no-cache \
|
||||||
unzip \
|
unzip \
|
||||||
nodejs \
|
nodejs \
|
||||||
npm \
|
npm \
|
||||||
shadow
|
shadow \
|
||||||
|
linux-headers \
|
||||||
|
$PHPIZE_DEPS
|
||||||
|
|
||||||
# Install PHP extensions
|
# Install PHP extensions
|
||||||
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd
|
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd
|
||||||
|
|
||||||
|
# Install Xdebug for code coverage
|
||||||
|
RUN pecl install xdebug && \
|
||||||
|
docker-php-ext-enable xdebug
|
||||||
|
|
||||||
|
# Configure Xdebug for coverage (not debugging)
|
||||||
|
RUN echo "xdebug.mode=coverage" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini && \
|
||||||
|
echo "xdebug.start_with_request=no" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
|
||||||
|
|
||||||
# Install Composer
|
# Install Composer
|
||||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue