Added test for location scoping

This commit is contained in:
snipe
2026-05-29 10:39:03 +01:00
parent 42df2f6c31
commit 0170fb7711
3 changed files with 280 additions and 23 deletions
@@ -114,7 +114,9 @@ class LocationsController extends Controller
->withCount('components as components_count')
->with('adminuser');
// Only scope locations if the setting is enabled
// scope_locations_fmcs is required for location-level company scoping (locations may not
// have company_id assigned unless the compatibility check has been completed in Settings).
// Without it, locations are visible to all authenticated users regardless of FMCS state.
if (Setting::getSettings()->scope_locations_fmcs) {
$locations = Company::scopeCompanyables($locations);
}
@@ -444,18 +446,6 @@ class LocationsController extends Controller
'locations.tag_color',
]);
// scope_locations_fmcs is an explicit opt-in to restrict locations by company.
// Without it, locations are visible to all users regardless of FMCS state.
if (Setting::getSettings()->scope_locations_fmcs) {
$locations = Company::scopeCompanyables($locations);
// Allow further narrowing to a specific company (e.g. passed via data-company-id
// from a checkout form). Superusers are exempt so they can always see all locations.
if ($request->filled('companyId') && ! auth()->user()->isSuperUser()) {
$locations->where('locations.company_id', $request->input('companyId'));
}
}
$page = 1;
if ($request->filled('page')) {
$page = $request->input('page');
+1 -10
View File
@@ -2,9 +2,7 @@
namespace App\Models\Traits;
use App\Models\Company\Company;
use App\Models\CompanyableScope;
use App\Models\Setting;
trait CompanyableTrait
{
@@ -18,13 +16,6 @@ trait CompanyableTrait
*/
public static function bootCompanyableTrait()
{
// In Version 7.0 and before locations weren't scoped by companies, so add a check for the backward compatibility setting
if (__CLASS__ != 'App\Models\Location') {
static::addGlobalScope(new CompanyableScope);
} else {
if (Setting::getSettings()?->scope_locations_fmcs == 1) {
static::addGlobalScope(new CompanyableScope);
}
}
static::addGlobalScope(new CompanyableScope);
}
}
@@ -0,0 +1,276 @@
<?php
namespace Tests\Feature\Locations\Api;
use App\Models\Company;
use App\Models\Location;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
/**
* Verifies FMCS scoping rules for the location index and selectlist endpoints.
*
* Rules under test:
* 1. FMCS OFF → all locations visible to any authorized user regardless of company
* 2. FMCS ON, user has companies → only locations whose company_id matches one of the user's companies
* 3. FMCS ON, user has companies → locations with NULL company_id are NOT visible
* 4. FMCS ON, user has companies → locations in OTHER companies are NOT visible
* 5. FMCS ON, user has NO companies → only locations with NULL company_id are visible
* 6. FMCS ON, user has NO companies → locations with a company_id are NOT visible
* 7. scope_locations_fmcs does not change visibility; rules 2-6 hold with or without it
*/
class LocationsFmcsScopingTest extends TestCase
{
// -----------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------
private function userInCompany(Company $company): User
{
$user = User::factory()->viewLocationHistory()->createUsers()->create();
DB::table('company_user')->insert([
'company_id' => $company->id,
'user_id' => $user->id,
'created_at' => now(),
'updated_at' => now(),
]);
return $user;
}
private function userWithNoCompany(): User
{
return User::factory()->viewLocationHistory()->createUsers()->create(['company_id' => null]);
}
private function indexIds(User $user): array
{
return collect(
$this->actingAsForApi($user)
->getJson(route('api.locations.index', ['limit' => 500]))
->assertOk()
->json('rows')
)->pluck('id')->all();
}
private function selectlistIds(User $user): array
{
return collect(
$this->actingAsForApi($user)
->getJson(route('api.locations.selectlist', ['limit' => 500]))
->assertOk()
->json('results')
)->pluck('id')->all();
}
// -----------------------------------------------------------------------
// FMCS OFF
// -----------------------------------------------------------------------
public function test_fmcs_off_user_sees_all_locations_on_index()
{
$this->settings->disableMultipleFullCompanySupport();
$companyA = Company::factory()->create();
$companyB = Company::factory()->create();
$locationA = Location::factory()->create(['company_id' => $companyA->id]);
$locationB = Location::factory()->create(['company_id' => $companyB->id]);
$locationNull = Location::factory()->create(['company_id' => null]);
$user = $this->userInCompany($companyA);
$ids = $this->indexIds($user);
$this->assertContains($locationA->id, $ids, 'Own-company location should be visible');
$this->assertContains($locationB->id, $ids, 'Other-company location should be visible when FMCS off');
$this->assertContains($locationNull->id, $ids, 'Null-company location should be visible when FMCS off');
}
public function test_fmcs_off_user_sees_all_locations_on_selectlist()
{
$this->settings->disableMultipleFullCompanySupport();
$companyA = Company::factory()->create();
$companyB = Company::factory()->create();
$locationA = Location::factory()->create(['company_id' => $companyA->id]);
$locationB = Location::factory()->create(['company_id' => $companyB->id]);
$locationNull = Location::factory()->create(['company_id' => null]);
$user = $this->userInCompany($companyA);
$ids = $this->selectlistIds($user);
$this->assertContains($locationA->id, $ids, 'Own-company location should be in selectlist');
$this->assertContains($locationB->id, $ids, 'Other-company location should be in selectlist when FMCS off');
$this->assertContains($locationNull->id, $ids, 'Null-company location should be in selectlist when FMCS off');
}
// -----------------------------------------------------------------------
// FMCS ON — user WITH companies
// -----------------------------------------------------------------------
public function test_fmcs_on_user_with_company_sees_own_company_location_on_index()
{
$this->settings->enableMultipleFullCompanySupport();
$company = Company::factory()->create();
$location = Location::factory()->create(['company_id' => $company->id]);
$user = $this->userInCompany($company);
$this->assertContains($location->id, $this->indexIds($user),
'Location in same company should be visible');
}
public function test_fmcs_on_user_with_company_cannot_see_other_company_location_on_index()
{
$this->settings->enableMultipleFullCompanySupport();
$companyA = Company::factory()->create();
$companyB = Company::factory()->create();
$locationB = Location::factory()->create(['company_id' => $companyB->id]);
$user = $this->userInCompany($companyA);
$this->assertNotContains($locationB->id, $this->indexIds($user),
'Location in a different company should not be visible');
}
public function test_fmcs_on_user_with_company_cannot_see_null_company_location_on_index()
{
$this->settings->enableMultipleFullCompanySupport();
$company = Company::factory()->create();
$locationNull = Location::factory()->create(['company_id' => null]);
$user = $this->userInCompany($company);
$this->assertNotContains($locationNull->id, $this->indexIds($user),
'Location with no company should not be visible to company-scoped user');
}
public function test_fmcs_on_user_with_company_sees_own_company_location_on_selectlist()
{
$this->settings->enableMultipleFullCompanySupport();
$company = Company::factory()->create();
$location = Location::factory()->create(['company_id' => $company->id]);
$user = $this->userInCompany($company);
$this->assertContains($location->id, $this->selectlistIds($user),
'Location in same company should appear in selectlist');
}
public function test_fmcs_on_user_with_company_cannot_see_other_company_location_on_selectlist()
{
$this->settings->enableMultipleFullCompanySupport();
$companyA = Company::factory()->create();
$companyB = Company::factory()->create();
$locationB = Location::factory()->create(['company_id' => $companyB->id]);
$user = $this->userInCompany($companyA);
$this->assertNotContains($locationB->id, $this->selectlistIds($user),
'Location in a different company should not appear in selectlist');
}
public function test_fmcs_on_user_with_company_cannot_see_null_company_location_on_selectlist()
{
$this->settings->enableMultipleFullCompanySupport();
$company = Company::factory()->create();
$locationNull = Location::factory()->create(['company_id' => null]);
$user = $this->userInCompany($company);
$this->assertNotContains($locationNull->id, $this->selectlistIds($user),
'Location with no company should not appear in selectlist for company-scoped user');
}
// -----------------------------------------------------------------------
// FMCS ON — user with NO companies
// -----------------------------------------------------------------------
public function test_fmcs_on_user_with_no_company_sees_null_company_locations_on_index()
{
$this->settings->enableMultipleFullCompanySupport();
$locationNull = Location::factory()->create(['company_id' => null]);
$user = $this->userWithNoCompany();
$this->assertContains($locationNull->id, $this->indexIds($user),
'Location with no company should be visible to user with no company');
}
public function test_fmcs_on_user_with_no_company_cannot_see_company_locations_on_index()
{
$this->settings->enableMultipleFullCompanySupport();
$company = Company::factory()->create();
$location = Location::factory()->create(['company_id' => $company->id]);
$user = $this->userWithNoCompany();
$this->assertNotContains($location->id, $this->indexIds($user),
'Location with a company should not be visible to user with no company');
}
public function test_fmcs_on_user_with_no_company_sees_null_company_locations_on_selectlist()
{
$this->settings->enableMultipleFullCompanySupport();
$locationNull = Location::factory()->create(['company_id' => null]);
$user = $this->userWithNoCompany();
$this->assertContains($locationNull->id, $this->selectlistIds($user),
'Location with no company should appear in selectlist for user with no company');
}
public function test_fmcs_on_user_with_no_company_cannot_see_company_locations_on_selectlist()
{
$this->settings->enableMultipleFullCompanySupport();
$company = Company::factory()->create();
$location = Location::factory()->create(['company_id' => $company->id]);
$user = $this->userWithNoCompany();
$this->assertNotContains($location->id, $this->selectlistIds($user),
'Location with a company should not appear in selectlist for user with no company');
}
// -----------------------------------------------------------------------
// scope_locations_fmcs does not change visibility rules
// -----------------------------------------------------------------------
public function test_scope_locations_fmcs_does_not_change_visibility_for_user_with_company()
{
$this->settings->enableScopedLocationsWithFullMultipleCompanySupport();
$companyA = Company::factory()->create();
$companyB = Company::factory()->create();
$locationA = Location::factory()->create(['company_id' => $companyA->id]);
$locationB = Location::factory()->create(['company_id' => $companyB->id]);
$locationNull = Location::factory()->create(['company_id' => null]);
$user = $this->userInCompany($companyA);
$ids = $this->indexIds($user);
$this->assertContains($locationA->id, $ids, 'Own-company location should still be visible');
$this->assertNotContains($locationB->id, $ids, 'Other-company location should still be hidden');
$this->assertNotContains($locationNull->id, $ids, 'Null-company location should still be hidden from company-scoped user');
}
public function test_scope_locations_fmcs_does_not_change_visibility_for_user_with_no_company()
{
$this->settings->enableScopedLocationsWithFullMultipleCompanySupport();
$company = Company::factory()->create();
$locationNull = Location::factory()->create(['company_id' => null]);
$locationA = Location::factory()->create(['company_id' => $company->id]);
$user = $this->userWithNoCompany();
$ids = $this->indexIds($user);
$this->assertContains($locationNull->id, $ids, 'Null-company location should still be visible to no-company user');
$this->assertNotContains($locationA->id, $ids, 'Company location should still be hidden from no-company user');
}
}