Merge remote-tracking branch 'origin/develop'

This commit is contained in:
snipe
2025-06-25 11:04:15 +01:00
25 changed files with 718 additions and 37 deletions

View File

@@ -7,6 +7,7 @@ use App\Events\CheckoutDeclined;
use App\Events\ItemAccepted;
use App\Events\ItemDeclined;
use App\Http\Controllers\Controller;
use App\Mail\CheckoutAcceptanceResponseMail;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\CheckoutAcceptance;
@@ -21,8 +22,10 @@ use App\Models\Component;
use App\Models\Consumable;
use App\Notifications\AcceptanceAssetAcceptedNotification;
use App\Notifications\AcceptanceAssetDeclinedNotification;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use App\Http\Controllers\SettingsController;
@@ -337,6 +340,21 @@ class AcceptanceController extends Controller
$return_msg = trans('admin/users/message.declined');
}
if ($acceptance->alert_on_response_id) {
try {
$recipient = User::find($acceptance->alert_on_response_id);
if ($recipient) {
Mail::to($recipient)->send(new CheckoutAcceptanceResponseMail(
$acceptance,
$recipient,
$request->input('asset_acceptance') === 'accepted',
));
}
} catch (Exception $e) {
Log::warning($e);
}
}
return redirect()->to('account/accept')->with('success', $return_msg);

View File

@@ -677,7 +677,6 @@ class UsersController extends Controller
$this->authorize('view', License::class);
if ($user = User::where('id', $id)->withTrashed()->first()) {
$this->authorize('update', $user);
$licenses = $user->licenses()->get();
return (new LicensesTransformer())->transformLicenses($licenses, $licenses->count());
}

View File

@@ -68,6 +68,7 @@ class CategoriesController extends Controller
$category->eula_text = $request->input('eula_text');
$category->use_default_eula = $request->input('use_default_eula', '0');
$category->require_acceptance = $request->input('require_acceptance', '0');
$category->alert_on_response = $request->input('alert_on_response', '0');
$category->checkin_email = $request->input('checkin_email', '0');
$category->notes = $request->input('notes');
$category->created_by = auth()->id();
@@ -121,6 +122,7 @@ class CategoriesController extends Controller
$category->eula_text = $request->input('eula_text');
$category->use_default_eula = $request->input('use_default_eula', '0');
$category->require_acceptance = $request->input('require_acceptance', '0');
$category->alert_on_response = $request->input('alert_on_response', '0');
$category->checkin_email = $request->input('checkin_email', '0');
$category->notes = $request->input('notes');

View File

@@ -12,6 +12,7 @@ use App\Mail\CheckoutConsumableMail;
use App\Mail\CheckoutLicenseMail;
use App\Models\Accessory;
use App\Models\Asset;
use App\Models\Category;
use App\Models\CheckoutAcceptance;
use App\Models\Component;
use App\Models\Consumable;
@@ -227,6 +228,7 @@ class CheckoutableListener
if ($checkedOutToType != "App\Models\User") {
return null;
}
if (!$event->checkoutable->requireAcceptance()) {
return null;
}
@@ -234,6 +236,13 @@ class CheckoutableListener
$acceptance = new CheckoutAcceptance;
$acceptance->checkoutable()->associate($event->checkoutable);
$acceptance->assignedTo()->associate($event->checkedOutTo);
$category = $this->getCategoryFromCheckoutable($event->checkoutable);
if ($category?->alert_on_response) {
$acceptance->alert_on_response_id = auth()->id();
}
$acceptance->save();
return $acceptance;
@@ -360,23 +369,6 @@ class CheckoutableListener
return in_array(get_class($checkoutable), $this->skipNotificationsFor);
}
private function shouldSendEmailNotifications(Model $checkoutable): bool
{
//runs a check if the category wants to send checkin/checkout emails to users
$category = match (true) {
$checkoutable instanceof Asset => $checkoutable->model->category,
$checkoutable instanceof Accessory,
$checkoutable instanceof Consumable => $checkoutable->category,
$checkoutable instanceof LicenseSeat => $checkoutable->license->category,
default => null,
};
if (!$category?->checkin_email) {
return false;
}
return true;
}
private function shouldSendWebhookNotification(): bool
{
return Setting::getSettings() && Setting::getSettings()->webhook_endpoint;
@@ -471,4 +463,14 @@ class CheckoutableListener
return array($to, $cc);
}
private function getCategoryFromCheckoutable(Model $checkoutable): ?Category
{
return match (true) {
$checkoutable instanceof Asset => $checkoutable->model->category,
$checkoutable instanceof Accessory,
$checkoutable instanceof Consumable => $checkoutable->category,
$checkoutable instanceof LicenseSeat => $checkoutable->license->category,
};
}
}

View File

@@ -6,6 +6,8 @@ use Livewire\Component;
class CategoryEditForm extends Component
{
public bool $alertOnResponse;
public $defaultEulaText;
public $eulaText;

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Mail;
use App\Models\CheckoutAcceptance;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class CheckoutAcceptanceResponseMail extends Mailable
{
use Queueable, SerializesModels;
public CheckoutAcceptance $acceptance;
public User $recipient;
public bool $wasAccepted;
/**
* Create a new message instance.
*/
public function __construct(CheckoutAcceptance $acceptance, User $recipient, bool $wasAccepted)
{
$this->acceptance = $acceptance;
$this->recipient = $recipient;
$this->wasAccepted = $wasAccepted;
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
$subject = $this->wasAccepted
? trans('mail.initiated_accepted')
: trans('mail.initiated_declined');
return new Envelope(
subject: $subject,
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
markdown: 'mail.markdown.checkout-acceptance-response',
with: [
'assignedTo' => $this->acceptance->assignedTo,
'introduction' => $this->introduction(),
'item' => $this->acceptance->checkoutable,
'note' => $this->acceptance->note,
'recipient' => $this->recipient,
]
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
private function introduction(): string
{
return $this->wasAccepted
? trans('mail.following_accepted')
: trans('mail.following_declined');
}
}

View File

@@ -32,6 +32,7 @@ class Category extends SnipeModel
protected $hidden = ['created_by', 'deleted_at'];
protected $casts = [
'alert_on_response' => 'boolean',
'created_by' => 'integer',
];
@@ -69,6 +70,7 @@ class Category extends SnipeModel
'eula_text',
'name',
'require_acceptance',
'alert_on_response',
'use_default_eula',
'created_by',
'notes',

View File

@@ -15,6 +15,7 @@ class CheckoutAcceptance extends Model
protected $casts = [
'accepted_at' => 'datetime',
'declined_at' => 'datetime',
'alert_on_response_id' => 'integer',
];
/**

View File

@@ -43,7 +43,7 @@ class License extends Depreciable
protected $rules = [
'name' => 'required|string|min:3|max:255',
'seats' => 'required|min:1|integer',
'seats' => 'required|min:1|integer|limit_change:10000', // limit_change is a "pseudo-rule" that translates into 'between', see prepareLimitChangeRule() below
'license_email' => 'email|nullable|max:120',
'license_name' => 'string|nullable|max:100',
'notes' => 'string|nullable',
@@ -148,6 +148,14 @@ class License extends Depreciable
});
}
public function prepareLimitChangeRule($parameters, $field)
{
$actual_seat_count = $this->licenseseats()->count(); //we use the *actual* seat count here, in case your license has gone wonky
$lower_bound = $actual_seat_count - $parameters[0];
$upper_bound = $actual_seat_count + $parameters[0];
return ["between", ($lower_bound <= 0 ? 1 : $lower_bound), $upper_bound];
}
/**
* Balance seat counts
*
@@ -164,21 +172,17 @@ class License extends Depreciable
// On Create, we just make one for each of the seats.
$change = abs($oldSeats - $newSeats);
if ($oldSeats > $newSeats) {
$license->load('licenseseats.user');
// Need to delete seats... lets see if if we have enough.
$seatsAvailableForDelete = $license->licenseseats->reject(function ($seat) {
return ((bool) $seat->assigned_to) || ((bool) $seat->asset_id);
});
$seatsAvailableForDelete = $license->licenseseats()->whereNull('assigned_to')->whereNull('asset_id')->limit($change);
if ($change > $seatsAvailableForDelete->count()) {
Session::flash('error', trans('admin/licenses/message.assoc_users'));
return false;
}
for ($i = 1; $i <= $change; $i++) {
$seatsAvailableForDelete->pop()->delete();
}
$seatsAvailableForDelete->delete();
// Log Deletion of seats.
$logAction = new Actionlog;
$logAction->item_type = self::class;

View File

@@ -64,6 +64,24 @@ class CheckoutAcceptanceFactory extends Factory
]);
}
public function withoutAlerting()
{
return $this->state(function () {
return [
'alert_on_response_id' => null,
];
});
}
public function withAlertingTo(User $user)
{
return $this->state(function () use ($user) {
return [
'alert_on_response_id' => $user->id,
];
});
}
private function createdAssociatedActionLogEntry(CheckoutAcceptance $acceptance): void
{
$acceptance->checkoutable->assetlog()->create([

View File

@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('checkout_acceptances', function (Blueprint $table) {
$table->unsignedBigInteger('alert_on_response_id')->nullable()->after('note');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('checkout_acceptances', function (Blueprint $table) {
$table->dropColumn('alert_on_response_id');
});
}
};

View File

@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('categories', function (Blueprint $table) {
$table->boolean('alert_on_response')->default(0)->after('require_acceptance');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('categories', function (Blueprint $table) {
$table->dropColumn('alert_on_response');
});
}
};

View File

@@ -4,6 +4,7 @@ return array(
'asset_categories' => 'Asset Categories',
'category_name' => 'Category Name',
'checkin_email' => 'Send email to user on checkin/checkout.',
'email_to_initiator' => 'Send email to you when user accepts or declines checkout.',
'checkin_email_notification' => 'This user will be sent an email on checkin/checkout.',
'clone' => 'Clone Category',
'create' => 'Create Category',

View File

@@ -52,9 +52,13 @@ return [
'days' => 'Days',
'expecting_checkin_date' => 'Expected Checkin Date',
'expires' => 'Expires',
'following_accepted' => 'The following was accepted',
'following_declined' => 'The following was declined',
'hello' => 'Hello',
'hi' => 'Hi',
'i_have_read' => 'I have read and agree to the terms of use, and have received this item.',
'initiated_accepted' => 'A checkout you initiated was accepted',
'initiated_declined' => 'A checkout you initiated was declined',
'inventory_report' => 'Inventory Report',
'item' => 'Item',
'item_checked_reminder' => 'This is a reminder that you currently have :count items checked out to you that you have not accepted or declined. Please click the link below to confirm your decision.',

View File

@@ -31,6 +31,7 @@
</div>
<livewire:category-edit-form
:alert-on-response="(bool) old('alert_on_response', $item->alert_on_response)"
:default-eula-text="$snipeSettings->default_eula_text"
:eula-text="old('eula_text', $item->eula_text)"
:require-acceptance="(bool) old('require_acceptance', $item->require_acceptance)"

View File

@@ -64,6 +64,22 @@
</div>
</div>
@if ($requireAcceptance)
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<label class="form-control">
<input
type="checkbox"
name="alert_on_response"
value="1"
wire:model="alertOnResponse"
/>
{{ trans('admin/categories/general.email_to_initiator') }}
</label>
</div>
</div>
@endif
<!-- Email on Checkin -->
<div class="form-group">
<div class="col-md-9 col-md-offset-3">

View File

@@ -0,0 +1,27 @@
@component('mail::message')
# {{ trans('mail.hello') }} {{ $recipient->present()->fullName() }},
{{ $introduction }}:
@if (($snipeSettings->show_images_in_email =='1') && (method_exists($item, 'getImageUrl') && $item->getImageUrl()))
<center><img src="{{ $item->getImageUrl() }}" alt="Asset" style="max-width: 570px;"></center>
@endif
@component('mail::table')
| | |
| ------------- | ------------- |
| **{{ trans('mail.user') }}** | {{ $assignedTo->present()->fullName() }} |
| **{{ trans('mail.name') }}** | {{ $item->present()->name() }} |
@if (isset($item->asset_tag))
| **{{ trans('mail.asset_tag') }}** | {{ $item->asset_tag }} |
@endif
@if ($note != '')
| **{{ trans('mail.notes') }}** | {{ $note }} |
@endif
@endcomponent
{{ trans('mail.best_regards') }}
{{ $snipeSettings->site_name }}
@endcomponent

View File

@@ -11,8 +11,6 @@ use Tests\TestCase;
class CreateCategoriesTest extends TestCase
{
public function testRequiresPermissionToCreateCategory()
{
$this->actingAsForApi(User::factory()->create())
@@ -28,6 +26,8 @@ class CreateCategoriesTest extends TestCase
'eula_text' => 'Test EULA',
'category_type' => 'accessory',
'notes' => 'Test Note',
'require_acceptance' => true,
'alert_on_response' => true,
])
->assertOk()
->assertStatusMessageIs('success')
@@ -41,6 +41,8 @@ class CreateCategoriesTest extends TestCase
$this->assertEquals('Test EULA', $category->eula_text);
$this->assertEquals('Test Note', $category->notes);
$this->assertEquals('accessory', $category->category_type);
$this->assertEquals(1, $category->require_acceptance);
$this->assertEquals(1, $category->alert_on_response);
}
public function testCannotCreateCategoryWithoutCategoryType()
@@ -81,5 +83,4 @@ class CreateCategoriesTest extends TestCase
$this->assertFalse(Category::where('name', 'Test Category')->exists());
}
}

View File

@@ -8,6 +8,40 @@ use Tests\TestCase;
class UpdateCategoriesTest extends TestCase
{
public function test_requires_permission_to_update_category()
{
$category = Category::factory()->create();
$this->actingAsForApi(User::factory()->create())
->patchJson(route('api.categories.update', $category))
->assertForbidden();
}
public function test_can_update_category()
{
$category = Category::factory()->forAssets()->create([
'name' => 'Test Category',
'require_acceptance' => false,
'alert_on_response' => false,
]);
$this->actingAsForApi(User::factory()->superuser()->create())
->patchJson(route('api.categories.update', $category), [
'name' => 'Test Category Edited',
'notes' => 'Test Note Edited',
'require_acceptance' => true,
'alert_on_response' => true,
])
->assertOk()
->assertStatusMessageIs('success')
->assertStatus(200);
$category->refresh();
$this->assertEquals('Test Category Edited', $category->name, 'Name was not updated');
$this->assertEquals('Test Note Edited', $category->notes, 'Note was not updated');
$this->assertEquals(1, $category->require_acceptance, 'Require acceptance was not updated');
$this->assertTrue($category->alert_on_response, 'Alert on response was not updated');
}
public function testCanUpdateCategoryViaPatchWithoutCategoryType()
{
@@ -55,5 +89,4 @@ class UpdateCategoriesTest extends TestCase
$this->assertNotEquals('accessory', $category->category_type, 'EULA was not updated');
}
}

View File

@@ -34,11 +34,20 @@ class CreateCategoriesTest extends TestCase
->post(route('categories.store'), [
'name' => 'Test Category',
'category_type' => 'asset',
'notes' => 'Test Note',
'eula_text' => 'Sample text',
'require_acceptance' => '1',
'notes' => 'My Note',
])
->assertRedirect(route('categories.index'));
$this->assertTrue(Category::where('name', 'Test Category')->where('notes', 'Test Note')->exists());
$this->assertDatabaseHas('categories', [
'name' => 'Test Category',
'category_type' => 'asset',
'eula_text' => 'Sample text',
'notes' => 'My Note',
'require_acceptance' => 1,
'alert_on_response' => 0,
]);
}
public function testUserCannotCreateCategoriesWithInvalidType()

View File

@@ -43,21 +43,33 @@ class UpdateCategoriesTest extends TestCase
public function testUserCanEditAssetCategory()
{
$category = Category::factory()->forAssets()->create(['name' => 'Test Category']);
$category = Category::factory()->forAssets()->create([
'name' => 'Test Category',
'require_acceptance' => false,
'alert_on_response' => false,
]);
$this->assertTrue(Category::where('name', 'Test Category')->exists());
$response = $this->actingAs(User::factory()->superuser()->create())
->put(route('categories.update', $category), [
'name' => 'Test Category Edited',
'notes' => 'Test Note Edited',
'require_acceptance' => '1',
'alert_on_response' => '1',
])
->assertStatus(302)
->assertSessionHasNoErrors()
->assertRedirect(route('categories.index'));
$this->followRedirects($response)->assertSee('Success');
$this->assertTrue(Category::where('name', 'Test Category Edited')->where('notes', 'Test Note Edited')->exists());
$this->assertDatabaseHas('categories', [
'name' => 'Test Category Edited',
'notes' => 'Test Note Edited',
'require_acceptance' => 1,
'alert_on_response' => 1,
]);
}
public function testUserCanChangeCategoryTypeIfNoAssetsAssociated()
@@ -102,5 +114,4 @@ class UpdateCategoriesTest extends TestCase
$this->assertFalse(Category::where('name', 'Test Category Edited')->where('notes', 'Test Note Edited')->exists());
}
}

View File

@@ -0,0 +1,163 @@
<?php
namespace Tests\Feature\Checkouts\General;
use App\Models\Accessory;
use App\Models\Asset;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\Statuslabel;
use App\Models\User;
use Tests\TestCase;
class SettingAlertOnResponseTest extends TestCase
{
private User $actor;
private User $assignedUser;
protected function setUp(): void
{
parent::setUp();
$this->actor = User::factory()->superuser()->create();
$this->assignedUser = User::factory()->create();
}
public function test_sets_alert_on_response_if_enabled_by_category_for_accessory()
{
$accessory = Accessory::factory()->create();
$accessory->category->update([
'require_acceptance' => true,
'alert_on_response' => true,
]);
$this->postAccessoryCheckout($accessory);
$this->assertDatabaseHas('checkout_acceptances', [
'checkoutable_type' => Accessory::class,
'checkoutable_id' => $accessory->id,
'assigned_to_id' => $this->assignedUser->id,
'alert_on_response_id' => $this->actor->id,
]);
}
public function test_does_not_set_alert_on_response_if_disabled_by_category_for_accessory()
{
$accessory = Accessory::factory()->create();
$accessory->category->update([
'require_acceptance' => true,
'alert_on_response' => false,
]);
$this->postAccessoryCheckout($accessory);
$this->assertDatabaseHas('checkout_acceptances', [
'checkoutable_type' => Accessory::class,
'checkoutable_id' => $accessory->id,
'assigned_to_id' => $this->assignedUser->id,
'alert_on_response_id' => null,
]);
}
public function test_sets_alert_on_response_if_enabled_by_category_for_asset()
{
$asset = Asset::factory()->create();
$asset->model->category->update([
'require_acceptance' => true,
'alert_on_response' => true,
]);
$this->postAssetCheckout($asset);
$this->assertDatabaseHas('checkout_acceptances', [
'checkoutable_type' => Asset::class,
'checkoutable_id' => $asset->id,
'assigned_to_id' => $this->assignedUser->id,
'alert_on_response_id' => $this->actor->id,
]);
}
public function test_does_not_set_alert_on_response_if_disabled_by_category_for_asset()
{
$asset = Asset::factory()->create();
$asset->model->category->update([
'require_acceptance' => true,
'alert_on_response' => false,
]);
$this->postAssetCheckout($asset);
$this->assertDatabaseHas('checkout_acceptances', [
'checkoutable_type' => Asset::class,
'checkoutable_id' => $asset->id,
'assigned_to_id' => $this->assignedUser->id,
'alert_on_response_id' => null,
]);
}
public function test_sets_alert_on_response_if_enabled_by_category_for_license()
{
$license = License::factory()->create();
$license->category->update([
'require_acceptance' => true,
'alert_on_response' => true,
]);
$this->postLicenseCheckout($license);
$this->assertDatabaseHas('checkout_acceptances', [
'checkoutable_type' => LicenseSeat::class,
'assigned_to_id' => $this->assignedUser->id,
'alert_on_response_id' => $this->actor->id,
]);
}
public function test_does_not_set_alert_on_response_if_disabled_by_category_for_license()
{
$license = License::factory()->create();
$license->category->update([
'require_acceptance' => true,
'alert_on_response' => false,
]);
$this->postLicenseCheckout($license);
$this->assertDatabaseHas('checkout_acceptances', [
'checkoutable_type' => LicenseSeat::class,
'assigned_to_id' => $this->assignedUser->id,
'alert_on_response_id' => null,
]);
}
private function postAssetCheckout(Asset $asset): void
{
$this->actingAs($this->actor)
->post(route('hardware.checkout.store', $asset), [
'checkout_to_type' => 'user',
'status_id' => (string) Statuslabel::factory()->readyToDeploy()->create()->id,
'assigned_user' => $this->assignedUser->id,
]);
}
private function postAccessoryCheckout(Accessory $accessory): void
{
$this->actingAs($this->actor)
->post(route('accessories.checkout.store', $accessory), [
'checkout_to_type' => 'user',
'status_id' => (string) Statuslabel::factory()->readyToDeploy()->create()->id,
'assigned_user' => $this->assignedUser->id,
'checkout_qty' => 1,
]);
}
private function postLicenseCheckout(License $license): void
{
$this->actingAs($this->actor)
->post("/licenses/{$license->id}/checkout/", [
'checkout_to_type' => 'user',
'assigned_to' => $this->assignedUser->id,
]);
}
}

View File

@@ -2,7 +2,6 @@
namespace Tests\Feature\Licenses\Ui;
use App\Models\AssetModel;
use App\Models\Category;
use App\Models\License;
use App\Models\Depreciation;
@@ -41,7 +40,48 @@ class CreateLicenseTest extends TestCase
$response->assertInvalid(['purchase_date']);
$response->assertSessionHasErrors(['purchase_date']);
$this->followRedirects($response)->assertSee(trans('general.error'));
$this->assertFalse(AssetModel::where('name', 'Test Invalid License')->exists());
$this->assertFalse(License::where('name', 'Test Invalid License')->exists());
}
public function testLicenseCreate()
{
$response = $this->actingAs(User::factory()->superuser()->create())
->from(route('licenses.create'))
->post(route('licenses.store'), [
'name' => 'Test Valid License',
'seats' => '10',
'category_id' => Category::factory()->forLicenses()->create()->id,
]);
$response->assertStatus(302);
$license = License::where('name', 'Test Valid License')->sole();
$this->assertNotNull($license);
//$license->assetlog()->has_one_of_();
$this->assertDatabaseHas('action_logs', ['action_type' => 'create', 'item_id' => $license->id, 'item_type' => License::class]);
$this->assertDatabaseHas('action_logs', ['action_type' => 'add seats', 'item_id' => $license->id, 'item_type' => License::class]);
$this->assertEquals($license->licenseseats()->count(), 10);
//test log entries? Sure.
}
public function testTooManySeatsLicenseCreate()
{
$response = $this->actingAs(User::factory()->superuser()->create())
->from(route('licenses.create'))
->post(route('licenses.store'), [
'name' => 'Test Valid License',
'seats' => '100000',
'category_id' => Category::factory()->forLicenses()->create()->id,
]);
$response->assertStatus(302);
$license = License::where('name', 'Test Valid License')->first();
$this->assertNull($license);
//$license->assetlog()->has_one_of_();
// $this->assertDatabaseMissing('action_logs', ['action_type' => 'create', 'item_id' => $license->id, 'item_type' => License::class]);
// $this->assertDatabaseMissing('action_logs', ['action_type' => 'add seats', 'item_id' => $license->id, 'item_type' => License::class]);
//test log entries? Sure.
}
}

View File

@@ -2,6 +2,7 @@
namespace Tests\Feature\Licenses\Ui;
use App\Models\Category;
use App\Models\License;
use App\Models\User;
use Tests\TestCase;
@@ -14,4 +15,90 @@ class UpdateLicenseTest extends TestCase
->get(route('licenses.edit', License::factory()->create()->id))
->assertOk();
}
public function testCanUpdateLicenseSeats()
{
$admin = User::factory()->superuser()->create();
$license_category = Category::factory()->forLicenses()->create()->id;
$response = $this->actingAs($admin)
->from(route('licenses.create'))
->post(route('licenses.store'), [
'name' => 'Test Update License',
'seats' => '9999',
'category_id' => $license_category,
]);
$response->assertStatus(302);
$license = License::where('name', 'Test Update License')->sole();
$this->assertNotNull($license);
$this->actingAs($admin)
->put(route('licenses.update', $license->id), [
'name' => 'Test Update License',
'seats' => '19999',
'category_id' => $license_category,
])
->assertStatus(302);
$license->refresh();
$this->assertEquals($license->licenseseats()->count(), $license->seats);
$this->assertEquals($license->licenseseats()->count(), 19999);
}
public function testCannotUpdateLicenseSeatsTooMuch()
{
$admin = User::factory()->superuser()->create();
$license_category = Category::factory()->forLicenses()->create()->id;
$response = $this->actingAs($admin)
->from(route('licenses.create'))
->post(route('licenses.store'), [
'name' => 'Test Update License',
'seats' => '9999',
'category_id' => $license_category,
]);
$response->assertStatus(302);
$license = License::where('name', 'Test Update License')->sole();
$this->assertNotNull($license);
$this->actingAs($admin)
->put(route('licenses.update', $license->id), [
'name' => 'Test Update License',
'seats' => '29999',
'category_id' => $license_category,
])
->assertStatus(302);
$license->refresh();
$this->assertEquals($license->licenseseats()->count(), $license->seats);
$this->assertEquals($license->licenseseats()->count(), 9999);
}
public function testCanRemoveLicenseSeats()
{
$admin = User::factory()->superuser()->create();
$license_category = Category::factory()->forLicenses()->create()->id;
$response = $this->actingAs($admin)
->from(route('licenses.create'))
->post(route('licenses.store'), [
'name' => 'Test Remove License Seats',
'seats' => '9999',
'category_id' => $license_category,
]);
$response->assertStatus(302);
$license = License::where('name', 'Test Remove License Seats')->sole();
$this->assertNotNull($license);
$this->actingAs($admin)
->put(route('licenses.update', $license->id), [
'name' => 'Test Remove License Seats',
'seats' => '5000',
'category_id' => $license_category,
])
->assertStatus(302);
$license->refresh();
$this->assertEquals($license->licenseseats()->count(), $license->seats);
$this->assertEquals($license->licenseseats()->count(), 5000);
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace Tests\Feature\Notifications\Email;
use App\Mail\CheckoutAcceptanceResponseMail;
use App\Models\CheckoutAcceptance;
use App\Models\User;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
class CheckoutResponseEmailTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
Mail::fake();
}
public function test_accepting_checkout_acceptance_configured_to_send_alert()
{
$initiator = User::factory()->create();
$checkoutAcceptance = CheckoutAcceptance::factory()
->pending()
->withAlertingTo($initiator)
->create();
$this->acceptCheckout($checkoutAcceptance);
$this->assertEmailSentTo($initiator, 'accepted');
}
public function test_declining_checkout_acceptance_configured_to_send_alert()
{
$initiator = User::factory()->create();
$checkoutAcceptance = CheckoutAcceptance::factory()
->pending()
->withAlertingTo($initiator)
->create();
$this->declineCheckout($checkoutAcceptance);
$this->assertEmailSentTo($initiator, 'declined');
}
public function test_accepting_checkout_acceptance_not_configured_to_send_alert()
{
$initiator = User::factory()->create();
$checkoutAcceptance = CheckoutAcceptance::factory()
->pending()
->withoutAlerting()
->create();
$this->acceptCheckout($checkoutAcceptance);
$this->assertEmailNotSentTo($initiator);
}
public function test_declining_checkout_acceptance_not_configured_to_send_alert()
{
$initiator = User::factory()->create();
$checkoutAcceptance = CheckoutAcceptance::factory()
->pending()
->withoutAlerting()
->create();
$this->declineCheckout($checkoutAcceptance);
$this->assertEmailNotSentTo($initiator);
}
private function assertEmailSentTo(User $user, string $type): void
{
Mail::assertSent(CheckoutAcceptanceResponseMail::class, function (CheckoutAcceptanceResponseMail $mail) use ($type, $user) {
return $mail->hasTo($user->email) && $mail->assertHasSubject('A checkout you initiated was ' . $type);
});
}
private function assertEmailNotSentTo(User $user): void
{
Mail::assertNotSent(CheckoutAcceptanceResponseMail::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
}
private function acceptCheckout(CheckoutAcceptance $checkoutAcceptance): void
{
$this->actingAs($checkoutAcceptance->assignedTo)
->post(route('account.store-acceptance', $checkoutAcceptance), [
'asset_acceptance' => 'accepted',
'note' => null,
]);
}
private function declineCheckout(CheckoutAcceptance $checkoutAcceptance): void
{
$this->actingAs($checkoutAcceptance->assignedTo)
->post(route('account.store-acceptance', $checkoutAcceptance), [
'asset_acceptance' => 'declined',
'note' => null,
]);
}
}