mirror of
https://github.com/usetrmnl/byos_laravel.git
synced 2026-01-13 15:07:49 +00:00
feat: inspect device logs
feat: create DeviceLog model on log request feat: implement logs route, logs view feat: implement details on device log timezone, latest 50 log items sort by latest device timestamp cleanup job add tests
This commit is contained in:
parent
04d089c445
commit
c045dc1e85
12 changed files with 425 additions and 0 deletions
31
app/Jobs/CleanupDeviceLogsJob.php
Normal file
31
app/Jobs/CleanupDeviceLogsJob.php
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\Device;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class CleanupDeviceLogsJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
Device::each(function ($device) {
|
||||||
|
$keepIds = $device->logs()->latest('device_timestamp')->take(50)->pluck('id');
|
||||||
|
|
||||||
|
// Delete all other logs for this device
|
||||||
|
$device->logs()
|
||||||
|
->whereNotIn('id', $keepIds)
|
||||||
|
->delete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -142,4 +142,9 @@ class Device extends Model
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Firmware::class, 'update_firmware_id');
|
return $this->belongsTo(Firmware::class, 'update_firmware_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function logs(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(DeviceLog::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
27
app/Models/DeviceLog.php
Normal file
27
app/Models/DeviceLog.php
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class DeviceLog extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
|
public function device(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Device::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'log_entry' => 'array',
|
||||||
|
'device_timestamp' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
24
database/factories/DeviceLogFactory.php
Normal file
24
database/factories/DeviceLogFactory.php
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Device;
|
||||||
|
use App\Models\DeviceLog;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
class DeviceLogFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = DeviceLog::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'log_entry' => ["creation_timestamp"=>fake()->dateTimeBetween('-1 month', 'now')->getTimestamp(),"device_status_stamp"=>["wifi_rssi_level"=>-65,"wifi_status"=>"connected","refresh_rate"=>900,"time_since_last_sleep_start"=>901,"current_fw_version"=>"1.5.5","special_function"=>"none","battery_voltage"=>4.052,"wakeup_reason"=>"timer","free_heap_size"=>215128,"max_alloc_size"=>192500],"log_id"=>17,"log_message"=>"Error fetching API display: 7, detail: HTTP Client failed with error: connection refused(-1)","log_codeline"=>586,"log_sourcefile"=>"src\/bl.cpp","additional_info"=>["filename_current"=>"UUID.png","filename_new"=>null,"retry_attempt"=>5]],
|
||||||
|
'device_timestamp' => fake()->dateTimeBetween('-1 month', 'now'),
|
||||||
|
'created_at' => Carbon::now(),
|
||||||
|
'updated_at' => Carbon::now(),
|
||||||
|
'device_id' => Device::first(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Device;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('device_logs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignIdFor(Device::class)->constrained('devices')->cascadeOnDelete();
|
||||||
|
$table->timestamp('device_timestamp');
|
||||||
|
$table->json('log_entry');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('device_logs');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -302,6 +302,7 @@ new class extends Component {
|
||||||
<flux:modal.trigger name="update-firmware">
|
<flux:modal.trigger name="update-firmware">
|
||||||
<flux:menu.item icon="arrow-up-circle">Update Firmware</flux:menu.item>
|
<flux:menu.item icon="arrow-up-circle">Update Firmware</flux:menu.item>
|
||||||
</flux:modal.trigger>
|
</flux:modal.trigger>
|
||||||
|
<flux:menu.item icon="bars-3" href="{{ route('devices.logs', $device) }}" wire:navigate>Show Logs</flux:menu.item>
|
||||||
<flux:modal.trigger name="delete-device">
|
<flux:modal.trigger name="delete-device">
|
||||||
<flux:menu.item icon="trash" variant="danger">Delete Device</flux:menu.item>
|
<flux:menu.item icon="trash" variant="danger">Delete Device</flux:menu.item>
|
||||||
</flux:modal.trigger>
|
</flux:modal.trigger>
|
||||||
|
|
|
||||||
193
resources/views/livewire/devices/logs.blade.php
Normal file
193
resources/views/livewire/devices/logs.blade.php
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Device;
|
||||||
|
use App\Models\DeviceLog;
|
||||||
|
use Livewire\Volt\Component;
|
||||||
|
|
||||||
|
new class extends Component {
|
||||||
|
public Device $device;
|
||||||
|
public $logs;
|
||||||
|
|
||||||
|
public function mount(Device $device)
|
||||||
|
{
|
||||||
|
abort_unless(auth()->user()->devices->contains($device), 403);
|
||||||
|
$this->device = $device;
|
||||||
|
$this->logs = $device->logs()->latest('device_timestamp')->take(50)->get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="py-12">
|
||||||
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h2 class="text-2xl font-semibold dark:text-gray-100">Device Logs - {{ $device->name }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="min-w-full table-fixed text-zinc-800 divide-y divide-zinc-800/10 dark:divide-white/20 text-zinc-800" data-flux-table="">
|
||||||
|
<thead data-flux-columns="">
|
||||||
|
<tr>
|
||||||
|
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white" data-flux-column="">
|
||||||
|
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Device Time</div>
|
||||||
|
</th>
|
||||||
|
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white" data-flux-column="">
|
||||||
|
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Log Level</div>
|
||||||
|
</th>
|
||||||
|
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white" data-flux-column="">
|
||||||
|
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Device Status</div>
|
||||||
|
</th>
|
||||||
|
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white" data-flux-column="">
|
||||||
|
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Message</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody class="divide-y divide-zinc-800/10 dark:divide-white/20" data-flux-rows="">
|
||||||
|
@foreach ($logs as $log)
|
||||||
|
<tr data-flux-row="">
|
||||||
|
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300">
|
||||||
|
{{ \Carbon\Carbon::createFromTimestamp($log->log_entry['creation_timestamp'])->setTimezone(config('app.timezone'))->format('Y-m-d H:i:s') }}
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300">
|
||||||
|
<div class="inline-flex items-center font-medium whitespace-nowrap -mt-1 -mb-1 text-xs py-1 px-2 rounded-md
|
||||||
|
@if(str_contains(strtolower($log->log_entry['log_message']), 'error'))
|
||||||
|
bg-red-400/15 text-red-700 dark:bg-red-400/40 dark:text-red-200
|
||||||
|
@elseif(str_contains(strtolower($log->log_entry['log_message']), 'warning'))
|
||||||
|
bg-yellow-400/15 text-yellow-700 dark:bg-yellow-400/40 dark:text-yellow-200
|
||||||
|
@else
|
||||||
|
bg-zinc-400/15 text-zinc-700 dark:bg-zinc-400/40 dark:text-zinc-200
|
||||||
|
@endif">
|
||||||
|
{{ str_contains(strtolower($log->log_entry['log_message']), 'error') ? 'Error' :
|
||||||
|
(str_contains(strtolower($log->log_entry['log_message']), 'warning') ? 'Warning' : 'Info') }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="inline-flex items-center font-medium whitespace-nowrap -mt-1 -mb-1 text-xs py-1 px-2 rounded-md bg-zinc-400/15 text-zinc-700 dark:bg-zinc-400/40 dark:text-zinc-200">
|
||||||
|
{{ $log->log_entry['device_status_stamp']['wifi_status'] ?? 'Unknown' }}
|
||||||
|
@if(isset($log->log_entry['device_status_stamp']['wifi_rssi_level']))
|
||||||
|
({{ $log->log_entry['device_status_stamp']['wifi_rssi_level'] }}dBm)
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@if(isset($log->log_entry['device_status_stamp']))
|
||||||
|
<flux:modal.trigger name="device-status-{{ $log->id }}">
|
||||||
|
<flux:button icon="information-circle" variant="ghost" size="xs" />
|
||||||
|
</flux:modal.trigger>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>{{ $log->log_entry['log_message'] }}</span>
|
||||||
|
<flux:modal.trigger name="log-details-{{ $log->id }}">
|
||||||
|
<flux:button icon="information-circle" variant="ghost" size="xs" />
|
||||||
|
</flux:modal.trigger>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
@if(isset($log->log_entry['device_status_stamp']))
|
||||||
|
<flux:modal name="device-status-{{ $log->id }}" class="md:w-96">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<flux:heading size="lg">Device Status Details</flux:heading>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl class="text-sm space-y-1">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<dt class="text-zinc-500">WiFi Status:</dt>
|
||||||
|
<dd>{{ $log->log_entry['device_status_stamp']['wifi_status'] ?? 'Unknown' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<dt class="text-zinc-500">WiFi RSSI:</dt>
|
||||||
|
<dd>{{ $log->log_entry['device_status_stamp']['wifi_rssi_level'] ?? 'Unknown' }} dBm</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<dt class="text-zinc-500">Refresh Rate:</dt>
|
||||||
|
<dd>{{ $log->log_entry['device_status_stamp']['refresh_rate'] ?? 'Unknown' }}s</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<dt class="text-zinc-500">Time Since Sleep:</dt>
|
||||||
|
<dd>{{ $log->log_entry['device_status_stamp']['time_since_last_sleep_start'] ?? 'Unknown' }}s</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<dt class="text-zinc-500">Firmware Version:</dt>
|
||||||
|
<dd>{{ $log->log_entry['device_status_stamp']['current_fw_version'] ?? 'Unknown' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<dt class="text-zinc-500">Special Function:</dt>
|
||||||
|
<dd>{{ $log->log_entry['device_status_stamp']['special_function'] ?? 'None' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<dt class="text-zinc-500">Battery Voltage:</dt>
|
||||||
|
<dd>{{ $log->log_entry['device_status_stamp']['battery_voltage'] ?? 'Unknown' }}V</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<dt class="text-zinc-500">Wakeup Reason:</dt>
|
||||||
|
<dd>{{ $log->log_entry['device_status_stamp']['wakeup_reason'] ?? 'Unknown' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<dt class="text-zinc-500">Free Heap:</dt>
|
||||||
|
<dd>{{ $log->log_entry['device_status_stamp']['free_heap_size'] ?? 'Unknown' }} bytes</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<dt class="text-zinc-500">Max Alloc Size:</dt>
|
||||||
|
<dd>{{ $log->log_entry['device_status_stamp']['max_alloc_size'] ?? 'Unknown' }} bytes</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div class="flex">
|
||||||
|
<flux:spacer/>
|
||||||
|
<flux:modal.close>
|
||||||
|
<flux:button variant="ghost">Close</flux:button>
|
||||||
|
</flux:modal.close>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</flux:modal>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<flux:modal name="log-details-{{ $log->id }}" class="md:w-192">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<flux:heading size="lg">Log Details</flux:heading>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl class="text-sm space-y-1">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<dt class="text-zinc-500">Source File:</dt>
|
||||||
|
<dd>{{ $log->log_entry['log_sourcefile'] ?? 'Unknown' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<dt class="text-zinc-500">Line Number:</dt>
|
||||||
|
<dd>{{ $log->log_entry['log_codeline'] ?? 'Unknown' }}</dd>
|
||||||
|
</div>
|
||||||
|
@if(isset($log->log_entry['additional_info']))
|
||||||
|
<div class="mt-2">
|
||||||
|
<dt class="text-zinc-500 mb-1">Additional Info</dt>
|
||||||
|
<dd class="space-y-1">
|
||||||
|
@foreach($log->log_entry['additional_info'] as $key => $value)
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-zinc-500">{{ str_replace('_', ' ', ucfirst($key)) }}:</span>
|
||||||
|
<span>{{ is_null($value) ? 'None' : $value }}</span>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div class="flex">
|
||||||
|
<flux:spacer/>
|
||||||
|
<flux:modal.close>
|
||||||
|
<flux:button variant="ghost">Close</flux:button>
|
||||||
|
</flux:modal.close>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</flux:modal>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
use App\Jobs\GenerateScreenJob;
|
use App\Jobs\GenerateScreenJob;
|
||||||
use App\Models\Device;
|
use App\Models\Device;
|
||||||
|
use App\Models\DeviceLog;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\ImageGenerationService;
|
use App\Services\ImageGenerationService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
@ -185,6 +186,11 @@ Route::post('/log', function (Request $request) {
|
||||||
$logs = $request->json('log.logs_array', []);
|
$logs = $request->json('log.logs_array', []);
|
||||||
foreach ($logs as $log) {
|
foreach ($logs as $log) {
|
||||||
\Log::info('Device Log', $log);
|
\Log::info('Device Log', $log);
|
||||||
|
DeviceLog::create([
|
||||||
|
'device_id' => $device->id,
|
||||||
|
'device_timestamp' => $log['creation_timestamp'] ?? now(),
|
||||||
|
'log_entry' => $log,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Jobs\CleanupDeviceLogsJob;
|
||||||
use App\Jobs\FetchProxyCloudResponses;
|
use App\Jobs\FetchProxyCloudResponses;
|
||||||
use App\Jobs\FirmwarePollJob;
|
use App\Jobs\FirmwarePollJob;
|
||||||
|
use Illuminate\Support\Facades\Schedule;
|
||||||
|
|
||||||
Schedule::job(FetchProxyCloudResponses::class, [])->cron(
|
Schedule::job(FetchProxyCloudResponses::class, [])->cron(
|
||||||
config('services.trmnl.proxy_refresh_cron') ? config('services.trmnl.proxy_refresh_cron') :
|
config('services.trmnl.proxy_refresh_cron') ? config('services.trmnl.proxy_refresh_cron') :
|
||||||
|
|
@ -9,3 +11,4 @@ Schedule::job(FetchProxyCloudResponses::class, [])->cron(
|
||||||
);
|
);
|
||||||
|
|
||||||
Schedule::job(FirmwarePollJob::class)->daily();
|
Schedule::job(FirmwarePollJob::class)->daily();
|
||||||
|
Schedule::job(CleanupDeviceLogsJob::class)->daily();
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ Route::middleware(['auth'])->group(function () {
|
||||||
|
|
||||||
Volt::route('/devices', 'devices.manage')->name('devices');
|
Volt::route('/devices', 'devices.manage')->name('devices');
|
||||||
Volt::route('/devices/{device}/configure', 'devices.configure')->name('devices.configure');
|
Volt::route('/devices/{device}/configure', 'devices.configure')->name('devices.configure');
|
||||||
|
Volt::route('/devices/{device}/logs', 'devices.logs')->name('devices.logs');
|
||||||
|
|
||||||
Volt::route('plugins', 'plugins.index')->name('plugins.index');
|
Volt::route('plugins', 'plugins.index')->name('plugins.index');
|
||||||
|
|
||||||
|
|
|
||||||
44
tests/Feature/Jobs/CleanupDeviceLogsJobTest.php
Normal file
44
tests/Feature/Jobs/CleanupDeviceLogsJobTest.php
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Jobs\CleanupDeviceLogsJob;
|
||||||
|
use App\Models\Device;
|
||||||
|
use App\Models\DeviceLog;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
test('it keeps only the 50 most recent logs per device', function () {
|
||||||
|
// Create two devices
|
||||||
|
$device1 = Device::factory()->create();
|
||||||
|
$device2 = Device::factory()->create();
|
||||||
|
|
||||||
|
// Create 60 logs for each device with different timestamps
|
||||||
|
for ($i = 0; $i < 60; $i++) {
|
||||||
|
DeviceLog::factory()->create([
|
||||||
|
'device_id' => $device1->id,
|
||||||
|
'device_timestamp' => now()->subMinutes($i),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DeviceLog::factory()->create([
|
||||||
|
'device_id' => $device2->id,
|
||||||
|
'device_timestamp' => now()->subMinutes($i),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the cleanup job
|
||||||
|
CleanupDeviceLogsJob::dispatchSync();
|
||||||
|
|
||||||
|
// Assert each device has exactly 50 logs
|
||||||
|
expect($device1->logs()->count())->toBe(50)
|
||||||
|
->and($device2->logs()->count())->toBe(50);
|
||||||
|
|
||||||
|
// Assert the remaining logs are the most recent ones
|
||||||
|
$device1Logs = $device1->logs()->orderByDesc('device_timestamp')->get();
|
||||||
|
$device2Logs = $device2->logs()->orderByDesc('device_timestamp')->get();
|
||||||
|
|
||||||
|
// Check that the timestamps are in descending order
|
||||||
|
for ($i = 0; $i < 49; $i++) {
|
||||||
|
expect($device1Logs[$i]->device_timestamp->gt($device1Logs[$i + 1]->device_timestamp))->toBeTrue()
|
||||||
|
->and($device2Logs[$i]->device_timestamp->gt($device2Logs[$i + 1]->device_timestamp))->toBeTrue();
|
||||||
|
}
|
||||||
|
});
|
||||||
66
tests/Unit/Models/DeviceLogTest.php
Normal file
66
tests/Unit/Models/DeviceLogTest.php
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Device;
|
||||||
|
use App\Models\DeviceLog;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
test('device log belongs to a device', function () {
|
||||||
|
$device = Device::factory()->create();
|
||||||
|
$log = DeviceLog::factory()->create(['device_id' => $device->id]);
|
||||||
|
|
||||||
|
expect($log->device)->toBeInstanceOf(Device::class)
|
||||||
|
->and($log->device->id)->toBe($device->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('device log casts log_entry to array', function () {
|
||||||
|
Device::factory()->create();
|
||||||
|
$log = DeviceLog::factory()->create([
|
||||||
|
'log_entry' => [
|
||||||
|
'message' => 'test message',
|
||||||
|
'level' => 'info',
|
||||||
|
'timestamp' => time()
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($log->log_entry)->toBeArray()
|
||||||
|
->and($log->log_entry['message'])->toBe('test message')
|
||||||
|
->and($log->log_entry['level'])->toBe('info');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('device log casts device_timestamp to datetime', function () {
|
||||||
|
Device::factory()->create();
|
||||||
|
$timestamp = now();
|
||||||
|
$log = DeviceLog::factory()->create([
|
||||||
|
'device_timestamp' => $timestamp
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($log->device_timestamp)->toBeInstanceOf(\Carbon\Carbon::class)
|
||||||
|
->and($log->device_timestamp->timestamp)->toBe($timestamp->timestamp);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('device log factory creates valid data', function () {
|
||||||
|
Device::factory()->create();
|
||||||
|
$log = DeviceLog::factory()->create();
|
||||||
|
|
||||||
|
expect($log->device_id)->toBeInt()
|
||||||
|
->and($log->device_timestamp)->toBeInstanceOf(\Carbon\Carbon::class)
|
||||||
|
->and($log->log_entry)->toBeArray()
|
||||||
|
->and($log->log_entry)->toHaveKeys(['creation_timestamp', 'device_status_stamp', 'log_id', 'log_message', 'log_codeline', 'log_sourcefile', 'additional_info']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('device log can be created with minimal required fields', function () {
|
||||||
|
$device = Device::factory()->create();
|
||||||
|
$log = DeviceLog::create([
|
||||||
|
'device_id' => $device->id,
|
||||||
|
'device_timestamp' => now(),
|
||||||
|
'log_entry' => [
|
||||||
|
'message' => 'test message'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($log->exists)->toBeTrue()
|
||||||
|
->and($log->device_id)->toBe($device->id)
|
||||||
|
->and($log->log_entry['message'])->toBe('test message');
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue