mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 00:02:15 +01:00
Added member and invitation endpoints
This commit is contained in:
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Membership;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
@@ -14,13 +15,20 @@ class CreateTeamTest extends TestCase
|
||||
|
||||
public function test_teams_can_be_created(): void
|
||||
{
|
||||
$this->actingAs($user = User::factory()->withPersonalOrganization()->create());
|
||||
// Arrange
|
||||
$user = User::factory()->withPersonalOrganization()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
// Act
|
||||
$response = $this->post('/teams', [
|
||||
'name' => 'Test Organization',
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$newOrganization = $user->fresh()->ownedTeams()->latest('id')->first();
|
||||
$this->assertCount(2, $user->fresh()->ownedTeams);
|
||||
$this->assertEquals('Test Organization', $user->fresh()->ownedTeams()->latest('id')->first()->name);
|
||||
$this->assertEquals('Test Organization', $newOrganization->name);
|
||||
$member = Membership::query()->whereBelongsTo($user, 'user')->whereBelongsTo($newOrganization, 'organization')->firstOrFail();
|
||||
$this->assertSame('owner', $member->role);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ class InviteTeamMemberTest extends TestCase
|
||||
// Assert
|
||||
$this->assertCount(0, $owner->currentTeam->fresh()->teamInvitations);
|
||||
$user->refresh();
|
||||
$this->assertCount(1, $user->organizations);
|
||||
$this->assertCount(2, $user->organizations);
|
||||
$this->assertContains($owner->currentTeam->getKey(), $user->organizations->pluck('id'));
|
||||
}
|
||||
|
||||
@@ -126,9 +126,7 @@ class InviteTeamMemberTest extends TestCase
|
||||
{
|
||||
// Arrange
|
||||
Mail::fake();
|
||||
$placeholder = User::factory()->withPersonalOrganization()->create([
|
||||
'is_placeholder' => true,
|
||||
]);
|
||||
$placeholder = User::factory()->withPersonalOrganization()->placeholder()->create();
|
||||
|
||||
$owner = User::factory()->withPersonalOrganization()->create();
|
||||
$owner->currentTeam->users()->attach($placeholder, ['role' => 'employee']);
|
||||
@@ -154,12 +152,11 @@ class InviteTeamMemberTest extends TestCase
|
||||
|
||||
// Assert
|
||||
$user->refresh();
|
||||
$placeholder->refresh();
|
||||
$this->assertDatabaseMissing(User::class, ['id' => $placeholder->id]);
|
||||
$this->assertCount(0, $owner->currentTeam->fresh()->teamInvitations);
|
||||
$this->assertCount(1, $user->organizations);
|
||||
$this->assertCount(2, $user->organizations);
|
||||
$this->assertContains($owner->currentTeam->getKey(), $user->organizations->pluck('id'));
|
||||
$this->assertCount(5, $user->timeEntries);
|
||||
$this->assertCount(0, $placeholder->timeEntries);
|
||||
}
|
||||
|
||||
public function test_team_member_accept_fails_if_user_with_that_email_does_not_exist(): void
|
||||
@@ -185,6 +182,6 @@ class InviteTeamMemberTest extends TestCase
|
||||
// Assert
|
||||
$this->assertCount(1, $owner->currentTeam->fresh()->teamInvitations);
|
||||
$user->refresh();
|
||||
$this->assertCount(0, $user->organizations);
|
||||
$this->assertCount(1, $user->organizations);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class LeaveTeamTest extends TestCase
|
||||
|
||||
$response = $this->delete('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id);
|
||||
|
||||
$this->assertCount(0, $user->currentTeam->fresh()->users);
|
||||
$this->assertCount(1, $user->currentTeam->fresh()->users);
|
||||
}
|
||||
|
||||
public function test_team_owners_cant_leave_their_own_team(): void
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Membership;
|
||||
use App\Models\User;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
@@ -52,7 +53,12 @@ class RegistrationTest extends TestCase
|
||||
$this->assertAuthenticated();
|
||||
$response->assertRedirect(RouteServiceProvider::HOME);
|
||||
$user = User::where('email', 'test@example.com')->firstOrFail();
|
||||
$this->assertSame('Test User', $user->name);
|
||||
$this->assertSame('UTC', $user->timezone);
|
||||
$organization = $user->organizations()->firstOrFail();
|
||||
$this->assertSame(true, $organization->personal_team);
|
||||
$member = Membership::query()->whereBelongsTo($user, 'user')->whereBelongsTo($organization, 'organization')->firstOrFail();
|
||||
$this->assertSame('owner', $member->role);
|
||||
}
|
||||
|
||||
public function test_new_users_can_register_and_frontend_can_send_timezone_for_user(): void
|
||||
|
||||
@@ -20,9 +20,9 @@ class RemoveTeamMemberTest extends TestCase
|
||||
$otherUser = User::factory()->create(), ['role' => 'admin']
|
||||
);
|
||||
|
||||
$response = $this->delete('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id);
|
||||
$response = $this->withoutExceptionHandling()->delete('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id);
|
||||
|
||||
$this->assertCount(0, $user->currentTeam->fresh()->users);
|
||||
$this->assertCount(1, $user->currentTeam->fresh()->users);
|
||||
}
|
||||
|
||||
public function test_only_team_owner_can_remove_team_members(): void
|
||||
|
||||
@@ -14,23 +14,69 @@ class UpdateTeamMemberRoleTest extends TestCase
|
||||
|
||||
public function test_team_member_roles_can_be_updated(): void
|
||||
{
|
||||
$this->actingAs($user = User::factory()->withPersonalOrganization()->create());
|
||||
// Arrange
|
||||
$user = User::factory()->withPersonalOrganization()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$user->currentTeam->users()->attach(
|
||||
$otherUser = User::factory()->create(), ['role' => 'admin']
|
||||
);
|
||||
|
||||
// Act
|
||||
$response = $this->put('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id, [
|
||||
'role' => 'employee',
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$this->assertTrue($otherUser->fresh()->hasTeamRole(
|
||||
$user->currentTeam->fresh(), 'employee'
|
||||
));
|
||||
}
|
||||
|
||||
public function test_team_member_roles_can_not_be_updated_to_placeholder(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = User::factory()->withPersonalOrganization()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$user->currentTeam->users()->attach(
|
||||
$otherUser = User::factory()->create(), ['role' => 'admin']
|
||||
);
|
||||
|
||||
// Act
|
||||
$response = $this->put('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id, [
|
||||
'role' => 'placeholder',
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$this->assertTrue($otherUser->fresh()->hasTeamRole(
|
||||
$user->currentTeam->fresh(), 'admin'
|
||||
));
|
||||
}
|
||||
|
||||
public function test_team_member_roles_can_be_updated_to_owner_which_changes_ownership(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = User::factory()->withPersonalOrganization()->create();
|
||||
$this->actingAs($user);
|
||||
$otherUser = User::factory()->create();
|
||||
$user->currentTeam->users()->attach($otherUser, ['role' => 'admin']);
|
||||
|
||||
// Act
|
||||
$response = $this->withoutExceptionHandling()->put('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->getKey(), [
|
||||
'role' => 'owner',
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$this->assertTrue($otherUser->fresh()->hasTeamRole(
|
||||
$user->currentTeam->fresh(), 'owner'
|
||||
));
|
||||
$this->assertSame($user->currentTeam->fresh()->user_id, $otherUser->getKey());
|
||||
}
|
||||
|
||||
public function test_only_team_owner_can_update_team_member_roles(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = User::factory()->withPersonalOrganization()->create();
|
||||
|
||||
$user->currentTeam->users()->attach(
|
||||
@@ -39,10 +85,13 @@ class UpdateTeamMemberRoleTest extends TestCase
|
||||
|
||||
$this->actingAs($otherUser);
|
||||
|
||||
// Act
|
||||
$response = $this->put('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id, [
|
||||
'role' => 'employee',
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(403);
|
||||
$this->assertTrue($otherUser->fresh()->hasTeamRole(
|
||||
$user->currentTeam->fresh(), 'admin'
|
||||
));
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Endpoint\Api\V1;
|
||||
|
||||
use App\Models\Membership;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
@@ -16,7 +17,7 @@ class ApiEndpointTestAbstract extends TestCase
|
||||
|
||||
/**
|
||||
* @param array<string> $permissions
|
||||
* @return object{user: User, organization: Organization}
|
||||
* @return object{user: User, organization: Organization, member: Membership}
|
||||
*/
|
||||
protected function createUserWithPermission(array $permissions, bool $isOwner = false): object
|
||||
{
|
||||
@@ -28,13 +29,14 @@ class ApiEndpointTestAbstract extends TestCase
|
||||
} else {
|
||||
$organization = Organization::factory()->create();
|
||||
}
|
||||
$organization->users()->attach($user, [
|
||||
$membership = Membership::factory()->forUser($user)->forOrganization($organization)->create([
|
||||
'role' => 'custom-test',
|
||||
]);
|
||||
|
||||
return (object) [
|
||||
'user' => $user,
|
||||
'organization' => $organization,
|
||||
'member' => $membership,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
79
tests/Unit/Endpoint/Api/V1/InvitationEndpointTest.php
Normal file
79
tests/Unit/Endpoint/Api/V1/InvitationEndpointTest.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Endpoint\Api\V1;
|
||||
|
||||
use App\Models\OrganizationInvitation;
|
||||
use Laravel\Passport\Passport;
|
||||
|
||||
class InvitationEndpointTest extends ApiEndpointTestAbstract
|
||||
{
|
||||
public function test_index_fails_if_user_has_no_permission_to_view_invitations(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
]);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->getJson(route('api.v1.invitations.index', $data->organization->id));
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_index_returns_invitations_of_organization(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'invitations:view',
|
||||
]);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->getJson(route('api.v1.invitations.index', $data->organization->getKey()));
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_store_fails_if_user_has_no_permission_to_create_invitations(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
]);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->postJson(route('api.v1.invitations.store', $data->organization->getKey()), [
|
||||
'email' => 'test@mail.test',
|
||||
'role' => 'employee',
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_store_invites_user_to_organization(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'invitations:create',
|
||||
]);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->postJson(route('api.v1.invitations.store', $data->organization->getKey()), [
|
||||
'email' => 'test@asdf.at',
|
||||
'role' => 'employee',
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(204);
|
||||
$invitation = OrganizationInvitation::first();
|
||||
$this->assertNotNull($invitation);
|
||||
$this->assertEquals('test@asdf.at', $invitation->email);
|
||||
$this->assertEquals('employee', $invitation->role);
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,27 @@ declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Endpoint\Api\V1;
|
||||
|
||||
use App\Models\Membership;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use Laravel\Passport\Passport;
|
||||
|
||||
class MemberEndpointTest extends ApiEndpointTestAbstract
|
||||
{
|
||||
public function test_index_fails_if_user_has_no_permission_to_view_members(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
]);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->getJson(route('api.v1.members.index', $data->organization->id));
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_index_returns_members_of_organization(): void
|
||||
{
|
||||
// Arrange
|
||||
@@ -19,12 +34,72 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->getJson(route('api.v1.members.index', $data->organization->id));
|
||||
$response = $this->getJson(route('api.v1.members.index', $data->organization->getKey()));
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_update_member_fails_if_user_has_no_permission_to_update_members(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
]);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->putJson(route('api.v1.members.update', [$data->organization->getKey(), $data->member->getKey()]), [
|
||||
'billable_rate' => 10001,
|
||||
'role' => 'employee',
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_update_member_fails_if_member_is_not_part_of_org(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'members:update',
|
||||
]);
|
||||
$otherData = $this->createUserWithPermission([
|
||||
'members:update',
|
||||
]);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->putJson(route('api.v1.members.update', [$data->organization->getKey(), $otherData->member->getKey()]), [
|
||||
'billable_rate' => 10001,
|
||||
'role' => 'employee',
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_update_member_succeeds_if_data_is_valid(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'members:update',
|
||||
]);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->putJson(route('api.v1.members.update', [$data->organization->id, $data->member]), [
|
||||
'billable_rate' => 10001,
|
||||
'role' => 'employee',
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(200);
|
||||
$member = $data->member;
|
||||
$member->refresh();
|
||||
$this->assertSame(10001, $member->billable_rate);
|
||||
$this->assertSame('employee', $member->role);
|
||||
}
|
||||
|
||||
public function test_invite_placeholder_succeeds_if_data_is_valid(): void
|
||||
{
|
||||
$data = $this->createUserWithPermission([
|
||||
@@ -33,15 +108,13 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
|
||||
$user = User::factory()->create([
|
||||
'is_placeholder' => true,
|
||||
]);
|
||||
$data->organization->users()->attach($user, [
|
||||
'role' => 'placeholder',
|
||||
]);
|
||||
$member = Membership::factory()->forUser($user)->forOrganization($data->organization)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->postJson(route('api.v1.members.invite-placeholder', [
|
||||
'organization' => $data->organization->id,
|
||||
'user' => $user->id,
|
||||
'organization' => $data->organization->getKey(),
|
||||
'member' => $member->getKey(),
|
||||
]));
|
||||
|
||||
// Assert
|
||||
@@ -49,6 +122,56 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
|
||||
$response->assertStatus(204);
|
||||
}
|
||||
|
||||
public function test_destroy_member_fails_if_user_has_no_permission_to_delete_members(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
]);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->deleteJson(route('api.v1.members.destroy', [$data->organization->getKey(), $data->member->getKey()]));
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_destroy_member_fails_if_member_is_not_part_of_org(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'members:delete',
|
||||
]);
|
||||
$otherData = $this->createUserWithPermission([
|
||||
'members:delete',
|
||||
]);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->deleteJson(route('api.v1.members.destroy', [$data->organization->getKey(), $otherData->member->getKey()]));
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_destroy_member_succeeds_if_data_is_valid(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'members:delete',
|
||||
]);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->deleteJson(route('api.v1.members.destroy', [$data->organization->getKey(), $data->member->getKey()]));
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(204);
|
||||
$this->assertDatabaseMissing(Membership::class, [
|
||||
'id' => $data->member->getKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_invite_placeholder_fails_if_user_does_not_have_permission(): void
|
||||
{
|
||||
// Arrange
|
||||
@@ -57,11 +180,14 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
|
||||
$user = User::factory()->create([
|
||||
'is_placeholder' => true,
|
||||
]);
|
||||
$data->organization->users()->attach($user);
|
||||
$member = Membership::factory()->forUser($user)->forOrganization($data->organization)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->postJson(route('api.v1.members.invite-placeholder', ['organization' => $data->organization->id, 'user' => $user->id]));
|
||||
$response = $this->postJson(route('api.v1.members.invite-placeholder', [
|
||||
'organization' => $data->organization->id,
|
||||
'member' => $member->id,
|
||||
]));
|
||||
|
||||
// Assert
|
||||
$response->assertForbidden();
|
||||
@@ -77,11 +203,14 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
|
||||
$user = User::factory()->create([
|
||||
'is_placeholder' => true,
|
||||
]);
|
||||
$otherOrganization->users()->attach($user);
|
||||
$member = Membership::factory()->forUser($user)->forOrganization($otherOrganization)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->postJson(route('api.v1.members.invite-placeholder', ['organization' => $data->organization->id, 'user' => $user->id]));
|
||||
$response = $this->postJson(route('api.v1.members.invite-placeholder', [
|
||||
'organization' => $data->organization->id,
|
||||
'member' => $member->id,
|
||||
]));
|
||||
|
||||
// Assert
|
||||
$response->assertForbidden();
|
||||
@@ -96,7 +225,10 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->postJson(route('api.v1.members.invite-placeholder', ['organization' => $data->organization->id, 'user' => $data->user->id]));
|
||||
$response = $this->postJson(route('api.v1.members.invite-placeholder', [
|
||||
'organization' => $data->organization->id,
|
||||
'member' => $data->member->id,
|
||||
]));
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(400);
|
||||
|
||||
@@ -142,6 +142,69 @@ class ProjectMemberEndpointTest extends ApiEndpointTestAbstract
|
||||
$response->assertInvalid(['user_id']);
|
||||
}
|
||||
|
||||
public function test_store_endpoint_fails_if_user_is_a_placeholder(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'project-members:create',
|
||||
]);
|
||||
$project = Project::factory()->forOrganization($data->organization)->create();
|
||||
$projectMemberFake = ProjectMember::factory()->make();
|
||||
$user = User::factory()->attachToOrganization($data->organization)->placeholder()->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->postJson(route('api.v1.project-members.store', [$data->organization->getKey(), $project->getKey()]), [
|
||||
'billable_rate' => $projectMemberFake->billable_rate,
|
||||
'user_id' => $user->getKey(),
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(400);
|
||||
$response->assertExactJson([
|
||||
'error' => true,
|
||||
'key' => 'inactive_user_can_not_be_used',
|
||||
'message' => 'Inactive user can not be used',
|
||||
]);
|
||||
$this->assertDatabaseMissing(ProjectMember::class, [
|
||||
'billable_rate' => $projectMemberFake->billable_rate,
|
||||
'user_id' => $user->getKey(),
|
||||
'project_id' => $project->getKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_store_endpoint_fails_if_user_is_already_member_of_project(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'project-members:create',
|
||||
]);
|
||||
$project = Project::factory()->forOrganization($data->organization)->create();
|
||||
$projectMemberFake = ProjectMember::factory()->make();
|
||||
$user = User::factory()->attachToOrganization($data->organization)->create();
|
||||
ProjectMember::factory()->forProject($project)->forUser($user)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->postJson(route('api.v1.project-members.store', [$data->organization->getKey(), $project->getKey()]), [
|
||||
'billable_rate' => $projectMemberFake->billable_rate,
|
||||
'user_id' => $user->getKey(),
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(400);
|
||||
$response->assertExactJson([
|
||||
'error' => true,
|
||||
'key' => 'user_is_already_member_of_project',
|
||||
'message' => 'User is already a member of the project',
|
||||
]);
|
||||
$this->assertDatabaseMissing(ProjectMember::class, [
|
||||
'billable_rate' => $projectMemberFake->billable_rate,
|
||||
'user_id' => $user->getKey(),
|
||||
'project_id' => $project->getKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_store_endpoint_creates_new_project_member(): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -11,6 +11,21 @@ use Laravel\Passport\Passport;
|
||||
|
||||
class TaskEndpointTest extends ApiEndpointTestAbstract
|
||||
{
|
||||
public function test_non_valid_uuid_for_organization_id_fails(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'tasks:view',
|
||||
]);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->getJson(route('api.v1.tasks.index', ['invalid-uuid']));
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(404);
|
||||
}
|
||||
|
||||
public function test_index_endpoint_fails_if_user_has_no_permission_to_view_tasks(): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Model;
|
||||
|
||||
use App\Models\Organization;
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectMember;
|
||||
use App\Models\User;
|
||||
@@ -40,4 +41,22 @@ class ProjectMemberModelTest extends ModelTestAbstract
|
||||
$this->assertNotNull($userRel);
|
||||
$this->assertTrue($userRel->is($user));
|
||||
}
|
||||
|
||||
public function test_scope_where_belongs_to_organization_filters_project_members_to_only_retrieve_project_members_that_belong_to_a_project_of_the_organization(): void
|
||||
{
|
||||
// Arrange
|
||||
$organization = Organization::factory()->create();
|
||||
$otherOrganization = Organization::factory()->create();
|
||||
$project = Project::factory()->forOrganization($organization)->create();
|
||||
$projectNotBelongingToOrganization = Project::factory()->forOrganization($otherOrganization)->create();
|
||||
$projectMember = ProjectMember::factory()->forProject($project)->create();
|
||||
$projectMemberNotBelongingToOrganization = ProjectMember::factory()->for($projectNotBelongingToOrganization)->create();
|
||||
|
||||
// Act
|
||||
$projectMembers = ProjectMember::whereBelongsToOrganization($organization)->get();
|
||||
|
||||
// Assert
|
||||
$this->assertCount(1, $projectMembers);
|
||||
$this->assertTrue($projectMembers->first()->is($projectMember));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Service;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Models\Membership;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectMember;
|
||||
use App\Models\TimeEntry;
|
||||
use App\Models\User;
|
||||
use App\Service\UserService;
|
||||
@@ -19,13 +23,17 @@ class UserServiceTest extends TestCase
|
||||
{
|
||||
// Arrange
|
||||
$organization = Organization::factory()->create();
|
||||
$project = Project::factory()->forOrganization($organization)->create();
|
||||
$otherUser = User::factory()->create();
|
||||
$fromUser = User::factory()->create();
|
||||
$toUser = User::factory()->create();
|
||||
TimeEntry::factory()->forOrganization($organization)->forUser($otherUser)->createMany(3);
|
||||
TimeEntry::factory()->forOrganization($organization)->forUser($fromUser)->createMany(3);
|
||||
ProjectMember::factory()->forProject($project)->forUser($otherUser)->create();
|
||||
ProjectMember::factory()->forProject($project)->forUser($fromUser)->create();
|
||||
|
||||
// Act
|
||||
/** @var UserService $userService */
|
||||
$userService = app(UserService::class);
|
||||
$userService->assignOrganizationEntitiesToDifferentUser($organization, $fromUser, $toUser);
|
||||
|
||||
@@ -33,5 +41,32 @@ class UserServiceTest extends TestCase
|
||||
$this->assertSame(3, TimeEntry::query()->whereBelongsTo($toUser, 'user')->count());
|
||||
$this->assertSame(3, TimeEntry::query()->whereBelongsTo($otherUser, 'user')->count());
|
||||
$this->assertSame(0, TimeEntry::query()->whereBelongsTo($fromUser, 'user')->count());
|
||||
$this->assertSame(1, ProjectMember::query()->whereBelongsTo($toUser, 'user')->count());
|
||||
$this->assertSame(1, ProjectMember::query()->whereBelongsTo($otherUser, 'user')->count());
|
||||
$this->assertSame(0, ProjectMember::query()->whereBelongsTo($fromUser, 'user')->count());
|
||||
}
|
||||
|
||||
public function test_change_ownership_changes_ownership_of_organization_to_new_user(): void
|
||||
{
|
||||
// Arrange
|
||||
$organization = Organization::factory()->create();
|
||||
$newOwner = User::factory()->create();
|
||||
$oldOwner = User::factory()->create();
|
||||
$organization->users()->attach($oldOwner->getKey(), [
|
||||
'role' => Role::Owner->value,
|
||||
]);
|
||||
$organization->users()->attach($newOwner->getKey(), [
|
||||
'role' => Role::Admin->value,
|
||||
]);
|
||||
|
||||
// Act
|
||||
/** @var UserService $userService */
|
||||
$userService = app(UserService::class);
|
||||
$userService->changeOwnership($organization, $newOwner);
|
||||
|
||||
// Assert
|
||||
$this->assertSame($newOwner->id, $organization->refresh()->user_id);
|
||||
$this->assertSame(Role::Owner->value, Membership::whereBelongsTo($newOwner)->whereBelongsTo($organization)->firstOrFail()->role);
|
||||
$this->assertSame(Role::Admin->value, Membership::whereBelongsTo($oldOwner)->whereBelongsTo($organization)->firstOrFail()->role);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user