From ce81377ca99d0746206ece88856d50bb836a1551 Mon Sep 17 00:00:00 2001 From: William Allen Date: Fri, 27 Feb 2026 16:05:05 -0500 Subject: [PATCH] Add user->projects relationship to GraphQL API Incremental progress in support of several upcoming changes. --- graphql/schema.graphql | 4 + tests/Feature/GraphQL/UserTypeTest.php | 168 +++++++++++++++++++------ 2 files changed, 132 insertions(+), 40 deletions(-) diff --git a/graphql/schema.graphql b/graphql/schema.graphql index de657e87e4..0f183f55db 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -160,6 +160,10 @@ type User { "Whether or not the user is a global administrator." admin: Boolean! @filterable + + projects( + filters: _ @filter + ): [Project!]! @hasMany(type: CONNECTION, scopes: ["forUser"]) @orderBy(column: "id") } diff --git a/tests/Feature/GraphQL/UserTypeTest.php b/tests/Feature/GraphQL/UserTypeTest.php index a9e71f9cdb..6ca635af36 100644 --- a/tests/Feature/GraphQL/UserTypeTest.php +++ b/tests/Feature/GraphQL/UserTypeTest.php @@ -2,38 +2,25 @@ namespace Tests\Feature\GraphQL; -use App\Models\User; +use App\Models\Project; +use Exception; use Illuminate\Foundation\Testing\DatabaseTransactions; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\TestCase; +use Tests\Traits\CreatesProjects; use Tests\Traits\CreatesUsers; class UserTypeTest extends TestCase { + use CreatesProjects; use CreatesUsers; use DatabaseTransactions; - private User $normalUser; - private User $adminUser; - - protected function setUp(): void - { - parent::setUp(); - - $this->normalUser = $this->makeNormalUser(); - $this->adminUser = $this->makeAdminUser(); - } - - protected function tearDown(): void - { - $this->normalUser->delete(); - $this->adminUser->delete(); - - parent::tearDown(); - } - public function testBasicFieldAccess(): void { - $this->actingAs($this->normalUser)->graphQL(' + $normalUser = $this->makeNormalUser(); + + $this->actingAs($normalUser)->graphQL(' query { me { id @@ -42,17 +29,27 @@ public function testBasicFieldAccess(): void lastname institution admin + projects { + edges { + node { + id + } + } + } } } ')->assertExactJson([ 'data' => [ 'me' => [ - 'id' => (string) $this->normalUser->id, - 'email' => $this->normalUser->email, - 'firstname' => $this->normalUser->firstname, - 'lastname' => $this->normalUser->lastname, - 'institution' => $this->normalUser->institution, - 'admin' => $this->normalUser->admin, + 'id' => (string) $normalUser->id, + 'email' => $normalUser->email, + 'firstname' => $normalUser->firstname, + 'lastname' => $normalUser->lastname, + 'institution' => $normalUser->institution, + 'admin' => $normalUser->admin, + 'projects' => [ + 'edges' => [], + ], ], ], ]); @@ -60,7 +57,9 @@ public function testBasicFieldAccess(): void public function testCanSeeOwnEmail(): void { - $this->actingAs($this->normalUser)->graphQL(' + $normalUser = $this->makeNormalUser(); + + $this->actingAs($normalUser)->graphQL(' query($userid: ID) { user(id: $userid) { id @@ -68,12 +67,12 @@ public function testCanSeeOwnEmail(): void } } ', [ - 'userid' => $this->normalUser->id, + 'userid' => $normalUser->id, ])->assertExactJson([ 'data' => [ 'user' => [ - 'id' => (string) $this->normalUser->id, - 'email' => $this->normalUser->email, + 'id' => (string) $normalUser->id, + 'email' => $normalUser->email, ], ], ]); @@ -81,7 +80,10 @@ public function testCanSeeOwnEmail(): void public function testCannotSeeEmailForOtherUsers(): void { - $this->actingAs($this->normalUser)->graphQL(' + $normalUser = $this->makeNormalUser(); + $adminUser = $this->makeNormalUser(); + + $this->actingAs($normalUser)->graphQL(' query($userid: ID) { user(id: $userid) { id @@ -89,11 +91,11 @@ public function testCannotSeeEmailForOtherUsers(): void } } ', [ - 'userid' => $this->adminUser->id, + 'userid' => $adminUser->id, ])->assertExactJson([ 'data' => [ 'user' => [ - 'id' => (string) $this->adminUser->id, + 'id' => (string) $adminUser->id, 'email' => null, ], ], @@ -102,6 +104,8 @@ public function testCannotSeeEmailForOtherUsers(): void public function testAnonUsersCannotSeeEmails(): void { + $normalUser = $this->makeNormalUser(); + $this->graphQL(' query($userid: ID) { user(id: $userid) { @@ -110,11 +114,11 @@ public function testAnonUsersCannotSeeEmails(): void } } ', [ - 'userid' => $this->normalUser->id, + 'userid' => $normalUser->id, ])->assertExactJson([ 'data' => [ 'user' => [ - 'id' => (string) $this->normalUser->id, + 'id' => (string) $normalUser->id, 'email' => null, ], ], @@ -123,7 +127,10 @@ public function testAnonUsersCannotSeeEmails(): void public function testAdminCanSeeAllEmails(): void { - $this->actingAs($this->adminUser)->graphQL(' + $normalUser = $this->makeNormalUser(); + $adminUser = $this->makeAdminUser(); + + $this->actingAs($adminUser)->graphQL(' query($userid: ID) { user(id: $userid) { id @@ -131,12 +138,93 @@ public function testAdminCanSeeAllEmails(): void } } ', [ - 'userid' => $this->normalUser->id, + 'userid' => $normalUser->id, + ])->assertExactJson([ + 'data' => [ + 'user' => [ + 'id' => (string) $normalUser->id, + 'email' => $normalUser->email, + ], + ], + ]); + } + + /** + * @return array> + */ + public static function projectsRelationshipVisibilityCases(): array + { + return [ + [null, true, false, false], + ['normal', true, true, false], + ['project_member', true, true, true], + ['project_admin', true, true, true], + ['admin', true, true, true], + ]; + } + + #[DataProvider('projectsRelationshipVisibilityCases')] + public function testProjectsRelationshipVisibility( + ?string $user, + bool $canSeePublicProject, + bool $canSeeProtectedProject, + bool $canSeePrivateProject, + ): void { + $publicProject = $this->makePublicProject(); + $protectedProject = $this->makeProtectedProject(); + $privateProject = $this->makePrivateProject(); + + $projectUser = $this->makeNormalUser(); + $publicProject->users()->attach($projectUser, ['role' => Project::PROJECT_USER]); + $protectedProject->users()->attach($projectUser, ['role' => Project::PROJECT_USER]); + $privateProject->users()->attach($projectUser, ['role' => Project::PROJECT_USER]); + + if ($user === 'normal') { + $user = $this->makeNormalUser(); + } elseif ($user === 'project_member') { + $user = $this->makeNormalUser(); + $publicProject->users()->attach($user, ['role' => Project::PROJECT_USER]); + $protectedProject->users()->attach($user, ['role' => Project::PROJECT_USER]); + $privateProject->users()->attach($user, ['role' => Project::PROJECT_USER]); + } elseif ($user === 'project_admin') { + $user = $this->makeNormalUser(); + $publicProject->users()->attach($user, ['role' => Project::PROJECT_ADMIN]); + $protectedProject->users()->attach($user, ['role' => Project::PROJECT_ADMIN]); + $privateProject->users()->attach($user, ['role' => Project::PROJECT_ADMIN]); + } elseif ($user === 'admin') { + $user = $this->makeAdminUser(); + } elseif ($user === null) { + $user = null; + } else { + throw new Exception('Invalid user.'); + } + + ($user === null ? $this : $this->actingAs($user))->graphQL(' + query($userid: ID) { + user(id: $userid) { + id + projects { + edges { + node { + id + } + } + } + } + } + ', [ + 'userid' => $projectUser->id, ])->assertExactJson([ 'data' => [ 'user' => [ - 'id' => (string) $this->normalUser->id, - 'email' => $this->normalUser->email, + 'id' => (string) $projectUser->id, + 'projects' => [ + 'edges' => [ + ...($canSeePublicProject ? [['node' => ['id' => (string) $publicProject->id]]] : []), + ...($canSeeProtectedProject ? [['node' => ['id' => (string) $protectedProject->id]]] : []), + ...($canSeePrivateProject ? [['node' => ['id' => (string) $privateProject->id]]] : []), + ], + ], ], ], ]);