From f5ef1deac05831e72dab932daedce48f0df6c4f6 Mon Sep 17 00:00:00 2001 From: Brian French Date: Tue, 28 Feb 2023 00:06:38 -0500 Subject: [PATCH] Adding more utilities and testing. --- composer.json | 1 + .../00000000000001_InitialMigration.php | 50 +++ src/Exception/Exception.php | 19 - src/Exception/InvalidCharException.php | 4 +- src/Exception/MissingMethodException.php | 23 + src/Exception/UtilitieException.php | 25 ++ src/Lib/Memory.php | 86 ++++ src/Model/Behavior/MemoryBehavior.php | 67 +++ src/Model/Behavior/SluggableBehavior.php | 125 ++++++ src/Model/Entity/Book.php | 22 + src/Model/Entity/Course.php | 26 ++ src/Model/Entity/CoursesStudent.php | 24 ++ src/Model/Entity/Student.php | 22 + src/Model/Table/ApplySettingsTrait.php | 106 +++++ src/Model/Table/BooksTable.php | 38 ++ src/Model/Table/CheckAddTrait.php | 406 ++++++++++++++++++ src/Model/Table/CoursesStudentsTable.php | 43 ++ src/Model/Table/CoursesTable.php | 88 ++++ src/Model/Table/MergeDeleteTrait.php | 149 +++++++ src/Model/Table/StudentsTable.php | 54 +++ src/Model/Table/ToggleTrait.php | 39 ++ src/Plugin.php | 3 +- src/View/Helper/VersionsHelper.php | 2 +- test | Bin 0 -> 28672 bytes tests/Fixture/BooksFixture.php | 30 ++ tests/Fixture/CoursesFixture.php | 26 ++ tests/Fixture/CoursesStudentsFixture.php | 37 ++ tests/Fixture/StudentsFixture.php | 26 ++ tests/TestCase/Lib/CommonNetworkTest.php | 2 +- .../TestCase/Model/Table/CoursesTableTest.php | 383 +++++++++++++++++ .../Model/Table/StudentsTableTest.php | 308 +++++++++++++ tests/bootstrap.php | 16 +- tests/test.db | Bin 0 -> 32768 bytes 33 files changed, 2219 insertions(+), 31 deletions(-) create mode 100644 config/Migrations/00000000000001_InitialMigration.php delete mode 100644 src/Exception/Exception.php create mode 100644 src/Exception/MissingMethodException.php create mode 100644 src/Exception/UtilitieException.php create mode 100644 src/Lib/Memory.php create mode 100644 src/Model/Behavior/MemoryBehavior.php create mode 100644 src/Model/Behavior/SluggableBehavior.php create mode 100644 src/Model/Entity/Book.php create mode 100644 src/Model/Entity/Course.php create mode 100644 src/Model/Entity/CoursesStudent.php create mode 100644 src/Model/Entity/Student.php create mode 100644 src/Model/Table/ApplySettingsTrait.php create mode 100644 src/Model/Table/BooksTable.php create mode 100644 src/Model/Table/CheckAddTrait.php create mode 100644 src/Model/Table/CoursesStudentsTable.php create mode 100644 src/Model/Table/CoursesTable.php create mode 100644 src/Model/Table/MergeDeleteTrait.php create mode 100644 src/Model/Table/StudentsTable.php create mode 100644 src/Model/Table/ToggleTrait.php create mode 100644 test create mode 100644 tests/Fixture/BooksFixture.php create mode 100644 tests/Fixture/CoursesFixture.php create mode 100644 tests/Fixture/CoursesStudentsFixture.php create mode 100644 tests/Fixture/StudentsFixture.php create mode 100644 tests/TestCase/Model/Table/CoursesTableTest.php create mode 100644 tests/TestCase/Model/Table/StudentsTableTest.php create mode 100644 tests/test.db diff --git a/composer.json b/composer.json index bc1ece0..c75ce41 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "fr3nch13/composer-lock-parser": "~1.0" }, "require-dev": { + "cakephp/migrations": "^3.7", "fr3nch13/cakephp-pta": "dev-2.x-dev" }, "autoload": { diff --git a/config/Migrations/00000000000001_InitialMigration.php b/config/Migrations/00000000000001_InitialMigration.php new file mode 100644 index 0000000..b040f15 --- /dev/null +++ b/config/Migrations/00000000000001_InitialMigration.php @@ -0,0 +1,50 @@ +table('books'); + $table->addColumn('name', 'string', ['length' => '255']) + ->addColumn('slug', 'string', ['length' => '255', 'null' => true]) + ->addColumn('student_id', 'integer', ['default' => null, 'null' => true]) + ->create(); + + $table = $this->table('courses'); + $table->addColumn('name', 'string', ['length' => '255']) + ->addColumn('name_other', 'string', ['length' => '255', 'null' => true]) + ->addColumn('slug', 'string', ['length' => '255', 'null' => true]) + ->addColumn('slug_other', 'string', ['length' => '255', 'null' => true]) + ->addColumn('updateme', 'string', ['length' => '255', 'null' => true]) + ->addColumn('available', 'boolean', ['default' => 1]) + ->addColumn('teachers_pet_id', 'integer', ['default' => null, 'null' => true]) + ->create(); + + $table = $this->table('students'); + $table->addColumn('name', 'string', ['length' => '255']) + ->addColumn('slug', 'string', ['length' => '255', 'null' => true]) + ->addColumn('updateme', 'string', ['length' => '255', 'null' => true]) + ->create(); + + $table = $this->table('courses_students'); + $table->addColumn('course_id', 'integer') + ->addColumn('student_id', 'integer') + ->addColumn('grade', 'integer') + ->create(); + } +} diff --git a/src/Exception/Exception.php b/src/Exception/Exception.php deleted file mode 100644 index 029d67b..0000000 --- a/src/Exception/Exception.php +++ /dev/null @@ -1,19 +0,0 @@ - + */ + protected $_defaultConfig = []; + + /** + * Trackes the start times. + * Format is ['(time_key)' => (unix timestamp)]. + * + * @var array + */ + protected $_startTimes = []; + + /** + * Trackes the end times. + * Format is ['(time_key)' => (unix timestamp)]. + * + * @var array + */ + protected $_endTimes = []; + + /** + * Tracks the highest recorded memory usage from when memoryUsage was called. + * + * @var float + */ + protected $memUsageHighest = 0; + + /** + * Reports the memory usage at the time it is called. + * + * @param bool $nice If we should return the bytes (false), of the calculated amount in a nice format (true). + * @param float|null $mem_usage The memory number to be made nice. + * @return string the memory usage stat. + */ + public function usage(bool $nice = true, ?float $mem_usage = null): string + { + if (!$mem_usage) { + $mem_usage = memory_get_usage(); + } + // track the highest usage. + if ($this->memUsageHighest < $mem_usage) { + $this->memUsageHighest = $mem_usage; + } + if ($nice) { + if ($mem_usage < 1024) { + $mem_usage = $mem_usage . ' B'; + } elseif ($mem_usage < 1048576) { + $mem_usage = round($mem_usage / 1024, 2) . ' KB'; + } elseif ($mem_usage < 1073741824) { + $mem_usage = round($mem_usage / 1048576, 2) . ' MB'; + } else { + $mem_usage = round($mem_usage / 1073741824, 2) . ' GB'; + } + } + + return strval($mem_usage); + } + + /** + * Reports the highest memory usage. + * + * @param bool $nice If we should return the bytes (false), of the calculated amount in a nice format (true). + * @return string the highest memory usage stat. + */ + public function usageHighest($nice = true): string + { + return $this->usage($nice, $this->memUsageHighest); + } +} diff --git a/src/Model/Behavior/MemoryBehavior.php b/src/Model/Behavior/MemoryBehavior.php new file mode 100644 index 0000000..2401cf3 --- /dev/null +++ b/src/Model/Behavior/MemoryBehavior.php @@ -0,0 +1,67 @@ +ensureMemory(); + + return $this->Memory->usage($nice, $mem_usage); + } + + /** + * Reports the highest memory usage. + * + * @param bool $nice If we should return the bytes (false), of the calculated amount in a nice format (true). + * @return string the highest memory usage stat. + */ + public function memoryUsageHighest($nice = true): string + { + $this->ensureMemory(); + + return $this->Memory->usageHighest($nice); + } + + /** + * Makes sure there is a Memory object created. + * + * @return void + */ + public function ensureMemory(): void + { + if (!$this->Memory) { + $this->Memory = new Memory(); + } + } +} diff --git a/src/Model/Behavior/SluggableBehavior.php b/src/Model/Behavior/SluggableBehavior.php new file mode 100644 index 0000000..0150db5 --- /dev/null +++ b/src/Model/Behavior/SluggableBehavior.php @@ -0,0 +1,125 @@ + + */ + protected $_defaultConfig = [ + 'field' => 'name', + 'slug' => 'slug', + 'updateSlug' => true, + ]; + + /** + * Gets the Model callbacks this behavior is interested in. + * + * @return array + */ + public function implementedEvents(): array + { + return [ + 'Model.beforeMarshal' => 'beforeMarshal', + 'Model.beforeSave' => 'beforeSave', + ]; + } + + /** + * Preparing the data + * + * @param \Cake\Event\Event $event the event instance + * @param \ArrayObject $data data to be used in the marshal + * @param \ArrayObject $options options for the marshal + * @return void + */ + public function beforeMarshal(\Cake\Event\Event $event, \ArrayObject $data, \ArrayObject $options): void + { + $config = $this->getConfig(); + if (isset($data[$config['field']]) && $data[$config['field']]) { + $data[$config['slug']] = $this->sluggableSlugify($data[$config['field']]); + } + } + + /** + * Manipulate data before saving it. + * + * @param \Cake\Event\Event $event The even tracking object. + * @param \Cake\Datasource\EntityInterface $entity The entity to slug. + * @param \ArrayObject $options Passed options. + * @return void + */ + public function beforeSave( + \Cake\Event\EventInterface $event, + \Cake\Datasource\EntityInterface $entity, + \ArrayObject $options + ): void { + $config = $this->getConfig(); + // if name is changed, update the slug + if ( + ($config['updateSlug'] || $entity->isNew()) + && $entity->has($config['field']) + && $entity->isDirty($config['field']) + && !$entity->isDirty($config['slug']) + ) { + $entity->set($config['slug'], $this->sluggableSlugify($entity->get($config['field']))); + } + } + + /** + * Manually regenerate the slug. + * + * @param \Cake\Datasource\EntityInterface $entity The entity to slug. + * @return \Cake\Datasource\EntityInterface The updated entity. + */ + public function sluggableRegenSlug(\Cake\Datasource\EntityInterface $entity): \Cake\Datasource\EntityInterface + { + $config = $this->getConfig(); + $entity->set($config['slug'], $this->sluggableSlugify($entity->get($config['field']))); + + return $entity; + } + + /** + * Creates a slug from the input + * + * @param mixed $input The input to create the slug from + * @return string the slugged string + */ + public function sluggableSlugify($input = null): string + { + if ($input === null) { + return ''; + } + if (is_object($input)) { + $input = (array)$input; + } + if (is_array($input)) { + $input = implode(' ', $input); + } + if (is_int($input)) { + $input = (string)$input; + } + $input = trim(strtolower($input)); + $input = Text::slug($input); + $input = sha1($input); + + return $input; + } +} diff --git a/src/Model/Entity/Book.php b/src/Model/Entity/Book.php new file mode 100644 index 0000000..a702ed9 --- /dev/null +++ b/src/Model/Entity/Book.php @@ -0,0 +1,22 @@ + $data The data from the form with the apply key set in it. + * @param array $ids The unique ids that will have these changes applied. + * @param null|\Cake\ORM\Entity $entity The entity to attach to records + * @param null|string $record_field The field on the records that the entity should be attached to. + * @return null|int If the save was successfull, return the record count. + * @throws \Cake\ORM\Exception\PersistenceFailedException + * @TODO Use a more specific Exception when the save fails + */ + public function applySettings( + array $data = [], + array $ids = [], + ?\Cake\ORM\Entity $entity = null, + ?string $record_field = null + ): ?int { + $fields = []; + $this->setApplyError(''); + + if (!isset($data['apply']) || !is_array($data['apply'])) { + $this->setApplyError(__('The data is malformed.')); + + return null; + } + + $columns = $this->getSchema()->columns(); + + foreach ($data['apply'] as $field => $applyThis) { + if ($applyThis && isset($data[$field]) && in_array($field, $columns)) { + $fields[$field] = $data[$field]; + } + } + + // get all of the records by ids + /** @var string $primaryKey */ + $primaryKey = $this->getPrimaryKey(); + $records = $this->find('all') + ->where(function (QueryExpression $exp) use ($ids, $primaryKey) { + return $exp->in($primaryKey, $ids); + }); + + $i = 0; + foreach ($records as $record) { + foreach ($fields as $field => $value) { + if ($record->get($field) !== $value) { + $record->set($field, $value); + } + } + if ($record->isDirty()) { + if ($entity && $record_field) { + $record->set($record_field, $entity); + } + $this->saveOrFail($record); + $i++; + } + } + + return $i; + } + + /** + * Sets an error if one happens. + * + * @param string $msg The error message. + * @return void + */ + public function setApplyError(string $msg): void + { + $this->applyError = $msg; + } + + /** + * Gets an error is one happened. + * + * @return string The error message. + */ + public function getApplyError(): string + { + return $this->applyError; + } +} diff --git a/src/Model/Table/BooksTable.php b/src/Model/Table/BooksTable.php new file mode 100644 index 0000000..940b8a0 --- /dev/null +++ b/src/Model/Table/BooksTable.php @@ -0,0 +1,38 @@ + $config The configuration for the Table. + * @return void + */ + public function initialize(array $config = []): void + { + parent::initialize($config); + $this->setTable('books'); + + $this->belongsTo('Students') + ->setClassName('Fr3nch13/Utilities.Students'); + } +} diff --git a/src/Model/Table/CheckAddTrait.php b/src/Model/Table/CheckAddTrait.php new file mode 100644 index 0000000..8d0f978 --- /dev/null +++ b/src/Model/Table/CheckAddTrait.php @@ -0,0 +1,406 @@ +> + */ + public $checkAddIds = []; + + /** + * Hold the last record checked/created + * + * @var \Cake\ORM\Entity|null + */ + protected $lastEntity = null; + + /** + * Track the new count for each query + * + * @var int + */ + public $checkAdd_new_cnt = 0; + + /** + * Track the updated count for each query + * + * @var int + */ + public $checkAdd_update_cnt = 0; + + /** + * track successful cache read counts. + * + * @var int + */ + public $cacheReadCnt = 0; + + /** + * track successful database read counts. + * + * @var int + */ + public $dbReadCnt = 0; + + /** + * track successful database write counts. + * + * @var int + */ + public $dbWriteCnt = 0; + + /** + * Initialize method + * + * @param array $config The configuration for the Table. + * @return void + */ + public function initCheckAdd(array $config = []): void + { + if (!$this->behaviors()->has('Fr3nch13/Utilities.Sluggable')) { + $this->addBehavior('Fr3nch13/Utilities.Sluggable', $config); + } + } + + /** + * Sets the slug field, if different from the default + * + * @param string $field The field name to have the slug set to + * @return void + */ + public function setSlugField(string $field): void + { + if ($field) { + $this->checkAddSlug = $field; + } + if ($this->behaviors()->has('Sluggable')) { + $this->behaviors() + ->get('Sluggable') + ->setConfig('slug', $this->getSlugField()); + } + } + + /** + * Gets the slugField() + * + * @return string The field name for slug + */ + public function getSlugField(): string + { + return $this->checkAddSlug; + } + + /** + * Sets the name field, if different from the default + * + * @param string $field The field name to have the name set to + * @return void + */ + public function setNameField(string $field): void + { + if ($field) { + $this->checkAddName = $field; + } + if ($this->behaviors()->has('Sluggable')) { + $this->behaviors() + ->get('Sluggable') + ->setConfig('field', $this->getNameField()); + } + } + + /** + * Gets the nameField() + * + * @return string The field name for name + */ + public function getNameField(): string + { + return $this->checkAddName; + } + + /** + * Checks if a record exist by it's slug, if not, it creates the record + * + * @param mixed $name The name of the record + * @param null|string $slug The unique slug to look for + * @param array $fields The list of other details to include in the new record + * @param bool|array $returnEntity If true, return the entity, otherwise return the entity id. If it's an array, treat it as options + * @return \Cake\Datasource\EntityInterface|int Either the existing entity, or the newly created entity's id. + * @throws \Cake\ORM\Exception\PersistenceFailedException + * @throws \Fr3nch13\Utilities\Exception\MissingMethodException; if fixName method isn't found. + * @TODO Use a more specific Exception when the save fails + */ + public function checkAdd($name = null, $slug = null, $fields = [], $returnEntity = false) + { + try { + // @phpstan-ignore-next-line + $name = $this->fixName($name); + } catch (\Throwable $e) { + throw new MissingMethodException([self::class, 'fixName', '']); + } + + $save = false; + $new = false; + + $forceOnlySlug = false; + if ($slug) { + $forceOnlySlug = true; + } + + if ($name && !$slug) { + $slug = $this->slugify($name); + } + + $options = []; + if (is_array($returnEntity)) { + $options = $returnEntity; + $returnEntity = (isset($options['returnEntity']) && $options['returnEntity'] ? true : false); + } + + if (isset($options['update']) && $options['update'] === true) { + $save = true; + } + + $entity = $this->getCheckAdd($slug); + if ($entity !== null && !$save) { + $this->setCheckAdd($slug, $entity); + if ($returnEntity === true) { + return $entity; + } + + return $entity->get('id'); + } + + $this->dbReadCnt++; + + // try and find the entity by it's old slug setting first, incase the slug needs to be updated. + $entity = null; + if (isset($options['old_slug']) && $options['old_slug']) { + /** @var \Cake\ORM\Entity|null $entity */ + $entity = $this->find('all')->where([$this->getSlugField() => $options['old_slug']])->first(); + } + if ($entity === null) { + /** @var \Cake\ORM\Entity|null $entity */ + $entity = $this->find('all')->where([$this->getSlugField() => $slug])->first(); + } + + if ($entity === null) { + if (!$forceOnlySlug) { + // unable to find by slug, see if we can find by name, if so, and the slug field is empty, update it. + /** @var \Cake\ORM\Entity $entity|null */ + $entity = $this->find('all')->where([$this->getNameField() => $name])->first(); + } + if (!$entity) { + /** @var \Cake\ORM\Entity $entity */ + $entity = $this->newEntity([]); + $new = true; + } + $save = true; + } + + if ($save) { + if ($entity->{$this->getSlugField()} != $slug) { + $entity->set($this->getSlugField(), $slug); + } + if ($entity->{$this->getNameField()} != $name) { + $entity->set($this->getNameField(), $name); + } + + foreach ($fields as $fname => $fval) { + if ($entity->{$fname} != $fval) { + $entity->{$fname} = $fval; + } + } + if ($entity->isDirty()) { + $dirty = $entity->getDirty(); + $this->saveOrFail($entity); + $this->dbWriteCnt++; + if ($new) { + $this->checkAdd_new_cnt++; + } else { + // don't count the ones with the last_seen field is the only dirty. + if (count($dirty) == 1 && $dirty[0] == 'last_seen') { + // yes, i'm being sloppy. + } elseif (count($dirty) == 1 && $dirty[0] == 'slug') { + } elseif ( + count($dirty) == 2 && + in_array($dirty[0], ['last_seen', 'slug']) && + in_array($dirty[1], ['last_seen', 'slug']) + ) { + } else { + $this->checkAdd_update_cnt++; + } + } + } + } + + $this->setCheckAdd($slug, $entity); + + if ($returnEntity === true) { + return $entity; + } + + return $entity->get('id'); + } + + /** + * Checks the checkadd cache and returns the id, or false + * + * @param string $slug The slug to look up + * @param null|string $alias The model alias so look up. + * @return \Cake\ORM\Entity|null This id if found, or false if not + */ + public function getCheckAdd(string $slug, $alias = null): ?\Cake\ORM\Entity + { + if (!$this->getUseCache()) { + return null; + } + if (!$slug) { + return null; + } + if (!$alias) { + $alias = $this->getAlias(); + } + + if (!array_key_exists($alias, $this->checkAddIds)) { + return null; + } + + if (!array_key_exists($slug, $this->checkAddIds[$alias])) { + return null; + } + $this->cacheReadCnt++; + + return $this->checkAddIds[$alias][$slug]; + } + + /** + * Adds the id to the checkadd cache + * + * @param string $slug The slug to look up + * @param \Cake\ORM\Entity $entity The object record that was added + * @param null|string $alias The model alias so look up. + * @return \Cake\ORM\Entity + */ + public function setCheckAdd(string $slug, \Cake\ORM\Entity $entity, ?string $alias = null): \Cake\ORM\Entity + { + if (!$this->getUseCache()) { + $this->setLastEntity($entity); + + return $entity; + } + if (!$alias) { + $alias = $this->getAlias(); + } + if (!array_key_exists($alias, $this->checkAddIds)) { + $this->checkAddIds[$alias] = []; + } + $this->checkAddIds[$alias][$slug] = $entity; + $this->setLastEntity($entity); + + return $this->checkAddIds[$alias][$slug]; + } + + /** + * Sets tracking on the last entity that was added to the cache + * + * @param \Cake\ORM\Entity $entity The last entity added to the cache + * @return void + */ + public function setLastEntity(\Cake\ORM\Entity $entity): void + { + $this->lastEntity = $entity; + } + + /** + * Returns the last entity added to the cache + * + * @return null|\Cake\ORM\Entity The last entity added to the cache + */ + public function getLastEntity(): ?\Cake\ORM\Entity + { + return $this->lastEntity; + } + + /** + * Gets if we're using memory caching. + * + * @return bool If we're using caching. + */ + public function getUseCache(): bool + { + return $this->useCaching ? true : false; + } + + /** + * Sets if we're using memory caching. + * + * @param bool $toCache Set whether or not to use memory caching. + * @return void + */ + public function setUseCache(bool $toCache = false): void + { + $this->useCaching = ($toCache ? true : false); + } + + /** + * Creates a slug from the input + * + * @param mixed $input The input to create the slug from + * @return string the slugged string + * @throws \Fr3nch13\Utilities\Exception\MissingMethodException; if sluggableSlugify method isn't found. + */ + public function slugify($input = null): string + { + try { + // @phpstan-ignore-next-line + return $this->sluggableSlugify($input); + } catch (\Throwable $e) { + throw new MissingMethodException([ + self::class, + 'sluggableSlugify', + ' Either add the SluggableBehavior, or add that method directly.', + ]); + } + } +} diff --git a/src/Model/Table/CoursesStudentsTable.php b/src/Model/Table/CoursesStudentsTable.php new file mode 100644 index 0000000..40f088d --- /dev/null +++ b/src/Model/Table/CoursesStudentsTable.php @@ -0,0 +1,43 @@ + $config The configuration for the Table. + * @return void + */ + public function initialize(array $config = []): void + { + parent::initialize($config); + $this->setTable('courses_students'); + + $this->belongsTo('Courses') + ->setClassName('Fr3nch13/Utilities.Courses'); + $this->belongsTo('Students') + ->setClassName('Fr3nch13/Utilities.Students'); + } +} diff --git a/src/Model/Table/CoursesTable.php b/src/Model/Table/CoursesTable.php new file mode 100644 index 0000000..d425eba --- /dev/null +++ b/src/Model/Table/CoursesTable.php @@ -0,0 +1,88 @@ + $config The configuration for the Table. + * @return void + */ + public function initialize(array $config = []): void + { + parent::initialize($config); + $this->setTable('courses'); + + $this->belongsToMany('Students') + ->setClassName('Fr3nch13/Utilities.Students') + ->setThrough('Fr3nch13/Utilities.CoursesStudents'); + + $this->hasOne('TeachersPet') + ->setProperty('teachers_pet') + ->setForeignKey('teachers_pet_id') + ->setClassName('Fr3nch13/Utilities.Students') + ->setDependent(true); + + $this->addBehavior('Fr3nch13/Utilities.Memory'); + $this->addBehavior('Fr3nch13/Utilities.Sluggable'); + + $this->initCheckAdd(); + } + + /** + * Finder methond to get all available Courses. + * + * @param \Cake\ORM\Query $query The query object to modify. + * @param array $options The options either specific to this finder, or to pass through. + * @return \Cake\ORM\Query Return the modified query object. + */ + public function findAvailable(Query $query, array $options = []): Query + { + return $query->where(function (QueryExpression $exp) { + return $exp->eq('Courses.available', 1); + }); + } + + /** + * Fixes the name if we need to. + * + * @param string $name The string to fix. + * @return string The fixed name + */ + public function fixName(string $name): string + { + return trim($name); + } +} diff --git a/src/Model/Table/MergeDeleteTrait.php b/src/Model/Table/MergeDeleteTrait.php new file mode 100644 index 0000000..41bdda7 --- /dev/null +++ b/src/Model/Table/MergeDeleteTrait.php @@ -0,0 +1,149 @@ +associations()->getByType('belongsToMany'); + foreach ($BelongsToMany as $belongsToManyObj) { + $underscore = Inflector::underscore($belongsToManyObj->getAlias()); + $contain[] = $belongsToManyObj->getAlias(); + } + /** @property \Cake\Datasource\EntityInterface $sourceRecord */ + $sourceRecord = $this->get($sourceId, ['contain' => $contain]); + + /** @var \Cake\ORM\Association\BelongsToMany $belongsToManyObj */ + foreach ($BelongsToMany as $belongsToManyObj) { + $underscore = Inflector::underscore($belongsToManyObj->getAlias()); + if ($sourceRecord->has($underscore)) { + $table = $belongsToManyObj->junction(); + foreach ($sourceRecord->get($underscore) as $sourceEntity) { + $joinEntity = $sourceEntity->_joinData; + $joinEntity->set($belongsToManyObj->getForeignKey(), $targetId); + $table->saveOrFail($joinEntity); + } + } + } + + $HasMany = $this->associations()->getByType('hasMany'); + foreach ($HasMany as $hasManyObj) { + $foreignKey = $hasManyObj->getForeignKey(); + if (is_array($foreignKey)) { + $foreignKey = array_shift($foreignKey); + } + $conditions = [$foreignKey => $sourceId]; + $count = $hasManyObj->find('all') + ->where($conditions) + ->count(); + if (!$count) { + continue; + } + + $fields = [$foreignKey => $targetId]; + $hasManyObj->updateAll($fields, $conditions); + } + + return $this->delete($sourceRecord); + } + + /** + * Gets the list of available records for determining which record to merge to. + * This is a custom finder. + * + * @param \Cake\ORM\Query $query The query object to modify. + * @param array $options The options either specific to this finder, or to pass through. There should be an 'sourceId' option to exclude. + * @return \Cake\ORM\Query Return the modified query object. + */ + public function findMergeRecords(\Cake\ORM\Query $query, array $options = []): \Cake\ORM\Query + { + $sourceId = null; + if (isset($options['sourceId'])) { + $sourceId = $options['sourceId']; + } + + $primaryKey = $this->getPrimaryKey(); + if (isset($options['primaryKey'])) { + $primaryKey = $options['primaryKey']; + } + $displayField = $this->getDisplayField(); + if (isset($options['displayField'])) { + $displayField = $options['displayField']; + } + $query->find('list', [ + 'keyField' => $options['keyField'] ?? $primaryKey, + 'valueField' => $options['valueField'] ?? $displayField, + ]); + if ($sourceId) { + $query->where(function (\Cake\Database\Expression\QueryExpression $exp) use ($sourceId, $primaryKey) { + return $exp->notEq($primaryKey, $sourceId); + }); + } + + return $query; + } + + /** + * Get merge stats for a record to be deleted. + * + * @param int $id The id of the record. + * @return array> The list of stats. + */ + public function getMergeStats(int $id): array + { + $stats = []; + + $HasMany = $this->associations()->getByType('hasMany'); + foreach ($HasMany as $hasManyObj) { + $foreignKey = $hasManyObj->getForeignKey(); + if (is_array($foreignKey)) { + $foreignKey = array_shift($foreignKey); + } + $stats[$hasManyObj->getAlias()] = [ + 'name' => $hasManyObj->getAlias(), + 'count' => $hasManyObj->find('all') + ->where([$hasManyObj->getAlias() . '.' . $foreignKey => $id]) + ->count(), + ]; + } + + $BelongsToMany = $this->associations()->getByType('belongsToMany'); + foreach ($BelongsToMany as $belongsToManyObj) { + /** @var string $primaryKey */ + $primaryKey = $this->getPrimaryKey(); + $stats[$belongsToManyObj->getAlias()] = [ + 'name' => $belongsToManyObj->getAlias(), + 'count' => $belongsToManyObj->find('all') + ->matching($this->getAlias(), function ($q) use ($id, $primaryKey) { + return $q->where([$this->getAlias() . '.' . $primaryKey => $id]); + }) + ->count(), + ]; + } + + return $stats; + } +} diff --git a/src/Model/Table/StudentsTable.php b/src/Model/Table/StudentsTable.php new file mode 100644 index 0000000..4d428cd --- /dev/null +++ b/src/Model/Table/StudentsTable.php @@ -0,0 +1,54 @@ + $config The configuration for the Table. + * @return void + */ + public function initialize(array $config = []): void + { + parent::initialize($config); + $this->setTable('students'); + + $this->hasMany('Books') + ->setClassName('Fr3nch13/Utilities.Books'); + + $this->belongsToMany('Courses') + ->setClassName('Fr3nch13/Utilities.Courses') + ->setThrough('Fr3nch13/Utilities.CoursesStudents'); + + $this->addBehavior('Fr3nch13/Utilities.Memory'); + } +} diff --git a/src/Model/Table/ToggleTrait.php b/src/Model/Table/ToggleTrait.php new file mode 100644 index 0000000..1b0158e --- /dev/null +++ b/src/Model/Table/ToggleTrait.php @@ -0,0 +1,39 @@ +get($id); + if ($result->get($field)) { + $result->set($field, false); + } else { + $result->set($field, true); + } + + $this->saveOrFail($result); + + return $result->get($field); + } +} diff --git a/src/Plugin.php b/src/Plugin.php index f8a857a..a932f87 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -30,7 +30,8 @@ public function bootstrap(\Cake\Core\PluginApplicationInterface $app): void ]); } - // By default will load `config/bootstrap.php` in the plugin. + $app->addPlugin('Migrations'); // mainly used for testing. + parent::bootstrap($app); } } diff --git a/src/View/Helper/VersionsHelper.php b/src/View/Helper/VersionsHelper.php index cbaba7c..c1a6334 100644 --- a/src/View/Helper/VersionsHelper.php +++ b/src/View/Helper/VersionsHelper.php @@ -142,7 +142,7 @@ public function app(): ?string /** * Gets info on a particular package * - * @param string $name The full name of the composer page ex: sis-plugins/core + * @param string $name The full name of the composer package ex: fr3nch13/utilities * @return object the object that has the info of that package * @throws \Exception throws an exception if the package info can't be found. * @TODO Use more specific exceptions diff --git a/test b/test new file mode 100644 index 0000000000000000000000000000000000000000..12a1ae015e68b90048ea5f9a2d1f91edf0499181 GIT binary patch literal 28672 zcmeI&-%ry}6bJBL1}jtNeB0~wfj|S8Z4D5KCYD)oxF0w=l&7|^3p8#Ww0DG879adK z{3HAa{HuKM!PBvcUFu?dFlPDQtn1p_cISNVX_Iz4cw6q@RGj}*p~7o01K zqK+n-4ck1uN7J~m@rNmMReADmFPr1Z{xZmn_W_S@~G{it^oQ)0Jv zQ(RE<%gb!=_%4}P661w*Lh$3hzY5xSyTajzMzj3dXs(ntHng9~MnQM_XTRyy_UF_2 z)m3&hj5xgP&@Gf5ip#$6PsGIHLM-Zu&Xp*HHQH0VIZUbfr6o4#-6@QQ^w&|Ej~y*G zfd7_&fZQwB>qPaJP%AIZS4OL9@uD_(&eVLl%s#SVVyA)bpPUzUyDvRr^QdAk&OQIS z(>t5eH5X5N%9fGBl?Xzw=R16-vO8JD6nED<3tUMX*zp1=77A1VRO!QtbpvTjFDekL zQZo-Mqqc8b=KI@ofwmBSduso2AlwfZJ2C4Ie6S}$$!;?wQY=C)o)Sapx`V!7`Mz0e>4 z0SG_<0uX=z1Rwwb2tWV=5ST%MR4S8+e*dRG{i8tu0uX=z1Rwwb2tWV=5P$##ATYxM zxc{Hw!9{8ifB*y_009U<00Izz00bZafsp|2|4|AMfB*y_009U<00Izz00bZaf!P

out(__('--- Running Fixture: {0} ---', [self::class])); + + $this->table = 'books'; + + $this->records = [ + ['id' => 1, 'name' => "Sorcerer's Stone", 'student_id' => 1], + ['id' => 2, 'name' => 'Chamber of Secrets', 'student_id' => 1], + ['id' => 3, 'name' => 'Prisoner of Azkaban', 'student_id' => 1], + ['id' => 4, 'name' => 'Goblet Of Fire', 'student_id' => 1], + ['id' => 5, 'name' => 'Half-Blood Prince', 'student_id' => 1], + ['id' => 6, 'name' => 'Order of the Phoenix', 'student_id' => 1], + ['id' => 7, 'name' => 'Deathly Hallows', 'student_id' => 1], + ]; + + parent::init(); + } +} diff --git a/tests/Fixture/CoursesFixture.php b/tests/Fixture/CoursesFixture.php new file mode 100644 index 0000000..b0e6e34 --- /dev/null +++ b/tests/Fixture/CoursesFixture.php @@ -0,0 +1,26 @@ +out(__('--- Running Fixture: {0} ---', [self::class])); + + $this->table = 'courses'; + + $this->records = [ + ['id' => 1, 'name' => 'Potions', 'available' => 1], + ['id' => 2, 'name' => 'Defence Against the Dark Arts', 'available' => 1], + ['id' => 3, 'name' => 'Charms', 'available' => 1], + ]; + + parent::init(); + } +} diff --git a/tests/Fixture/CoursesStudentsFixture.php b/tests/Fixture/CoursesStudentsFixture.php new file mode 100644 index 0000000..2e9de84 --- /dev/null +++ b/tests/Fixture/CoursesStudentsFixture.php @@ -0,0 +1,37 @@ +out(__('--- Running Fixture: {0} ---', [self::class])); + + $this->table = 'courses_students'; + + $this->records = [ + ['id' => 1, 'course_id' => 1, 'student_id' => 1, 'grade' => 100], + ['id' => 4, 'course_id' => 2, 'student_id' => 1, 'grade' => 100], + ['id' => 7, 'course_id' => 3, 'student_id' => 1, 'grade' => 100], + ['id' => 2, 'course_id' => 1, 'student_id' => 2, 'grade' => 80], + ['id' => 5, 'course_id' => 2, 'student_id' => 2, 'grade' => 80], + ['id' => 3, 'course_id' => 1, 'student_id' => 3, 'grade' => 100], + ['id' => 6, 'course_id' => 2, 'student_id' => 3, 'grade' => 100], + ['id' => 9, 'course_id' => 3, 'student_id' => 3, 'grade' => 100], + ]; + + parent::init(); + } +} diff --git a/tests/Fixture/StudentsFixture.php b/tests/Fixture/StudentsFixture.php new file mode 100644 index 0000000..be394e2 --- /dev/null +++ b/tests/Fixture/StudentsFixture.php @@ -0,0 +1,26 @@ +out(__('--- Running Fixture: {0} ---', [self::class])); + + $this->table = 'students'; + + $this->records = [ + ['id' => 1, 'name' => 'Harry', 'slug' => 'slug-1'], + ['id' => 2, 'name' => 'Ron', 'slug' => 'slug-2'], + ['id' => 3, 'name' => 'Hermione', 'slug' => 'slug-3'], + ]; + + parent::init(); + } +} diff --git a/tests/TestCase/Lib/CommonNetworkTest.php b/tests/TestCase/Lib/CommonNetworkTest.php index a791539..e5b58f0 100644 --- a/tests/TestCase/Lib/CommonNetworkTest.php +++ b/tests/TestCase/Lib/CommonNetworkTest.php @@ -121,7 +121,7 @@ public function testNetmaskToArray(): void ], $this->CN->netmaskToArray('10.10.10.0', '255.255.255.255')); } - public function testNLong2ip(): void + public function testLong2ip(): void { $this->assertSame('10.10.10.0', $this->CN->long2ip(168430080)); $this->assertNull($this->CN->long2ip('10.10.10.10')); diff --git a/tests/TestCase/Model/Table/CoursesTableTest.php b/tests/TestCase/Model/Table/CoursesTableTest.php new file mode 100644 index 0000000..9fd9c04 --- /dev/null +++ b/tests/TestCase/Model/Table/CoursesTableTest.php @@ -0,0 +1,383 @@ + + */ + public function getFixtures(): array + { + $fixtures = [ + 'plugin.Fr3nch13/Utilities.Books', + 'plugin.Fr3nch13/Utilities.Courses', + 'plugin.Fr3nch13/Utilities.Students', + 'plugin.Fr3nch13/Utilities.CoursesStudents', + ]; + + return $fixtures; + } + + /** + * Connect the model. + */ + public function setUp(): void + { + parent::setUp(); + + /** @var \Cake\ORM\Locator\TableLocator $Locator */ + $Locator = $this->getTableLocator(); + $Locator->allowFallbackClass(false); + + /** @var \Fr3nch13\Utilities\Model\Table\CoursesTable $Courses */ + $Courses = $Locator->get('Fr3nch13/Utilities.Courses'); + $this->Courses = $Courses; + } + + /** + * Tests the class name of the Table + * + * @return void + */ + public function testClassInstance(): void + { + $this->assertInstanceOf(\Fr3nch13\Utilities\Model\Table\CoursesTable::class, $this->Courses); + } + + /** + * Testing a method. + * + * @return void + */ + public function testInitialize(): void + { + $this->assertEquals('courses', $this->Courses->getTable()); + $this->assertEquals('name', $this->Courses->getDisplayField()); + $this->assertEquals('id', $this->Courses->getPrimaryKey()); + } + + /** + * Test the behaviors + * + * @return void + */ + public function testBehaviors(): void + { + $behaviors = [ + 'Memory' => \Fr3nch13\Utilities\Model\Behavior\MemoryBehavior::class, + 'Sluggable' => \Fr3nch13\Utilities\Model\Behavior\SluggableBehavior::class, + ]; + foreach ($behaviors as $name => $class) { + $behavior = $this->Courses->behaviors()->get($name); + $this->assertNotNull($behavior, __('Behavior `{0}` is null.', [$name])); + $this->assertInstanceOf($class, $behavior, __('Behavior `{0}` isn\'t an instance of {1}.', [ + $name, + $class, + ])); + } + } + + /** + * Test Associations method + * + * @return void + */ + public function testAssociations(): void + { + // get all of the associations + $Associations = $this->Courses->associations(); + + // make sure the association exists + $this->assertNotNull($Associations->get('Students')); + $this->assertInstanceOf(\Cake\ORM\Association\BelongsToMany::class, $Associations->get('Students')); + $Association = $Associations->get('Students'); + $this->assertSame('Students', $Association->getName()); + $this->assertSame('course_id', $Association->getForeignKey()); + $this->assertSame('student_id', $Association->getTargetForeignKey()); + $this->assertSame('Fr3nch13/Utilities.Students', $Association->getClassName()); + } + + /** + * Test the entity itself + * + * @return void + */ + public function testEntity(): void + { + $entity = $this->Courses->get(3, [ + 'contain' => [ + 'Students', + ], + ]); + + $this->assertSame('Charms', $entity->get('name')); + } + + /** + * Test the apply settting trait itself + * + * @return void + */ + public function testApplySettingsTrait(): void + { + $entities = $this->Courses->find('available'); + $this->assertSame(3, $entities->count()); + + $this->Courses->applySettings( + ['available' => 0], + [2, 3] + ); + + $entities = $this->Courses->find('available'); + $this->assertSame(3, $entities->count()); + $this->assertSame('The data is malformed.', $this->Courses->getApplyError()); + + $this->Courses->applySettings( + ['available' => 0, 'apply' => ['available' => true]], + [2, 3], + $this->Courses->Students->get(1), + 'teachers_pet' + ); + + $entities = $this->Courses->find('available'); + $this->assertSame(1, $entities->count()); + } + + /** + * Testing getters and setters on the Check Add Trait + * + * @return void + */ + public function testCheckAddTraitGetAndSet(): void + { + $this->assertSame('slug', $this->Courses->getSlugField()); + $this->Courses->setSlugField('slug_other'); + $this->assertSame('slug_other', $this->Courses->getSlugField()); + + $this->assertSame('name', $this->Courses->getNameField()); + $this->Courses->setNameField('name_other'); + $this->assertSame('name_other', $this->Courses->getNameField()); + } + + /** + * Testing checkAdd() on the Check Add Trait + * + * @return void + */ + public function testCheckAddTraitGetCheckAdd(): void + { + // an new course, custom slug, and fields, return entity + $entity = $this->Courses->checkAdd('New Course 4', 'custom slug 4', ['updateme' => 'updateme 4'], true); + $this->assertSame(4, $entity->get('id')); + $this->assertSame('New Course 4', $entity->get('name')); + $this->assertSame('custom slug 4', $entity->get('slug')); + $this->assertSame('updateme 4', $entity->get('updateme')); + + $result = $this->Courses->getCheckAdd('custom slug 4'); + $this->assertSame('New Course 4', $result->get('name')); + + $this->assertNull($this->Courses->getCheckAdd('')); + } + + /** + * Testing checkAdd() on the Check Add Trait + * + * @return void + */ + public function testCheckAddTraitCheckAddCacheOn(): void + { + $this->Courses->setUseCache(true); + + $this->runCheckAddTests(); + } + + /** + * Testing checkAdd() on the Check Add Trait + * + * @return void + */ + public function testCheckAddTraitCheckAddCacheOff(): void + { + $this->Courses->setUseCache(false); + + $this->runCheckAddTests(); + } + + /** + * Keepin it dry + * + * @return void + */ + public function runCheckAddTests(): void + { + // add a new course. + $course_id = $this->Courses->checkAdd('New Course'); + $this->assertSame(4, $course_id); + + // an existing course + $course_id = $this->Courses->checkAdd('New Course'); + $this->assertSame(4, $course_id); + + // add a new course with a custom slug + $course_id = $this->Courses->checkAdd('New Course 2', 'custom slug 2'); + $this->assertSame(5, $course_id); + $entity = $this->Courses->get($course_id); + $this->assertSame('New Course 2', $entity->get('name')); + $this->assertSame('custom slug 2', $entity->get('slug')); + + // an existing course with a custom slug + $course_id = $this->Courses->checkAdd('New Course 2', 'custom slug 2'); + $this->assertSame(5, $course_id); + $entity = $this->Courses->get($course_id); + $this->assertSame('New Course 2', $entity->get('name')); + $this->assertSame('custom slug 2', $entity->get('slug')); + + // an new course, custom slug, and fields + $course_id = $this->Courses->checkAdd('New Course 3', 'custom slug 3', ['updateme' => 'updateme']); + $this->assertSame(6, $course_id); + $entity = $this->Courses->get($course_id); + $this->assertSame('New Course 3', $entity->get('name')); + $this->assertSame('custom slug 3', $entity->get('slug')); + $this->assertSame('updateme', $entity->get('updateme')); + + // an existing course, custom slug, and fields + $course_id = $this->Courses->checkAdd('New Course 3', 'custom slug 3', ['updateme' => 'updateme']); + $this->assertSame(6, $course_id); + $entity = $this->Courses->get($course_id); + $this->assertSame('New Course 3', $entity->get('name')); + $this->assertSame('custom slug 3', $entity->get('slug')); + $this->assertSame('updateme', $entity->get('updateme')); + + // an new course, custom slug, and fields, return entity + $entity = $this->Courses->checkAdd('New Course 4', 'custom slug 4', ['updateme' => 'updateme 4'], true); + $this->assertSame(7, $entity->get('id')); + $this->assertSame('New Course 4', $entity->get('name')); + $this->assertSame('custom slug 4', $entity->get('slug')); + $this->assertSame('updateme 4', $entity->get('updateme')); + + // an existing course, custom slug, and fields, return entity + $entity = $this->Courses->checkAdd('New Course 4', 'custom slug 4', ['updateme' => 'updateme 4'], true); + $this->assertSame(7, $entity->get('id')); + $this->assertSame('custom slug 4', $entity->get('slug')); + $this->assertSame('updateme 4', $entity->get('updateme')); + + // an new course, custom slug, and fields, return entity array with false + $course_id = $this->Courses->checkAdd('New Course 5', 'custom slug 5', ['updateme' => 'updateme 5'], ['returnEntity' => false]); + $this->assertSame(8, $course_id); + $entity = $this->Courses->get($course_id); + $this->assertSame(8, $entity->get('id')); + $this->assertSame('New Course 5', $entity->get('name')); + $this->assertSame('custom slug 5', $entity->get('slug')); + $this->assertSame('updateme 5', $entity->get('updateme')); + + // an existing course, custom slug, and fields, return entity array with true + $entity = $this->Courses->checkAdd('New Course 5', 'custom slug 5', ['updateme' => 'updateme 5'], ['returnEntity' => true]); + $this->assertSame(8, $entity->get('id')); + $this->assertSame('New Course 5', $entity->get('name')); + $this->assertSame('custom slug 5', $entity->get('slug')); + $this->assertSame('updateme 5', $entity->get('updateme')); + + // an existing course, custom slug, and fields, return entity array with true + $entity = $this->Courses->checkAdd('New Course 5', 'custom slug 5', ['updateme' => 'updateme updated'], ['returnEntity' => true, 'update' => true]); + $this->assertSame(8, $entity->get('id')); + $this->assertSame('New Course 5', $entity->get('name')); + $this->assertSame('custom slug 5', $entity->get('slug')); + $this->assertSame('updateme updated', $entity->get('updateme')); + + // an existing course, custom slug, and fields, return entity array with true + $entity = $this->Courses->checkAdd('New Course 5', 'custom slug 9', ['updateme' => 'updateme updated'], ['returnEntity' => true, 'old_slug' => 'custom slug 4']); + $this->assertSame(7, $entity->get('id')); + $this->assertSame('New Course 4', $entity->get('name')); + $this->assertSame('custom slug 4', $entity->get('slug')); + $this->assertSame('updateme 4', $entity->get('updateme')); + + // an existing course, custom slug, and fields, return entity array with true + $entity = $this->Courses->checkAdd('New Course 5', 'custom slug 9', ['updateme' => 'updateme updated'], ['returnEntity' => true, 'update' => true, 'old_slug' => 'custom slug 4']); + $this->assertSame(7, $entity->get('id')); + $this->assertSame('New Course 5', $entity->get('name')); + $this->assertSame('custom slug 9', $entity->get('slug')); + $this->assertSame('updateme updated', $entity->get('updateme')); + + // test tracking of last entity. + $entity = $this->Courses->getLastEntity(); + $this->assertSame('New Course 5', $entity->get('name')); + $this->assertSame('custom slug 9', $entity->get('slug')); + $this->assertSame('updateme updated', $entity->get('updateme')); + } + + /** + * Testing a finder method. + * + * @return void + */ + public function testToggleTrait(): void + { + $entity = $this->Courses->get(3); + $this->assertTrue($entity->get('available')); + + $this->Courses->toggle($entity->get('id'), 'available'); + + $entity = $this->Courses->get(3); + $this->assertFalse($entity->get('available')); + + $this->Courses->toggle($entity->get('id'), 'available'); + + $entity = $this->Courses->get(3); + $this->assertTrue($entity->get('available')); + } + + /** + * Testing a finder method. + * + * @return void + */ + public function testSluggableTrait(): void + { + $entity = $this->Courses->newEntity([ + 'name' => 'Herbology', + ]); + $this->Courses->saveOrFail($entity); + $this->assertTrue($entity->has('slug')); + $this->assertSame('21ab3a26fe6438ba3de4ec0a1ad91679a02cb78e', $entity->get('slug')); + + $entity = $this->Courses->get(1); + $this->assertSame(null, $entity->get('slug')); + $entity->set('name', 'Potions Updated'); + $this->Courses->saveOrFail($entity); + $this->assertSame('3567d12373aab20a1b9b1a369fe86c2187031e8d', $entity->get('slug')); + + $entity = $this->Courses->get(2); + $this->assertSame(null, $entity->get('slug')); + $entity = $this->Courses->sluggableRegenSlug($entity); + $this->assertSame('510d996e747239c8e90d4fba7b088a7024bd4516', $entity->get('slug')); + + $this->assertSame('', $this->Courses->sluggableSlugify()); + $this->assertSame('', $this->Courses->sluggableSlugify(null)); + $this->assertSame('310b86e0b62b828562fc91c7be5380a992b2786a', $this->Courses->sluggableSlugify(100)); + $this->assertSame('6ae999552a0d2dca14d62e2bc8b764d377b1dd6c', $this->Courses->sluggableSlugify('name')); + $this->assertSame('6ae999552a0d2dca14d62e2bc8b764d377b1dd6c', $this->Courses->sluggableSlugify([ + 'name' => 'name', + ])); + $this->assertSame('6ae999552a0d2dca14d62e2bc8b764d377b1dd6c', $this->Courses->sluggableSlugify(new \ArrayObject([ + 'name' => 'name', + ]))); + } +} diff --git a/tests/TestCase/Model/Table/StudentsTableTest.php b/tests/TestCase/Model/Table/StudentsTableTest.php new file mode 100644 index 0000000..48ce201 --- /dev/null +++ b/tests/TestCase/Model/Table/StudentsTableTest.php @@ -0,0 +1,308 @@ + + */ + public function getFixtures(): array + { + $fixtures = [ + 'plugin.Fr3nch13/Utilities.Books', + 'plugin.Fr3nch13/Utilities.Courses', + 'plugin.Fr3nch13/Utilities.Students', + 'plugin.Fr3nch13/Utilities.CoursesStudents', + ]; + + return $fixtures; + } + + /** + * Connect the model. + */ + public function setUp(): void + { + parent::setUp(); + + /** @var \Cake\ORM\Locator\TableLocator $Locator */ + $Locator = $this->getTableLocator(); + $Locator->allowFallbackClass(false); + + /** @var \Fr3nch13\Utilities\Model\Table\StudentsTable $Students */ + $Students = $Locator->get('Fr3nch13/Utilities.Students'); + $this->Students = $Students; + } + + /** + * Tests the class name of the Table + * + * @return void + */ + public function testClassInstance(): void + { + $this->assertInstanceOf(\Fr3nch13\Utilities\Model\Table\StudentsTable::class, $this->Students); + } + + /** + * Testing a method. + * + * @return void + */ + public function testInitialize(): void + { + $this->assertEquals('students', $this->Students->getTable()); + $this->assertEquals('name', $this->Students->getDisplayField()); + $this->assertEquals('id', $this->Students->getPrimaryKey()); + } + + /** + * Test the behaviors + * + * @return void + */ + public function testBehaviors(): void + { + $behaviors = [ + 'Memory' => \Fr3nch13\Utilities\Model\Behavior\MemoryBehavior::class, + ]; + foreach ($behaviors as $name => $class) { + $behavior = $this->Students->behaviors()->get($name); + $this->assertNotNull($behavior, __('Behavior `{0}` is null.', [$name])); + $this->assertInstanceOf($class, $behavior, __('Behavior `{0}` isn\'t an instance of {1}.', [ + $name, + $class, + ])); + } + } + + /** + * Test Associations method + * + * @return void + */ + public function testAssociations(): void + { + // get all of the associations + $Associations = $this->Students->associations(); + + // make sure the association exists + $this->assertNotNull($Associations->get('Books')); + $this->assertInstanceOf(\Cake\ORM\Association\HasMany::class, $Associations->get('Books')); + $Association = $Associations->get('Books'); + $this->assertSame('Books', $Association->getName()); + $this->assertSame('student_id', $Association->getForeignKey()); + $this->assertSame('Fr3nch13/Utilities.Books', $Association->getClassName()); + } + + /** + * Test the entity itself + * + * @return void + */ + public function testEntity(): void + { + $entity = $this->Students->get(1, [ + 'contain' => [ + 'Books', + ], + ]); + + $this->assertSame('Harry', $entity->get('name')); + } + + /** + * Test the apply settting trait itself + * + * @return void + */ + public function testMergeDeleteTraitMergeDelete(): void + { + $Harry = $this->Students->get(1, [ + 'contain' => [ + 'Books', + 'Courses', + ], + ]); + + $Ron = $this->Students->get(2, [ + 'contain' => [ + 'Books', + 'Courses', + ], + ]); + + $this->assertSame('Harry', $Harry->get('name')); + $this->assertSame(7, count($Harry->get('books'))); + $this->assertSame(3, count($Harry->get('courses'))); // Harry is in 3 + + $this->assertSame('Ron', $Ron->get('name')); + $this->assertSame(0, count($Ron->get('books'))); + $this->assertSame(2, count($Ron->get('courses'))); // Ron is only in 2 + + // Ron gets all of Harry's books, and any courses that he's not already in. + $this->Students->mergeDelete(1, 2); + + $Ron = $this->Students->get(2, [ + 'contain' => [ + 'Books', + 'Courses', + ], + ]); + + $this->assertSame('Ron', $Ron->get('name')); + $this->assertSame(7, count($Ron->get('books'))); // He was given harry's books. + $this->assertSame(5, count($Ron->get('courses'))); // Ron should have 5 course entries now, because he was given Harry's + } + + /** + * Test the apply settting trait itself + * + * @return void + */ + public function testMergeDeleteTraitFindMergeRecords(): void + { + $records = $this->Students->find('mergeRecords', [ + 'sourceId' => 1, + ]); + $this->assertSame(2, $records->count()); + $this->assertSame([ + 2 => 'Ron', + 3 => 'Hermione', + ], $records->toArray()); + + $records = $this->Students->find('mergeRecords', [ + 'sourceId' => 'Harry', + 'primaryKey' => 'name', + 'displayField' => 'slug', + ]); + + $this->assertSame(2, $records->count()); + $this->assertSame([ + 'Ron' => 'slug-2', + 'Hermione' => 'slug-3', + ], $records->toArray()); + } + + /** + * Test the apply settting trait itself + * + * @return void + */ + public function testMergeDeleteTraitGetMergeStats(): void + { + $stats = $this->Students->getMergeStats(1); + $this->assertSame([ + 'Books' => [ + 'name' => 'Books', + 'count' => 7, + ], + 'Courses' => [ + 'name' => 'Courses', + 'count' => 3, + ], + ], $stats); + } + + /** + * Test the memory behavior + * + * @return void + */ + public function testMemoryBehavior(): void + { + $usage = $this->Students->memoryUsage(false, 100); + $this->assertSame('100', $usage); + + $usage = $this->Students->memoryUsage(true, 100); + $this->assertSame('100 B', $usage); + + $usage = $this->Students->memoryUsage(false, 1024); + $this->assertSame('1024', $usage); + + $usage = $this->Students->memoryUsage(true, 1024); + $this->assertSame('1 KB', $usage); + + $usage = $this->Students->memoryUsageHighest(false); + $this->assertSame('1024', $usage); + + $usage = $this->Students->memoryUsageHighest(true); + $this->assertSame('1 KB', $usage); + + $usage = $this->Students->memoryUsage(true, 1024 * 1024); + $this->assertSame('1 MB', $usage); + + $usage = $this->Students->memoryUsageHighest(false); + $this->assertSame('1048576', $usage); + + $usage = $this->Students->memoryUsageHighest(true); + $this->assertSame('1 MB', $usage); + + $usage = $this->Students->memoryUsage(true, 1024 * 1024 * 1024); + $this->assertSame('1 GB', $usage); + + $usage = $this->Students->memoryUsageHighest(false); + $this->assertSame('1073741824', $usage); + + $usage = $this->Students->memoryUsageHighest(true); + $this->assertSame('1 GB', $usage); + + $usage = $this->Students->memoryUsage(false, 1024); + + $usage = $this->Students->memoryUsageHighest(false); + $this->assertSame('1073741824', $usage); + + $usage = $this->Students->memoryUsageHighest(true); + $this->assertSame('1 GB', $usage); + + $usage = $this->Students->memoryUsage(); + $this->assertGreaterThan(2, $usage); + + $usage = $this->Students->memoryUsage(true); + $this->assertIsString($usage); + } + + /** + * Tests if the fixName method doesn't exist in the CheckAddTrait + * + * @return void + */ + public function testCheckAddTraitExceptionCheckAdd(): void + { + $this->expectException(\Fr3nch13\Utilities\Exception\MissingMethodException::class); + $this->expectExceptionCode(500); + $this->expectExceptionMessage('Missing the `Fr3nch13\Utilities\Model\Table\StudentsTable::fixName()` method.'); + $this->Students->checkAdd('New Guy'); + } + + /** + * Tests if the fixName method doesn't exist in the CheckAddTrait + * + * @return void + */ + public function testCheckAddTraitExceptionSlugify(): void + { + $this->expectException(\Fr3nch13\Utilities\Exception\MissingMethodException::class); + $this->expectExceptionCode(500); + $this->expectExceptionMessage('Missing the `Fr3nch13\Utilities\Model\Table\StudentsTable::sluggableSlugify()` method.'); + $this->Students->slugify('New Guy'); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 5cf6a99..fbcede4 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -14,21 +14,25 @@ // Configure your stuff here for the plugin_bootstrap.php below. define('TESTS', __DIR__ . DS); -$dotenv = new \Symfony\Component\Dotenv\Dotenv(); -if (is_file(TESTS . '.env')) { - $dotenv->load(TESTS . '.env'); -} elseif (is_file(TESTS . '.env.test')) { - $dotenv->load(TESTS . '.env.test'); -} +Configure::write('Tests.DbConfig', [ + 'className' => \Cake\Database\Connection::class, + 'driver' => \Cake\Database\Driver\Sqlite::class, + 'database' => ':memory:', +]); Configure::write('Tests.Plugins', [ 'Fr3nch13/Utilities', ]); Configure::write('Tests.Helpers', [ + 'Colors' => ['className' => 'Fr3nch13/Utilities.Colors'], 'Versions' => ['className' => 'Fr3nch13/Utilities.Versions'], ]); +Configure::write('Tests.Migrations', [ + ['plugin' => 'Fr3nch13/Utilities'], +]); + ////// Ensure we can setup an environment for the Test Application instance. $root = dirname(__DIR__); chdir($root); diff --git a/tests/test.db b/tests/test.db new file mode 100644 index 0000000000000000000000000000000000000000..450bf4a28f5a9920c9468f2cdfa3a71296c304a4 GIT binary patch literal 32768 zcmeI&-%is|90%~0jTI1_H`)tAaxPh5W?NlM5Tj)loa_&dPUWgCTY-krjDU%SVq%u>$;NuluPwjxIp;JvgWb0!%QM-5(>XCbCUFlq zp66aN#&O&gy=UkhmT5YOhZXwdZyb+0yy70ef3uMM!3lF8x%)=))562~FZ1zxn^Z-C z00bZa0SG_<0uX?JFL03&1W^?E-V4vzZ<+g!bL<8`W(qY$Ru!ho8zqJ5L7UE&b*rhf zVntQ9lp3p4RaU8&N^GZAEX%b$wyo?jxvo}=6}q~tRMb_b+s28hvv+c>@LH}d=koc~ z@9QY)wz`LdQM#ia@w!dZ_O!?^q)_VOSwaxgX}*^VQf@fij%&JF|9p3_E<`&r7`{`V zu5S`et#2_Z^L{vWu|6$`OG|ujJIFrFI{ZvTvkjZ?l;vIJyX~gonYTyH$KQ!3#FZ6( z?>hO@7VT#=*F5c-cEfa|)#*rA27=3ea;tP@s#lB&;^HEIY26&o{xHQO_t5*T|3Je} zM{~R*vom5gj-IC!8F%FvO`0plnPIj3m8Y|fYPF=u746pQdZld2^^(f6biQXA4NB70 z+UAg@(RYO=9KS=b=WaHIzfxh#%v}-&w+5I8y!#cEPuwS#j>)i%H}N)E~HZRsf1`1c+Rq$=6Qd{8eNZWOm%IHgq9r>jL~y8*xN7A z0iXQL(F+9v5P$##AOHafKmY;|fB*y_0D=EjV4Y9QrX^{nXw%mzMynityOGP}q;w{i z&OK#0X)T*ydy*g0G)Mj7lRr6np+Eov5P$##AOHafKmY;|fB*y_Fo6QG*zBzT_ka4Q ze-sEn00Izz00bZa0SG_<0uX=z1SVJ@-2W%PC%+}XPH;!jH3&ce0uX=z1Rwwb2tWV= z5P-l$2|OCwoBNU0VAmhCg!}(U7!?RW00Izz00bZa0SG_<0uX=z1SU!V`~Qg=PILwW x5P$##AOHafKmY;|fB*y_FjN5h|Dh~sga8B}009U<00Izz00bZa0SHW#z%N}z)}R0Y literal 0 HcmV?d00001