create([ 'group_id' => $group->id, 'user_id' => $user->id, 'invited_by_user_id' => $group->owner_user_id, 'role' => $role, 'status' => Group::STATUS_ACTIVE, 'invited_at' => now(), 'accepted_at' => now(), ]); } it('allows contributors into studio but not management or publishing policy actions', function () { $owner = User::factory()->create(); $contributor = User::factory()->create(); $group = Group::factory()->for($owner, 'owner')->create(); app(GroupMembershipService::class)->ensureOwnerMembership($group); attachGroupMember($group, $contributor, Group::ROLE_MEMBER); $policy = app(GroupPolicy::class); expect($policy->viewStudio($contributor, $group))->toBeTrue() ->and($policy->update($contributor, $group))->toBeFalse() ->and($policy->manageMembers($contributor, $group))->toBeFalse() ->and($policy->publishArtworks($contributor, $group))->toBeFalse() ->and($policy->manageCollections($contributor, $group))->toBeFalse(); }); it('allows editors to publish artworks and manage collections without member administration', function () { $owner = User::factory()->create(); $editor = User::factory()->create(); $group = Group::factory()->for($owner, 'owner')->create(); app(GroupMembershipService::class)->ensureOwnerMembership($group); attachGroupMember($group, $editor, Group::ROLE_EDITOR); $policy = app(GroupPolicy::class); expect($policy->viewStudio($editor, $group))->toBeTrue() ->and($policy->publishArtworks($editor, $group))->toBeTrue() ->and($policy->manageCollections($editor, $group))->toBeTrue() ->and($policy->manageMembers($editor, $group))->toBeFalse() ->and($policy->archive($editor, $group))->toBeFalse(); }); it('allows admins to manage group settings and members but not archive ownership actions', function () { $owner = User::factory()->create(); $admin = User::factory()->create(); $group = Group::factory()->for($owner, 'owner')->create(); app(GroupMembershipService::class)->ensureOwnerMembership($group); attachGroupMember($group, $admin, Group::ROLE_ADMIN); $policy = app(GroupPolicy::class); expect($policy->viewStudio($admin, $group))->toBeTrue() ->and($policy->update($admin, $group))->toBeTrue() ->and($policy->manageMembers($admin, $group))->toBeTrue() ->and($policy->publishArtworks($admin, $group))->toBeTrue() ->and($policy->archive($admin, $group))->toBeFalse(); }); it('blocks studio access for suspended groups even for active non-owner members', function () { $owner = User::factory()->create(); $editor = User::factory()->create(); $group = Group::factory()->for($owner, 'owner')->create([ 'status' => Group::LIFECYCLE_SUSPENDED, ]); app(GroupMembershipService::class)->ensureOwnerMembership($group); attachGroupMember($group, $editor, Group::ROLE_EDITOR); $policy = app(GroupPolicy::class); expect($policy->viewStudio($editor, $group))->toBeFalse() ->and($policy->publishArtworks($editor, $group))->toBeFalse() ->and($policy->manageCollections($editor, $group))->toBeFalse(); }); it('keeps archive authority with the owner', function () { $owner = User::factory()->create(); $group = Group::factory()->for($owner, 'owner')->create(); app(GroupMembershipService::class)->ensureOwnerMembership($group); $policy = app(GroupPolicy::class); expect($policy->update($owner, $group))->toBeTrue() ->and($policy->manageMembers($owner, $group))->toBeTrue() ->and($policy->publishArtworks($owner, $group))->toBeTrue() ->and($policy->archive($owner, $group))->toBeTrue(); }); it('does not allow admins or editors to transfer ownership through policy update access alone', function () { $owner = User::factory()->create(); $admin = User::factory()->create(); $editor = User::factory()->create(); $group = Group::factory()->for($owner, 'owner')->create(); app(GroupMembershipService::class)->ensureOwnerMembership($group); attachGroupMember($group, $admin, Group::ROLE_ADMIN); attachGroupMember($group, $editor, Group::ROLE_EDITOR); $policy = app(GroupPolicy::class); expect($policy->update($admin, $group))->toBeTrue() ->and($policy->archive($admin, $group))->toBeFalse() ->and($policy->manageMembers($editor, $group))->toBeFalse(); }); it('exposes explicit v3 event and private access policy hooks', function () { $owner = User::factory()->create(); $editor = User::factory()->create(); $member = User::factory()->create(); $group = Group::factory()->for($owner, 'owner')->create(); app(GroupMembershipService::class)->ensureOwnerMembership($group); attachGroupMember($group, $editor, Group::ROLE_EDITOR); attachGroupMember($group, $member, Group::ROLE_MEMBER); $policy = app(GroupPolicy::class); expect($policy->publishEventUpdates($editor, $group))->toBeTrue() ->and($policy->viewInternalEvents($editor, $group))->toBeTrue() ->and($policy->viewPrivateProject($editor, $group))->toBeTrue() ->and($policy->participateInChallenge($member, $group))->toBeTrue() ->and($policy->publishEventUpdates($member, $group))->toBeFalse(); }); it('exposes explicit v4 release, milestone, badge, and trust policy hooks', function () { $owner = User::factory()->create(); $admin = User::factory()->create(); $editor = User::factory()->create(); $member = User::factory()->create(); $group = Group::factory()->for($owner, 'owner')->create(); app(GroupMembershipService::class)->ensureOwnerMembership($group); attachGroupMember($group, $admin, Group::ROLE_ADMIN); attachGroupMember($group, $editor, Group::ROLE_EDITOR); attachGroupMember($group, $member, Group::ROLE_MEMBER); $policy = app(GroupPolicy::class); expect($policy->manageReleases($editor, $group))->toBeTrue() ->and($policy->publishReleases($editor, $group))->toBeTrue() ->and($policy->moveReleaseStage($editor, $group))->toBeTrue() ->and($policy->manageMilestones($editor, $group))->toBeTrue() ->and($policy->assignReleaseLead($editor, $group))->toBeTrue() ->and($policy->viewReputationDashboard($editor, $group))->toBeFalse() ->and($policy->manageBadges($admin, $group))->toBeTrue() ->and($policy->viewInternalTrustMetrics($admin, $group))->toBeTrue() ->and($policy->featureRelease($admin, $group))->toBeTrue() ->and($policy->manageReleases($member, $group))->toBeFalse() ->and($policy->viewReputationDashboard($member, $group))->toBeFalse(); });