Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show error when assigned_to not null but assigned_type is null #14748

Open
wants to merge 10 commits into
base: snipeit_v7_laravel10
Choose a base branch
from
21 changes: 14 additions & 7 deletions app/Http/Controllers/Api/AssetsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,8 @@ public function update(ImageUploadRequest $request, $id)
}

$asset = $request->handleImages($asset);


$model = AssetModel::find($asset->model_id);

// Update custom fields
Expand Down Expand Up @@ -716,16 +718,21 @@ public function update(ImageUploadRequest $request, $id)
}
}

if (($request->filled('assigned_user')) || ($request->filled('assigned_asset')) || ($request->filled('assigned_location'))) {
app('App\Http\Requests\AssetCheckoutRequest');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't given this a full look yet but this is really standing out to me. I think calling a request in this way and not in the expected "parameter in a controller method" is, for me, jarring and not in the spirit how they should be used 😅

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opted to do it this way to consider PATCH and POST, so we skip the whole validation altogether if the request doesn't have the field or the field is empty.

Open to hearing alternate suggestions tho.

Copy link
Collaborator

@uberbrady uberbrady May 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I completely misunderstood - leaving it here in case someone was scratching their head looking at it.

Wait @marcusmoore I think I understand what you're saying - you're saying

Instead of saying:

$request->filled('assigned_user')

We should be saying:

$this->assigned_user

To fit with the rest of the whole "FormRequest ethos"? If that's all that's no big deal and, yes, you're right.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We invoke FormRequests independently using the app facade elsewhere in the code. (See the image/file upload requests sprinkled throughout), so it's not like a one-time thing. 90% of the time, the update is just an update. Sometimes it's not and it's also a checkout.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@uberbrady I think he's talking about the method signature itself and the manual invocation of the form request (so we can use more than one form request within a controller) - but I could be wrong :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opted to do it this way to consider PATCH and POST, so we skip the whole validation altogether if the request doesn't have the field or the field is empty.

Can we introduce that logic to the form request itself? On a similar note, I'm not all that adverse to the duplication that comes with having dedicated "Store" and "Update" form requests because of how often we end up with different rules for each operation but that's a bigger topic for another time.

We invoke FormRequests independently using the app facade elsewhere in the code. (See the image/file upload requests sprinkled throughout), so it's not like a one-time thing. 90% of the time, the update is just an update. Sometimes it's not and it's also a checkout.

But it still goes against the traditional way form requests are invoked and intended to be used. I'm all for finding unique ways to play with the tools we're provided but I'm hesitant/side-eyeing this one...

}


if ($asset->save()) {
if (($request->filled('assigned_user')) && ($target = User::find($request->get('assigned_user')))) {
$location = $target->location_id;
} elseif (($request->filled('assigned_asset')) && ($target = Asset::find($request->get('assigned_asset')))) {
$location = $target->location_id;

Asset::where('assigned_type', \App\Models\Asset::class)->where('assigned_to', $id)
->update(['location_id' => $target->location_id]);
} elseif (($request->filled('assigned_location')) && ($target = Location::find($request->get('assigned_location')))) {
if ($request->filled('assigned_user')) {
$target = User::find($request->get('assigned_user'));
$location = $target->location_id;
} elseif ($request->filled('assigned_asset')) {
$target = Asset::find($request->get('assigned_asset'));
$location = $target->location_id;
} elseif ($request->filled('assigned_location')) {
$target = Location::find($request->get('assigned_location'));
$location = $target->id;
}

Expand Down
25 changes: 21 additions & 4 deletions app/Http/Requests/AssetCheckoutRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ public function authorize()
return true;
}

protected function prepareForValidation(): void
{
if (!empty($this->assigned_user)) {
$this->assigned_type = 'App\Models\User';
} elseif (!empty($this->assigned_asset)) {
$this->assigned_type = 'App\Models\Asset';
} elseif (!empty($this->assigned_location)) {
$this->assigned_type = 'App\Models\Location';
}

$this->merge([
'assigned_type' => $this->assigned_type,
]);
}


/**
* Get the validation rules that apply to the request.
*
Expand All @@ -22,13 +38,14 @@ public function authorize()
public function rules()
{
$rules = [
'assigned_user' => 'required_without_all:assigned_asset,assigned_location',
'assigned_asset' => 'required_without_all:assigned_user,assigned_location',
'assigned_location' => 'required_without_all:assigned_user,assigned_asset',
'assigned_user' => ['required_without_all:assigned_asset,assigned_location','nullable','exists:users,id,deleted_at,NULL'],
'assigned_asset' => ['required_without_all:assigned_user,assigned_location','nullable','exists:assets,id,deleted_at,NULL'],
'assigned_location' => ['required_without_all:assigned_user,assigned_asset','nullable','exists:locations,id,deleted_at,NULL'],
'status_id' => 'exists:status_labels,id,deployable,1',
'checkout_to_type' => 'required|in:asset,location,user',
];

return $rules;
}


}
6 changes: 6 additions & 0 deletions database/factories/LocationFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,10 @@ public function definition()
'image' => rand(1, 9).'.jpg',
];
}

public function deleted()
{
return $this->state(['deleted_at' => $this->faker->dateTime()]);
}

}
5 changes: 5 additions & 0 deletions database/factories/UserFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ public function superuser()
return $this->appendPermission(['superuser' => '1']);
}

public function deleted()
{
return $this->state(['deleted_at' => $this->faker->dateTime()]);
}

public function admin()
{
return $this->state(function () {
Expand Down
4 changes: 2 additions & 2 deletions resources/views/settings/index.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -446,8 +446,8 @@
</div>
</div> <!-- /box-body-->
</div> <!--/box-default-->
</div><!--/col-md-8-->
</div><!--/row-->





Expand Down
120 changes: 120 additions & 0 deletions tests/Feature/Api/Assets/AssetUpdateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use App\Models\Asset;
use App\Models\CustomField;
use App\Models\User;
use App\Models\Location;
use Illuminate\Support\Facades\Crypt;
use Tests\TestCase;

Expand Down Expand Up @@ -52,4 +53,123 @@ public function testPermissionNeededToUpdateEncryptedField()
$asset->refresh();
$this->assertEquals("encrypted value should not change", Crypt::decrypt($asset->{$field->db_column_name()}));
}

public function testCheckoutToUserOnAssetUpdate()
{
$asset = Asset::factory()->create();
$user = User::factory()->editAssets()->create();
$assigned_user = User::factory()->create();

$response = $this->actingAsForApi($user)
->patchJson(route('api.assets.update', ['hardware' => $asset->id]), [
'assigned_user' => $assigned_user->id,
])
->assertOk()
->assertStatusMessageIs('success')
->json();

$asset->refresh();
$this->assertEquals($assigned_user->id, $asset->assigned_to);
$this->assertEquals($asset->assigned_type, 'App\Models\User');
}

public function testCheckoutToDeletedUserFailsOnAssetUpdate()
{
$asset = Asset::factory()->create();
$user = User::factory()->editAssets()->create();
$assigned_user = User::factory()->deleted()->create();

$this->actingAsForApi($user)
->patchJson(route('api.assets.update', ['hardware' => $asset->id]), [
'assigned_user' => $assigned_user->id,
])
->assertOk()
->assertStatusMessageIs('error')
->json();

$asset->refresh();
$this->assertNull($asset->assigned_to);
$this->assertNull($asset->assigned_type);
}

public function testCheckoutToLocationOnAssetUpdate()
{
$asset = Asset::factory()->create();
$user = User::factory()->editAssets()->create();
$assigned_location = Location::factory()->create();

$this->actingAsForApi($user)
->patchJson(route('api.assets.update', ['hardware' => $asset->id]), [
'assigned_location' => $assigned_location->id,
])
->assertOk()
->assertStatusMessageIs('success')
->json();

$asset->refresh();
$this->assertEquals($assigned_location->id, $asset->assigned_to);
$this->assertEquals($asset->assigned_type, 'App\Models\Location');

}

public function testCheckoutToDeletedLocationFailsOnAssetUpdate()
{
$asset = Asset::factory()->create();
$user = User::factory()->editAssets()->create();
$assigned_location = Location::factory()->deleted()->create();

$this->actingAsForApi($user)
->patchJson(route('api.assets.update', ['hardware' => $asset->id]), [
'assigned_location' => $assigned_location->id,
])
->assertOk()
->assertStatusMessageIs('error')
->json();

$asset->refresh();
$this->assertNull($asset->assigned_to);
$this->assertNull($asset->assigned_type);
}

public function testCheckoutAssetOnAssetUpdate()
{
$asset = Asset::factory()->create();
$user = User::factory()->editAssets()->create();
$assigned_asset = Asset::factory()->create();

$this->actingAsForApi($user)
->patchJson(route('api.assets.update', ['hardware' => $asset->id]), [
'assigned_asset' => $assigned_asset->id,
'checkout_to_type' => 'user',
])
->assertOk()
->assertStatusMessageIs('success')
->json();

$asset->refresh();
$this->assertEquals($assigned_asset->id, $asset->assigned_to);
$this->assertEquals($asset->assigned_type, 'App\Models\Asset');

}

public function testCheckoutToDeletedAssetFailsOnAssetUpdate()
{
$asset = Asset::factory()->create();
$user = User::factory()->editAssets()->create();
$assigned_asset = Asset::factory()->deleted()->create();

$this->actingAsForApi($user)
->patchJson(route('api.assets.update', ['hardware' => $asset->id]), [
'assigned_asset' => $assigned_asset->id,
])
->assertOk()
->assertStatusMessageIs('error')
->json();

$asset->refresh();
$this->assertNull($asset->assigned_to);
$this->assertNull($asset->assigned_type);
}


}
Loading