Commit workspace changes

This commit is contained in:
2026-04-05 19:42:33 +02:00
parent 148a3bbe43
commit 08ad757bcb
312 changed files with 35149 additions and 399 deletions

View File

@@ -0,0 +1,168 @@
<?php
use App\Models\Group;
use App\Models\GroupMember;
use App\Models\User;
use App\Policies\GroupPolicy;
use App\Services\GroupMembershipService;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
function attachGroupMember(Group $group, User $user, string $role): GroupMember
{
return GroupMember::query()->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();
});

View File

@@ -0,0 +1,19 @@
<?php
use Inertia\Testing\AssertableInertia;
it('renders the groups faq page with related help links', function () {
$this->get(route('help.groups.faq'))
->assertOk()
->assertInertia(fn (AssertableInertia $page) => $page
->component('Group/GroupFaqPage')
->where('title', 'Groups FAQ')
->where('seo.canonical', route('help.groups.faq'))
->where('links.full_documentation', route('help.groups'))
->where('links.quickstart', route('help.groups.quickstart'))
->where('links.group_studio', route('studio.groups.index'))
->where('links.create_group', route('studio.groups.create'))
->where('links.contact_support', route('contact.show'))
->where('links.report_issue', route('bug-report'))
);
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
<?php
use Inertia\Testing\AssertableInertia;
it('renders the groups help page with real internal links', function () {
$this->get(route('help.groups'))
->assertOk()
->assertInertia(fn (AssertableInertia $page) => $page
->component('Group/GroupHelpPage')
->where('title', 'Groups Help & Guide')
->where('seo.canonical', route('help.groups'))
->where('links.groups_directory', route('groups.index'))
->where('links.create_group', route('studio.groups.create'))
->where('links.group_studio', route('studio.groups.index'))
->where('links.contact_support', route('contact.show'))
->where('links.report_issue', route('bug-report'))
);
});

View File

@@ -0,0 +1,17 @@
<?php
use Inertia\Testing\AssertableInertia;
it('renders the groups quickstart page with onboarding links', function () {
$this->get(route('help.groups.quickstart'))
->assertOk()
->assertInertia(fn (AssertableInertia $page) => $page
->component('Group/GroupQuickstartPage')
->where('title', 'Groups Quickstart')
->where('seo.canonical', route('help.groups.quickstart'))
->where('links.create_group', route('studio.groups.create'))
->where('links.group_studio', route('studio.groups.index'))
->where('links.groups_directory', route('groups.index'))
->where('links.full_documentation', route('help.groups'))
);
});