Added member and invitation endpoints

This commit is contained in:
Constantin Graf
2024-04-10 17:45:53 +02:00
parent 234fa06324
commit b67961cb07
48 changed files with 1186 additions and 106 deletions

View File

@@ -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,
];
}
}

View 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);
}
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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