mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 00:02:15 +01:00
Added placeholder users; Better exception handling; Enhanced local setup
This commit is contained in:
@@ -4,9 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\TimeEntry;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Laravel\Jetstream\Mail\TeamInvitation;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -31,6 +33,49 @@ class InviteTeamMemberTest extends TestCase
|
||||
$this->assertCount(1, $user->currentTeam->fresh()->teamInvitations);
|
||||
}
|
||||
|
||||
public function test_team_member_can_not_be_invited_to_team_if_already_on_team(): void
|
||||
{
|
||||
// Arrange
|
||||
Mail::fake();
|
||||
$user = User::factory()->withPersonalOrganization()->create();
|
||||
$existingUser = User::factory()->create();
|
||||
$user->currentTeam->users()->attach($existingUser, ['role' => 'admin']);
|
||||
$this->actingAs($user);
|
||||
|
||||
// Act
|
||||
$response = $this->post('/teams/'.$user->currentTeam->id.'/members', [
|
||||
'email' => $existingUser->email,
|
||||
'role' => 'admin',
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertInvalid(['email'], 'addTeamMember');
|
||||
Mail::assertNotSent(TeamInvitation::class);
|
||||
$this->assertCount(0, $user->currentTeam->fresh()->teamInvitations);
|
||||
}
|
||||
|
||||
public function test_team_member_can_be_invited_to_team_if_already_on_team_as_placeholder(): void
|
||||
{
|
||||
// Arrange
|
||||
Mail::fake();
|
||||
$user = User::factory()->withPersonalOrganization()->create();
|
||||
$existingUser = User::factory()->create([
|
||||
'is_placeholder' => true,
|
||||
]);
|
||||
$user->currentTeam->users()->attach($existingUser, ['role' => 'employee']);
|
||||
$this->actingAs($user);
|
||||
|
||||
// Act
|
||||
$response = $this->post('/teams/'.$user->currentTeam->id.'/members', [
|
||||
'email' => $existingUser->email,
|
||||
'role' => 'employee',
|
||||
]);
|
||||
|
||||
// Assert
|
||||
Mail::assertSent(TeamInvitation::class);
|
||||
$this->assertCount(1, $user->currentTeam->fresh()->teamInvitations);
|
||||
}
|
||||
|
||||
public function test_team_member_invitations_can_be_cancelled(): void
|
||||
{
|
||||
// Arrange
|
||||
@@ -49,4 +94,97 @@ class InviteTeamMemberTest extends TestCase
|
||||
// Assert
|
||||
$this->assertCount(0, $user->currentTeam->fresh()->teamInvitations);
|
||||
}
|
||||
|
||||
public function test_team_member_invitations_can_be_accepted(): void
|
||||
{
|
||||
// Arrange
|
||||
Mail::fake();
|
||||
$owner = User::factory()->withPersonalOrganization()->create();
|
||||
$user = User::factory()->withPersonalOrganization()->create();
|
||||
$invitation = $owner->currentTeam->teamInvitations()->create([
|
||||
'email' => $user->email,
|
||||
'role' => 'employee',
|
||||
]);
|
||||
$this->actingAs($user);
|
||||
|
||||
// Act
|
||||
$acceptUrl = URL::temporarySignedRoute(
|
||||
'team-invitations.accept',
|
||||
now()->addMinutes(60),
|
||||
[$invitation->getKey()]
|
||||
);
|
||||
$response = $this->get($acceptUrl);
|
||||
|
||||
// Assert
|
||||
$this->assertCount(0, $owner->currentTeam->fresh()->teamInvitations);
|
||||
$user->refresh();
|
||||
$this->assertCount(1, $user->organizations);
|
||||
$this->assertContains($owner->currentTeam->getKey(), $user->organizations->pluck('id'));
|
||||
}
|
||||
|
||||
public function test_team_member_invitations_of_placeholder_can_be_accepted_and_migrates_date_to_real_user(): void
|
||||
{
|
||||
// Arrange
|
||||
Mail::fake();
|
||||
$placeholder = User::factory()->withPersonalOrganization()->create([
|
||||
'is_placeholder' => true,
|
||||
]);
|
||||
|
||||
$owner = User::factory()->withPersonalOrganization()->create();
|
||||
$owner->currentTeam->users()->attach($placeholder, ['role' => 'employee']);
|
||||
$timeEntries = TimeEntry::factory()->forOrganization($owner->currentTeam)->forUser($placeholder)->createMany(5);
|
||||
|
||||
$user = User::factory()->withPersonalOrganization()->create([
|
||||
'email' => $placeholder->email,
|
||||
]);
|
||||
|
||||
$invitation = $owner->currentTeam->teamInvitations()->create([
|
||||
'email' => $user->email,
|
||||
'role' => 'employee',
|
||||
]);
|
||||
$this->actingAs($user);
|
||||
|
||||
// Act
|
||||
$acceptUrl = URL::temporarySignedRoute(
|
||||
'team-invitations.accept',
|
||||
now()->addMinutes(60),
|
||||
[$invitation->getKey()]
|
||||
);
|
||||
$response = $this->get($acceptUrl);
|
||||
|
||||
// Assert
|
||||
$user->refresh();
|
||||
$placeholder->refresh();
|
||||
$this->assertCount(0, $owner->currentTeam->fresh()->teamInvitations);
|
||||
$this->assertCount(1, $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
|
||||
{
|
||||
// Arrange
|
||||
Mail::fake();
|
||||
$owner = User::factory()->withPersonalOrganization()->create();
|
||||
$user = User::factory()->withPersonalOrganization()->create();
|
||||
$invitation = $owner->currentTeam->teamInvitations()->create([
|
||||
'email' => 'firstname.lastname@mail.test',
|
||||
'role' => 'employee',
|
||||
]);
|
||||
$this->actingAs($user);
|
||||
|
||||
// Act
|
||||
$acceptUrl = URL::temporarySignedRoute(
|
||||
'team-invitations.accept',
|
||||
now()->addMinutes(60),
|
||||
[$invitation->getKey()]
|
||||
);
|
||||
$response = $this->get($acceptUrl);
|
||||
|
||||
// Assert
|
||||
$this->assertCount(1, $owner->currentTeam->fresh()->teamInvitations);
|
||||
$user->refresh();
|
||||
$this->assertCount(0, $user->organizations);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Fortify\Features;
|
||||
@@ -38,10 +39,47 @@ class RegistrationTest extends TestCase
|
||||
|
||||
public function test_new_users_can_register(): void
|
||||
{
|
||||
if (! Features::enabled(Features::registration())) {
|
||||
$this->markTestSkipped('Registration support is not enabled.');
|
||||
}
|
||||
$response = $this->post('/register', [
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password',
|
||||
'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature(),
|
||||
]);
|
||||
|
||||
$this->assertAuthenticated();
|
||||
$response->assertRedirect(RouteServiceProvider::HOME);
|
||||
}
|
||||
|
||||
public function test_new_users_can_not_register_if_user_with_email_already_exists(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = User::factory()->create([
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
|
||||
// Act
|
||||
$response = $this->post('/register', [
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password',
|
||||
'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature(),
|
||||
]);
|
||||
|
||||
$this->assertFalse($this->isAuthenticated(), 'The user is authenticated');
|
||||
$response->assertInvalid(['email']);
|
||||
}
|
||||
|
||||
public function test_new_users_can_register_if_placeholder_user_with_email_already_exists(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = User::factory()->create([
|
||||
'email' => 'test@example.com',
|
||||
'is_placeholder' => true,
|
||||
]);
|
||||
|
||||
// Act
|
||||
$response = $this->post('/register', [
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Tests\Unit\Endpoint\Api\V1;
|
||||
|
||||
use App\Models\Organization;
|
||||
use App\Service\Import\Importers\ReportDto;
|
||||
use App\Service\Import\ImportService;
|
||||
use Laravel\Passport\Passport;
|
||||
use Mockery\MockInterface;
|
||||
@@ -20,7 +21,7 @@ class ImportEndpointTest extends ApiEndpointTestAbstract
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->postJson(route('api.v1.import', ['organization' => $data->organization->id]), [
|
||||
$response = $this->postJson(route('api.v1.import.import', ['organization' => $data->organization->id]), [
|
||||
'type' => 'toggl_time_entries',
|
||||
'data' => 'some data',
|
||||
'options' => [],
|
||||
@@ -41,6 +42,14 @@ class ImportEndpointTest extends ApiEndpointTestAbstract
|
||||
->withArgs(function (Organization $organization, string $importerType, string $data, array $options) use (&$user): bool {
|
||||
return $organization->is($user->organization) && $importerType === 'toggl_time_entries' && $data === 'some data' && $options === [];
|
||||
})
|
||||
->andReturn(new ReportDto(
|
||||
clientsCreated: 1,
|
||||
projectsCreated: 2,
|
||||
tasksCreated: 3,
|
||||
timeEntriesCreated: 4,
|
||||
tagsCreated: 5,
|
||||
usersCreated: 6,
|
||||
))
|
||||
->once();
|
||||
});
|
||||
Passport::actingAs($user->user);
|
||||
@@ -54,5 +63,27 @@ class ImportEndpointTest extends ApiEndpointTestAbstract
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(200);
|
||||
$response->assertExactJson([
|
||||
'report' => [
|
||||
'clients' => [
|
||||
'created' => 1,
|
||||
],
|
||||
'projects' => [
|
||||
'created' => 2,
|
||||
],
|
||||
'tasks' => [
|
||||
'created' => 3,
|
||||
],
|
||||
'time-entries' => [
|
||||
'created' => 4,
|
||||
],
|
||||
'tags' => [
|
||||
'created' => 5,
|
||||
],
|
||||
'users' => [
|
||||
'created' => 6,
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
85
tests/Unit/Endpoint/Api/V1/UserEndpointTest.php
Normal file
85
tests/Unit/Endpoint/Api/V1/UserEndpointTest.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Endpoint\Api\V1;
|
||||
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use Laravel\Passport\Passport;
|
||||
|
||||
class UserEndpointTest extends ApiEndpointTestAbstract
|
||||
{
|
||||
public function test_index_returns_members_of_organization(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'users:view',
|
||||
]);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->getJson(route('api.v1.users.index', $data->organization->id));
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_invite_placeholder_fails_if_user_does_not_have_permission(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
]);
|
||||
$user = User::factory()->create([
|
||||
'is_placeholder' => true,
|
||||
]);
|
||||
$data->organization->users()->attach($user);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->postJson(route('api.v1.users.invite-placeholder', ['organization' => $data->organization->id, 'user' => $user->id]));
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_invite_placeholder_fails_if_user_is_not_part_of_organization(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'users:invite-placeholder',
|
||||
]);
|
||||
$otherOrganization = Organization::factory()->create();
|
||||
$user = User::factory()->create([
|
||||
'is_placeholder' => true,
|
||||
]);
|
||||
$otherOrganization->users()->attach($user);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->postJson(route('api.v1.users.invite-placeholder', ['organization' => $data->organization->id, 'user' => $user->id]));
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_invite_placeholder_returns_400_if_user_is_not_placeholder(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'users:invite-placeholder',
|
||||
]);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->postJson(route('api.v1.users.invite-placeholder', ['organization' => $data->organization->id, 'user' => $data->user->id]));
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(400);
|
||||
$response->assertExactJson([
|
||||
'error' => true,
|
||||
'key' => 'user_not_placeholder',
|
||||
'message' => 'The given user is not a placeholder',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Model;
|
||||
|
||||
use App\Models\Organization;
|
||||
use App\Models\TimeEntry;
|
||||
use App\Models\User;
|
||||
use App\Providers\Filament\AdminPanelProvider;
|
||||
use Filament\Panel;
|
||||
@@ -42,4 +44,47 @@ class UserModelTest extends ModelTestAbstract
|
||||
// Assert
|
||||
$this->assertTrue($canAccess);
|
||||
}
|
||||
|
||||
public function test_scope_belongs_to_organization_returns_only_users_of_organization_including_owners(): void
|
||||
{
|
||||
// Arrange
|
||||
$owner = User::factory()->create();
|
||||
$organization = Organization::factory()->withOwner($owner)->create();
|
||||
$user = User::factory()->create();
|
||||
$user->organizations()->attach($organization, [
|
||||
'role' => 'employee',
|
||||
]);
|
||||
$otherOrganization = Organization::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
$otherUser->organizations()->attach($otherOrganization, [
|
||||
'role' => 'employee',
|
||||
]);
|
||||
|
||||
// Act
|
||||
$users = User::query()
|
||||
->belongsToOrganization($organization)
|
||||
->get();
|
||||
|
||||
// Assert
|
||||
$this->assertCount(2, $users);
|
||||
$userIds = $users->pluck('id')->toArray();
|
||||
$this->assertContains($user->getKey(), $userIds);
|
||||
$this->assertContains($owner->getKey(), $userIds);
|
||||
}
|
||||
|
||||
public function test_it_has_many_time_entries(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = User::factory()->create();
|
||||
$timeEntries = TimeEntry::factory()->forUser($user)->createMany(3);
|
||||
|
||||
// Act
|
||||
$user->refresh();
|
||||
$timeEntriesRel = $user->timeEntries;
|
||||
|
||||
// Assert
|
||||
$this->assertNotNull($timeEntriesRel);
|
||||
$this->assertCount(3, $timeEntriesRel);
|
||||
$this->assertTrue($timeEntriesRel->first()->is($timeEntries->first()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,21 +51,27 @@ class ImportDatabaseHelperTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_get_key_not_attach_to_existing_returns_key_for_identifier_without_creating_model(): void
|
||||
public function test_get_key_not_attach_to_existing_is_not_implemented_yet(): void
|
||||
{
|
||||
// Arrange
|
||||
$project = Project::factory()->create();
|
||||
$helper = new ImportDatabaseHelper(Project::class, ['name', 'organization_id'], false);
|
||||
|
||||
// Act
|
||||
$key = $helper->getKey([
|
||||
'name' => $project->name,
|
||||
'organization_id' => $project->organization_id,
|
||||
], [
|
||||
'color' => '#000000',
|
||||
]);
|
||||
try {
|
||||
$key = $helper->getKey([
|
||||
'name' => $project->name,
|
||||
'organization_id' => $project->organization_id,
|
||||
], [
|
||||
'color' => '#000000',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$this->assertSame('Not implemented', $e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Assert
|
||||
$this->assertNotSame($project->getKey(), $key);
|
||||
$this->fail();
|
||||
}
|
||||
}
|
||||
|
||||
37
tests/Unit/Service/UserServiceTest.php
Normal file
37
tests/Unit/Service/UserServiceTest.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Service;
|
||||
|
||||
use App\Models\Organization;
|
||||
use App\Models\TimeEntry;
|
||||
use App\Models\User;
|
||||
use App\Service\UserService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UserServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_assign_organization_entities_to_different_user(): void
|
||||
{
|
||||
// Arrange
|
||||
$organization = Organization::factory()->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);
|
||||
|
||||
// Act
|
||||
$userService = app(UserService::class);
|
||||
$userService->assignOrganizationEntitiesToDifferentUser($organization, $fromUser, $toUser);
|
||||
|
||||
// Assert
|
||||
$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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user