user profiles

This commit is contained in:
2026-02-06 14:33:11 +02:00
parent d458e36f4f
commit b2fd2a5eac
20 changed files with 3936 additions and 9 deletions
@@ -0,0 +1,83 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
class ProfileController extends Controller
{
/**
* Display the specified user's profile.
*/
public function show(User $user)
{
return view('profile.show', compact('user'));
}
/**
* Show the form for editing the authenticated user's profile.
*/
public function edit()
{
$user = Auth::user();
return view('profile.edit', compact('user'));
}
/**
* Update the authenticated user's profile.
*/
public function update(Request $request)
{
$user = Auth::user();
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
'avatar' => ['nullable', 'image', 'mimes:jpeg,png,jpg,gif', 'max:2048'],
'bio' => ['nullable', 'string', 'max:1000'],
'website' => ['nullable', 'url', 'max:255'],
'twitter' => ['nullable', 'string', 'max:255'],
'github' => ['nullable', 'string', 'max:255'],
'linkedin' => ['nullable', 'string', 'max:255'],
]);
// Handle avatar upload
if ($request->hasFile('avatar')) {
// Delete old avatar if exists
if ($user->avatar) {
Storage::disk('public')->delete('avatars/' . $user->avatar);
}
// Store new avatar
$avatarName = time() . '_' . $user->id . '.' . $request->avatar->extension();
$request->avatar->storeAs('avatars', $avatarName, 'public');
$validated['avatar'] = $avatarName;
}
// Update user profile
$user->update($validated);
return redirect()->route('profile.show', $user)
->with('success', 'Profile updated successfully!');
}
/**
* Delete the user's avatar.
*/
public function deleteAvatar()
{
$user = Auth::user();
if ($user->avatar) {
Storage::disk('public')->delete('avatars/' . $user->avatar);
$user->update(['avatar' => null]);
}
return redirect()->route('profile.edit')
->with('success', 'Avatar deleted successfully!');
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class IsAdmin
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (!auth()->check() || !auth()->user()->isAdmin()) {
abort(403, 'Unauthorized action. Admin access required.');
}
return $next($request);
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class IsModerator
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (!auth()->check() || !auth()->user()->hasAnyRole(['moderator', 'admin'])) {
abort(403, 'Unauthorized action. Moderator access required.');
}
return $next($request);
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Role extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'slug',
'description',
];
/**
* Get the users that belong to the role.
*/
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class);
}
}
+66
View File
@@ -4,6 +4,7 @@ namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail; // use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
@@ -21,6 +22,12 @@ class User extends Authenticatable
'name', 'name',
'email', 'email',
'password', 'password',
'avatar',
'bio',
'website',
'twitter',
'github',
'linkedin',
]; ];
/** /**
@@ -45,4 +52,63 @@ class User extends Authenticatable
'password' => 'hashed', 'password' => 'hashed',
]; ];
} }
/**
* Get the roles that belong to the user.
*/
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class);
}
/**
* Check if user has a specific role.
*/
public function hasRole(string $role): bool
{
return $this->roles()->where('slug', $role)->exists();
}
/**
* Check if user has any of the given roles.
*/
public function hasAnyRole(array $roles): bool
{
return $this->roles()->whereIn('slug', $roles)->exists();
}
/**
* Check if user is an admin.
*/
public function isAdmin(): bool
{
return $this->hasRole('admin');
}
/**
* Check if user is a moderator.
*/
public function isModerator(): bool
{
return $this->hasRole('moderator');
}
/**
* Check if user is a regular user.
*/
public function isUser(): bool
{
return $this->hasRole('user');
}
/**
* Get the user's avatar URL.
*/
public function getAvatarUrlAttribute(): ?string
{
if ($this->avatar) {
return asset('storage/avatars/' . $this->avatar);
}
return null;
}
} }
+46 -1
View File
@@ -2,6 +2,7 @@
namespace App\Providers; namespace App\Providers;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
@@ -19,6 +20,50 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function boot(): void public function boot(): void
{ {
// // Product-related Gates
Gate::define('create-product', function ($user) {
return $user->hasAnyRole(['user', 'moderator', 'admin']);
});
Gate::define('edit-product', function ($user, $product) {
return $user->id === $product->user_id || $user->hasAnyRole(['moderator', 'admin']);
});
Gate::define('delete-product', function ($user, $product) {
return $user->id === $product->user_id || $user->hasAnyRole(['moderator', 'admin']);
});
Gate::define('approve-product', function ($user) {
return $user->hasAnyRole(['moderator', 'admin']);
});
Gate::define('reject-product', function ($user) {
return $user->hasAnyRole(['moderator', 'admin']);
});
// Comment-related Gates
Gate::define('delete-comment', function ($user, $comment) {
return $user->id === $comment->user_id || $user->hasAnyRole(['moderator', 'admin']);
});
// User management Gates
Gate::define('manage-users', function ($user) {
return $user->isAdmin();
});
Gate::define('manage-roles', function ($user) {
return $user->isAdmin();
});
Gate::define('manage-settings', function ($user) {
return $user->isAdmin();
});
// Admin check
Gate::before(function ($user, $ability) {
if ($user->isAdmin()) {
return true;
}
});
} }
} }
+4 -1
View File
@@ -11,7 +11,10 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware): void { ->withMiddleware(function (Middleware $middleware): void {
// $middleware->alias([
'admin' => \App\Http\Middleware\IsAdmin::class,
'moderator' => \App\Http\Middleware\IsModerator::class,
]);
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions): void {
// //
@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('avatar')->nullable()->after('email');
$table->text('bio')->nullable()->after('avatar');
$table->string('website')->nullable()->after('bio');
$table->string('twitter')->nullable()->after('website');
$table->string('github')->nullable()->after('twitter');
$table->string('linkedin')->nullable()->after('github');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['avatar', 'bio', 'website', 'twitter', 'github', 'linkedin']);
});
}
};
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('roles', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('roles');
}
};
@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('role_user', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('role_id')->constrained()->onDelete('cascade');
$table->timestamps();
$table->unique(['user_id', 'role_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('role_user');
}
};
+5 -5
View File
@@ -15,11 +15,11 @@ class DatabaseSeeder extends Seeder
*/ */
public function run(): void public function run(): void
{ {
// User::factory(10)->create(); $this->call([
RoleSeeder::class,
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
]); ]);
// Uncomment to create additional users with factory
// User::factory(10)->create();
} }
} }
+74
View File
@@ -0,0 +1,74 @@
<?php
namespace Database\Seeders;
use App\Models\Role;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class RoleSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// Create roles
$adminRole = Role::create([
'name' => 'Admin',
'slug' => 'admin',
'description' => 'Full access to all features and settings',
]);
$moderatorRole = Role::create([
'name' => 'Moderator',
'slug' => 'moderator',
'description' => 'Can approve/reject products and delete comments',
]);
$userRole = Role::create([
'name' => 'User',
'slug' => 'user',
'description' => 'Can submit products, upvote, and comment',
]);
// Create test users
$admin = User::create([
'name' => 'Admin User',
'email' => 'admin@example.com',
'password' => Hash::make('password'),
'bio' => 'Administrator of the platform',
'email_verified_at' => now(),
]);
$admin->roles()->attach($adminRole);
$moderator = User::create([
'name' => 'Moderator User',
'email' => 'moderator@example.com',
'password' => Hash::make('password'),
'bio' => 'Content moderator',
'email_verified_at' => now(),
]);
$moderator->roles()->attach($moderatorRole);
$regularUser = User::create([
'name' => 'Regular User',
'email' => 'user@example.com',
'password' => Hash::make('password'),
'bio' => 'Regular platform user',
'website' => 'https://example.com',
'twitter' => 'example',
'github' => 'example',
'linkedin' => 'example',
'email_verified_at' => now(),
]);
$regularUser->roles()->attach($userRole);
$this->command->info('Roles and test users created successfully!');
$this->command->info('Admin: admin@example.com / password');
$this->command->info('Moderator: moderator@example.com / password');
$this->command->info('User: user@example.com / password');
}
}
+2856
View File
File diff suppressed because it is too large Load Diff
+153
View File
@@ -0,0 +1,153 @@
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">Edit Profile</div>
<div class="card-body">
@if (session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
<form method="POST" action="{{ route('profile.update') }}" enctype="multipart/form-data">
@csrf
@method('PUT')
<!-- Avatar Section -->
<div class="mb-4">
<label class="form-label">Profile Picture</label>
<div class="d-flex align-items-center">
<div class="me-3">
@if ($user->avatar)
<img src="{{ $user->avatar_url }}" alt="{{ $user->name }}" class="rounded-circle" style="width: 100px; height: 100px; object-fit: cover;">
@else
<div class="bg-secondary text-white rounded-circle d-flex align-items-center justify-content-center" style="width: 100px; height: 100px; font-size: 36px;">
{{ strtoupper(substr($user->name, 0, 1)) }}
</div>
@endif
</div>
<div class="flex-grow-1">
<input type="file" class="form-control @error('avatar') is-invalid @enderror" id="avatar" name="avatar" accept="image/*">
@error('avatar')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
<small class="form-text text-muted">JPG, PNG, or GIF. Max size 2MB.</small>
@if ($user->avatar)
<div class="mt-2">
<form method="POST" action="{{ route('profile.avatar.delete') }}" class="d-inline">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to delete your avatar?')">
Delete Avatar
</button>
</form>
</div>
@endif
</div>
</div>
</div>
<!-- Name -->
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control @error('name') is-invalid @enderror" id="name" name="name" value="{{ old('name', $user->name) }}" required>
@error('name')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
<!-- Email -->
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control @error('email') is-invalid @enderror" id="email" name="email" value="{{ old('email', $user->email) }}" required>
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
<!-- Bio -->
<div class="mb-3">
<label for="bio" class="form-label">Bio</label>
<textarea class="form-control @error('bio') is-invalid @enderror" id="bio" name="bio" rows="4" maxlength="1000">{{ old('bio', $user->bio) }}</textarea>
@error('bio')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
<small class="form-text text-muted">Max 1000 characters</small>
</div>
<!-- Website -->
<div class="mb-3">
<label for="website" class="form-label">Website</label>
<input type="url" class="form-control @error('website') is-invalid @enderror" id="website" name="website" value="{{ old('website', $user->website) }}" placeholder="https://example.com">
@error('website')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
<!-- Social Links -->
<div class="mb-3">
<label for="twitter" class="form-label">Twitter Username</label>
<div class="input-group">
<span class="input-group-text">@</span>
<input type="text" class="form-control @error('twitter') is-invalid @enderror" id="twitter" name="twitter" value="{{ old('twitter', $user->twitter) }}" placeholder="username">
@error('twitter')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="mb-3">
<label for="github" class="form-label">GitHub Username</label>
<div class="input-group">
<span class="input-group-text">github.com/</span>
<input type="text" class="form-control @error('github') is-invalid @enderror" id="github" name="github" value="{{ old('github', $user->github) }}" placeholder="username">
@error('github')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="mb-3">
<label for="linkedin" class="form-label">LinkedIn Username</label>
<div class="input-group">
<span class="input-group-text">linkedin.com/in/</span>
<input type="text" class="form-control @error('linkedin') is-invalid @enderror" id="linkedin" name="linkedin" value="{{ old('linkedin', $user->linkedin) }}" placeholder="username">
@error('linkedin')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="d-flex justify-content-between">
<a href="{{ route('profile.show', $user) }}" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">Update Profile</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
+93
View File
@@ -0,0 +1,93 @@
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>{{ $user->name }}'s Profile</span>
@auth
@if (Auth::id() === $user->id)
<a href="{{ route('profile.edit') }}" class="btn btn-sm btn-primary">Edit Profile</a>
@endif
@endauth
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3 text-center">
@if ($user->avatar)
<img src="{{ $user->avatar_url }}" alt="{{ $user->name }}" class="img-fluid rounded-circle mb-3" style="max-width: 150px;">
@else
<div class="bg-secondary text-white rounded-circle d-flex align-items-center justify-content-center mb-3" style="width: 150px; height: 150px; font-size: 48px; margin: 0 auto;">
{{ strtoupper(substr($user->name, 0, 1)) }}
</div>
@endif
</div>
<div class="col-md-9">
<h3>{{ $user->name }}</h3>
<p class="text-muted">{{ $user->email }}</p>
@if ($user->bio)
<div class="mt-3">
<h5>Bio</h5>
<p>{{ $user->bio }}</p>
</div>
@endif
@if ($user->website || $user->twitter || $user->github || $user->linkedin)
<div class="mt-4">
<h5>Links</h5>
<ul class="list-unstyled">
@if ($user->website)
<li class="mb-2">
<strong>Website:</strong>
<a href="{{ $user->website }}" target="_blank" rel="noopener">{{ $user->website }}</a>
</li>
@endif
@if ($user->twitter)
<li class="mb-2">
<strong>Twitter:</strong>
<a href="https://twitter.com/{{ $user->twitter }}" target="_blank" rel="noopener">@{{ $user->twitter }}</a>
</li>
@endif
@if ($user->github)
<li class="mb-2">
<strong>GitHub:</strong>
<a href="https://github.com/{{ $user->github }}" target="_blank" rel="noopener">{{ $user->github }}</a>
</li>
@endif
@if ($user->linkedin)
<li class="mb-2">
<strong>LinkedIn:</strong>
<a href="https://linkedin.com/in/{{ $user->linkedin }}" target="_blank" rel="noopener">{{ $user->linkedin }}</a>
</li>
@endif
</ul>
</div>
@endif
@if ($user->roles->isNotEmpty())
<div class="mt-4">
<h5>Roles</h5>
<div>
@foreach ($user->roles as $role)
<span class="badge bg-primary me-1">{{ $role->name }}</span>
@endforeach
</div>
</div>
@endif
<div class="mt-3 text-muted">
<small>Member since {{ $user->created_at->format('F Y') }}</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
File diff suppressed because one or more lines are too long
+36
View File
@@ -1,5 +1,6 @@
<?php <?php
use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::get('/', function () { Route::get('/', function () {
@@ -9,3 +10,38 @@ Route::get('/', function () {
Auth::routes(); Auth::routes();
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home'); Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
// Profile Routes
Route::middleware(['auth'])->group(function () {
Route::get('/profile/edit', [ProfileController::class, 'edit'])->name('profile.edit');
Route::put('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile/avatar', [ProfileController::class, 'deleteAvatar'])->name('profile.avatar.delete');
});
Route::get('/profile/{user}', [ProfileController::class, 'show'])->name('profile.show');
// Admin Routes (example)
Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(function () {
Route::get('/dashboard', function () {
return 'Admin Dashboard';
})->name('dashboard');
Route::get('/users', function () {
return 'User Management (Admin Only)';
})->name('users.index');
Route::get('/settings', function () {
return 'Settings (Admin Only)';
})->name('settings');
});
// Moderator Routes (example)
Route::middleware(['auth', 'moderator'])->prefix('moderator')->name('moderator.')->group(function () {
Route::get('/dashboard', function () {
return 'Moderator Dashboard';
})->name('dashboard');
Route::get('/products/pending', function () {
return 'Pending Products (Moderator Only)';
})->name('products.pending');
});
+146
View File
@@ -0,0 +1,146 @@
<?php
use App\Models\User;
use App\Models\Role;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
uses(RefreshDatabase::class);
beforeEach(function () {
Storage::fake('public');
});
test('users can view a profile', function () {
$user = User::factory()->create([
'name' => 'John Doe',
'bio' => 'Test bio',
]);
$response = $this->get(route('profile.show', $user));
$response->assertStatus(200);
$response->assertSee('John Doe');
$response->assertSee('Test bio');
});
test('authenticated users can view their edit profile page', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)->get(route('profile.edit'));
$response->assertStatus(200);
$response->assertSee('Edit Profile');
});
test('guests cannot view edit profile page', function () {
$response = $this->get(route('profile.edit'));
$response->assertRedirect(route('login'));
});
test('users can update their profile', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)->put(route('profile.update'), [
'name' => 'Updated Name',
'email' => $user->email,
'bio' => 'Updated bio',
'website' => 'https://example.com',
'twitter' => 'twitter_handle',
'github' => 'github_handle',
'linkedin' => 'linkedin_handle',
]);
$response->assertRedirect(route('profile.show', $user));
$response->assertSessionHas('success');
$user->refresh();
expect($user->name)->toBe('Updated Name');
expect($user->bio)->toBe('Updated bio');
expect($user->website)->toBe('https://example.com');
expect($user->twitter)->toBe('twitter_handle');
});
test('users can upload an avatar', function () {
$user = User::factory()->create();
$file = UploadedFile::fake()->image('avatar.jpg');
$response = $this->actingAs($user)->put(route('profile.update'), [
'name' => $user->name,
'email' => $user->email,
'avatar' => $file,
]);
$response->assertRedirect(route('profile.show', $user));
$user->refresh();
expect($user->avatar)->not->toBeNull();
Storage::disk('public')->assertExists('avatars/' . $user->avatar);
});
test('avatar must be an image', function () {
$user = User::factory()->create();
$file = UploadedFile::fake()->create('document.pdf');
$response = $this->actingAs($user)->put(route('profile.update'), [
'name' => $user->name,
'email' => $user->email,
'avatar' => $file,
]);
$response->assertSessionHasErrors('avatar');
});
test('avatar cannot exceed 2MB', function () {
$user = User::factory()->create();
$file = UploadedFile::fake()->image('avatar.jpg')->size(3000);
$response = $this->actingAs($user)->put(route('profile.update'), [
'name' => $user->name,
'email' => $user->email,
'avatar' => $file,
]);
$response->assertSessionHasErrors('avatar');
});
test('users can delete their avatar', function () {
Storage::fake('public');
$user = User::factory()->create(['avatar' => 'test_avatar.jpg']);
Storage::disk('public')->put('avatars/test_avatar.jpg', 'fake content');
$response = $this->actingAs($user)->delete(route('profile.avatar.delete'));
$response->assertRedirect(route('profile.edit'));
$response->assertSessionHas('success');
$user->refresh();
expect($user->avatar)->toBeNull();
Storage::disk('public')->assertMissing('avatars/test_avatar.jpg');
});
test('email must be unique when updating profile', function () {
$existingUser = User::factory()->create(['email' => 'existing@example.com']);
$user = User::factory()->create(['email' => 'user@example.com']);
$response = $this->actingAs($user)->put(route('profile.update'), [
'name' => $user->name,
'email' => 'existing@example.com',
]);
$response->assertSessionHasErrors('email');
});
test('website must be a valid url', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)->put(route('profile.update'), [
'name' => $user->name,
'email' => $user->email,
'website' => 'not-a-valid-url',
]);
$response->assertSessionHasErrors('website');
});
+197
View File
@@ -0,0 +1,197 @@
<?php
use App\Models\User;
use App\Models\Role;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
uses(RefreshDatabase::class);
beforeEach(function () {
// Create roles
$this->adminRole = Role::create([
'name' => 'Admin',
'slug' => 'admin',
'description' => 'Full access',
]);
$this->moderatorRole = Role::create([
'name' => 'Moderator',
'slug' => 'moderator',
'description' => 'Moderate content',
]);
$this->userRole = Role::create([
'name' => 'User',
'slug' => 'user',
'description' => 'Regular user',
]);
});
test('users can be assigned roles', function () {
$user = User::factory()->create();
$user->roles()->attach($this->userRole);
expect($user->roles)->toHaveCount(1);
expect($user->roles->first()->slug)->toBe('user');
});
test('user can have multiple roles', function () {
$user = User::factory()->create();
$user->roles()->attach([$this->userRole->id, $this->moderatorRole->id]);
expect($user->roles)->toHaveCount(2);
});
test('hasRole method works correctly', function () {
$user = User::factory()->create();
$user->roles()->attach($this->userRole);
expect($user->hasRole('user'))->toBeTrue();
expect($user->hasRole('admin'))->toBeFalse();
});
test('hasAnyRole method works correctly', function () {
$user = User::factory()->create();
$user->roles()->attach($this->moderatorRole);
expect($user->hasAnyRole(['moderator', 'admin']))->toBeTrue();
expect($user->hasAnyRole(['user', 'admin']))->toBeFalse();
});
test('isAdmin method works correctly', function () {
$admin = User::factory()->create();
$admin->roles()->attach($this->adminRole);
$user = User::factory()->create();
$user->roles()->attach($this->userRole);
expect($admin->isAdmin())->toBeTrue();
expect($user->isAdmin())->toBeFalse();
});
test('isModerator method works correctly', function () {
$moderator = User::factory()->create();
$moderator->roles()->attach($this->moderatorRole);
$user = User::factory()->create();
$user->roles()->attach($this->userRole);
expect($moderator->isModerator())->toBeTrue();
expect($user->isModerator())->toBeFalse();
});
test('admin middleware blocks non-admin users', function () {
$user = User::factory()->create();
$user->roles()->attach($this->userRole);
$response = $this->actingAs($user)->get('/admin/dashboard');
$response->assertStatus(403);
});
test('admin middleware allows admin users', function () {
$admin = User::factory()->create();
$admin->roles()->attach($this->adminRole);
$response = $this->actingAs($admin)->get('/admin/dashboard');
$response->assertStatus(200);
});
test('moderator middleware blocks regular users', function () {
$user = User::factory()->create();
$user->roles()->attach($this->userRole);
$response = $this->actingAs($user)->get('/moderator/dashboard');
$response->assertStatus(403);
});
test('moderator middleware allows moderators', function () {
$moderator = User::factory()->create();
$moderator->roles()->attach($this->moderatorRole);
$response = $this->actingAs($moderator)->get('/moderator/dashboard');
$response->assertStatus(200);
});
test('moderator middleware allows admins', function () {
$admin = User::factory()->create();
$admin->roles()->attach($this->adminRole);
$response = $this->actingAs($admin)->get('/moderator/dashboard');
$response->assertStatus(200);
});
test('guests cannot access protected routes', function () {
$response = $this->get('/admin/dashboard');
$response->assertStatus(302);
$response = $this->get('/moderator/dashboard');
$response->assertStatus(302);
});
test('create product gate allows users moderators and admins', function () {
$user = User::factory()->create();
$user->roles()->attach($this->userRole);
expect(Gate::forUser($user)->allows('create-product'))->toBeTrue();
$moderator = User::factory()->create();
$moderator->roles()->attach($this->moderatorRole);
expect(Gate::forUser($moderator)->allows('create-product'))->toBeTrue();
$admin = User::factory()->create();
$admin->roles()->attach($this->adminRole);
expect(Gate::forUser($admin)->allows('create-product'))->toBeTrue();
});
test('approve product gate allows only moderators and admins', function () {
$user = User::factory()->create();
$user->roles()->attach($this->userRole);
expect(Gate::forUser($user)->allows('approve-product'))->toBeFalse();
$moderator = User::factory()->create();
$moderator->roles()->attach($this->moderatorRole);
expect(Gate::forUser($moderator)->allows('approve-product'))->toBeTrue();
$admin = User::factory()->create();
$admin->roles()->attach($this->adminRole);
expect(Gate::forUser($admin)->allows('approve-product'))->toBeTrue();
});
test('manage users gate allows only admins', function () {
$user = User::factory()->create();
$user->roles()->attach($this->userRole);
expect(Gate::forUser($user)->allows('manage-users'))->toBeFalse();
$moderator = User::factory()->create();
$moderator->roles()->attach($this->moderatorRole);
expect(Gate::forUser($moderator)->allows('manage-users'))->toBeFalse();
$admin = User::factory()->create();
$admin->roles()->attach($this->adminRole);
expect(Gate::forUser($admin)->allows('manage-users'))->toBeTrue();
});
test('admin has access to all gates via gate before', function () {
$admin = User::factory()->create();
$admin->roles()->attach($this->adminRole);
expect(Gate::forUser($admin)->allows('create-product'))->toBeTrue();
expect(Gate::forUser($admin)->allows('approve-product'))->toBeTrue();
expect(Gate::forUser($admin)->allows('manage-users'))->toBeTrue();
expect(Gate::forUser($admin)->allows('manage-roles'))->toBeTrue();
expect(Gate::forUser($admin)->allows('manage-settings'))->toBeTrue();
});
+6 -1
View File
@@ -6,5 +6,10 @@ use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase abstract class TestCase extends BaseTestCase
{ {
// protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
}
} }