mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-16 04:02:15 +01:00
Added trial and blocking to billing contract, fixed bug in running time tracker command
This commit is contained in:
committed by
Constantin Graf
parent
7c593f8f87
commit
5b7df869ad
@@ -137,4 +137,28 @@ class TimeEntrySendStillRunningMailsCommandTest extends TestCaseWithDatabase
|
||||
'Start sending email to user "'.$user->user->email.'" ('.$user->user->getKey().') for time entry '.$timeEntryRunningLongerThanThreshold->getKey()."\n".
|
||||
"Finished sending 1 still running time entry emails...\n", $output);
|
||||
}
|
||||
|
||||
public function test_does_not_send_emails_for_placeholder_users(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = $this->createUserWithPermission();
|
||||
$user->user->is_placeholder = true;
|
||||
$user->user->save();
|
||||
$timeEntryRunningLongerThanThreshold = TimeEntry::factory()->forMember($user->member)->create([
|
||||
'start' => Carbon::now()->subHours(8)->subSecond(),
|
||||
'end' => null,
|
||||
]);
|
||||
|
||||
// Act
|
||||
$exitCode = $this->withoutMockingConsoleOutput()->artisan('time-entry:send-still-running-mails');
|
||||
|
||||
// Assert
|
||||
Mail::assertNothingOutgoing();
|
||||
$timeEntryRunningLongerThanThreshold->refresh();
|
||||
$this->assertNull($timeEntryRunningLongerThanThreshold->still_active_email_sent_at);
|
||||
$this->assertSame(Command::SUCCESS, $exitCode);
|
||||
$output = Artisan::output();
|
||||
$this->assertSame("Sending still running time entry emails...\n".
|
||||
"Finished sending 0 still running time entry emails...\n", $output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
||||
namespace Tests\Unit\Endpoint\Api\V1;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Events\MemberMadeToPlaceholder;
|
||||
use App\Events\MemberRemoved;
|
||||
use App\Http\Controllers\Api\V1\MemberController;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
@@ -13,6 +15,7 @@ use App\Models\ProjectMember;
|
||||
use App\Models\TimeEntry;
|
||||
use App\Models\User;
|
||||
use App\Service\BillableRateService;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Laravel\Passport\Passport;
|
||||
use Mockery\MockInterface;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
@@ -303,12 +306,16 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission();
|
||||
Passport::actingAs($data->user);
|
||||
Event::fake([
|
||||
MemberRemoved::class,
|
||||
]);
|
||||
|
||||
// Act
|
||||
$response = $this->deleteJson(route('api.v1.members.destroy', [$data->organization->getKey(), $data->member->getKey()]));
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(403);
|
||||
Event::assertNotDispatched(MemberRemoved::class);
|
||||
}
|
||||
|
||||
public function test_destroy_member_fails_if_member_is_owner(): void
|
||||
@@ -319,6 +326,9 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
|
||||
]);
|
||||
$memberToDelete = Member::factory()->forOrganization($data->organization)->role(Role::Owner)->create();
|
||||
Passport::actingAs($data->user);
|
||||
Event::fake([
|
||||
MemberRemoved::class,
|
||||
]);
|
||||
|
||||
// Act
|
||||
$response = $this->deleteJson(route('api.v1.members.destroy', [$data->organization->getKey(), $memberToDelete->getKey()]));
|
||||
@@ -326,6 +336,7 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
|
||||
// Assert
|
||||
$response->assertStatus(400);
|
||||
$response->assertJsonPath('message', 'Can not remove owner from organization');
|
||||
Event::assertNotDispatched(MemberRemoved::class);
|
||||
}
|
||||
|
||||
public function test_destroy_member_fails_if_member_is_not_part_of_org(): void
|
||||
@@ -338,12 +349,16 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
|
||||
'members:delete',
|
||||
]);
|
||||
Passport::actingAs($data->user);
|
||||
Event::fake([
|
||||
MemberRemoved::class,
|
||||
]);
|
||||
|
||||
// Act
|
||||
$response = $this->deleteJson(route('api.v1.members.destroy', [$data->organization->getKey(), $otherData->member->getKey()]));
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(403);
|
||||
Event::assertNotDispatched(MemberRemoved::class);
|
||||
}
|
||||
|
||||
public function test_destroy_endpoint_fails_if_member_is_still_in_use_by_a_time_entry(): void
|
||||
@@ -354,6 +369,9 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
|
||||
]);
|
||||
TimeEntry::factory()->forMember($data->member)->forOrganization($data->organization)->create();
|
||||
Passport::actingAs($data->user);
|
||||
Event::fake([
|
||||
MemberRemoved::class,
|
||||
]);
|
||||
|
||||
// Act
|
||||
$response = $this->deleteJson(route('api.v1.members.destroy', [$data->organization->getKey(), $data->member->getKey()]));
|
||||
@@ -364,6 +382,7 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
|
||||
$this->assertDatabaseHas(Member::class, [
|
||||
'id' => $data->member->getKey(),
|
||||
]);
|
||||
Event::assertNotDispatched(MemberRemoved::class);
|
||||
}
|
||||
|
||||
public function test_destroy_endpoint_fails_if_member_is_still_in_use_by_a_project_member(): void
|
||||
@@ -375,6 +394,9 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
|
||||
$project = Project::factory()->forOrganization($data->organization)->create();
|
||||
ProjectMember::factory()->forProject($project)->forMember($data->member)->create();
|
||||
Passport::actingAs($data->user);
|
||||
Event::fake([
|
||||
MemberRemoved::class,
|
||||
]);
|
||||
|
||||
// Act
|
||||
$response = $this->deleteJson(route('api.v1.members.destroy', [$data->organization->getKey(), $data->member->getKey()]));
|
||||
@@ -385,6 +407,7 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
|
||||
$this->assertDatabaseHas(Member::class, [
|
||||
'id' => $data->member->getKey(),
|
||||
]);
|
||||
Event::assertNotDispatched(MemberRemoved::class);
|
||||
}
|
||||
|
||||
public function test_destroy_member_succeeds_if_data_is_valid(): void
|
||||
@@ -394,6 +417,9 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
|
||||
'members:delete',
|
||||
]);
|
||||
Passport::actingAs($data->user);
|
||||
Event::fake([
|
||||
MemberRemoved::class,
|
||||
]);
|
||||
|
||||
// Act
|
||||
$response = $this->deleteJson(route('api.v1.members.destroy', [$data->organization->getKey(), $data->member->getKey()]));
|
||||
@@ -403,6 +429,118 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
|
||||
$this->assertDatabaseMissing(Member::class, [
|
||||
'id' => $data->member->getKey(),
|
||||
]);
|
||||
Event::assertDispatched(function (MemberRemoved $event) use ($data): bool {
|
||||
return $event->organization->is($data->organization) &&
|
||||
$event->member->is($data->member);
|
||||
}, 1);
|
||||
}
|
||||
|
||||
public function test_make_placeholder_fails_if_user_has_no_permission(): void
|
||||
{
|
||||
// Arrange
|
||||
Event::fake([
|
||||
MemberMadeToPlaceholder::class,
|
||||
]);
|
||||
$data = $this->createUserWithPermission();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->postJson(route('api.v1.members.make-placeholder', [
|
||||
'organization' => $data->organization->getKey(),
|
||||
'member' => $data->member->getKey(),
|
||||
]));
|
||||
|
||||
// Assert
|
||||
$response->assertForbidden();
|
||||
Event::assertNotDispatched(MemberMadeToPlaceholder::class);
|
||||
}
|
||||
|
||||
public function test_make_placeholder_fails_if_member_is_owner(): void
|
||||
{
|
||||
// Arrange
|
||||
Event::fake([
|
||||
MemberMadeToPlaceholder::class,
|
||||
]);
|
||||
$data = $this->createUserWithPermission([
|
||||
'members:make-placeholder',
|
||||
]);
|
||||
$member = Member::factory()->forOrganization($data->organization)->role(Role::Owner)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->postJson(route('api.v1.members.make-placeholder', [
|
||||
'organization' => $data->organization->getKey(),
|
||||
'member' => $member->getKey(),
|
||||
]));
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(400);
|
||||
$response->assertJsonPath('message', 'Can not remove owner from organization');
|
||||
Event::assertNotDispatched(MemberMadeToPlaceholder::class);
|
||||
}
|
||||
|
||||
public function test_make_placeholder_fails_if_member_is_not_part_of_org(): void
|
||||
{
|
||||
// Arrange
|
||||
Event::fake([
|
||||
MemberMadeToPlaceholder::class,
|
||||
]);
|
||||
$data = $this->createUserWithPermission([
|
||||
'members:make-placeholder',
|
||||
]);
|
||||
$otherData = $this->createUserWithPermission([
|
||||
'members:make-placeholder',
|
||||
]);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->postJson(route('api.v1.members.make-placeholder', [
|
||||
'organization' => $data->organization->getKey(),
|
||||
'member' => $otherData->member->getKey(),
|
||||
]));
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_make_placeholder_creates_placeholder_and_attaches_resources_to_the_new_user(): void
|
||||
{
|
||||
// Arrange
|
||||
Event::fake([
|
||||
MemberMadeToPlaceholder::class,
|
||||
]);
|
||||
$data = $this->createUserWithPermission([
|
||||
'members:make-placeholder',
|
||||
]);
|
||||
$user = User::factory()->create();
|
||||
$member = Member::factory()->forOrganization($data->organization)->forUser($user)->role(Role::Admin)->create();
|
||||
$timeEntry = TimeEntry::factory()->forMember($member)->forOrganization($data->organization)->create();
|
||||
$project = Project::factory()->forOrganization($data->organization)->create();
|
||||
$projectMember = ProjectMember::factory()->forProject($project)->forMember($member)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->postJson(route('api.v1.members.make-placeholder', [
|
||||
'organization' => $data->organization->getKey(),
|
||||
'member' => $member->getKey(),
|
||||
]));
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(204);
|
||||
$member->refresh();
|
||||
$this->assertSame(Role::Placeholder->value, $member->role);
|
||||
$this->assertTrue($member->user->is_placeholder);
|
||||
$this->assertCount(1, $user->organizations);
|
||||
$this->assertCount(1, $member->user->organizations);
|
||||
$this->assertNotEquals($user->getKey(), $member->user->getKey());
|
||||
$timeEntry->refresh();
|
||||
$this->assertSame($member->user_id, $timeEntry->user_id);
|
||||
$projectMember->refresh();
|
||||
$this->assertSame($member->user_id, $projectMember->user_id);
|
||||
Event::assertDispatched(function (MemberMadeToPlaceholder $event) use ($data, $member): bool {
|
||||
return $event->organization->is($data->organization) &&
|
||||
$event->member->is($member);
|
||||
}, 1);
|
||||
}
|
||||
|
||||
public function test_invite_placeholder_fails_if_user_does_not_have_permission(): void
|
||||
|
||||
@@ -4,10 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Endpoint\Web;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
use Tests\TestCaseWithDatabase;
|
||||
|
||||
abstract class EndpointTestAbstract extends TestCase
|
||||
abstract class EndpointTestAbstract extends TestCaseWithDatabase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
}
|
||||
|
||||
109
tests/Unit/Middleware/CheckOrganizationBlockedMiddlewareTest.php
Normal file
109
tests/Unit/Middleware/CheckOrganizationBlockedMiddlewareTest.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Middleware;
|
||||
|
||||
use App\Http\Middleware\CheckOrganizationBlocked;
|
||||
use App\Models\Organization;
|
||||
use App\Service\BillingContract;
|
||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Passport\Passport;
|
||||
use Mockery\MockInterface;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
|
||||
#[CoversClass(CheckOrganizationBlocked::class)]
|
||||
#[UsesClass(CheckOrganizationBlocked::class)]
|
||||
class CheckOrganizationBlockedMiddlewareTest extends MiddlewareTestAbstract
|
||||
{
|
||||
private function createTestRoute(): void
|
||||
{
|
||||
Route::get('/test-route/{organization}', function (Organization $organization) {
|
||||
return response()->json(['message' => 'Test route', 'id' => $organization->getKey()]);
|
||||
})->middleware([StartSession::class, SubstituteBindings::class, CheckOrganizationBlocked::class]);
|
||||
|
||||
}
|
||||
|
||||
private function createTestRouteNoModelBinding(): string
|
||||
{
|
||||
$route = Route::get('/test-route', function () {
|
||||
return response()->json(['message' => 'Test route']);
|
||||
})->middleware([StartSession::class, SubstituteBindings::class, CheckOrganizationBlocked::class]);
|
||||
|
||||
return $route->uri;
|
||||
}
|
||||
|
||||
public function test_request_fails_if_organization_is_blocked_by_the_billing_system(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = $this->createUserWithPermission();
|
||||
$this->createTestRoute();
|
||||
$this->mock(BillingContract::class, function (MockInterface $mock) {
|
||||
$mock->shouldReceive('isBlocked')->andReturn(true)->once();
|
||||
});
|
||||
Passport::actingAs($user->user);
|
||||
|
||||
// Act
|
||||
$response = $this->get('/test-route/'.$user->organization->getKey());
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(400);
|
||||
$response->assertJson(['message' => 'Organization has no subscription but multiple members']);
|
||||
}
|
||||
|
||||
public function test_request_fails_if_organization_is_not_found(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = $this->createUserWithPermission();
|
||||
$this->createTestRoute();
|
||||
$this->mock(BillingContract::class, function (MockInterface $mock) {
|
||||
$mock->shouldReceive('isBlocked')->never();
|
||||
});
|
||||
Passport::actingAs($user->user);
|
||||
|
||||
// Act
|
||||
$response = $this->get('/test-route/'.Str::uuid());
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(404);
|
||||
}
|
||||
|
||||
public function test_request_fails_on_route_without_organization_model_binding(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = $this->createUserWithPermission();
|
||||
$route = $this->createTestRouteNoModelBinding();
|
||||
$this->mock(BillingContract::class, function (MockInterface $mock) {
|
||||
$mock->shouldReceive('isBlocked')->never();
|
||||
});
|
||||
Passport::actingAs($user->user);
|
||||
|
||||
// Act
|
||||
$response = $this->get($route);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(500);
|
||||
}
|
||||
|
||||
public function test_request_succeeds_if_organization_is_not_blocked_by_the_billing_system(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = $this->createUserWithPermission();
|
||||
$this->createTestRoute();
|
||||
$this->mock(BillingContract::class, function (MockInterface $mock) {
|
||||
$mock->shouldReceive('isBlocked')->andReturn(false)->once();
|
||||
});
|
||||
Passport::actingAs($user->user);
|
||||
|
||||
// Act
|
||||
$response = $this->get('/test-route/'.$user->organization->getKey());
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(200);
|
||||
$response->assertJson(['message' => 'Test route', 'id' => $user->organization->getKey()]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Middleware;
|
||||
|
||||
use App\Http\Middleware\HandleInertiaRequests;
|
||||
use App\Service\BillingContract;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Laravel\Passport\Passport;
|
||||
use Mockery\MockInterface;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
|
||||
#[CoversClass(HandleInertiaRequests::class)]
|
||||
#[UsesClass(HandleInertiaRequests::class)]
|
||||
class HandleInertiaRequestsMiddlewareTest extends MiddlewareTestAbstract
|
||||
{
|
||||
private function createTestRoute(): string
|
||||
{
|
||||
return Route::get('/test-route', function () {
|
||||
return Inertia::render('Welcome');
|
||||
})->middleware([StartSession::class, HandleInertiaRequests::class])->uri;
|
||||
}
|
||||
|
||||
public function test_adds_billing_information_to_shared_data_of_inertia_requests(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = $this->createUserWithPermission();
|
||||
$route = $this->createTestRoute();
|
||||
$this->mock(BillingContract::class, function (MockInterface $mock) {
|
||||
$mock->shouldReceive('hasSubscription')->andReturn(false);
|
||||
$mock->shouldReceive('hasTrial')->andReturn(false);
|
||||
$mock->shouldReceive('isBlocked')->andReturn(false);
|
||||
});
|
||||
Passport::actingAs($user->user);
|
||||
|
||||
// Act
|
||||
$response = $this->get($route);
|
||||
|
||||
// Assert
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->where('billing.has_subscription', false)
|
||||
->where('billing.has_trial', false)
|
||||
->where('billing.is_blocked', false)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,9 @@ declare(strict_types=1);
|
||||
namespace Tests\Unit\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
use Tests\TestCaseWithDatabase;
|
||||
|
||||
abstract class MiddlewareTestAbstract extends TestCase
|
||||
abstract class MiddlewareTestAbstract extends TestCaseWithDatabase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
}
|
||||
|
||||
31
tests/Unit/Model/OrganizationModelTest.php
Normal file
31
tests/Unit/Model/OrganizationModelTest.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Model;
|
||||
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
|
||||
#[CoversClass(Organization::class)]
|
||||
#[UsesClass(Organization::class)]
|
||||
class OrganizationModelTest extends ModelTestAbstract
|
||||
{
|
||||
public function test_it_has_many_members(): void
|
||||
{
|
||||
// Arrange
|
||||
$organization = Organization::factory()->create();
|
||||
$members = Member::factory()->forOrganization($organization)->createMany(3);
|
||||
|
||||
// Act
|
||||
$organization->refresh();
|
||||
$membersRel = $organization->members;
|
||||
|
||||
// Assert
|
||||
$this->assertNotNull($membersRel);
|
||||
$this->assertCount(3, $membersRel);
|
||||
$this->assertTrue($membersRel->first()->is($members->first()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user