Updated invitation flow, Moved jetstream function to REST endpoints; Lower case email

This commit is contained in:
Constantin Graf
2026-02-25 17:28:34 +01:00
committed by Constantin Graf
parent d732064f31
commit a30d192ea2
37 changed files with 942 additions and 209 deletions

View File

@@ -5,9 +5,15 @@ declare(strict_types=1);
namespace Tests\Unit\Endpoint\Api\V1;
use App\Enums\Role;
use App\Events\AfterCreateOrganization;
use App\Http\Controllers\Api\V1\OrganizationController;
use App\Models\Member;
use App\Models\Organization;
use App\Service\BillableRateService;
use App\Service\IpLookup\IpLookupResponseDto;
use App\Service\IpLookup\IpLookupServiceContract;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Laravel\Passport\Passport;
use Mockery\MockInterface;
use PHPUnit\Framework\Attributes\UsesClass;
@@ -93,6 +99,121 @@ class OrganizationEndpointTest extends ApiEndpointTestAbstract
$response->assertJsonPath('data.billable_rate', null);
}
public function test_store_endpoint_creates_new_organization(): void
{
// Arrange
$data = $this->createUserWithPermission();
$organizationFake = Organization::factory()->make();
Event::fake([
AfterCreateOrganization::class,
]);
Passport::actingAs($data->user);
// Act
$response = $this->postJson(route('api.v1.organizations.store'), [
'name' => $organizationFake->name,
]);
// Assert
$response->assertStatus(201);
$response->assertJson(fn (AssertableJson $json) => $json
->has('data')
->where('data.name', $organizationFake->name)
->where('data.is_personal', false)
->where('data.currency', config('app.localization.default_currency'))
->etc()
);
/** @var Organization $newOrganization */
$newOrganization = Organization::query()->where('name', $organizationFake->name)->firstOrFail();
$this->assertTrue($newOrganization->owner->is($data->user));
$this->assertSame($newOrganization->getKey(), $data->user->fresh()->current_team_id);
$this->assertDatabaseHas(Member::class, [
'organization_id' => $newOrganization->getKey(),
'user_id' => $data->user->getKey(),
'role' => Role::Owner->value,
]);
Event::assertDispatched(AfterCreateOrganization::class, function (AfterCreateOrganization $event) use ($newOrganization): bool {
return $event->organization->is($newOrganization);
});
}
public function test_store_endpoint_uses_ip_lookup_currency_for_new_organization(): void
{
// Arrange
$data = $this->createUserWithPermission();
$this->mock(IpLookupServiceContract::class, function (MockInterface $mock): void {
$mock->shouldReceive('lookup')
->once()
->andReturn(new IpLookupResponseDto(null, null, 'USD'));
});
Passport::actingAs($data->user);
// Act
$response = $this->postJson(route('api.v1.organizations.store'), [
'name' => 'Test Organization',
]);
// Assert
$response->assertStatus(201);
$response->assertJsonPath('data.currency', 'USD');
$this->assertDatabaseHas(Organization::class, [
'name' => 'Test Organization',
'currency' => 'USD',
'user_id' => $data->user->getKey(),
'personal_team' => false,
]);
}
public function test_store_endpoint_fails_if_name_is_missing(): void
{
// Arrange
$data = $this->createUserWithPermission();
Passport::actingAs($data->user);
// Act
$response = $this->postJson(route('api.v1.organizations.store'), []);
// Assert
$response->assertStatus(422);
$response->assertJsonValidationErrors(['name']);
$this->assertDatabaseCount(Organization::class, 1);
}
public function test_store_endpoint_fails_if_name_is_not_a_string(): void
{
// Arrange
$data = $this->createUserWithPermission();
Passport::actingAs($data->user);
// Act
$response = $this->postJson(route('api.v1.organizations.store'), [
'name' => ['Test Organization'],
]);
// Assert
$response->assertStatus(422);
$response->assertJsonValidationErrors(['name']);
$this->assertDatabaseCount(Organization::class, 1);
}
public function test_store_endpoint_fails_if_name_is_too_long(): void
{
// Arrange
$data = $this->createUserWithPermission();
Passport::actingAs($data->user);
// Act
$response = $this->postJson(route('api.v1.organizations.store'), [
'name' => str_repeat('a', 256),
]);
// Assert
$response->assertStatus(422);
$response->assertJsonValidationErrors(['name']);
$this->assertDatabaseCount(Organization::class, 1);
}
public function test_update_endpoint_fails_if_user_has_no_permission_to_update_organizations(): void
{
// Arrange
@@ -260,4 +381,51 @@ class OrganizationEndpointTest extends ApiEndpointTestAbstract
'billable_rate' => $organizationFake->billable_rate,
]);
}
public function test_delete_endpoint_if_user_does_not_have_permission(): void
{
// Arrange
$data = $this->createUserWithPermission();
Passport::actingAs($data->user);
// Act
$response = $this->deleteJson(route('api.v1.organizations.destroy', [$data->organization->getKey()]));
// Assert
$response->assertForbidden();
}
public function test_delete_endpoint_fails_with_not_found_if_id_is_not_uuid(): void
{
// Arrange
$data = $this->createUserWithPermission([
'organizations:delete',
]);
Passport::actingAs($data->user);
// Act
$response = $this->deleteJson(route('api.v1.organizations.destroy', ['not-uuid']));
// Assert
$response->assertNotFound();
}
public function test_delete_endpoint_can_delete_organization(): void
{
// Arrange
$this->mockPrivateStorage();
$data = $this->createUserWithPermission([
'organizations:delete',
]);
Passport::actingAs($data->user);
// Act
$response = $this->deleteJson(route('api.v1.organizations.destroy', [$data->organization->getKey()]));
// Assert
$response->assertNoContent();
$this->assertDatabaseMissing(Organization::class, [
'id' => $data->organization->getKey(),
]);
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Tests\Unit\Endpoint\Api\V1;
use App\Models\User;
use Laravel\Passport\Passport;
class UserEndpointTest extends ApiEndpointTestAbstract
@@ -40,4 +41,57 @@ class UserEndpointTest extends ApiEndpointTestAbstract
],
]);
}
public function test_delete_fails_if_given_user_is_not_the_authenticated_user(): void
{
// Arrange
$data = $this->createUserWithPermission();
$otherData = $this->createUserWithPermission();
Passport::actingAs($otherData->user);
// Act
$response = $this->deleteJson(route('api.v1.users.destroy', $data->user->getKey()));
// Assert
$response->assertForbidden();
}
public function test_delete_fails_if_not_authenticated(): void
{
// Arrange
$data = $this->createUserWithPermission();
// Act
$response = $this->deleteJson(route('api.v1.users.destroy', $data->user->getKey()));
// Assert
$response->assertUnauthorized();
}
public function test_delete_fails_if_user_does_not_exist(): void
{
// Arrange
$data = $this->createUserWithPermission();
Passport::actingAs($data->user);
// Act
$response = $this->deleteJson(route('api.v1.users.destroy', 'not-valid'));
// Assert
$response->assertNotFound();
}
public function test_delete_removes_user(): void
{
// Arrange
$data = $this->createUserWithPermission();
Passport::actingAs($data->user);
// Act
$response = $this->deleteJson(route('api.v1.users.destroy', $data->user->getKey()));
// Assert
$response->assertNoContent();
$this->assertDatabaseMissing(User::class, ['id' => $data->user->getKey()]);
}
}