Merge remote-tracking branch 'origin/develop'

This commit is contained in:
snipe 2024-11-21 14:41:42 +00:00
commit b99b56b32b
21 changed files with 510 additions and 133 deletions

View file

@ -17,3 +17,5 @@ DB_PORT=3306
DB_DATABASE=null DB_DATABASE=null
DB_USERNAME=null DB_USERNAME=null
DB_PASSWORD=null DB_PASSWORD=null
MAIL_FROM_ADDR=you@example.com

View file

@ -43,6 +43,9 @@ jobs:
cp -v .env.testing.example .env cp -v .env.testing.example .env
cp -v .env.testing.example .env.testing cp -v .env.testing.example .env.testing
- name: Create database file
run: touch database/database.sqlite
- name: Install Dependencies - name: Install Dependencies
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
@ -57,5 +60,5 @@ jobs:
- name: Execute tests (Unit and Feature tests) via PHPUnit - name: Execute tests (Unit and Feature tests) via PHPUnit
env: env:
DB_CONNECTION: sqlite_testing DB_CONNECTION: sqlite
run: php artisan test run: php artisan test

View file

@ -33,6 +33,8 @@ use Illuminate\Http\Request;
use App\Http\Requests\ImageUploadRequest; use App\Http\Requests\ImageUploadRequest;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use App\View\Label;
use Illuminate\Support\Facades\Storage;
/** /**
@ -126,8 +128,19 @@ class AssetsController extends Controller
} }
$assets = Asset::select('assets.*') $assets = Asset::select('assets.*')
->with('location', 'assetstatus', 'company', 'defaultLoc','assignedTo', 'adminuser','model.depreciation', ->with(
'model.category', 'model.manufacturer', 'model.fieldset','supplier'); //it might be tempting to add 'assetlog' here, but don't. It blows up update-heavy users. 'location',
'assetstatus',
'company',
'defaultLoc',
'assignedTo',
'adminuser',
'model.depreciation',
'model.category',
'model.manufacturer',
'model.fieldset',
'supplier'
); //it might be tempting to add 'assetlog' here, but don't. It blows up update-heavy users.
if ($filter_non_deprecable_assets) { if ($filter_non_deprecable_assets) {
@ -265,7 +278,6 @@ class AssetsController extends Controller
$join->on('status_alias.id', '=', 'assets.status_id'); $join->on('status_alias.id', '=', 'assets.status_id');
}); });
} }
} }
@ -399,7 +411,6 @@ class AssetsController extends Controller
} else { } else {
$assets->orderBy($sort_override, $order); $assets->orderBy($sort_override, $order);
} }
} else { } else {
$assets->orderBy($column_sort, $order); $assets->orderBy($column_sort, $order);
} }
@ -463,12 +474,10 @@ class AssetsController extends Controller
} else { } else {
return (new AssetsTransformer)->transformAssets($assets, $assets->count()); return (new AssetsTransformer)->transformAssets($assets, $assets->count());
} }
} }
// If there are 0 results, return the "no such asset" response // If there are 0 results, return the "no such asset" response
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200); return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200);
} }
/** /**
@ -495,7 +504,6 @@ class AssetsController extends Controller
// If there are 0 results, return the "no such asset" response // If there are 0 results, return the "no such asset" response
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200); return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200);
} }
/** /**
@ -510,13 +518,13 @@ class AssetsController extends Controller
{ {
if ($asset = Asset::with('assetstatus') if ($asset = Asset::with('assetstatus')
->with('assignedTo')->withTrashed() ->with('assignedTo')->withTrashed()
->withCount('checkins as checkins_count', 'checkouts as checkouts_count', 'userRequests as user_requests_count')->find($id)) { ->withCount('checkins as checkins_count', 'checkouts as checkouts_count', 'userRequests as user_requests_count')->find($id)
) {
$this->authorize('view', $asset); $this->authorize('view', $asset);
return (new AssetsTransformer)->transformAsset($asset, $request->input('components')); return (new AssetsTransformer)->transformAsset($asset, $request->input('components'));
} }
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200); return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200);
} }
public function licenses(Request $request, $id): array public function licenses(Request $request, $id): array
@ -827,7 +835,6 @@ class AssetsController extends Controller
} }
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200); return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200);
} }
/** /**
@ -875,14 +882,12 @@ class AssetsController extends Controller
$asset->location_id = ($target) ? $target->id : ''; $asset->location_id = ($target) ? $target->id : '';
$error_payload['target_id'] = $request->input('assigned_location'); $error_payload['target_id'] = $request->input('assigned_location');
$error_payload['target_type'] = 'location'; $error_payload['target_type'] = 'location';
} elseif (request('checkout_to_type') == 'asset') { } elseif (request('checkout_to_type') == 'asset') {
$target = Asset::where('id', '!=', $asset_id)->find(request('assigned_asset')); $target = Asset::where('id', '!=', $asset_id)->find(request('assigned_asset'));
// Override with the asset's location_id if it has one // Override with the asset's location_id if it has one
$asset->location_id = (($target) && (isset($target->location_id))) ? $target->location_id : ''; $asset->location_id = (($target) && (isset($target->location_id))) ? $target->location_id : '';
$error_payload['target_id'] = $request->input('assigned_asset'); $error_payload['target_id'] = $request->input('assigned_asset');
$error_payload['target_type'] = 'asset'; $error_payload['target_type'] = 'asset';
} elseif (request('checkout_to_type') == 'user') { } elseif (request('checkout_to_type') == 'user') {
// Fetch the target and set the asset's new location_id // Fetch the target and set the asset's new location_id
$target = User::find(request('assigned_user')); $target = User::find(request('assigned_user'));
@ -987,7 +992,8 @@ class AssetsController extends Controller
[Asset::class], [Asset::class],
function (Builder $query) use ($asset) { function (Builder $query) use ($asset) {
$query->where('id', $asset->id); $query->where('id', $asset->id);
}) }
)
->get() ->get()
->map(function ($acceptance) { ->map(function ($acceptance) {
$acceptance->delete(); $acceptance->delete();
@ -1108,7 +1114,6 @@ class AssetsController extends Controller
'asset_tag' => e($asset->asset_tag), 'asset_tag' => e($asset->asset_tag),
'error' => $asset->getErrors()->first(), 'error' => $asset->getErrors()->first(),
], trans('admin/hardware/message.audit.error', ['error' => $asset->getErrors()->first()])), 200); ], trans('admin/hardware/message.audit.error', ['error' => $asset->getErrors()->first()])), 200);
} }
@ -1117,8 +1122,6 @@ class AssetsController extends Controller
'asset_tag' => e($request->input('asset_tag')), 'asset_tag' => e($request->input('asset_tag')),
'error' => trans('admin/hardware/message.audit.error'), 'error' => trans('admin/hardware/message.audit.error'),
], trans('admin/hardware/message.audit.error', ['error' => trans('admin/hardware/message.does_not_exist')])), 200); ], trans('admin/hardware/message.audit.error', ['error' => trans('admin/hardware/message.does_not_exist')])), 200);
} }
@ -1150,8 +1153,18 @@ class AssetsController extends Controller
} }
$assets = Asset::select('assets.*') $assets = Asset::select('assets.*')
->with('location', 'assetstatus', 'assetlog', 'company','assignedTo', ->with(
'model.category', 'model.manufacturer', 'model.fieldset', 'supplier', 'requests'); 'location',
'assetstatus',
'assetlog',
'company',
'assignedTo',
'model.category',
'model.manufacturer',
'model.fieldset',
'supplier',
'requests'
);
@ -1200,4 +1213,89 @@ class AssetsController extends Controller
return (new AssetsTransformer)->transformRequestedAssets($assets, $total); return (new AssetsTransformer)->transformRequestedAssets($assets, $total);
} }
/**
* Generate asset labels by tag
*
* @author [Nebelkreis] [https://github.com/NebelKreis]
*
* @param Request $request Contains asset_tags array of asset tags to generate labels for
* @return JsonResponse Returns base64 encoded PDF on success, error message on failure
*/
public function getLabels(Request $request): JsonResponse
{
try {
$this->authorize('view', Asset::class);
// Validate that asset tags were provided in the request
if (!$request->filled('asset_tags')) {
return response()->json(Helper::formatStandardApiResponse('error', null,
trans('admin/hardware/message.no_assets_selected')), 400);
}
// Convert asset tags from request into collection and fetch matching assets
$asset_tags = collect($request->input('asset_tags'));
$assets = Asset::whereIn('asset_tag', $asset_tags)->get();
// Return error if no assets were found for the provided tags
if ($assets->isEmpty()) {
return response()->json(Helper::formatStandardApiResponse('error', null,
trans('admin/hardware/message.does_not_exist')), 404);
}
try {
$settings = Setting::getSettings();
// Check if logo file exists in storage and disable logo if not found
// This prevents errors when trying to include a non-existent logo in the PDF
$settings->label_logo = ($original_logo = $settings->label_logo) && !Storage::disk('public')->exists('/' . $original_logo) ? null : $settings->label_logo;
$label = new Label();
if (!$label) {
throw new \Exception('Label object could not be created');
}
// Configure label with assets and settings
// bulkedit=false and count=0 are default values for label generation
$label = $label->with('assets', $assets)
->with('settings', $settings)
->with('bulkedit', false)
->with('count', 0);
// Generate PDF using callback function
// The callback captures the PDF content in $pdf_content variable
$pdf_content = '';
$label->render(function($pdf) use (&$pdf_content) {
$pdf_content = $pdf->Output('', 'S');
return $pdf;
});
// Verify PDF was generated successfully
if (empty($pdf_content)) {
throw new \Exception('PDF content is empty');
}
$encoded_content = base64_encode($pdf_content);
return response()->json(Helper::formatStandardApiResponse('success', [
'pdf' => $encoded_content
], trans('admin/hardware/message.labels_generated')));
} catch (\Exception $e) {
return response()->json(Helper::formatStandardApiResponse('error', [
'error_message' => $e->getMessage(),
'error_line' => $e->getLine(),
'error_file' => $e->getFile()
], trans('admin/hardware/message.error_generating_labels')), 500);
}
} catch (\Exception $e) {
return response()->json(Helper::formatStandardApiResponse('error', [
'error_message' => $e->getMessage(),
'error_line' => $e->getLine(),
'error_file' => $e->getFile()
], $e->getMessage()), 500);
}
}
} }

View file

@ -193,7 +193,7 @@ class ComponentsController extends Controller
$this->authorize('delete', $component); $this->authorize('delete', $component);
// Remove the image if one exists // Remove the image if one exists
if (Storage::disk('public')->exists('components/'.$component->image)) { if ($component->image && Storage::disk('public')->exists('components/' . $component->image)) {
try { try {
Storage::disk('public')->delete('components/'.$component->image); Storage::disk('public')->delete('components/'.$component->image);
} catch (\Exception $e) { } catch (\Exception $e) {

View file

@ -307,7 +307,7 @@ class CheckoutableListener
return $event->checkedOutTo->manager?->email ?? ''; return $event->checkedOutTo->manager?->email ?? '';
} }
else{ else{
return $event->checkedOutTo->email; return $event->checkedOutTo?->email ?? '';
} }
} }

View file

@ -34,7 +34,7 @@ class CheckinAccessoryMail extends Mailable
*/ */
public function envelope(): Envelope public function envelope(): Envelope
{ {
$from = new Address(env('MAIL_FROM_ADDR','service@snipe-it.io')); $from = new Address(config('mail.from.address'));
return new Envelope( return new Envelope(
from: $from, from: $from,

View file

@ -43,7 +43,7 @@ class CheckinAssetMail extends Mailable
*/ */
public function envelope(): Envelope public function envelope(): Envelope
{ {
$from = new Address(env('MAIL_FROM_ADDR','service@snipe-it.io')); $from = new Address(config('mail.from.address'));
return new Envelope( return new Envelope(
from: $from, from: $from,

View file

@ -34,7 +34,7 @@ class CheckinLicenseMail extends Mailable
*/ */
public function envelope(): Envelope public function envelope(): Envelope
{ {
$from = new Address(env('MAIL_FROM_ADDR','service@snipe-it.io')); $from = new Address(config('mail.from.address'));
return new Envelope( return new Envelope(
from: $from, from: $from,

View file

@ -37,7 +37,7 @@ class CheckoutAccessoryMail extends Mailable
*/ */
public function envelope(): Envelope public function envelope(): Envelope
{ {
$from = new Address(env('MAIL_FROM_ADDR','service@snipe-it.io')); $from = new Address(config('mail.from.address'));
return new Envelope( return new Envelope(
from: $from, from: $from,

View file

@ -52,7 +52,7 @@ class CheckoutAssetMail extends Mailable
*/ */
public function envelope(): Envelope public function envelope(): Envelope
{ {
$from = new Address(env('MAIL_FROM_ADDR', 'service@snipe-it.io')); $from = new Address(config('mail.from.address'));
return new Envelope( return new Envelope(
from: $from, from: $from,

View file

@ -38,7 +38,7 @@ class CheckoutConsumableMail extends Mailable
*/ */
public function envelope(): Envelope public function envelope(): Envelope
{ {
$from = new Address(env('MAIL_FROM_ADDR','service@snipe-it.io')); $from = new Address(config('mail.from.address'));
return new Envelope( return new Envelope(
from: $from, from: $from,

View file

@ -36,7 +36,7 @@ class CheckoutLicenseMail extends Mailable
*/ */
public function envelope(): Envelope public function envelope(): Envelope
{ {
$from = new Address(env('MAIL_FROM_ADDR','service@snipe-it.io')); $from = new Address(config('mail.from.address'));
return new Envelope( return new Envelope(
from: $from, from: $from,

View file

@ -9,6 +9,9 @@ return [
'does_not_exist_or_not_requestable' => 'That asset does not exist or is not requestable.', 'does_not_exist_or_not_requestable' => 'That asset does not exist or is not requestable.',
'assoc_users' => 'This asset is currently checked out to a user and cannot be deleted. Please check the asset in first, and then try deleting again. ', 'assoc_users' => 'This asset is currently checked out to a user and cannot be deleted. Please check the asset in first, and then try deleting again. ',
'warning_audit_date_mismatch' => 'This asset\'s next audit date (:next_audit_date) is before the last audit date (:last_audit_date). Please update the next audit date.', 'warning_audit_date_mismatch' => 'This asset\'s next audit date (:next_audit_date) is before the last audit date (:last_audit_date). Please update the next audit date.',
'labels_generated' => 'Labels were successfully generated.',
'error_generating_labels' => 'Error while generating labels.',
'no_assets_selected' => 'No assets selected.',
'create' => [ 'create' => [
'error' => 'Asset was not created, please try again. :(', 'error' => 'Asset was not created, please try again. :(',

View file

@ -16,6 +16,9 @@
@if (($item->name!=$item->asset_tag)) @if (($item->name!=$item->asset_tag))
| **{{ trans('mail.asset_tag') }}** | {{ $item->asset_tag }} | | **{{ trans('mail.asset_tag') }}** | {{ $item->asset_tag }} |
@endif @endif
@if (isset($item->model->category))
| **{{ trans('general.category') }}** | {{ $item->model->category->name }} |
@endif
@if (isset($item->manufacturer)) @if (isset($item->manufacturer))
| **{{ trans('general.manufacturer') }}** | {{ $item->manufacturer->name }} | | **{{ trans('general.manufacturer') }}** | {{ $item->manufacturer->name }} |
@endif @endif

View file

@ -16,6 +16,9 @@
@if (($item->name!=$item->asset_tag)) @if (($item->name!=$item->asset_tag))
| **{{ trans('mail.asset_tag') }}** | {{ $item->asset_tag }} | | **{{ trans('mail.asset_tag') }}** | {{ $item->asset_tag }} |
@endif @endif
@if (isset($item->model->category))
| **{{ trans('general.category') }}** | {{ $item->model->category->name }} |
@endif
@if (isset($item->manufacturer)) @if (isset($item->manufacturer))
| **{{ trans('general.manufacturer') }}** | {{ $item->manufacturer->name }} | | **{{ trans('general.manufacturer') }}** | {{ $item->manufacturer->name }} |
@endif @endif

View file

@ -22,6 +22,9 @@
@if ((isset($item_tag)) && ($item_tag!='')) @if ((isset($item_tag)) && ($item_tag!=''))
| **{{ trans('mail.asset_tag') }}** | {{ $item_tag }} | | **{{ trans('mail.asset_tag') }}** | {{ $item_tag }} |
@endif @endif
@if (isset($item->model->category))
| **{{ trans('general.category') }}** | {{ $item->model->category->name }} |
@endif
@if ((isset($item_model)) && ($item_model!='')) @if ((isset($item_model)) && ($item_model!=''))
| **{{ trans('mail.asset_name') }}** | {{ $item_model }} | | **{{ trans('mail.asset_name') }}** | {{ $item_model }} |
@endif @endif

View file

@ -18,6 +18,9 @@
@if ((isset($item->asset_tag)) && ($item->asset_tag!='')) @if ((isset($item->asset_tag)) && ($item->asset_tag!=''))
| **{{ trans('mail.asset_tag') }}** | {{ $item->asset_tag }} | | **{{ trans('mail.asset_tag') }}** | {{ $item->asset_tag }} |
@endif @endif
@if (isset($item->model->category))
| **{{ trans('general.category') }}** | {{ $item->model->category->name }} |
@endif
@if ((isset($item->name)) && ($item->name!='')) @if ((isset($item->name)) && ($item->name!=''))
| **{{ trans('mail.asset_name') }}** | {{ $item->name }} | | **{{ trans('mail.asset_name') }}** | {{ $item->name }} |
@endif @endif

View file

@ -1282,4 +1282,14 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
], 404); ], 404);
}); // end fallback routes }); // end fallback routes
/**
* Generate label routes
*/
Route::post('hardware/labels', [
Api\AssetsController::class,
'getLabels'
])->name('api.assets.labels');
// end generate label routes
}); // end API routes }); // end API routes

View file

@ -0,0 +1,140 @@
<?php
namespace Tests\Feature\Checkouts\Api;
use App\Models\Asset;
use App\Models\Company;
use App\Models\Component;
use App\Models\Location;
use App\Models\User;
use Carbon\Carbon;
use Tests\Concerns\TestsFullMultipleCompaniesSupport;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\TestCase;
class ComponentCheckoutTest extends TestCase implements TestsFullMultipleCompaniesSupport, TestsPermissionsRequirement
{
public function testRequiresPermission()
{
$component = Component::factory()->create();
$this->actingAsForApi(User::factory()->create())
->postJson(route('api.components.checkout', $component->id))
->assertForbidden();
}
public function testCannotCheckoutNonExistentComponent()
{
$this->actingAsForApi(User::factory()->checkoutComponents()->create())
->postJson(route('api.components.checkout', 1000))
->assertOk()
->assertStatusMessageIs('error')
->assertMessagesAre('Component does not exist.');
}
public function testCheckingOutComponentRequiresValidFields()
{
$component = Component::factory()->create();
$this->actingAsForApi(User::factory()->checkoutComponents()->create())
->postJson(route('api.components.checkout', $component->id), [
//
])
->assertOk()
->assertStatusMessageIs('error')
->assertPayloadContains('assigned_to')
->assertPayloadContains('assigned_qty');
}
public function testCannotCheckoutComponentIfRequestedAmountIsMoreThanComponentQuantity()
{
$asset = Asset::factory()->create();
$component = Component::factory()->create(['qty' => 2]);
$this->actingAsForApi(User::factory()->checkoutComponents()->create())
->postJson(route('api.components.checkout', $component->id), [
'assigned_to' => $asset->id,
'assigned_qty' => 3,
])
->assertOk()
->assertStatusMessageIs('error')
->assertMessagesAre(trans('admin/components/message.checkout.unavailable', ['remaining' => 2, 'requested' => 3]));
}
public function testCannotCheckoutComponentIfRequestedAmountIsMoreThanWhatIsRemaining()
{
$asset = Asset::factory()->create();
$component = Component::factory()->create(['qty' => 2]);
$component->assets()->attach($component->id, [
'component_id' => $component->id,
'created_at' => Carbon::now(),
'assigned_qty' => 1,
'asset_id' => $asset->id,
]);
$this->actingAsForApi(User::factory()->checkoutComponents()->create())
->postJson(route('api.components.checkout', $component->id), [
'assigned_to' => $asset->id,
'assigned_qty' => 3,
])
->assertOk()
->assertStatusMessageIs('error');
}
public function testCanCheckoutComponent()
{
$user = User::factory()->checkoutComponents()->create();
$asset = Asset::factory()->create();
$component = Component::factory()->create();
$this->actingAsForApi($user)
->postJson(route('api.components.checkout', $component->id), [
'assigned_to' => $asset->id,
'assigned_qty' => 1,
])
->assertOk()
->assertStatusMessageIs('success');
$this->assertTrue($component->assets->first()->is($asset));
}
public function testComponentCheckoutIsLogged()
{
$user = User::factory()->checkoutComponents()->create();
$location = Location::factory()->create();
$asset = Asset::factory()->create(['location_id' => $location->id]);
$component = Component::factory()->create();
$this->actingAsForApi($user)
->postJson(route('api.components.checkout', $component->id), [
'assigned_to' => $asset->id,
'assigned_qty' => 1,
]);
$this->assertDatabaseHas('action_logs', [
'created_by' => $user->id,
'action_type' => 'checkout',
'target_id' => $asset->id,
'target_type' => Asset::class,
'location_id' => $location->id,
'item_type' => Component::class,
'item_id' => $component->id,
]);
}
public function testAdheresToFullMultipleCompaniesSupportScoping()
{
[$companyA, $companyB] = Company::factory()->count(2)->create();
$userForCompanyA = User::factory()->for($companyA)->create();
$assetForCompanyB = Asset::factory()->for($companyB)->create();
$componentForCompanyB = Component::factory()->for($companyB)->create();
$this->actingAsForApi($userForCompanyA)
->postJson(route('api.components.checkout', $componentForCompanyB->id), [
'assigned_to' => $assetForCompanyB->id,
'assigned_qty' => 1,
])
->assertForbidden();
}
}

View file

@ -0,0 +1,88 @@
<?php
namespace Tests\Feature\Components\Ui;
use App\Models\Company;
use App\Models\Component;
use App\Models\User;
use Illuminate\Support\Facades\Storage;
use Tests\Concerns\TestsFullMultipleCompaniesSupport;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\TestCase;
class DeleteComponentTest extends TestCase implements TestsFullMultipleCompaniesSupport, TestsPermissionsRequirement
{
public function testRequiresPermission()
{
$component = Component::factory()->create();
$this->actingAs(User::factory()->create())
->delete(route('components.destroy', $component->id))
->assertForbidden();
}
public function testHandlesNonExistentComponent()
{
$this->actingAs(User::factory()->deleteComponents()->create())
->delete(route('components.destroy', 10000))
->assertSessionHas('error');
}
public function testCanDeleteComponent()
{
$component = Component::factory()->create();
$this->actingAs(User::factory()->deleteComponents()->create())
->delete(route('components.destroy', $component->id))
->assertSessionHas('success')
->assertRedirect(route('components.index'));
$this->assertSoftDeleted($component);
}
public function testDeletingComponentRemovesComponentImage()
{
Storage::fake('public');
$component = Component::factory()->create(['image' => 'component-image.jpg']);
Storage::disk('public')->put('components/component-image.jpg', 'content');
Storage::disk('public')->assertExists('components/component-image.jpg');
$this->actingAs(User::factory()->deleteComponents()->create())->delete(route('components.destroy', $component->id));
Storage::disk('public')->assertMissing('components/component-image.jpg');
}
public function testDeletingComponentIsLogged()
{
$user = User::factory()->deleteComponents()->create();
$component = Component::factory()->create();
$this->actingAs($user)->delete(route('components.destroy', $component->id));
$this->assertDatabaseHas('action_logs', [
'created_by' => $user->id,
'action_type' => 'delete',
'item_type' => Component::class,
'item_id' => $component->id,
]);
}
public function testAdheresToFullMultipleCompaniesSupportScoping()
{
$this->settings->enableMultipleFullCompanySupport();
[$companyA, $companyB] = Company::factory()->count(2)->create();
$userInCompanyA = User::factory()->for($companyA)->create();
$componentForCompanyB = Component::factory()->for($companyB)->create();
$this->actingAs($userInCompanyA)
->delete(route('components.destroy', $componentForCompanyB->id))
->assertSessionHas('error');
$this->assertNotSoftDeleted($componentForCompanyB);
}
}

View file

@ -121,5 +121,26 @@ trait CustomTestMacros
return $this; return $this;
} }
); );
TestResponse::macro(
'assertPayloadContains',
function (array|string $keys) {
Assert::assertArrayHasKey('payload', $this, 'Response did not contain a payload');
if (is_string($keys)) {
$keys = [$keys];
}
foreach ($keys as $key) {
Assert::assertArrayHasKey(
$key,
$this['payload'],
"Response messages did not contain the key: {$key}"
);
}
return $this;
}
);
} }
} }