diff --git a/LibreNMS/Component.php b/LibreNMS/Component.php index a9705ea6fb..7975e83276 100644 --- a/LibreNMS/Component.php +++ b/LibreNMS/Component.php @@ -25,6 +25,11 @@ namespace LibreNMS; +use App\Models\ComponentPref; +use App\Models\ComponentStatusLog; +use Illuminate\Support\Arr; +use Log; + class Component { /* @@ -32,45 +37,32 @@ class Component * so that they can be modified but they can not be set as user attributes. We * also set their default values. */ - private $reserved = array( - 'type' => '', - 'label' => '', - 'status' => 0, - 'ignore' => 0, - 'disabled' => 0, - 'error' => '', - ); + private $reserved = [ + 'type' => '', + 'label' => '', + 'status' => 0, + 'ignore' => 0, + 'disabled' => 0, + 'error' => '', + ]; public function getComponentCount($device_id = null) { - if (is_null($device_id)) { - // SELECT type, count(*) as count FROM component GROUP BY type - $SQL = "SELECT `type` as `name`, count(*) as count FROM `component` GROUP BY `type`"; - $rows = dbFetchRows($SQL, array()); - } else { - $SQL = "SELECT `type` as `name`, count(*) as count FROM `component` WHERE `device_id` = ? GROUP BY `type`"; - $rows = dbFetchRows($SQL, array($device_id)); - } + $counts = \App\Models\Component::query()->when($device_id, function ($query, $device_id) { + $query->where('device_id', $device_id); + })->selectRaw('type, count(*) as count')->groupBy('type')->pluck('count', 'type'); - if (isset($rows)) { - // We found some, lets re-process to make more accessible - $result = array(); - foreach ($rows as $value) { - $result[$value['name']] = $value['count']; - } - return $result; - } - // We didn't find any components - return false; + return $counts->isEmpty() ? false : $counts->all(); } + public function getComponentType($TYPE = null) { if (is_null($TYPE)) { $SQL = "SELECT DISTINCT `type` as `name` FROM `component` ORDER BY `name`"; - $row = dbFetchRow($SQL, array()); + $row = dbFetchRow($SQL, []); } else { $SQL = "SELECT DISTINCT `type` as `name` FROM `component` WHERE `type` = ? ORDER BY `name`"; - $row = dbFetchRow($SQL, array($TYPE)); + $row = dbFetchRow($SQL, [$TYPE]); } if (!isset($row)) { @@ -82,109 +74,56 @@ class Component } } - public function getComponents($device_id = null, $options = array()) + public function getComponents($device_id = null, $options = []) { - // Define our results array, this will be set even if no rows are returned. - $RESULT = array(); - $PARAM = array(); - - // Our base SQL Query, with no options. - $SQL = "SELECT `C`.`id`,`C`.`device_id`,`C`.`type`,`C`.`label`,`C`.`status`,`C`.`disabled`,`C`.`ignore`,`C`.`error`,`CP`.`attribute`,`CP`.`value` FROM `component` as `C` LEFT JOIN `component_prefs` as `CP` on `C`.`id`=`CP`.`component` WHERE "; + $query = \App\Models\Component::query() + ->with('prefs'); // Device_id is shorthand for filter C.device_id = $device_id. if (!is_null($device_id)) { - $options['filter']['device_id'] = array('=', $device_id); + $options['filter']['device_id'] = ['=', $device_id]; } // Type is shorthand for filter type = $type. if (isset($options['type'])) { - $options['filter']['type'] = array('=', $options['type']); + $options['filter']['type'] = ['=', $options['type']]; } + $validFields = ['device_id', 'type', 'id', 'label', 'status', 'disabled', 'ignore', 'error']; + // filter field => array(operator,value) // Filters results based on the field, operator and value - $COUNT = 0; - if (isset($options['filter'])) { - $COUNT++; - $validFields = array('device_id','type','id','label','status','disabled','ignore','error'); - $SQL .= " ( "; - foreach ($options['filter'] as $field => $array) { - // Only add valid fields to the query - if (in_array($field, $validFields)) { - if ($array[0] == 'LIKE') { - $SQL .= "`C`.`".$field."` LIKE ? AND "; - $array[1] = "%".$array[1]."%"; - } else { - // Equals operator is the default - $SQL .= "`C`.`".$field."` = ? AND "; - } - array_push($PARAM, $array[1]); - } - } - // Strip the last " AND " before closing the bracket. - $SQL = substr($SQL, 0, -5)." )"; - } - - if ($COUNT == 0) { - // Strip the " WHERE " that we didn't use. - $SQL = substr($SQL, 0, -7); + foreach (array_intersect_key($options['filter'], array_flip($validFields)) as $field => $filter) { + $op = $filter[0]; + $value = $op == 'LIKE' ? "%{$filter[1]}%" : $filter[1]; + $query->where($field, $op, $value); } // sort column direction // Add SQL sorting to the results if (isset($options['sort'])) { - $SQL .= " ORDER BY ".$options['sort']; - } - - // Get our component records using our built SQL. - $COMPONENTS = dbFetchRows($SQL, $PARAM); - - // if we have no components we need to return nothing - if (count($COMPONENTS) == 0) { - return $RESULT; - } - - // Add the AVP's to the array. - foreach ($COMPONENTS as $COMPONENT) { - if ($COMPONENT['attribute'] != "") { - // if this component has attributes, set them in the array. - $RESULT[$COMPONENT['device_id']][$COMPONENT['id']][$COMPONENT['attribute']] = $COMPONENT['value']; - } - } - - // Populate our reserved fields into the Array, these cant be used as user attributes. - foreach ($COMPONENTS as $COMPONENT) { - $component_device_id = (int)$COMPONENT['device_id']; - foreach ($this->reserved as $k => $v) { - $RESULT[$component_device_id][$COMPONENT['id']][$k] = $COMPONENT[$k]; - } + [$column, $direction] = explode(' ', $options['sort']); + $query->orderBy($column, $direction); } // limit array(start,count) if (isset($options['limit'])) { - $TEMP = array(); - $COUNT = 0; - // k = device_id, v = array of components for that device_id - foreach ($RESULT as $k => $v) { - // k1 = component id, v1 = component array - foreach ($v as $k1 => $v1) { - if (($COUNT >= $options['limit'][0]) && ($COUNT < $options['limit'][0]+$options['limit'][1])) { - $TEMP[$k][$k1] = $v1; - } - // We are counting components. - $COUNT++; - } - } - $RESULT = $TEMP; + $query->offset($options['limit'][0])->limit($options['limit'][1]); } - return $RESULT; + // get and format results as expected by receivers + return $query->get()->groupBy('device_id')->map(function ($group) { + return $group->keyBy('id')->map(function ($component) { + return $component->prefs->pluck('value', 'attribute') + ->merge($component->only(array_keys($this->reserved))); + }); + })->toArray(); } public function getComponentStatus($device = null) { $sql_query = "SELECT status, count(status) as count FROM component WHERE"; - $sql_param = array(); + $sql_param = []; $add = 0; if (!is_null($device)) { @@ -196,22 +135,22 @@ class Component if ($add == 0) { // No filters, remove " WHERE" -6 - $sql_query = substr($sql_query, 0, strlen($sql_query)-6); + $sql_query = substr($sql_query, 0, strlen($sql_query) - 6); } $sql_query .= " GROUP BY status"; - d_echo("SQL Query: ".$sql_query); + d_echo("SQL Query: " . $sql_query); // $service is not null, get only what we want. $result = dbFetchRows($sql_query, $sql_param); // Set our defaults to 0 - $count = array(0 => 0, 1 => 0, 2 => 0); + $count = [0 => 0, 1 => 0, 2 => 0]; // Rebuild the array in a more convenient method foreach ($result as $v) { $count[$v['status']] = $v['count']; } - d_echo("Component Count by Status: ".print_r($count, true)."\n"); + d_echo("Component Count by Status: " . print_r($count, true) . "\n"); return $count; } @@ -219,16 +158,16 @@ class Component { if (($component_id == null) || ($start == null) || ($end == null)) { // Error... - d_echo("Required arguments are missing. Component ID: ".$component_id.", Start: ".$start.", End: ".$end."\n"); + d_echo("Required arguments are missing. Component ID: " . $component_id . ", Start: " . $start . ", End: " . $end . "\n"); return false; } // Create our return array. - $return = array(); + $return = []; // 1. find the previous value, this is the value when $start occurred. $sql_query = "SELECT status FROM `component_statuslog` WHERE `component_id` = ? AND `timestamp` < ? ORDER BY `id` desc LIMIT 1"; - $sql_param = array($component_id, $start); + $sql_param = [$component_id, $start]; $result = dbFetchRow($sql_query, $sql_param); if ($result == false) { $return['initial'] = false; @@ -238,136 +177,96 @@ class Component // 2. Then we just need a list of all the entries for the time period. $sql_query = "SELECT status, `timestamp`, message FROM `component_statuslog` WHERE `component_id` = ? AND `timestamp` >= ? AND `timestamp` < ? ORDER BY `timestamp`"; - $sql_param = array($component_id, $start,$end); + $sql_param = [$component_id, $start, $end]; $return['data'] = dbFetchRows($sql_query, $sql_param); - d_echo("Status Log Data: ".print_r($return, true)."\n"); + d_echo("Status Log Data: " . print_r($return, true) . "\n"); return $return; } - public function createComponent($device_id, $TYPE) + public function createComponent($device_id, $type) { - // Prepare our default values to be inserted. - $DATA = $this->reserved; - - // Add the device_id and type - $DATA['device_id'] = $device_id; - $DATA['type'] = $TYPE; - - // Insert a new component into the database. - $id = dbInsert($DATA, 'component'); + $component = \App\Models\Component::create(['device_id' => $device_id, 'type' => $type]); // Add a default status log entry - we always start ok. - $this->createStatusLogEntry($id, 0, 'Component Created'); + $this->createStatusLogEntry($component->id, 0, 'Component Created'); // Create a default component array based on what was inserted. - $ARRAY = array(); - $ARRAY[$id] = $DATA; - unset($ARRAY[$id]['device_id']); // This doesn't belong here. - return $ARRAY; + return [$component->id => $component->only(array_keys($this->reserved))]; } public function createStatusLogEntry($component_id, $status, $message) { - // Add an entry to the statuslog table for a particular component. - $DATA = array( - 'component_id' => $component_id, - 'status' => $status, - 'message' => $message, - ); + try { + return ComponentStatusLog::create(['component_id' => $component_id, 'status' => $status, 'message' => $message])->id; + } catch (\Exception $e) { + Log::debug("Failed to create component status log"); + } - return dbInsert($DATA, 'component_statuslog'); + return 0; } public function deleteComponent($id) { // Delete a component from the database. - return dbDelete('component', "`id` = ?", array($id)); + return \App\Models\Component::destroy($id); } - public function setComponentPrefs($device_id, $ARRAY) + public function setComponentPrefs($device_id, $updated) { - // Compare the arrays. Update/Insert where necessary. + $updated = Arr::wrap($updated); + \App\Models\Component::whereIn('id', array_keys($updated)) + ->with('prefs') + ->get() + ->each(function (\App\Models\Component $component) use ($updated) { + unset($updated[$component->id]['type']); // can't change type - $OLD = $this->getComponents($device_id); - // Loop over each component. - foreach ((array)$ARRAY as $COMPONENT => $AVP) { - // Make sure the component already exists. - if (!isset($OLD[$device_id][$COMPONENT])) { - // Error. Component doesn't exist in the database. - continue; - } + // update component attributes + $component->fill($updated[$component->id]); + if ($component->isDirty()) { + // Log the update to the Eventlog. + $message = "Component $component->id has been modified: "; + $message .= collect($component->getDirty())->map(function ($value, $key) { + return "$key => $value"; + })->implode(','); - // Ignore type, we cant change that. - unset($AVP['type'], $OLD[$device_id][$COMPONENT]['type']); - - // If the Status has changed we need to add a log entry - if ($AVP['status'] != $OLD[$device_id][$COMPONENT]['status']) { - d_echo("Status Changed - Old: ".$OLD[$device_id][$COMPONENT]['status'].", New: ".$AVP['status']."\n"); - $this->createStatusLogEntry($COMPONENT['id'], $AVP['status'], $AVP['error']); - } - - // Process our reserved components first. - $UPDATE = array(); - foreach ($this->reserved as $k => $v) { - // does the reserved field exist, if not skip. - if (isset($AVP[$k])) { - // Has the value changed? - if ($AVP[$k] != $OLD[$device_id][$COMPONENT][$k]) { - // The value has been modified, add it to our update array. - $UPDATE[$k] = $AVP[$k]; + // If the Status has changed we need to add a log entry + if ($component->isDirty('status')) { + Log::debug("Status Changed - Old: " . $component->getOriginal('status') . ", New: $component->status\n"); + $this->createStatusLogEntry($component->id, $component->status, $component->error); } + $component->save(); - // Unset the reserved field. We don't want to insert it below. - unset($AVP[$k], $OLD[$device_id][$COMPONENT][$k]); + Log::event($message, $component->device_id, 'component', 3, $component->id); } - } - // Has anything changed, do we need to update? - if (count($UPDATE) > 0) { - // We have data to update - dbUpdate($UPDATE, 'component', '`id` = ?', array($COMPONENT)); + // update preferences + $prefs = collect($updated[$component->id])->filter(function ($value, $attr) { + return !array_key_exists($attr, $this->reserved); + }); - // Log the update to the Eventlog. - $MSG = "Component ".$COMPONENT." has been modified: "; - foreach ($UPDATE as $k => $v) { - $MSG .= $k." => ".$v.","; + $invalid = $component->prefs->keyBy('id'); + + foreach ($prefs as $attribute => $value) { + $existing = $component->prefs->firstWhere('attribute', $attribute); + if ($existing) { + $invalid->forget($existing->id); + $existing->fill(['value' => $value]); + if ($existing->isDirty()) { + Log::event("Component: $component->type($component->id). Attribute: $attribute, was modified from: " . $existing->getOriginal('value') . ", to: $value", $device_id, 'component', 3, $component_id); + $existing->save(); + } + } else { + $component->prefs()->save(new ComponentPref(['attribute' => $attribute, 'value' => $value])); + Log::event("Component: $component->type($component->id). Attribute: $attribute, was added with value: $value", $component->device_id, 'component', 3, $component->id); + } } - $MSG = substr($MSG, 0, -1); - log_event($MSG, $device_id, 'component', 3, $COMPONENT); - } - // Process our AVP Adds and Updates - foreach ($AVP as $ATTR => $VALUE) { - // We have our AVP, lets see if we need to do anything with it. - - if (!isset($OLD[$device_id][$COMPONENT][$ATTR])) { - // We have a newly added attribute, need to insert into the DB - $DATA = array('component'=>$COMPONENT, 'attribute'=>$ATTR, 'value'=>$VALUE); - dbInsert($DATA, 'component_prefs'); - - // Log the addition to the Eventlog. - log_event("Component: " . $AVP[$COMPONENT]['type'] . "(" . $COMPONENT . "). Attribute: " . $ATTR . ", was added with value: " . $VALUE, $device_id, 'component', 3, $COMPONENT); - } elseif ($OLD[$device_id][$COMPONENT][$ATTR] != $VALUE) { - // Attribute exists but the value is different, need to update - $DATA = array('value'=>$VALUE); - dbUpdate($DATA, 'component_prefs', '`component` = ? AND `attribute` = ?', array($COMPONENT, $ATTR)); - - // Add the modification to the Eventlog. - log_event("Component: " . $AVP[$COMPONENT]['type'] . "(" . $COMPONENT . "). Attribute: " . $ATTR . ", was modified from: " . $OLD[$device_id][$COMPONENT][$ATTR] . ", to: " . $VALUE, $device_id, 'component', 3, $COMPONENT); + foreach ($invalid as $pref) { + $pref->delete(); + Log::event("Component: $component->type($component->id). Attribute: $pref->attribute, was deleted.", $component->device_id, 'component', 4); } - } // End Foreach AVP - - // Process our Deletes. - $DELETE = array_diff_key($OLD[$device_id][$COMPONENT], $AVP); - foreach ($DELETE as $KEY => $VALUE) { - // As the Attribute has been removed from the array, we should remove it from the database. - dbDelete('component_prefs', "`component` = ? AND `attribute` = ?", array($COMPONENT,$KEY)); - - // Log the addition to the Eventlog. - log_event("Component: " . $AVP[$COMPONENT]['type'] . "(" . $COMPONENT . "). Attribute: " . $KEY . ", was deleted.", 4, $COMPONENT); - } - } + }); return true; } diff --git a/app/Models/Component.php b/app/Models/Component.php index 9bf9e9873d..2748e967a7 100644 --- a/app/Models/Component.php +++ b/app/Models/Component.php @@ -29,4 +29,34 @@ class Component extends DeviceRelatedModel { public $timestamps = false; protected $table = 'component'; + protected $fillable = ['device_id', 'type', 'label', 'status', 'disabled', 'ignore', 'error']; + + // ---- Accessors/Mutators ---- + + public function setStatusAttribute($status) + { + $this->attributes['status'] = (int)$status; + } + + public function setDisabledAttribute($disabled) + { + $this->attributes['disabled'] = (int)$disabled; + } + + public function setIgnoreAttribute($ignore) + { + $this->attributes['ignore'] = (int)$ignore; + } + + // ---- Define Relationships ---- + + public function logs() + { + return $this->hasMany(\App\Models\ComponentStatusLog::class, 'component_id', 'id'); + } + + public function prefs() + { + return $this->hasMany(\App\Models\ComponentPref::class, 'component', 'id'); + } } diff --git a/app/Models/ComponentPref.php b/app/Models/ComponentPref.php new file mode 100644 index 0000000000..512993a802 --- /dev/null +++ b/app/Models/ComponentPref.php @@ -0,0 +1,16 @@ +attributes['value'] = is_array($value) ? json_encode($value) : (string)$value; + } +} diff --git a/app/Models/ComponentStatusLog.php b/app/Models/ComponentStatusLog.php new file mode 100644 index 0000000000..abac88fd1c --- /dev/null +++ b/app/Models/ComponentStatusLog.php @@ -0,0 +1,19 @@ +attributes['status'] = (int)$status; + } +} diff --git a/app/Models/DeviceAttrib.php b/app/Models/DeviceAttrib.php index 99feed448b..72eaa924ed 100644 --- a/app/Models/DeviceAttrib.php +++ b/app/Models/DeviceAttrib.php @@ -31,4 +31,5 @@ class DeviceAttrib extends DeviceRelatedModel protected $primaryKey = 'attrib_id'; public $timestamps = false; protected $fillable = ['attrib_type', 'attrib_value']; +// protected $casts = ['attrib_value' => 'array']; } diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index fb3a4aae64..3ab6dd8c19 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -131,3 +131,10 @@ $factory->define(\App\Models\Vminfo::class, function (Faker\Generator $faker) { 'vmwVmState' => $faker->randomElement(['powered on', 'powered off', 'suspended']), ]; }); + +$factory->define(\App\Models\Component::class, function (Faker\Generator $faker) { + return [ + 'device_id' => $faker->randomDigit, + 'type' => $faker->regexify('[A-Za-z0-9]{4,20}'), + ]; +}); diff --git a/tests/Unit/ComponentTest.php b/tests/Unit/ComponentTest.php new file mode 100644 index 0000000000..2b4a7bf363 --- /dev/null +++ b/tests/Unit/ComponentTest.php @@ -0,0 +1,208 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2020 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Tests\Unit; + +use App\Models\ComponentPref; +use App\Models\ComponentStatusLog; +use Illuminate\Foundation\Testing\DatabaseTransactions; +use Illuminate\Support\Str; +use LibreNMS\Component; +use LibreNMS\Tests\DBTestCase; + +class ComponentTest extends DBTestCase +{ + use DatabaseTransactions; + + public function testDeleteComponent() + { + $target = factory(\App\Models\Component::class)->create(); + + $this->assertTrue(\App\Models\Component::where('id', $target->id)->exists(), 'Failed to create component, this shouldn\'t happen'); + + $component = new Component(); + $component->deleteComponent($target->id); + + $this->assertFalse(\App\Models\Component::where('id', $target->id)->exists(), 'deleteComponent failed to delete the component.'); + } + + public function testGetComponentsEmpty() + { + $this->assertEquals([], (new Component())->getComponents(43)); + } + + public function testGetComponentsOptionsType() + { + $target = factory(\App\Models\Component::class)->create(); + $component = new Component(); + + $actual = $component->getComponents($target->device_id, ['type' => $target->type]); + + $this->assertCount(1, $actual); + $this->assertArrayHasKey($target->device_id, $actual); + $this->assertEquals($this->buildExpected($target), $actual); + } + + public function testGetComponentsOptionsFilterNotIgnore() + { + factory(\App\Models\Component::class)->create(['device_id' => 1, 'ignore' => 1]); + $target = factory(\App\Models\Component::class)->times(2)->create(['device_id' => 1, 'ignore' => 0]); + $component = new Component(); + + $actual = $component->getComponents(1, ['filter' => ['ignore' => ['=', 0]]]); + + $this->assertEquals($this->buildExpected($target), $actual); + } + + public function testGetComponentsOptionsComplex() + { + factory(\App\Models\Component::class)->create(['label' => 'Search Phrase']); + factory(\App\Models\Component::class)->times(2)->create(['label' => 'Something Else']); + $target = factory(\App\Models\Component::class)->times(2)->create(['label' => 'Search Phrase']); + factory(\App\Models\Component::class)->create(['label' => 'Search Phrase']); + $component = new Component(); + + $options = [ + 'filter' => ['label' => ['like', 'Search Phrase']], + 'sort' => 'id desc', + 'limit' => [1, 2], + ]; + $actual = $component->getComponents(null, $options); + + $this->assertEquals($this->buildExpected($target->reverse()->values()), $actual); + } + + public function testGetFirstComponentID() + { + $input = [ + 1 => [37 => [], 14 => []], + 2 => [19 => []], + ]; + $component = new Component(); + $this->assertEquals(19, $component->getFirstComponentID($input, 2)); + $this->assertEquals(37, $component->getFirstComponentID($input[1])); + } + + public function testGetComponentCount() + { + factory(\App\Models\Component::class)->times(2)->create(['device_id' => 1, 'type' => 'three']); + factory(\App\Models\Component::class)->create(['device_id' => 2, 'type' => 'three']); + factory(\App\Models\Component::class)->create(['device_id' => 2, 'type' => 'one']); + + $component = new Component(); + $this->assertEquals(['three' => 3, 'one' => 1], $component->getComponentCount()); + $this->assertEquals(['three' => 2], $component->getComponentCount(1)); + $this->assertEquals(['three' => 1, 'one' => 1], $component->getComponentCount(2)); + } + + public function testSetComponentPrefs() + { + // Nightmare function, no where near exhaustive + $base = factory(\App\Models\Component::class)->create(); + $component = new Component(); + + \Log::shouldReceive('event')->withArgs(["Component: $base->type($base->id). Attribute: null_val, was added with value: ", $base->device_id, 'component', 3, $base->id]); + $nullVal = $this->buildExpected($base)[$base->device_id]; + $nullVal[$base->id]['null_val'] = null; + $component->setComponentPrefs($base->device_id, $nullVal); + $this->assertEquals('', ComponentPref::where(['component' => $base->id, 'attribute' => 'null_val'])->first()->value); + + \Log::shouldReceive('event')->withArgs(["Component $base->id has been modified: label => new label", $base->device_id, 'component', 3, $base->id]); + \Log::shouldReceive('event')->withArgs(["Component: $base->type($base->id). Attribute: thirty, was added with value: 30", $base->device_id, 'component', 3, $base->id]); + \Log::shouldReceive('event')->withArgs(["Component: $base->type($base->id). Attribute: json, was added with value: {\"json\":\"array\"}", $base->device_id, 'component', 3, $base->id]); + \Log::shouldReceive('event')->withArgs(["Component: $base->type($base->id). Attribute: null_val, was deleted.", $base->device_id, 'component', 4]); + $multiple = $this->buildExpected($base)[$base->device_id]; + $multiple[$base->id]['label'] = 'new label'; + $multiple[$base->id]['thirty'] = 30; + $multiple[$base->id]['json'] = json_encode(['json' => 'array']); + $component->setComponentPrefs($base->device_id, $multiple); + + \Log::shouldReceive('event')->withArgs(["Component $base->id has been modified: label => ", $base->device_id, 'component', 3, $base->id]); + \Log::shouldReceive('event')->withArgs(["Component: $base->type($base->id). Attribute: thirty, was deleted.", $base->device_id, 'component', 4]); + \Log::shouldReceive('event')->withArgs(["Component: $base->type($base->id). Attribute: json, was deleted.", $base->device_id, 'component', 4]); + $uc = \App\Models\Component::find($base->id); + $this->assertEquals('new label', $uc->label); + $this->assertEquals(30, $uc->prefs->where('attribute', 'thirty')->first()->value); + $this->assertEquals($multiple[$base->id]['json'], $uc->prefs->where('attribute', 'json')->first()->value); + + \Log::shouldReceive('event')->times(0); + $remove = $this->buildExpected($base)[$base->device_id]; + $component->setComponentPrefs($base->device_id, $remove); + $this->assertFalse(ComponentPref::where('component', $base->id)->exists()); + } + + public function testCreateComponent() + { + $device_id = rand(1, 32); + $type = Str::random(9); + $component = (new Component())->createComponent($device_id, $type); + + $this->assertCount(1, $component); + $component_id = array_key_first($component); + + $expected = ['type' => $type, 'label' => '', 'status' => 0, 'ignore' => 0, 'disabled' => 0, 'error' => '']; + + $this->assertEquals([$component_id => $expected], $component); + + $fromDb = \App\Models\Component::find($component_id); + $this->assertEquals($device_id, $fromDb->device_id); + $this->assertEquals($type, $fromDb->type); + + $log = $fromDb->logs->first(); + $this->assertEquals($log->status, 0); + $this->assertEquals($log->message, 'Component Created'); + } + + public function testGetComponentStatusLog() + { + // invalid id fails + $component = new Component(); + + $this->assertEquals(0, $component->createStatusLogEntry(434242, 0, 'failed'), 'incorrectly added log'); + + $message = Str::random(8); + $model = factory(\App\Models\Component::class)->create(); + $log_id = $component->createStatusLogEntry($model->id, 1, $message); + $this->assertNotEquals(0, $log_id, ' failed to create log'); + + $log = ComponentStatusLog::find($log_id); + $this->assertEquals(1, $log->status); + $this->assertEquals($message, $log->message); + } + + private function buildExpected($target) + { + $collection = $target instanceof \App\Models\Component ? collect([$target]) : $target; + return $collection->groupBy('device_id')->map(function ($group) { + return $group->keyBy('id')->map(function ($model) { + $base = ['type' => null, 'label' => null, 'status' => 0, 'ignore' => 0, 'disabled' => 0, 'error' => null]; + $merge = $model->toArray(); + unset($merge['device_id'], $merge['id']); + + return array_merge($base, $merge); + }); + })->toArray(); + } +}