diff --git a/.chipperci.yml b/.chipperci.yml index 663a06350..c45d4dded 100644 --- a/.chipperci.yml +++ b/.chipperci.yml @@ -14,6 +14,11 @@ on: - master - develop + pull_request: + branches: + - master + - develop + pipeline: - name: Setup cmd: | diff --git a/app/Http/Controllers/Api/AssetsController.php b/app/Http/Controllers/Api/AssetsController.php index 16cef00d4..dc87dc999 100644 --- a/app/Http/Controllers/Api/AssetsController.php +++ b/app/Http/Controllers/Api/AssetsController.php @@ -115,7 +115,7 @@ class AssetsController extends Controller $allowed_columns[] = $field->db_column_name(); } - $assets = Company::scopeCompanyables(Asset::select('assets.*'), 'company_id', 'assets') + $assets = Asset::select('assets.*') ->with('location', 'assetstatus', 'company', 'defaultLoc','assignedTo', 'model.category', 'model.manufacturer', 'model.fieldset','supplier'); //it might be tempting to add 'assetlog' here, but don't. It blows up update-heavy users. @@ -480,7 +480,7 @@ class AssetsController extends Controller public function selectlist(Request $request) { - $assets = Company::scopeCompanyables(Asset::select([ + $assets = Asset::select([ 'assets.id', 'assets.name', 'assets.asset_tag', @@ -488,7 +488,7 @@ class AssetsController extends Controller 'assets.assigned_to', 'assets.assigned_type', 'assets.status_id', - ])->with('model', 'assetstatus', 'assignedTo')->NotArchived(), 'company_id', 'assets'); + ])->with('model', 'assetstatus', 'assignedTo')->NotArchived(); if ($request->filled('assetStatusType') && $request->input('assetStatusType') === 'RTD') { $assets = $assets->RTD(); @@ -1033,9 +1033,10 @@ class AssetsController extends Controller { $this->authorize('viewRequestable', Asset::class); - $assets = Company::scopeCompanyables(Asset::select('assets.*'), 'company_id', 'assets') + $assets = Asset::select('assets.*') ->with('location', 'assetstatus', 'assetlog', 'company', 'defaultLoc','assignedTo', - 'model.category', 'model.manufacturer', 'model.fieldset', 'supplier')->requestableAssets(); + 'model.category', 'model.manufacturer', 'model.fieldset', 'supplier') + ->requestableAssets(); $offset = request('offset', 0); $limit = $request->input('limit', 50); diff --git a/app/Http/Controllers/Api/ComponentsController.php b/app/Http/Controllers/Api/ComponentsController.php index 24eb1044b..8b5134442 100644 --- a/app/Http/Controllers/Api/ComponentsController.php +++ b/app/Http/Controllers/Api/ComponentsController.php @@ -44,9 +44,8 @@ class ComponentsController extends Controller 'notes', ]; - - $components = Company::scopeCompanyables(Component::select('components.*') - ->with('company', 'location', 'category', 'assets', 'supplier')); + $components = Component::select('components.*') + ->with('company', 'location', 'category', 'assets', 'supplier'); if ($request->filled('search')) { $components = $components->TextSearch($request->input('search')); diff --git a/app/Http/Controllers/Api/ConsumablesController.php b/app/Http/Controllers/Api/ConsumablesController.php index bac9440dc..ba7e6fb30 100644 --- a/app/Http/Controllers/Api/ConsumablesController.php +++ b/app/Http/Controllers/Api/ConsumablesController.php @@ -45,11 +45,8 @@ class ConsumablesController extends Controller 'notes', ]; - - $consumables = Company::scopeCompanyables( - Consumable::select('consumables.*') - ->with('company', 'location', 'category', 'users', 'manufacturer') - ); + $consumables = Consumable::select('consumables.*') + ->with('company', 'location', 'category', 'users', 'manufacturer'); if ($request->filled('search')) { $consumables = $consumables->TextSearch(e($request->input('search'))); diff --git a/app/Http/Controllers/Api/LicensesController.php b/app/Http/Controllers/Api/LicensesController.php index df74b6089..e021fc3d3 100644 --- a/app/Http/Controllers/Api/LicensesController.php +++ b/app/Http/Controllers/Api/LicensesController.php @@ -26,8 +26,8 @@ class LicensesController extends Controller public function index(Request $request) { $this->authorize('view', License::class); - $licenses = Company::scopeCompanyables(License::with('company', 'manufacturer', 'supplier','category')->withCount('freeSeats as free_seats_count')); + $licenses = License::with('company', 'manufacturer', 'supplier','category')->withCount('freeSeats as free_seats_count'); if ($request->filled('company_id')) { $licenses->where('company_id', '=', $request->input('company_id')); diff --git a/app/Http/Transformers/AssetsTransformer.php b/app/Http/Transformers/AssetsTransformer.php index d431ec890..85f5f9294 100644 --- a/app/Http/Transformers/AssetsTransformer.php +++ b/app/Http/Transformers/AssetsTransformer.php @@ -38,7 +38,7 @@ class AssetsTransformer 'byod' => ($asset->byod ? true : false), 'model_number' => (($asset->model) && ($asset->model->model_number)) ? e($asset->model->model_number) : null, - 'eol' => ($asset->model->eol != '') ? $asset->model->eol : null, + 'eol' => (($asset->model) && ($asset->model->eol != '')) ? $asset->model->eol : null, 'asset_eol_date' => ($asset->asset_eol_date != '') ? Helper::getFormattedDateObject($asset->asset_eol_date, 'date') : null, 'status_label' => ($asset->assetstatus) ? [ 'id' => (int) $asset->assetstatus->id, diff --git a/app/Models/Depreciable.php b/app/Models/Depreciable.php index 250894820..9bbf4fcbf 100644 --- a/app/Models/Depreciable.php +++ b/app/Models/Depreciable.php @@ -127,7 +127,7 @@ class Depreciable extends SnipeModel $yearsPast = 0; } - return round($yearsPast / $deprecationYears * $this->purchase_cost, 2); + return $this->purchase_cost - round($yearsPast / $deprecationYears * $this->purchase_cost, 2); } /** diff --git a/composer.json b/composer.json index 165a4a08f..cf4a52437 100644 --- a/composer.json +++ b/composer.json @@ -74,6 +74,7 @@ "watson/validating": "^6.1" }, "require-dev": { + "brianium/paratest": "^6.6", "fakerphp/faker": "^1.16", "laravel/dusk": "^6.25", "mockery/mockery": "^1.4", diff --git a/composer.lock b/composer.lock index 1ef75690a..4e522d8a9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4c82b2e171fb02a3ef024906db5d74c9", + "content-hash": "217a3619f0f4eebdb280299efdd7297e", "packages": [ { "name": "alek13/slack", @@ -12122,6 +12122,99 @@ ], "time": "2021-03-30T17:13:30+00:00" }, + { + "name": "brianium/paratest", + "version": "v6.6.2", + "source": { + "type": "git", + "url": "https://github.com/paratestphp/paratest.git", + "reference": "5249af4e25e79da66d1ec3b54b474047999c10b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/5249af4e25e79da66d1ec3b54b474047999c10b8", + "reference": "5249af4e25e79da66d1ec3b54b474047999c10b8", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-simplexml": "*", + "jean85/pretty-package-versions": "^2.0.5", + "php": "^7.3 || ^8.0", + "phpunit/php-code-coverage": "^9.2.15", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-timer": "^5.0.3", + "phpunit/phpunit": "^9.5.21", + "sebastian/environment": "^5.1.4", + "symfony/console": "^5.4.9 || ^6.1.2", + "symfony/polyfill-php80": "^v1.26.0", + "symfony/process": "^5.4.8 || ^6.1.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0.0", + "ext-pcov": "*", + "ext-posix": "*", + "infection/infection": "^0.26.13", + "malukenho/mcbumpface": "^1.1.5", + "squizlabs/php_codesniffer": "^3.7.1", + "symfony/filesystem": "^5.4.9 || ^6.1.0", + "vimeo/psalm": "^4.26.0" + }, + "bin": [ + "bin/paratest", + "bin/paratest.bat", + "bin/paratest_for_phpstorm" + ], + "type": "library", + "autoload": { + "psr-4": { + "ParaTest\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Scaturro", + "email": "scaturrob@gmail.com", + "role": "Developer" + }, + { + "name": "Filippo Tessarotto", + "email": "zoeslam@gmail.com", + "role": "Developer" + } + ], + "description": "Parallel testing for PHP", + "homepage": "https://github.com/paratestphp/paratest", + "keywords": [ + "concurrent", + "parallel", + "phpunit", + "testing" + ], + "support": { + "issues": "https://github.com/paratestphp/paratest/issues", + "source": "https://github.com/paratestphp/paratest/tree/v6.6.2" + }, + "funding": [ + { + "url": "https://github.com/sponsors/Slamdunk", + "type": "github" + }, + { + "url": "https://paypal.me/filippotessarotto", + "type": "paypal" + } + ], + "time": "2022-08-22T10:45:51+00:00" + }, { "name": "composer/ca-bundle", "version": "1.3.5", @@ -13232,6 +13325,65 @@ }, "time": "2020-07-09T08:09:16+00:00" }, + { + "name": "jean85/pretty-package-versions", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/ae547e455a3d8babd07b96966b17d7fd21d9c6af", + "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.17", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^0.12.66", + "phpunit/phpunit": "^7.5|^8.5|^9.4", + "vimeo/psalm": "^4.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.5" + }, + "time": "2021-10-08T21:21:46+00:00" + }, { "name": "justinrainbow/json-schema", "version": "5.2.12", @@ -16570,5 +16722,5 @@ "ext-pdo": "*" }, "platform-dev": [], - "plugin-api-version": "2.0.0" + "plugin-api-version": "2.3.0" } diff --git a/database/factories/AssetFactory.php b/database/factories/AssetFactory.php index cd3e0f839..0e0c3931d 100644 --- a/database/factories/AssetFactory.php +++ b/database/factories/AssetFactory.php @@ -328,4 +328,14 @@ class AssetFactory extends Factory ]; }); } + + public function requestable() + { + return $this->state(['requestable' => true]); + } + + public function nonrequestable() + { + return $this->state(['requestable' => false]); + } } diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 8aa38d232..586cb7186 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -271,6 +271,15 @@ class UserFactory extends Factory }); } + public function viewDepartments() + { + return $this->state(function () { + return [ + 'permissions' => '{"departments.view":"1"}', + ]; + }); + } + public function viewLicenses() { return $this->state(function () { diff --git a/resources/lang/en/admin/hardware/general.php b/resources/lang/en/admin/hardware/general.php index b0a48f2ce..7201d3705 100644 --- a/resources/lang/en/admin/hardware/general.php +++ b/resources/lang/en/admin/hardware/general.php @@ -19,7 +19,7 @@ return [ 'requestable' => 'Requestable', 'requested' => 'Requested', 'not_requestable' => 'Not Requestable', - 'requestable_status_warning' => 'Do not change requestable status', + 'requestable_status_warning' => 'Do not change requestable status', 'restore' => 'Restore Asset', 'pending' => 'Pending', 'undeployable' => 'Undeployable', diff --git a/resources/lang/en/general.php b/resources/lang/en/general.php index 28f9fd82b..f515325b1 100644 --- a/resources/lang/en/general.php +++ b/resources/lang/en/general.php @@ -459,7 +459,7 @@ return [ 'checked_out_to_email' => 'Checked Out to: Email', 'checked_out_to_tag' => 'Checked Out to: Asset Tag', 'manager_first_name' => 'Manager First Name', - 'manager_last_name' => 'Manager First Name', + 'manager_last_name' => 'Manager Last Name', 'manager_full_name' => 'Manager Full Name', 'manager_username' => 'Manager Username', 'checkout_type' => 'Checkout Type', diff --git a/resources/views/hardware/view.blade.php b/resources/views/hardware/view.blade.php index e1cd8dc4f..d183df09b 100755 --- a/resources/views/hardware/view.blade.php +++ b/resources/views/hardware/view.blade.php @@ -934,6 +934,14 @@ {{ $asset->location->state }} {{ $asset->location->zip }} @endif +
  • + {{ trans('admin/hardware/form.checkout_date') }}: {{ Helper::getFormattedDateObject($asset->last_checkout, 'date', false) }} +
  • + @if (isset($asset->expected_checkin)) +
  • + {{ trans('admin/hardware/form.expected_checkin') }}: {{ Helper::getFormattedDateObject($asset->expected_checkin, 'date', false) }} +
  • + @endif @endif diff --git a/tests/Feature/Api/Assets/AssetIndexTest.php b/tests/Feature/Api/Assets/AssetIndexTest.php index 3618c6e01..778483c1c 100644 --- a/tests/Feature/Api/Assets/AssetIndexTest.php +++ b/tests/Feature/Api/Assets/AssetIndexTest.php @@ -3,9 +3,9 @@ namespace Tests\Feature\Api\Assets; use App\Models\Asset; +use App\Models\Company; use App\Models\User; use Illuminate\Testing\Fluent\AssertableJson; -use Laravel\Passport\Passport; use Tests\Support\InteractsWithSettings; use Tests\TestCase; @@ -17,14 +17,14 @@ class AssetIndexTest extends TestCase { Asset::factory()->count(3)->create(); - Passport::actingAs(User::factory()->superuser()->create()); - $this->getJson( - route('api.assets.index', [ - 'sort' => 'name', - 'order' => 'asc', - 'offset' => '0', - 'limit' => '20', - ])) + $this->actingAsForApi(User::factory()->superuser()->create()) + ->getJson( + route('api.assets.index', [ + 'sort' => 'name', + 'order' => 'asc', + 'offset' => '0', + 'limit' => '20', + ])) ->assertOk() ->assertJsonStructure([ 'total', @@ -32,4 +32,50 @@ class AssetIndexTest extends TestCase ]) ->assertJson(fn(AssertableJson $json) => $json->has('rows', 3)->etc()); } + + public function testAssetIndexAdheresToCompanyScoping() + { + [$companyA, $companyB] = Company::factory()->count(2)->create(); + + $assetA = Asset::factory()->for($companyA)->create(); + $assetB = Asset::factory()->for($companyB)->create(); + + $superUser = $companyA->users()->save(User::factory()->superuser()->make()); + $userInCompanyA = $companyA->users()->save(User::factory()->viewAssets()->make()); + $userInCompanyB = $companyB->users()->save(User::factory()->viewAssets()->make()); + + $this->settings->disableMultipleFullCompanySupport(); + + $this->actingAsForApi($superUser) + ->getJson(route('api.assets.index')) + ->assertResponseContainsInRows($assetA, 'asset_tag') + ->assertResponseContainsInRows($assetB, 'asset_tag'); + + $this->actingAsForApi($userInCompanyA) + ->getJson(route('api.assets.index')) + ->assertResponseContainsInRows($assetA, 'asset_tag') + ->assertResponseContainsInRows($assetB, 'asset_tag'); + + $this->actingAsForApi($userInCompanyB) + ->getJson(route('api.assets.index')) + ->assertResponseContainsInRows($assetA, 'asset_tag') + ->assertResponseContainsInRows($assetB, 'asset_tag'); + + $this->settings->enableMultipleFullCompanySupport(); + + $this->actingAsForApi($superUser) + ->getJson(route('api.assets.index')) + ->assertResponseContainsInRows($assetA, 'asset_tag') + ->assertResponseContainsInRows($assetB, 'asset_tag'); + + $this->actingAsForApi($userInCompanyA) + ->getJson(route('api.assets.index')) + ->assertResponseContainsInRows($assetA, 'asset_tag') + ->assertResponseDoesNotContainInRows($assetB, 'asset_tag'); + + $this->actingAsForApi($userInCompanyB) + ->getJson(route('api.assets.index')) + ->assertResponseDoesNotContainInRows($assetA, 'asset_tag') + ->assertResponseContainsInRows($assetB, 'asset_tag'); + } } diff --git a/tests/Feature/Api/Assets/AssetsForSelectListTest.php b/tests/Feature/Api/Assets/AssetsForSelectListTest.php new file mode 100644 index 000000000..cccae38d3 --- /dev/null +++ b/tests/Feature/Api/Assets/AssetsForSelectListTest.php @@ -0,0 +1,76 @@ +create(['asset_tag' => '0001']); + Asset::factory()->create(['asset_tag' => '0002']); + + $response = $this->actingAsForApi(User::factory()->create()) + ->getJson(route('assets.selectlist', ['search' => '000'])) + ->assertOk(); + + $results = collect($response->json('results')); + + $this->assertEquals(2, $results->count()); + $this->assertTrue($results->pluck('text')->contains(fn($text) => str_contains($text, '0001'))); + $this->assertTrue($results->pluck('text')->contains(fn($text) => str_contains($text, '0002'))); + } + + public function testAssetsAreScopedToCompanyWhenMultipleCompanySupportEnabled() + { + [$companyA, $companyB] = Company::factory()->count(2)->create(); + + $assetA = Asset::factory()->for($companyA)->create(['asset_tag' => '0001']); + $assetB = Asset::factory()->for($companyB)->create(['asset_tag' => '0002']); + + $superUser = $companyA->users()->save(User::factory()->superuser()->make()); + $userInCompanyA = $companyA->users()->save(User::factory()->viewAssets()->make()); + $userInCompanyB = $companyB->users()->save(User::factory()->viewAssets()->make()); + + $this->settings->disableMultipleFullCompanySupport(); + + $this->actingAsForApi($superUser) + ->getJson(route('assets.selectlist', ['search' => '000'])) + ->assertResponseContainsInResults($assetA) + ->assertResponseContainsInResults($assetB); + + $this->actingAsForApi($userInCompanyA) + ->getJson(route('assets.selectlist', ['search' => '000'])) + ->assertResponseContainsInResults($assetA) + ->assertResponseContainsInResults($assetB); + + $this->actingAsForApi($userInCompanyB) + ->getJson(route('assets.selectlist', ['search' => '000'])) + ->assertResponseContainsInResults($assetA) + ->assertResponseContainsInResults($assetB); + + $this->settings->enableMultipleFullCompanySupport(); + + $this->actingAsForApi($superUser) + ->getJson(route('assets.selectlist', ['search' => '000'])) + ->assertResponseContainsInResults($assetA) + ->assertResponseContainsInResults($assetB); + + $this->actingAsForApi($userInCompanyA) + ->getJson(route('assets.selectlist', ['search' => '000'])) + ->assertResponseContainsInResults($assetA) + ->assertResponseDoesNotContainInResults($assetB); + + $this->actingAsForApi($userInCompanyB) + ->getJson(route('assets.selectlist', ['search' => '000'])) + ->assertResponseDoesNotContainInResults($assetA) + ->assertResponseContainsInResults($assetB); + } +} diff --git a/tests/Feature/Api/Assets/RequestableAssetsTest.php b/tests/Feature/Api/Assets/RequestableAssetsTest.php new file mode 100644 index 000000000..8649b1b00 --- /dev/null +++ b/tests/Feature/Api/Assets/RequestableAssetsTest.php @@ -0,0 +1,79 @@ +actingAsForApi(User::factory()->create()) + ->getJson(route('api.assets.requestable')) + ->assertForbidden(); + } + + public function testReturnsRequestableAssets() + { + $requestableAsset = Asset::factory()->requestable()->create(['asset_tag' => 'requestable']); + $nonRequestableAsset = Asset::factory()->nonrequestable()->create(['asset_tag' => 'non-requestable']); + + $this->actingAsForApi(User::factory()->viewRequestableAssets()->create()) + ->getJson(route('api.assets.requestable')) + ->assertOk() + ->assertResponseContainsInRows($requestableAsset, 'asset_tag') + ->assertResponseDoesNotContainInRows($nonRequestableAsset, 'asset_tag'); + } + + public function testRequestableAssetsAreScopedToCompanyWhenMultipleCompanySupportEnabled() + { + [$companyA, $companyB] = Company::factory()->count(2)->create(); + + $assetA = Asset::factory()->requestable()->for($companyA)->create(['asset_tag' => '0001']); + $assetB = Asset::factory()->requestable()->for($companyB)->create(['asset_tag' => '0002']); + + $superUser = $companyA->users()->save(User::factory()->superuser()->make()); + $userInCompanyA = $companyA->users()->save(User::factory()->viewRequestableAssets()->make()); + $userInCompanyB = $companyB->users()->save(User::factory()->viewRequestableAssets()->make()); + + $this->settings->disableMultipleFullCompanySupport(); + + $this->actingAsForApi($superUser) + ->getJson(route('api.assets.requestable')) + ->assertResponseContainsInRows($assetA, 'asset_tag') + ->assertResponseContainsInRows($assetB, 'asset_tag'); + + $this->actingAsForApi($userInCompanyA) + ->getJson(route('api.assets.requestable')) + ->assertResponseContainsInRows($assetA, 'asset_tag') + ->assertResponseContainsInRows($assetB, 'asset_tag'); + + $this->actingAsForApi($userInCompanyB) + ->getJson(route('api.assets.requestable')) + ->assertResponseContainsInRows($assetA, 'asset_tag') + ->assertResponseContainsInRows($assetB, 'asset_tag'); + + $this->settings->enableMultipleFullCompanySupport(); + + $this->actingAsForApi($superUser) + ->getJson(route('api.assets.requestable')) + ->assertResponseContainsInRows($assetA, 'asset_tag') + ->assertResponseContainsInRows($assetB, 'asset_tag'); + + $this->actingAsForApi($userInCompanyA) + ->getJson(route('api.assets.requestable')) + ->assertResponseContainsInRows($assetA, 'asset_tag') + ->assertResponseDoesNotContainInRows($assetB, 'asset_tag'); + + $this->actingAsForApi($userInCompanyB) + ->getJson(route('api.assets.requestable')) + ->assertResponseDoesNotContainInRows($assetA, 'asset_tag') + ->assertResponseContainsInRows($assetB, 'asset_tag'); + } +} diff --git a/tests/Feature/Api/Components/ComponentIndexTest.php b/tests/Feature/Api/Components/ComponentIndexTest.php new file mode 100644 index 000000000..ee83b7a46 --- /dev/null +++ b/tests/Feature/Api/Components/ComponentIndexTest.php @@ -0,0 +1,60 @@ +count(2)->create(); + + $componentA = Component::factory()->for($companyA)->create(); + $componentB = Component::factory()->for($companyB)->create(); + + $superUser = $companyA->users()->save(User::factory()->superuser()->make()); + $userInCompanyA = $companyA->users()->save(User::factory()->viewComponents()->make()); + $userInCompanyB = $companyB->users()->save(User::factory()->viewComponents()->make()); + + $this->settings->disableMultipleFullCompanySupport(); + + $this->actingAsForApi($superUser) + ->getJson(route('api.components.index')) + ->assertResponseContainsInRows($componentA) + ->assertResponseContainsInRows($componentB); + + $this->actingAsForApi($userInCompanyA) + ->getJson(route('api.components.index')) + ->assertResponseContainsInRows($componentA) + ->assertResponseContainsInRows($componentB); + + $this->actingAsForApi($userInCompanyB) + ->getJson(route('api.components.index')) + ->assertResponseContainsInRows($componentA) + ->assertResponseContainsInRows($componentB); + + $this->settings->enableMultipleFullCompanySupport(); + + $this->actingAsForApi($superUser) + ->getJson(route('api.components.index')) + ->assertResponseContainsInRows($componentA) + ->assertResponseContainsInRows($componentB); + + $this->actingAsForApi($userInCompanyA) + ->getJson(route('api.components.index')) + ->assertResponseContainsInRows($componentA) + ->assertResponseDoesNotContainInRows($componentB); + + $this->actingAsForApi($userInCompanyB) + ->getJson(route('api.components.index')) + ->assertResponseDoesNotContainInRows($componentA) + ->assertResponseContainsInRows($componentB); + } +} diff --git a/tests/Feature/Api/Consumables/ConsumablesIndexTest.php b/tests/Feature/Api/Consumables/ConsumablesIndexTest.php new file mode 100644 index 000000000..33c10ed07 --- /dev/null +++ b/tests/Feature/Api/Consumables/ConsumablesIndexTest.php @@ -0,0 +1,60 @@ +count(2)->create(); + + $consumableA = Consumable::factory()->for($companyA)->create(); + $consumableB = Consumable::factory()->for($companyB)->create(); + + $superUser = $companyA->users()->save(User::factory()->superuser()->make()); + $userInCompanyA = $companyA->users()->save(User::factory()->viewConsumables()->make()); + $userInCompanyB = $companyB->users()->save(User::factory()->viewConsumables()->make()); + + $this->settings->disableMultipleFullCompanySupport(); + + $this->actingAsForApi($superUser) + ->getJson(route('api.consumables.index')) + ->assertResponseContainsInRows($consumableA) + ->assertResponseContainsInRows($consumableB); + + $this->actingAsForApi($userInCompanyA) + ->getJson(route('api.consumables.index')) + ->assertResponseContainsInRows($consumableA) + ->assertResponseContainsInRows($consumableB); + + $this->actingAsForApi($userInCompanyB) + ->getJson(route('api.consumables.index')) + ->assertResponseContainsInRows($consumableA) + ->assertResponseContainsInRows($consumableB); + + $this->settings->enableMultipleFullCompanySupport(); + + $this->actingAsForApi($superUser) + ->getJson(route('api.consumables.index')) + ->assertResponseContainsInRows($consumableA) + ->assertResponseContainsInRows($consumableB); + + $this->actingAsForApi($userInCompanyA) + ->getJson(route('api.consumables.index')) + ->assertResponseContainsInRows($consumableA) + ->assertResponseDoesNotContainInRows($consumableB); + + $this->actingAsForApi($userInCompanyB) + ->getJson(route('api.consumables.index')) + ->assertResponseDoesNotContainInRows($consumableA) + ->assertResponseContainsInRows($consumableB); + } +} diff --git a/tests/Feature/Api/Licenses/LicensesIndexTest.php b/tests/Feature/Api/Licenses/LicensesIndexTest.php new file mode 100644 index 000000000..a21a27da7 --- /dev/null +++ b/tests/Feature/Api/Licenses/LicensesIndexTest.php @@ -0,0 +1,60 @@ +count(2)->create(); + + $licenseA = License::factory()->for($companyA)->create(); + $licenseB = License::factory()->for($companyB)->create(); + + $superUser = $companyA->users()->save(User::factory()->superuser()->make()); + $userInCompanyA = $companyA->users()->save(User::factory()->viewLicenses()->make()); + $userInCompanyB = $companyB->users()->save(User::factory()->viewLicenses()->make()); + + $this->settings->disableMultipleFullCompanySupport(); + + $this->actingAsForApi($superUser) + ->getJson(route('api.licenses.index')) + ->assertResponseContainsInRows($licenseA) + ->assertResponseContainsInRows($licenseB); + + $this->actingAsForApi($userInCompanyA) + ->getJson(route('api.licenses.index')) + ->assertResponseContainsInRows($licenseA) + ->assertResponseContainsInRows($licenseB); + + $this->actingAsForApi($userInCompanyB) + ->getJson(route('api.licenses.index')) + ->assertResponseContainsInRows($licenseA) + ->assertResponseContainsInRows($licenseB); + + $this->settings->enableMultipleFullCompanySupport(); + + $this->actingAsForApi($superUser) + ->getJson(route('api.licenses.index')) + ->assertResponseContainsInRows($licenseA) + ->assertResponseContainsInRows($licenseB); + + $this->actingAsForApi($userInCompanyA) + ->getJson(route('api.licenses.index')) + ->assertResponseContainsInRows($licenseA) + ->assertResponseDoesNotContainInRows($licenseB); + + $this->actingAsForApi($userInCompanyB) + ->getJson(route('api.licenses.index')) + ->assertResponseDoesNotContainInRows($licenseA) + ->assertResponseContainsInRows($licenseB); + } +} diff --git a/tests/Feature/DashboardTest.php b/tests/Feature/DashboardTest.php new file mode 100644 index 000000000..4e9459fb0 --- /dev/null +++ b/tests/Feature/DashboardTest.php @@ -0,0 +1,19 @@ +actingAs(User::factory()->create()) + ->get(route('home')) + ->assertRedirect(route('view-assets')); + } +} diff --git a/tests/Support/CustomTestMacros.php b/tests/Support/CustomTestMacros.php new file mode 100644 index 000000000..0a30d7c24 --- /dev/null +++ b/tests/Support/CustomTestMacros.php @@ -0,0 +1,66 @@ +{$property})) { + throw new RuntimeException( + "The property ({$property}) either does not exist or is null on the model which isn't helpful for comparison." + ); + } + }; + + TestResponse::macro( + 'assertResponseContainsInRows', + function (Model $model, string $property = 'name') use ($guardAgainstNullProperty) { + $guardAgainstNullProperty($model, $property); + + Assert::assertTrue(collect($this['rows'])->pluck($property)->contains($model->{$property})); + + return $this; + } + ); + + TestResponse::macro( + 'assertResponseDoesNotContainInRows', + function (Model $model, string $property = 'name') use ($guardAgainstNullProperty) { + $guardAgainstNullProperty($model, $property); + + Assert::assertFalse(collect($this['rows'])->pluck($property)->contains($model->{$property})); + + return $this; + } + ); + + TestResponse::macro( + 'assertResponseContainsInResults', + function (Model $model, string $property = 'id') use ($guardAgainstNullProperty) { + $guardAgainstNullProperty($model, $property); + + Assert::assertTrue(collect($this->json('results'))->pluck('id')->contains($model->{$property})); + + return $this; + } + ); + + TestResponse::macro( + 'assertResponseDoesNotContainInResults', + function (Model $model, string $property = 'id') use ($guardAgainstNullProperty) { + $guardAgainstNullProperty($model, $property); + + Assert::assertFalse(collect($this->json('results'))->pluck('id')->contains($model->{$property})); + + return $this; + } + ); + } +} diff --git a/tests/Support/InteractsWithAuthentication.php b/tests/Support/InteractsWithAuthentication.php new file mode 100644 index 000000000..27b20e382 --- /dev/null +++ b/tests/Support/InteractsWithAuthentication.php @@ -0,0 +1,16 @@ +update(['full_multiple_companies_support' => 1]); } + public function disableMultipleFullCompanySupport(): Settings + { + return $this->update(['full_multiple_companies_support' => 0]); + } + public function enableWebhook(): Settings { return $this->update([ diff --git a/tests/TestCase.php b/tests/TestCase.php index 28051c7c7..30237f317 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -5,11 +5,15 @@ namespace Tests; use App\Http\Middleware\SecurityHeaders; use Illuminate\Foundation\Testing\LazilyRefreshDatabase; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; +use Tests\Support\CustomTestMacros; +use Tests\Support\InteractsWithAuthentication; use Tests\Support\InteractsWithSettings; abstract class TestCase extends BaseTestCase { use CreatesApplication; + use CustomTestMacros; + use InteractsWithAuthentication; use LazilyRefreshDatabase; private array $globallyDisabledMiddleware = [ @@ -25,5 +29,7 @@ abstract class TestCase extends BaseTestCase if (collect(class_uses_recursive($this))->contains(InteractsWithSettings::class)) { $this->initializeSettings(); } + + $this->registerCustomMacros(); } } diff --git a/tests/Unit/CompanyScopingTest.php b/tests/Unit/CompanyScopingTest.php new file mode 100644 index 000000000..669dd5ed4 --- /dev/null +++ b/tests/Unit/CompanyScopingTest.php @@ -0,0 +1,169 @@ + [Accessory::class], + 'Assets' => [Asset::class], + 'Components' => [Component::class], + 'Consumables' => [Consumable::class], + 'Licenses' => [License::class], + ]; + } + + /** @dataProvider models */ + public function testCompanyScoping($model) + { + [$companyA, $companyB] = Company::factory()->count(2)->create(); + + $modelA = $model::factory()->for($companyA)->create(); + $modelB = $model::factory()->for($companyB)->create(); + + $superUser = $companyA->users()->save(User::factory()->superuser()->make()); + $userInCompanyA = $companyA->users()->save(User::factory()->make()); + $userInCompanyB = $companyB->users()->save(User::factory()->make()); + + $this->settings->disableMultipleFullCompanySupport(); + + $this->actingAs($superUser); + $this->assertCanSee($modelA); + $this->assertCanSee($modelB); + + $this->actingAs($userInCompanyA); + $this->assertCanSee($modelA); + $this->assertCanSee($modelB); + + $this->actingAs($userInCompanyB); + $this->assertCanSee($modelA); + $this->assertCanSee($modelB); + + $this->settings->enableMultipleFullCompanySupport(); + + $this->actingAs($superUser); + $this->assertCanSee($modelA); + $this->assertCanSee($modelB); + + $this->actingAs($userInCompanyA); + $this->assertCanSee($modelA); + $this->assertCannotSee($modelB); + + $this->actingAs($userInCompanyB); + $this->assertCannotSee($modelA); + $this->assertCanSee($modelB); + } + + public function testAssetMaintenanceCompanyScoping() + { + [$companyA, $companyB] = Company::factory()->count(2)->create(); + + $assetMaintenanceForCompanyA = AssetMaintenance::factory()->for(Asset::factory()->for($companyA))->create(); + $assetMaintenanceForCompanyB = AssetMaintenance::factory()->for(Asset::factory()->for($companyB))->create(); + + $superUser = $companyA->users()->save(User::factory()->superuser()->make()); + $userInCompanyA = $companyA->users()->save(User::factory()->make()); + $userInCompanyB = $companyB->users()->save(User::factory()->make()); + + $this->settings->disableMultipleFullCompanySupport(); + + $this->actingAs($superUser); + $this->assertCanSee($assetMaintenanceForCompanyA); + $this->assertCanSee($assetMaintenanceForCompanyB); + + $this->actingAs($userInCompanyA); + $this->assertCanSee($assetMaintenanceForCompanyA); + $this->assertCanSee($assetMaintenanceForCompanyB); + + $this->actingAs($userInCompanyB); + $this->assertCanSee($assetMaintenanceForCompanyA); + $this->assertCanSee($assetMaintenanceForCompanyB); + + $this->settings->enableMultipleFullCompanySupport(); + + $this->actingAs($superUser); + $this->assertCanSee($assetMaintenanceForCompanyA); + $this->assertCanSee($assetMaintenanceForCompanyB); + + $this->actingAs($userInCompanyA); + $this->assertCanSee($assetMaintenanceForCompanyA); + $this->assertCannotSee($assetMaintenanceForCompanyB); + + $this->actingAs($userInCompanyB); + $this->assertCannotSee($assetMaintenanceForCompanyA); + $this->assertCanSee($assetMaintenanceForCompanyB); + } + + public function testLicenseSeatCompanyScoping() + { + [$companyA, $companyB] = Company::factory()->count(2)->create(); + + $licenseSeatA = LicenseSeat::factory()->for(Asset::factory()->for($companyA))->create(); + $licenseSeatB = LicenseSeat::factory()->for(Asset::factory()->for($companyB))->create(); + + $superUser = $companyA->users()->save(User::factory()->superuser()->make()); + $userInCompanyA = $companyA->users()->save(User::factory()->make()); + $userInCompanyB = $companyB->users()->save(User::factory()->make()); + + $this->settings->disableMultipleFullCompanySupport(); + + $this->actingAs($superUser); + $this->assertCanSee($licenseSeatA); + $this->assertCanSee($licenseSeatB); + + $this->actingAs($userInCompanyA); + $this->assertCanSee($licenseSeatA); + $this->assertCanSee($licenseSeatB); + + $this->actingAs($userInCompanyB); + $this->assertCanSee($licenseSeatA); + $this->assertCanSee($licenseSeatB); + + $this->settings->enableMultipleFullCompanySupport(); + + $this->actingAs($superUser); + $this->assertCanSee($licenseSeatA); + $this->assertCanSee($licenseSeatB); + + $this->actingAs($userInCompanyA); + $this->assertCanSee($licenseSeatA); + $this->assertCannotSee($licenseSeatB); + + $this->actingAs($userInCompanyB); + $this->assertCannotSee($licenseSeatA); + $this->assertCanSee($licenseSeatB); + } + + private function assertCanSee(Model $model) + { + $this->assertTrue( + get_class($model)::all()->contains($model), + 'User was not able to see expected model' + ); + } + + private function assertCannotSee(Model $model) + { + $this->assertFalse( + get_class($model)::all()->contains($model), + 'User was able to see model from a different company' + ); + } +}