diff --git a/lib/GaletteObjectsLend/Fixtures.php b/lib/GaletteObjectsLend/Fixtures.php new file mode 100644 index 0000000..9e909b5 --- /dev/null +++ b/lib/GaletteObjectsLend/Fixtures.php @@ -0,0 +1,49 @@ + + */ +class Fixtures implements FixturesProviderInterface +{ + /** + * Load sample data, rents given to fixture members + * + * @param FixturesContext $context Fixtures context + */ + public function seedFixtures(FixturesContext $context): string + { + $created = (new SampleData($context->zdb))->load(array_values($context->members)); + return sprintf( + 'Created %d categories, %d statuses, %d objects and %d rents', + $created['categories'], + $created['statuses'], + $created['objects'], + $created['rents'] + ); + } + + /** + * Remove sample data + * + * @param FixturesContext $context Fixtures context + */ + public function cleanFixtures(FixturesContext $context): void + { + (new SampleData($context->zdb))->remove(); + } +} diff --git a/lib/GaletteObjectsLend/SampleData.php b/lib/GaletteObjectsLend/SampleData.php index 6273392..5bdda6a 100644 --- a/lib/GaletteObjectsLend/SampleData.php +++ b/lib/GaletteObjectsLend/SampleData.php @@ -141,6 +141,108 @@ public function load(array $members): array return $created; } + /** + * Remove sample data, in a single transaction + * + * Objects are found by their serial number, with their rents. Categories + * and statuses are found by name, and only removed when nothing else uses + * them. + * + * @return array{categories: int, statuses: int, objects: int, rents: int} What was removed + */ + public function remove(): array + { + $data = $this->read(); + $removed = ['categories' => 0, 'statuses' => 0, 'objects' => 0, 'rents' => 0]; + + $need_transaction = !$this->zdb->inTransaction(); + try { + if ($need_transaction) { + $this->zdb->beginTransaction(); + } + + $select = $this->zdb->select(LEND_PREFIX . LendObject::TABLE) + ->columns([LendObject::PK]); + $select->where->in('serial_number', array_column($data['objects'], 'serial')); + $objects = []; + foreach ($this->zdb->execute($select) as $row) { + $objects[] = (int)$row->{LendObject::PK}; + } + + if ($objects !== []) { + //objects and rents reference each other: drop current rent first + $update = $this->zdb->update(LEND_PREFIX . LendObject::TABLE) + ->set([LendRent::PK => null]); + $update->where->in(LendObject::PK, $objects); + $this->zdb->execute($update); + + $delete = $this->zdb->delete(LEND_PREFIX . LendRent::TABLE); + $delete->where->in(LendObject::PK, $objects); + $removed['rents'] = $this->zdb->execute($delete)->getAffectedRows(); + + $delete = $this->zdb->delete(LEND_PREFIX . LendObject::TABLE); + $delete->where->in(LendObject::PK, $objects); + $removed['objects'] = $this->zdb->execute($delete)->getAffectedRows(); + } + + $removed['categories'] = $this->removeUnused( + LEND_PREFIX . LendCategory::TABLE, + LendCategory::PK, + 'name', + array_column($data['categories'], 'name'), + LEND_PREFIX . LendObject::TABLE + ); + $removed['statuses'] = $this->removeUnused( + LEND_PREFIX . LendStatus::TABLE, + LendStatus::PK, + 'status_text', + array_column($data['statuses'], 'text'), + LEND_PREFIX . LendRent::TABLE + ); + + if ($need_transaction) { + $this->zdb->commit(); + } + } catch (Throwable $e) { + if ($need_transaction) { + $this->zdb->rollback(); + } + throw $e; + } + + return $removed; + } + + /** + * Remove named rows nothing references anymore + * + * @param string $table Table name + * @param string $pk Primary key, also the referencing column + * @param string $column Name column + * @param list $names Names to remove + * @param string $referencer Table referencing rows + * + * @return int Number of removed rows + */ + private function removeUnused(string $table, string $pk, string $column, array $names, string $referencer): int + { + $select = $this->zdb->select($referencer) + ->columns([$pk]) + ->quantifier('DISTINCT'); + $select->where->isNotNull($pk); + $used = []; + foreach ($this->zdb->execute($select) as $row) { + $used[] = (int)$row->{$pk}; + } + + $delete = $this->zdb->delete($table); + $delete->where->in($column, $names); + if ($used !== []) { + $delete->where->notIn($pk, $used); + } + return $this->zdb->execute($delete)->getAffectedRows(); + } + /** * Read and check data file * diff --git a/tests/GaletteObjectsLend/tests/units/Fixtures.php b/tests/GaletteObjectsLend/tests/units/Fixtures.php new file mode 100644 index 0000000..9987073 --- /dev/null +++ b/tests/GaletteObjectsLend/tests/units/Fixtures.php @@ -0,0 +1,98 @@ + + */ +class Fixtures extends GaletteTestCase +{ + protected int $seed = 20261004130000; + protected bool $load_plugins = true; + + /** + * Set up tests: sample data need an empty catalog + */ + public function setUp(): void + { + parent::setUp(); + $this->emptyCatalog(); + } + + /** + * Cleanup after each test method + */ + public function tearDown(): void + { + $this->emptyCatalog(); + parent::tearDown(); + } + + /** + * Remove objects, rents, categories and statuses + */ + private function emptyCatalog(): void + { + $this->zdb->execute($this->zdb->update(LEND_PREFIX . LendObject::TABLE)->set([LendRent::PK => null])); + foreach ([LendRent::TABLE, LendObject::TABLE, LendCategory::TABLE, LendStatus::TABLE] as $table) { + $this->zdb->execute($this->zdb->delete(LEND_PREFIX . $table)); + } + } + + /** + * Test seeding and cleaning fixtures + */ + public function testFixtures(): void + { + $member_one = $this->getMemberOne(); + $member_two = $this->getMemberTwo(); + $this->logSuperAdmin(); + + $context = new FixturesContext( + zdb: $this->zdb, + login: $this->login, + preferences: $this->preferences, + history: $this->history, + plugins: $this->plugins, + members: ['one' => $member_one->id, 'two' => $member_two->id] + ); + + $fixtures = new \GaletteObjectsLend\Fixtures(); + $this->assertMatchesRegularExpression( + '/^Created 5 categories, 6 statuses, \d+ objects and \d+ rents$/', + $fixtures->seedFixtures($context) + ); + + //rents are given to context members only + $select = $this->zdb->select(LEND_PREFIX . LendRent::TABLE)->columns(['adherent_id'])->quantifier('DISTINCT'); + $select->where->isNotNull('adherent_id'); + $members = []; + foreach ($this->zdb->execute($select) as $row) { + $members[] = (int)$row->adherent_id; + } + sort($members); + $this->assertSame([$member_one->id, $member_two->id], $members); + + $fixtures->cleanFixtures($context); + foreach ([LendRent::TABLE, LendObject::TABLE, LendCategory::TABLE, LendStatus::TABLE] as $table) { + $this->assertSame(0, $this->zdb->execute($this->zdb->select(LEND_PREFIX . $table))->count(), $table); + } + } +} diff --git a/tests/GaletteObjectsLend/tests/units/SampleData.php b/tests/GaletteObjectsLend/tests/units/SampleData.php index 393c186..531441d 100644 --- a/tests/GaletteObjectsLend/tests/units/SampleData.php +++ b/tests/GaletteObjectsLend/tests/units/SampleData.php @@ -122,6 +122,58 @@ public function testLoad(): void } } + /** + * Remove sample data, keeping what is not part of it + */ + public function testRemove(): void + { + $data = json_decode( + (string)file_get_contents(\GaletteObjectsLend\SampleData::FILE), + true, + flags: JSON_THROW_ON_ERROR + ); + $this->assertIsArray($data); + + $sample = new \GaletteObjectsLend\SampleData($this->zdb); + $created = $sample->load([(int)$this->getMemberOne()->id]); + + //an object of the user, in a sample category and rented with a sample status + $select = $this->zdb->select(LEND_PREFIX . LendCategory::TABLE) + ->where(['name' => $data['categories'][0]['name']]); + $category_id = (int)$this->zdb->execute($select)->current()->{LendCategory::PK}; + $select = $this->zdb->select(LEND_PREFIX . LendStatus::TABLE) + ->where(['status_text' => $data['statuses'][0]['text']]); + $status_id = (int)$this->zdb->execute($select)->current()->{LendStatus::PK}; + + $object = new LendObject($this->zdb); + $object->setName('User object')->setSerialNumber('USER-001')->setCategoryId($category_id); + $object->store(); + $rent = new LendRent($this->zdb); + $rent->setObjectId((int)$object->getId())->setStatusId($status_id)->setComments(''); + $rent->store(); + + $removed = $sample->remove(); + $this->assertSame( + [ + 'categories' => $created['categories'] - 1, + 'statuses' => $created['statuses'] - 1, + 'objects' => $created['objects'], + 'rents' => $created['rents'] + ], + $removed + ); + $this->assertSame(1, $this->countRows(LendObject::TABLE)); + $this->assertSame(1, $this->countRows(LendRent::TABLE)); + $this->assertSame(1, $this->countRows(LendCategory::TABLE)); + $this->assertSame(1, $this->countRows(LendStatus::TABLE)); + + //nothing left to remove + $this->assertSame( + ['categories' => 0, 'statuses' => 0, 'objects' => 0, 'rents' => 0], + $sample->remove() + ); + } + /** * Rents are anonymous without members */