feat: add routes, lang, tests, stubs, docs, and docker configurations
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Permission;
|
||||
use App\Models\User;
|
||||
|
||||
function grantManageAccessRightsForPerms(User $user): void
|
||||
{
|
||||
$perm = Permission::firstOrCreate(['name' => 'manage access rights', 'guard_name' => 'web']);
|
||||
Permission::firstOrCreate(['name' => 'view access rights', 'guard_name' => 'web']);
|
||||
$user->givePermissionTo($perm);
|
||||
}
|
||||
|
||||
test('guest cannot access permissions index', function () {
|
||||
$this->get('/permissions')->assertRedirect('/login');
|
||||
});
|
||||
|
||||
test('user without permission gets 403', function () {
|
||||
$u = User::factory()->create();
|
||||
$this->actingAs($u)->get('/permissions')->assertForbidden();
|
||||
});
|
||||
|
||||
test('store creates a permission with web guard', function () {
|
||||
$admin = User::factory()->create();
|
||||
grantManageAccessRightsForPerms($admin);
|
||||
|
||||
$response = $this->actingAs($admin)->postJson('/permissions', [
|
||||
'name' => 'view.reports',
|
||||
'guard_name' => 'web',
|
||||
]);
|
||||
|
||||
$response->assertOk()->assertJson(['success' => true]);
|
||||
$this->assertDatabaseHas('permissions', [
|
||||
'name' => 'view.reports',
|
||||
'guard_name' => 'web',
|
||||
]);
|
||||
});
|
||||
|
||||
test('same name allowed across different guards', function () {
|
||||
$admin = User::factory()->create();
|
||||
grantManageAccessRightsForPerms($admin);
|
||||
Permission::create(['name' => 'shared.perm', 'guard_name' => 'web']);
|
||||
|
||||
$this->actingAs($admin)->postJson('/permissions', [
|
||||
'name' => 'shared.perm',
|
||||
'guard_name' => 'api',
|
||||
])->assertOk();
|
||||
});
|
||||
|
||||
test('store rejects duplicate name within same guard', function () {
|
||||
$admin = User::factory()->create();
|
||||
grantManageAccessRightsForPerms($admin);
|
||||
Permission::create(['name' => 'duplicate.perm', 'guard_name' => 'web']);
|
||||
|
||||
$this->actingAs($admin)->postJson('/permissions', [
|
||||
'name' => 'duplicate.perm',
|
||||
'guard_name' => 'web',
|
||||
])->assertStatus(422);
|
||||
});
|
||||
|
||||
test('store rejects invalid guard', function () {
|
||||
$admin = User::factory()->create();
|
||||
grantManageAccessRightsForPerms($admin);
|
||||
|
||||
$this->actingAs($admin)->postJson('/permissions', [
|
||||
'name' => 'some.perm',
|
||||
'guard_name' => 'console',
|
||||
])->assertStatus(422);
|
||||
});
|
||||
|
||||
test('store rejects illegal characters in name', function () {
|
||||
$admin = User::factory()->create();
|
||||
grantManageAccessRightsForPerms($admin);
|
||||
|
||||
$this->actingAs($admin)->postJson('/permissions', [
|
||||
'name' => 'bad name with space!',
|
||||
'guard_name' => 'web',
|
||||
])->assertStatus(422);
|
||||
});
|
||||
|
||||
test('update can rename a permission', function () {
|
||||
$admin = User::factory()->create();
|
||||
grantManageAccessRightsForPerms($admin);
|
||||
$p = Permission::create(['name' => 'old.name', 'guard_name' => 'web']);
|
||||
|
||||
$this->actingAs($admin)->putJson("/permissions/{$p->id}", [
|
||||
'name' => 'new.name',
|
||||
'guard_name' => 'web',
|
||||
])->assertOk();
|
||||
|
||||
expect($p->fresh()->name)->toBe('new.name');
|
||||
});
|
||||
|
||||
test('toggleStatus flips is_active', function () {
|
||||
$admin = User::factory()->create();
|
||||
grantManageAccessRightsForPerms($admin);
|
||||
$p = Permission::create(['name' => 'flip.able', 'guard_name' => 'web', 'is_active' => 1]);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->postJson('/permissions/toggle-status', ['id' => $p->id, 'status' => 'deactivate'])
|
||||
->assertOk();
|
||||
expect((bool) $p->fresh()->is_active)->toBeFalse();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->postJson('/permissions/toggle-status', ['id' => $p->id, 'status' => 'activate'])
|
||||
->assertOk();
|
||||
expect((bool) $p->fresh()->is_active)->toBeTrue();
|
||||
});
|
||||
|
||||
test('destroy soft deletes permission', function () {
|
||||
$admin = User::factory()->create();
|
||||
grantManageAccessRightsForPerms($admin);
|
||||
$p = Permission::create(['name' => 'to.delete', 'guard_name' => 'web']);
|
||||
|
||||
$this->actingAs($admin)->deleteJson("/permissions/{$p->id}")->assertOk();
|
||||
expect(Permission::withTrashed()->find($p->id)->trashed())->toBeTrue();
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\WithFaker;
|
||||
|
||||
uses(WithFaker::class);
|
||||
|
||||
function grantManageAccessRights(User $user): void
|
||||
{
|
||||
$perm = Permission::firstOrCreate(['name' => 'manage access rights', 'guard_name' => 'web']);
|
||||
Permission::firstOrCreate(['name' => 'view access rights', 'guard_name' => 'web']);
|
||||
$user->givePermissionTo($perm);
|
||||
}
|
||||
|
||||
test('guest cannot access roles index', function () {
|
||||
$this->get('/roles')->assertRedirect('/login');
|
||||
});
|
||||
|
||||
test('authenticated user without permission gets 403 on roles index', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user)->get('/roles')->assertForbidden();
|
||||
});
|
||||
|
||||
test('user with manage access rights can store a new role', function () {
|
||||
$user = User::factory()->create();
|
||||
grantManageAccessRights($user);
|
||||
$p = Permission::firstOrCreate(['name' => 'view dashboard', 'guard_name' => 'web']);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->postJson('/roles', [
|
||||
'name' => 'editor-role',
|
||||
'guard_name' => 'web',
|
||||
'permissions' => [$p->id],
|
||||
]);
|
||||
|
||||
$response->assertOk()->assertJson(['success' => true]);
|
||||
$this->assertDatabaseHas('roles', ['name' => 'editor-role']);
|
||||
|
||||
$role = Role::where('name', 'editor-role')->first();
|
||||
expect($role->hasPermissionTo('view dashboard'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('store rejects duplicate role name', function () {
|
||||
$user = User::factory()->create();
|
||||
grantManageAccessRights($user);
|
||||
$p = Permission::firstOrCreate(['name' => 'view dashboard', 'guard_name' => 'web']);
|
||||
Role::create(['name' => 'dup-role', 'guard_name' => 'web']);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->postJson('/roles', [
|
||||
'name' => 'dup-role',
|
||||
'guard_name' => 'web',
|
||||
'permissions' => [$p->id],
|
||||
]);
|
||||
|
||||
$response->assertStatus(422);
|
||||
});
|
||||
|
||||
test('store rejects empty permissions array', function () {
|
||||
$user = User::factory()->create();
|
||||
grantManageAccessRights($user);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->postJson('/roles', [
|
||||
'name' => 'no-perm-role',
|
||||
'guard_name' => 'web',
|
||||
'permissions' => [],
|
||||
]);
|
||||
|
||||
$response->assertStatus(422);
|
||||
});
|
||||
|
||||
test('store rejects invalid characters in role name', function () {
|
||||
$user = User::factory()->create();
|
||||
grantManageAccessRights($user);
|
||||
$p = Permission::firstOrCreate(['name' => 'view dashboard', 'guard_name' => 'web']);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->postJson('/roles', [
|
||||
'name' => 'role with space!',
|
||||
'guard_name' => 'web',
|
||||
'permissions' => [$p->id],
|
||||
]);
|
||||
|
||||
$response->assertStatus(422);
|
||||
});
|
||||
|
||||
test('update replaces role permissions', function () {
|
||||
$user = User::factory()->create();
|
||||
grantManageAccessRights($user);
|
||||
$p1 = Permission::firstOrCreate(['name' => 'view dashboard', 'guard_name' => 'web']);
|
||||
$p2 = Permission::firstOrCreate(['name' => 'view user directory', 'guard_name' => 'web']);
|
||||
$role = Role::create(['name' => 'original-role', 'guard_name' => 'web']);
|
||||
$role->givePermissionTo('view dashboard');
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->putJson("/roles/{$role->id}", [
|
||||
'name' => 'renamed-role',
|
||||
'guard_name' => 'web',
|
||||
'permissions' => [$p2->id],
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$role->refresh();
|
||||
expect($role->name)->toBe('renamed-role');
|
||||
expect($role->hasPermissionTo('view user directory'))->toBeTrue();
|
||||
expect($role->hasPermissionTo('view dashboard'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('toggleStatus activates and deactivates role', function () {
|
||||
$user = User::factory()->create();
|
||||
grantManageAccessRights($user);
|
||||
$role = Role::create(['name' => 'toggleable', 'guard_name' => 'web', 'is_active' => true]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->postJson('/roles/toggle-status', ['id' => $role->id, 'status' => 'deactivate'])
|
||||
->assertOk();
|
||||
expect((bool) $role->fresh()->is_active)->toBeFalse();
|
||||
|
||||
$this->actingAs($user)
|
||||
->postJson('/roles/toggle-status', ['id' => $role->id, 'status' => 'activate'])
|
||||
->assertOk();
|
||||
expect((bool) $role->fresh()->is_active)->toBeTrue();
|
||||
});
|
||||
|
||||
test('destroy soft deletes role when no users assigned', function () {
|
||||
$user = User::factory()->create();
|
||||
grantManageAccessRights($user);
|
||||
$role = Role::create(['name' => 'doomed', 'guard_name' => 'web']);
|
||||
|
||||
$this->actingAs($user)
|
||||
->deleteJson("/roles/{$role->id}")
|
||||
->assertOk();
|
||||
|
||||
expect(Role::withTrashed()->find($role->id)->trashed())->toBeTrue();
|
||||
});
|
||||
|
||||
test('destroy blocks deletion when role is still assigned to a user', function () {
|
||||
$admin = User::factory()->create();
|
||||
grantManageAccessRights($admin);
|
||||
$role = Role::create(['name' => 'in-use', 'guard_name' => 'web']);
|
||||
$victim = User::factory()->create();
|
||||
$victim->assignRole($role);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->deleteJson("/roles/{$role->id}")
|
||||
->assertStatus(422)
|
||||
->assertJson(['success' => false]);
|
||||
|
||||
expect(Role::find($role->id))->not->toBeNull();
|
||||
});
|
||||
|
||||
test('restore brings back a soft-deleted role', function () {
|
||||
$user = User::factory()->create();
|
||||
grantManageAccessRights($user);
|
||||
$role = Role::create(['name' => 'restorable', 'guard_name' => 'web']);
|
||||
$role->delete();
|
||||
|
||||
$this->actingAs($user)
|
||||
->postJson("/roles/{$role->id}/restore")
|
||||
->assertOk();
|
||||
|
||||
expect(Role::find($role->id)->trashed())->toBeFalse();
|
||||
});
|
||||
|
||||
test('forceDelete permanently removes role', function () {
|
||||
$user = User::factory()->create();
|
||||
grantManageAccessRights($user);
|
||||
$role = Role::create(['name' => 'gone-forever', 'guard_name' => 'web']);
|
||||
$role->delete();
|
||||
|
||||
$this->actingAs($user)
|
||||
->deleteJson("/roles/{$role->id}/force")
|
||||
->assertOk();
|
||||
|
||||
expect(Role::withTrashed()->find($role->id))->toBeNull();
|
||||
});
|
||||
|
||||
test('forceDelete blocks when role has users assigned', function () {
|
||||
$admin = User::factory()->create();
|
||||
grantManageAccessRights($admin);
|
||||
$role = Role::create(['name' => 'sticky', 'guard_name' => 'web']);
|
||||
$victim = User::factory()->create();
|
||||
$victim->assignRole($role);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->deleteJson("/roles/{$role->id}/force")
|
||||
->assertStatus(422);
|
||||
|
||||
expect(Role::find($role->id))->not->toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
|
||||
function grantManageUserDirectory(User $user): void
|
||||
{
|
||||
$perm = Permission::firstOrCreate(['name' => 'manage user directory', 'guard_name' => 'web']);
|
||||
Permission::firstOrCreate(['name' => 'view user directory', 'guard_name' => 'web']);
|
||||
$user->givePermissionTo($perm);
|
||||
}
|
||||
|
||||
function defaultRole(): Role
|
||||
{
|
||||
return Role::firstOrCreate(['name' => 'member', 'guard_name' => 'web']);
|
||||
}
|
||||
|
||||
const STRONG_PASSWORD = 'Str0ng!Passw0rd2026';
|
||||
|
||||
test('guest cannot access user index', function () {
|
||||
$this->get('/users')->assertRedirect('/login');
|
||||
});
|
||||
|
||||
test('user without permission gets 403', function () {
|
||||
$u = User::factory()->create();
|
||||
$this->actingAs($u)->get('/users')->assertForbidden();
|
||||
});
|
||||
|
||||
test('store creates user, hashes password, assigns roles', function () {
|
||||
$admin = User::factory()->create();
|
||||
grantManageUserDirectory($admin);
|
||||
$role = defaultRole();
|
||||
|
||||
$response = $this->actingAs($admin)->postJson('/users', [
|
||||
'name' => 'Jane Doe',
|
||||
'email' => 'jane@example.com',
|
||||
'password' => STRONG_PASSWORD,
|
||||
'roles' => [$role->id],
|
||||
]);
|
||||
|
||||
$response->assertOk()->assertJson(['success' => true]);
|
||||
|
||||
$created = User::where('email', 'jane@example.com')->first();
|
||||
expect($created)->not->toBeNull();
|
||||
expect($created->password)->not->toBe(STRONG_PASSWORD);
|
||||
expect($created->hasRole('member'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('store rejects weak password', function () {
|
||||
$admin = User::factory()->create();
|
||||
grantManageUserDirectory($admin);
|
||||
$role = defaultRole();
|
||||
|
||||
$this->actingAs($admin)->postJson('/users', [
|
||||
'name' => 'Weak Pass',
|
||||
'email' => 'weak@example.com',
|
||||
'password' => 'short',
|
||||
'roles' => [$role->id],
|
||||
])->assertStatus(422);
|
||||
});
|
||||
|
||||
test('store rejects duplicate email', function () {
|
||||
$admin = User::factory()->create();
|
||||
grantManageUserDirectory($admin);
|
||||
$role = defaultRole();
|
||||
User::factory()->create(['email' => 'taken@example.com']);
|
||||
|
||||
$this->actingAs($admin)->postJson('/users', [
|
||||
'name' => 'Dup Email',
|
||||
'email' => 'taken@example.com',
|
||||
'password' => STRONG_PASSWORD,
|
||||
'roles' => [$role->id],
|
||||
])->assertStatus(422);
|
||||
});
|
||||
|
||||
test('store rejects name with digits', function () {
|
||||
$admin = User::factory()->create();
|
||||
grantManageUserDirectory($admin);
|
||||
$role = defaultRole();
|
||||
|
||||
$this->actingAs($admin)->postJson('/users', [
|
||||
'name' => 'John123',
|
||||
'email' => 'john@example.com',
|
||||
'password' => STRONG_PASSWORD,
|
||||
'roles' => [$role->id],
|
||||
])->assertStatus(422);
|
||||
});
|
||||
|
||||
test('store requires at least one role', function () {
|
||||
$admin = User::factory()->create();
|
||||
grantManageUserDirectory($admin);
|
||||
|
||||
$this->actingAs($admin)->postJson('/users', [
|
||||
'name' => 'No Role',
|
||||
'email' => 'norole@example.com',
|
||||
'password' => STRONG_PASSWORD,
|
||||
'roles' => [],
|
||||
])->assertStatus(422);
|
||||
});
|
||||
|
||||
test('update can change name and reset roles without touching password', function () {
|
||||
$admin = User::factory()->create(['name' => 'Admin User']);
|
||||
grantManageUserDirectory($admin);
|
||||
$r1 = defaultRole();
|
||||
$r2 = Role::firstOrCreate(['name' => 'editor', 'guard_name' => 'web']);
|
||||
$target = User::factory()->create(['name' => 'Old Name']);
|
||||
$target->assignRole($r1);
|
||||
$originalHash = $target->password;
|
||||
|
||||
$this->actingAs($admin)->putJson("/users/{$target->id}", [
|
||||
'name' => 'Renamed User',
|
||||
'email' => $target->email,
|
||||
'roles' => [$r2->id],
|
||||
])->assertOk();
|
||||
|
||||
$target->refresh();
|
||||
expect($target->name)->toBe('Renamed User');
|
||||
expect($target->password)->toBe($originalHash);
|
||||
expect($target->hasRole('editor'))->toBeTrue();
|
||||
expect($target->hasRole('member'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('update changes password when provided', function () {
|
||||
$admin = User::factory()->create(['name' => 'Admin User']);
|
||||
grantManageUserDirectory($admin);
|
||||
$role = defaultRole();
|
||||
$target = User::factory()->create(['name' => 'Target User']);
|
||||
$target->assignRole($role);
|
||||
$original = $target->password;
|
||||
|
||||
$this->actingAs($admin)->putJson("/users/{$target->id}", [
|
||||
'name' => 'Target User',
|
||||
'email' => $target->email,
|
||||
'password' => STRONG_PASSWORD,
|
||||
'roles' => [$role->id],
|
||||
])->assertOk();
|
||||
|
||||
expect($target->fresh()->password)->not->toBe($original);
|
||||
});
|
||||
|
||||
test('toggleStatus deactivates and activates user', function () {
|
||||
$admin = User::factory()->create();
|
||||
grantManageUserDirectory($admin);
|
||||
$target = User::factory()->create(['is_active' => 1]);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->postJson('/users/toggle-status', ['id' => $target->id, 'status' => 'deactivate'])
|
||||
->assertOk();
|
||||
expect((bool) $target->fresh()->is_active)->toBeFalse();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->postJson('/users/toggle-status', ['id' => $target->id, 'status' => 'activate'])
|
||||
->assertOk();
|
||||
expect((bool) $target->fresh()->is_active)->toBeTrue();
|
||||
});
|
||||
|
||||
test('destroy soft deletes user', function () {
|
||||
$admin = User::factory()->create();
|
||||
grantManageUserDirectory($admin);
|
||||
$target = User::factory()->create();
|
||||
|
||||
$this->actingAs($admin)->deleteJson("/users/{$target->id}")->assertOk();
|
||||
expect(User::withTrashed()->find($target->id)->trashed())->toBeTrue();
|
||||
});
|
||||
|
||||
test('restore brings soft-deleted user back', function () {
|
||||
$admin = User::factory()->create();
|
||||
grantManageUserDirectory($admin);
|
||||
$target = User::factory()->create();
|
||||
$target->delete();
|
||||
|
||||
$this->actingAs($admin)->postJson("/users/{$target->id}/restore")->assertOk();
|
||||
expect(User::find($target->id)->trashed())->toBeFalse();
|
||||
});
|
||||
|
||||
test('forceDelete blocks self-deletion', function () {
|
||||
$admin = User::factory()->create();
|
||||
grantManageUserDirectory($admin);
|
||||
$admin->delete();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->deleteJson("/users/{$admin->id}/force")
|
||||
->assertStatus(403)
|
||||
->assertJson(['success' => false]);
|
||||
|
||||
expect(User::withTrashed()->find($admin->id))->not->toBeNull();
|
||||
});
|
||||
|
||||
test('forceDelete permanently removes another user', function () {
|
||||
$admin = User::factory()->create();
|
||||
grantManageUserDirectory($admin);
|
||||
$target = User::factory()->create();
|
||||
$target->delete();
|
||||
|
||||
$this->actingAs($admin)->deleteJson("/users/{$target->id}/force")->assertOk();
|
||||
expect(User::withTrashed()->find($target->id))->toBeNull();
|
||||
});
|
||||
Reference in New Issue
Block a user