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'
+ );
+ }
+}