326 lines
9.6 KiB
PHP
326 lines
9.6 KiB
PHP
<?php
|
|
|
|
use App\Actions\CreateDefaultCategories;
|
|
use App\Enums\CategoryCashflowDirection;
|
|
use App\Enums\CategoryType;
|
|
use App\Models\Category;
|
|
use App\Models\User;
|
|
use Illuminate\Database\UniqueConstraintViolationException;
|
|
|
|
test('authenticated users can view their categories', function () {
|
|
$user = User::factory()->create();
|
|
$categories = Category::factory()->count(3)->create(['user_id' => $user->id]);
|
|
|
|
$response = $this->actingAs($user)->get(route('categories.index'));
|
|
|
|
$response->assertSuccessful();
|
|
$response->assertInertia(fn ($page) => $page
|
|
->component('settings/categories')
|
|
->has('categories', 3)
|
|
);
|
|
});
|
|
|
|
test('authenticated users can only view their own categories', function () {
|
|
$user = User::factory()->create();
|
|
$otherUser = User::factory()->create();
|
|
|
|
Category::factory()->create(['user_id' => $user->id, 'name' => 'My Category']);
|
|
Category::factory()->create(['user_id' => $otherUser->id, 'name' => 'Other Category']);
|
|
|
|
$response = $this->actingAs($user)->get(route('categories.index'));
|
|
|
|
$response->assertSuccessful();
|
|
$response->assertInertia(fn ($page) => $page
|
|
->component('settings/categories')
|
|
->has('categories', 1)
|
|
->where('categories.0.name', 'My Category')
|
|
);
|
|
});
|
|
|
|
test('authenticated users can create a category', function () {
|
|
$user = User::factory()->create();
|
|
|
|
$categoryData = [
|
|
'name' => 'Shopping',
|
|
'icon' => 'ShoppingBag',
|
|
'color' => 'blue',
|
|
'type' => 'expense',
|
|
'cashflow_direction' => 'outflow',
|
|
];
|
|
|
|
$response = $this->actingAs($user)->post(route('categories.store'), $categoryData);
|
|
|
|
$response->assertRedirect(route('categories.index'));
|
|
|
|
$this->assertDatabaseHas('categories', [
|
|
'user_id' => $user->id,
|
|
'name' => 'Shopping',
|
|
'icon' => 'ShoppingBag',
|
|
'color' => 'blue',
|
|
'type' => 'expense',
|
|
'cashflow_direction' => CategoryCashflowDirection::Hidden->value,
|
|
]);
|
|
});
|
|
|
|
test('category name is required', function () {
|
|
$user = User::factory()->create();
|
|
|
|
$response = $this->actingAs($user)->post(route('categories.store'), [
|
|
'icon' => 'ShoppingBag',
|
|
'color' => 'blue',
|
|
'type' => 'expense',
|
|
'cashflow_direction' => 'hidden',
|
|
]);
|
|
|
|
$response->assertSessionHasErrors(['name']);
|
|
});
|
|
|
|
test('category icon is required', function () {
|
|
$user = User::factory()->create();
|
|
|
|
$response = $this->actingAs($user)->post(route('categories.store'), [
|
|
'name' => 'Shopping',
|
|
'color' => 'blue',
|
|
'type' => 'expense',
|
|
]);
|
|
|
|
$response->assertSessionHasErrors(['icon']);
|
|
});
|
|
|
|
test('category color is required', function () {
|
|
$user = User::factory()->create();
|
|
|
|
$response = $this->actingAs($user)->post(route('categories.store'), [
|
|
'name' => 'Shopping',
|
|
'icon' => 'ShoppingBag',
|
|
'type' => 'expense',
|
|
]);
|
|
|
|
$response->assertSessionHasErrors(['color']);
|
|
});
|
|
|
|
test('category color must be valid', function () {
|
|
$user = User::factory()->create();
|
|
|
|
$response = $this->actingAs($user)->post(route('categories.store'), [
|
|
'name' => 'Shopping',
|
|
'icon' => 'ShoppingBag',
|
|
'color' => 'invalid-color',
|
|
'type' => 'expense',
|
|
]);
|
|
|
|
$response->assertSessionHasErrors(['color']);
|
|
});
|
|
|
|
test('category type is required', function () {
|
|
$user = User::factory()->create();
|
|
|
|
$response = $this->actingAs($user)->post(route('categories.store'), [
|
|
'name' => 'Shopping',
|
|
'icon' => 'ShoppingBag',
|
|
'color' => 'blue',
|
|
]);
|
|
|
|
$response->assertSessionHasErrors(['type']);
|
|
});
|
|
|
|
test('category type must be valid', function () {
|
|
$user = User::factory()->create();
|
|
|
|
$response = $this->actingAs($user)->post(route('categories.store'), [
|
|
'name' => 'Shopping',
|
|
'icon' => 'ShoppingBag',
|
|
'color' => 'blue',
|
|
'type' => 'invalid-type',
|
|
]);
|
|
|
|
$response->assertSessionHasErrors(['type']);
|
|
});
|
|
|
|
test('authenticated users can update their own category', function () {
|
|
$user = User::factory()->create();
|
|
$category = Category::factory()->create(['user_id' => $user->id]);
|
|
|
|
$updateData = [
|
|
'name' => 'Updated Name',
|
|
'icon' => 'Home',
|
|
'color' => 'green',
|
|
'type' => 'transfer',
|
|
'cashflow_direction' => 'outflow',
|
|
];
|
|
|
|
$response = $this->actingAs($user)->patch(
|
|
route('categories.update', $category),
|
|
$updateData
|
|
);
|
|
|
|
$response->assertRedirect(route('categories.index'));
|
|
|
|
$this->assertDatabaseHas('categories', [
|
|
'id' => $category->id,
|
|
'name' => 'Updated Name',
|
|
'icon' => 'Home',
|
|
'color' => 'green',
|
|
'type' => 'transfer',
|
|
'cashflow_direction' => 'outflow',
|
|
]);
|
|
});
|
|
|
|
test('transfer categories can store a cashflow direction', function () {
|
|
$user = User::factory()->create();
|
|
|
|
$response = $this->actingAs($user)->post(route('categories.store'), [
|
|
'name' => 'Investment',
|
|
'icon' => 'TrendingUp',
|
|
'color' => 'green',
|
|
'type' => 'transfer',
|
|
'cashflow_direction' => 'outflow',
|
|
]);
|
|
|
|
$response->assertRedirect(route('categories.index'));
|
|
|
|
$this->assertDatabaseHas('categories', [
|
|
'user_id' => $user->id,
|
|
'name' => 'Investment',
|
|
'type' => 'transfer',
|
|
'cashflow_direction' => 'outflow',
|
|
]);
|
|
});
|
|
|
|
test('non-transfer categories are forced to hidden cashflow direction', function () {
|
|
$user = User::factory()->create();
|
|
|
|
$response = $this->actingAs($user)->post(route('categories.store'), [
|
|
'name' => 'Salary',
|
|
'icon' => 'Coins',
|
|
'color' => 'green',
|
|
'type' => 'income',
|
|
'cashflow_direction' => 'outflow',
|
|
]);
|
|
|
|
$response->assertRedirect(route('categories.index'));
|
|
|
|
$this->assertDatabaseHas('categories', [
|
|
'user_id' => $user->id,
|
|
'name' => 'Salary',
|
|
'type' => 'income',
|
|
'cashflow_direction' => CategoryCashflowDirection::Hidden->value,
|
|
]);
|
|
});
|
|
|
|
test('users cannot update categories they do not own', function () {
|
|
$user = User::factory()->create();
|
|
$otherUser = User::factory()->create();
|
|
$category = Category::factory()->create(['user_id' => $otherUser->id]);
|
|
|
|
$response = $this->actingAs($user)->patch(
|
|
route('categories.update', $category),
|
|
[
|
|
'name' => 'Updated Name',
|
|
'icon' => 'Home',
|
|
'color' => 'green',
|
|
'type' => 'expense',
|
|
]
|
|
);
|
|
|
|
$response->assertForbidden();
|
|
});
|
|
|
|
test('authenticated users can delete their own category', function () {
|
|
$user = User::factory()->create();
|
|
$category = Category::factory()->create(['user_id' => $user->id]);
|
|
|
|
$response = $this->actingAs($user)->delete(route('categories.destroy', $category));
|
|
|
|
$response->assertRedirect(route('categories.index'));
|
|
|
|
$this->assertSoftDeleted('categories', [
|
|
'id' => $category->id,
|
|
]);
|
|
});
|
|
|
|
test('users cannot delete categories they do not own', function () {
|
|
$user = User::factory()->create();
|
|
$otherUser = User::factory()->create();
|
|
$category = Category::factory()->create(['user_id' => $otherUser->id]);
|
|
|
|
$response = $this->actingAs($user)->delete(route('categories.destroy', $category));
|
|
|
|
$response->assertForbidden();
|
|
|
|
$this->assertDatabaseHas('categories', [
|
|
'id' => $category->id,
|
|
'deleted_at' => null,
|
|
]);
|
|
});
|
|
|
|
test('guests cannot access category management', function () {
|
|
$response = $this->get(route('categories.index'));
|
|
$response->assertRedirect(route('login'));
|
|
|
|
$response = $this->post(route('categories.store'), []);
|
|
$response->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('default categories are created when user registers', function () {
|
|
$user = User::factory()->create();
|
|
|
|
$service = new CreateDefaultCategories;
|
|
$service->handle($user);
|
|
|
|
expect($user->categories()->count())->toBe(64);
|
|
|
|
$categoryNames = $user->categories->pluck('name')->toArray();
|
|
expect($categoryNames)->toContain('Food', 'Transportation', 'Salary', 'Insurance');
|
|
|
|
$user->refresh();
|
|
|
|
expect($user->categories()->firstWhere('name', 'Investments'))
|
|
->type->toBe(CategoryType::Transfer)
|
|
->cashflow_direction->toBe(CategoryCashflowDirection::Outflow);
|
|
expect($user->categories()->firstWhere('name', 'Savings'))
|
|
->type->toBe(CategoryType::Transfer)
|
|
->cashflow_direction->toBe(CategoryCashflowDirection::Outflow);
|
|
expect($user->categories()->firstWhere('name', 'Other investments'))
|
|
->type->toBe(CategoryType::Transfer)
|
|
->cashflow_direction->toBe(CategoryCashflowDirection::Outflow);
|
|
expect($user->categories()->firstWhere('name', 'From account of relatives'))
|
|
->type->toBe(CategoryType::Transfer)
|
|
->cashflow_direction->toBe(CategoryCashflowDirection::Inflow);
|
|
});
|
|
|
|
test('default categories are not created twice for the same user', function () {
|
|
$user = User::factory()->create();
|
|
|
|
$service = new CreateDefaultCategories;
|
|
$service->handle($user);
|
|
|
|
expect($user->categories()->count())->toBe(64);
|
|
|
|
$service->handle($user);
|
|
|
|
expect($user->categories()->count())->toBe(64);
|
|
});
|
|
|
|
test('category names are unique per user', function () {
|
|
$user = User::factory()->create();
|
|
|
|
$category = $user->categories()->create([
|
|
'name' => 'Test Category',
|
|
'icon' => 'Tag',
|
|
'color' => 'red',
|
|
'type' => 'expense',
|
|
]);
|
|
|
|
expect($category)->toBeInstanceOf(Category::class);
|
|
|
|
$this->expectException(UniqueConstraintViolationException::class);
|
|
|
|
$user->categories()->create([
|
|
'name' => 'Test Category',
|
|
'icon' => 'Tag',
|
|
'color' => 'blue',
|
|
'type' => 'expense',
|
|
]);
|
|
});
|