From d4c295d1d87f32af6741b05192bb2a70f1213b90 Mon Sep 17 00:00:00 2001 From: Travis Garrison Date: Tue, 18 Aug 2026 21:08:59 +0400 Subject: [PATCH] Add unit tests for `Stock_location` model, covering default location handling, sort order management, and deletion behavior. Signed-off-by: Travis Garrison --- tests/Models/Stock_locationTest.php | 241 ++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 tests/Models/Stock_locationTest.php diff --git a/tests/Models/Stock_locationTest.php b/tests/Models/Stock_locationTest.php new file mode 100644 index 000000000..ab39cdee3 --- /dev/null +++ b/tests/Models/Stock_locationTest.php @@ -0,0 +1,241 @@ +call('TestDatabaseBootstrapSeeder'); + } + + protected function setUp(): void + { + parent::setUp(); + + session()->set('person_id', 1); + + $this->stockLocation = model(Stock_location::class); + } + + protected function tearDown(): void + { + parent::tearDown(); + } + + private function nextSortOrder(): int + { + $max = $this->db->table('stock_locations')->selectMax('sort_order')->get()->getRow()->sort_order; + + return $max === null ? 0 : ((int) $max) + 1; + } + + private function insertLocation(string $name, bool $isDefault = false, bool $withPermissions = true): int + { + $sortOrder = $this->nextSortOrder(); + + $builder = $this->db->table('stock_locations'); + $builder->insert([ + 'location_name' => $name, + 'deleted' => 0, + 'is_default' => $isDefault ? 1 : 0, + 'sort_order' => $sortOrder, + ]); + $locationId = $this->db->insertID(); + + if ($withPermissions) { + foreach (['items', 'sales', 'receivings'] as $module) { + $permissionId = $module . '_' . str_replace(' ', '_', $name); + + $this->db->table('permissions')->insert([ + 'permission_id' => $permissionId, + 'module_id' => $module, + 'location_id' => $locationId, + ]); + + $this->db->table('grants')->insert([ + 'permission_id' => $permissionId, + 'person_id' => 1, + ]); + } + } + + return $locationId; + } + + public function testGetDefaultLocationIdReturnsRowMarkedDefaultRegardlessOfSortOrder(): void + { + $this->db->table('stock_locations')->update(['is_default' => 0]); + + $this->insertLocation('Alpha'); + $defaultId = $this->insertLocation('Beta', true); + + $result = $this->stockLocation->getDefaultLocationId('items'); + + $this->assertSame($defaultId, $result); + } + + public function testGetDefaultLocationIdFallsBackToLowestSortOrderWhenNoneMarkedDefault(): void + { + // Remove the seeded base location so the inserted rows below are the only + // candidates and are unambiguously ordered by sort_order. + $this->db->table('permissions')->emptyTable(); + $this->db->table('grants')->emptyTable(); + $this->db->table('stock_locations')->emptyTable(); + + $lowestId = $this->insertLocation('Delta'); + $this->insertLocation('Epsilon'); + $this->insertLocation('Gamma'); + + $result = $this->stockLocation->getDefaultLocationId('items'); + + $this->assertSame($lowestId, $result); + } + + public function testGetDefaultLocationIdReturnsNullWhenNoAllowedLocationForModule(): void + { + $this->db->table('grants')->emptyTable(); + $this->db->table('permissions')->emptyTable(); + + $result = $this->stockLocation->getDefaultLocationId('items'); + + $this->assertNull($result); + } + + public function testSetDefaultLocationSetsTargetAndClearsOthers(): void + { + $firstId = $this->insertLocation('Zeta', true); + $secondId = $this->insertLocation('Eta'); + + $result = $this->stockLocation->setDefaultLocation($secondId); + + $this->assertTrue($result); + + $rows = $this->db->table('stock_locations')->orderBy('location_id')->get()->getResultArray(); + $defaults = array_column($rows, 'is_default', 'location_id'); + + $this->assertEquals(0, $defaults[$firstId]); + $this->assertEquals(1, $defaults[$secondId]); + } + + public function testSaveSortOrderAppliesNewOrderExactly(): void + { + // saveSortOrder() only reassigns sort_order for the ids passed in, starting + // the new range at 0 — any pre-existing row left out of the batch would + // collide with that range, so the batch here is every row in the table. + $idA = $this->insertLocation('Theta', false, false); + $idB = $this->insertLocation('Iota', false, false); + $idC = $this->insertLocation('Kappa', false, false); + + $allIds = array_column($this->db->table('stock_locations')->get()->getResultArray(), 'location_id'); + $ordered = array_values(array_diff($allIds, [$idA, $idB, $idC])); + array_unshift($ordered, $idC, $idA, $idB); + + $result = $this->stockLocation->saveSortOrder($ordered); + + $this->assertTrue($result); + + $rows = $this->db->table('stock_locations') + ->whereIn('location_id', [$idA, $idB, $idC]) + ->get() + ->getResultArray(); + $sortOrders = array_column($rows, 'sort_order', 'location_id'); + + $this->assertLessThan($sortOrders[$idA], $sortOrders[$idC]); + $this->assertLessThan($sortOrders[$idB], $sortOrders[$idA]); + } + + public function testSaveSortOrderHandlesCollidingRotationWithoutError(): void + { + // A reverse rotation is exactly the case naive sequential UPDATEs would + // collide on against the unique sort_order index. + $idA = $this->insertLocation('Lambda', false, false); + $idB = $this->insertLocation('Mu', false, false); + $idC = $this->insertLocation('Nu', false, false); + + $allIds = array_column($this->db->table('stock_locations')->get()->getResultArray(), 'location_id'); + $ordered = array_values(array_diff($allIds, [$idA, $idB, $idC])); + array_unshift($ordered, $idC, $idB, $idA); + + $result = $this->stockLocation->saveSortOrder($ordered); + + $this->assertTrue($result); + + $rows = $this->db->table('stock_locations') + ->whereIn('location_id', [$idA, $idB, $idC]) + ->get() + ->getResultArray(); + $sortOrders = array_column($rows, 'sort_order', 'location_id'); + + $this->assertLessThan($sortOrders[$idB], $sortOrders[$idC]); + $this->assertLessThan($sortOrders[$idA], $sortOrders[$idB]); + + $distinctCount = $this->db->table('stock_locations')->distinct()->select('sort_order')->get()->getNumRows(); + $totalCount = $this->db->table('stock_locations')->countAllResults(); + $this->assertSame($totalCount, $distinctCount, 'sort_order values must remain unique after rotation'); + } + + public function testDeletePushesSortOrderPastTableMax(): void + { + $idA = $this->insertLocation('Xi', false, false); + $idB = $this->insertLocation('Omicron', false, false); + + $maxBefore = (int) $this->db->table('stock_locations')->selectMax('sort_order')->get()->getRow()->sort_order; + + $result = $this->stockLocation->delete($idB); + + $this->assertTrue($result); + + $deletedRow = $this->db->table('stock_locations')->where('location_id', $idB)->get()->getRow(); + + $this->assertEquals(1, $deletedRow->deleted); + $this->assertGreaterThan($maxBefore, $deletedRow->sort_order); + } + + public function testDeleteExcludesLocationFromGetAll(): void + { + $idA = $this->insertLocation('Pi', false, false); + $idB = $this->insertLocation('Rho', false, false); + + $this->stockLocation->delete($idB); + + $remainingIds = array_map('intval', array_column($this->stockLocation->getAll()->getResultArray(), 'location_id')); + + $this->assertContains($idA, $remainingIds); + $this->assertNotContains($idB, $remainingIds); + } + + public function testSaveValueNewInsertAssignsNextSortOrder(): void + { + $maxBefore = (int) $this->db->table('stock_locations')->selectMax('sort_order')->get()->getRow()->sort_order; + + $locationData = ['location_name' => 'Sigma New Location']; + + $this->assertTrue($this->stockLocation->saveValue($locationData, NEW_ENTRY)); + $this->assertNotEmpty($locationData['location_id']); + + $savedRow = $this->db->table('stock_locations') + ->where('location_id', $locationData['location_id']) + ->get() + ->getRow(); + + $this->assertSame($maxBefore + 1, (int) $savedRow->sort_order); + } +}