feat: Add Transport module with repository, controller, and views

- Implemented TransportRepository for managing transport data with methods for listing, finding, saving, and retrieving transport costs.
- Created ShopTransportController to handle transport-related actions, including listing, editing, and saving transports.
- Added views for transport management: transports list and transport edit forms.
- Introduced JavaScript for responsive tabs in transport edit view.
- Updated testing suite with comprehensive unit tests for TransportRepository and ShopTransportController.
- Increased test coverage with new assertions and scenarios for transport functionalities.
This commit is contained in:
2026-02-14 20:16:18 +01:00
parent 9c23e7f16b
commit 6543f8dc31
20 changed files with 1215 additions and 377 deletions

View File

@@ -0,0 +1,334 @@
<?php
namespace Tests\Unit\Domain\Transport;
use PHPUnit\Framework\TestCase;
use Domain\Transport\TransportRepository;
class TransportRepositoryTest extends TestCase
{
public function testFindReturnsNullForInvalidId(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->never())->method('get');
$repository = new TransportRepository($mockDb);
$this->assertNull($repository->find(0));
$this->assertNull($repository->find(-1));
}
public function testFindReturnsNullWhenNotFound(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('get')
->with('pp_shop_transports', '*', ['id' => 5])
->willReturn(null);
$repository = new TransportRepository($mockDb);
$this->assertNull($repository->find(5));
}
public function testFindNormalizesDataAndIncludesPaymentMethods(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('get')
->with('pp_shop_transports', '*', ['id' => 3])
->willReturn([
'id' => '3',
'name' => 'Kurier DPD',
'name_visible' => 'DPD',
'description' => 'Opis',
'status' => '1',
'cost' => '15.99',
'max_wp' => '30',
'default' => '0',
'delivery_free' => '1',
'apilo_carrier_account_id' => '42',
'o' => '2',
]);
$mockDb->expects($this->once())
->method('select')
->with('pp_shop_transport_payment_methods', 'id_payment_method', ['id_transport' => 3])
->willReturn([1, 3, 5]);
$repository = new TransportRepository($mockDb);
$result = $repository->find(3);
$this->assertIsArray($result);
$this->assertSame(3, $result['id']);
$this->assertSame('Kurier DPD', $result['name']);
$this->assertSame(1, $result['status']);
$this->assertSame(15.99, $result['cost']);
$this->assertSame(30, $result['max_wp']);
$this->assertSame(0, $result['default']);
$this->assertSame(1, $result['delivery_free']);
$this->assertSame(42, $result['apilo_carrier_account_id']);
$this->assertSame(2, $result['o']);
$this->assertSame([1, 3, 5], $result['payment_methods']);
}
public function testFindHandlesNullMaxWpAndApiloId(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->method('get')
->willReturn([
'id' => '1',
'name' => 'Test',
'status' => '0',
'cost' => '0',
'max_wp' => null,
'default' => '0',
'delivery_free' => '0',
'apilo_carrier_account_id' => null,
'o' => '0',
]);
$mockDb->method('select')->willReturn([]);
$repository = new TransportRepository($mockDb);
$result = $repository->find(1);
$this->assertNull($result['max_wp']);
$this->assertNull($result['apilo_carrier_account_id']);
$this->assertSame([], $result['payment_methods']);
}
public function testSaveInsertReturnsNewId(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('insert')
->with('pp_shop_transports', $this->callback(function ($data) {
return $data['name'] === 'Nowy transport'
&& $data['status'] === 1
&& $data['cost'] === 10.5;
}));
$mockDb->expects($this->once())
->method('id')
->willReturn('7');
$repository = new TransportRepository($mockDb);
$id = $repository->save([
'id' => 0,
'name' => 'Nowy transport',
'status' => 'on',
'cost' => '10.5',
'default' => '0',
'delivery_free' => 0,
]);
$this->assertSame(7, $id);
}
public function testSaveUpdateReturnsExistingId(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('update')
->with('pp_shop_transports', $this->isType('array'), ['id' => 4]);
$mockDb->expects($this->once())
->method('delete')
->with('pp_shop_transport_payment_methods', ['id_transport' => 4]);
$repository = new TransportRepository($mockDb);
$id = $repository->save([
'id' => 4,
'name' => 'Update',
'status' => 1,
'cost' => 5.0,
'default' => 0,
'delivery_free' => 'on',
]);
$this->assertSame(4, $id);
}
public function testSaveInsertReturnsNullOnFailure(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->method('insert');
$mockDb->method('id')->willReturn(null);
$repository = new TransportRepository($mockDb);
$this->assertNull($repository->save([
'id' => 0,
'name' => 'Fail',
]));
}
public function testSaveResetsDefaultWhenSettingNew(): void
{
$mockDb = $this->createMock(\medoo::class);
$updateCalls = [];
$mockDb->method('update')
->willReturnCallback(function ($table, $data, $where = null) use (&$updateCalls) {
$updateCalls[] = ['table' => $table, 'data' => $data, 'where' => $where];
});
$mockDb->method('delete');
$repository = new TransportRepository($mockDb);
$repository->save([
'id' => 2,
'name' => 'X',
'default' => 'on',
'status' => 0,
'delivery_free' => 0,
]);
$this->assertCount(2, $updateCalls);
$this->assertSame(['default' => 0], $updateCalls[0]['data']);
$this->assertNull($updateCalls[0]['where']);
}
public function testSaveSwitchValuesNormalization(): void
{
$mockDb = $this->createMock(\medoo::class);
$savedData = null;
$mockDb->method('update')
->willReturnCallback(function ($table, $data) use (&$savedData) {
$savedData = $data;
});
$mockDb->method('delete');
$repository = new TransportRepository($mockDb);
$repository->save([
'id' => 1,
'name' => 'T',
'status' => 'on',
'default' => 'true',
'delivery_free' => '1',
'cost' => 0,
]);
$this->assertSame(1, $savedData['status']);
$this->assertSame(1, $savedData['default']);
$this->assertSame(1, $savedData['delivery_free']);
}
public function testListForAdminWhitelistsSortColumn(): void
{
$mockDb = $this->createMock(\medoo::class);
$queries = [];
$mockDb->method('query')
->willReturnCallback(function ($sql, $params = []) use (&$queries) {
$queries[] = ['sql' => $sql, 'params' => $params];
if (strpos($sql, 'COUNT(0)') !== false) {
return new class {
public function fetchAll() { return [[1]]; }
};
}
return new class {
public function fetchAll() {
return [[
'id' => 1, 'name' => 'DPD', 'name_visible' => '',
'description' => '', 'status' => 1, 'cost' => 10.0,
'max_wp' => null, 'default' => 0,
'apilo_carrier_account_id' => null, 'delivery_free' => 0, 'o' => 1,
]];
}
};
});
$repository = new TransportRepository($mockDb);
$result = $repository->listForAdmin(
[],
'name DESC; DROP TABLE pp_shop_transports; --',
'DESC; DELETE FROM pp_users; --',
1,
999
);
$this->assertCount(2, $queries);
$dataSql = $queries[1]['sql'];
$this->assertMatchesRegularExpression('/ORDER BY\s+st\.name\s+ASC,\s+st\.id\s+ASC/i', $dataSql);
$this->assertStringNotContainsString('DROP TABLE', $dataSql);
$this->assertMatchesRegularExpression('/LIMIT\s+100\s+OFFSET\s+0/i', $dataSql);
}
public function testAllActiveReturnsNormalizedRows(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('select')
->willReturn([
[
'id' => '2', 'name' => 'InPost', 'status' => '1',
'cost' => '9.99', 'max_wp' => '25', 'default' => '1',
'delivery_free' => '0', 'apilo_carrier_account_id' => null, 'o' => '1',
],
]);
$repository = new TransportRepository($mockDb);
$rows = $repository->allActive();
$this->assertCount(1, $rows);
$this->assertSame(2, $rows[0]['id']);
$this->assertSame(9.99, $rows[0]['cost']);
$this->assertSame(1, $rows[0]['status']);
$this->assertSame(1, $rows[0]['default']);
}
public function testGetApiloCarrierAccountIdReturnsNullForInvalidId(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->never())->method('get');
$repository = new TransportRepository($mockDb);
$this->assertNull($repository->getApiloCarrierAccountId(0));
}
public function testGetApiloCarrierAccountIdReturnsIntOrNull(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->exactly(2))
->method('get')
->willReturnOnConsecutiveCalls(null, '55');
$repository = new TransportRepository($mockDb);
$this->assertNull($repository->getApiloCarrierAccountId(1));
$this->assertSame(55, $repository->getApiloCarrierAccountId(2));
}
public function testGetTransportCostReturnsFloatOrNull(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->exactly(2))
->method('get')
->willReturnOnConsecutiveCalls(null, '12.50');
$repository = new TransportRepository($mockDb);
$this->assertNull($repository->getTransportCost(99));
$this->assertSame(12.5, $repository->getTransportCost(1));
}
public function testAllForAdminReturnsAllTransports(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('select')
->willReturn([
['id' => '1', 'name' => 'A', 'status' => '1', 'cost' => '5', 'max_wp' => null, 'default' => '0', 'delivery_free' => '0', 'apilo_carrier_account_id' => null, 'o' => '1'],
['id' => '2', 'name' => 'B', 'status' => '0', 'cost' => '10', 'max_wp' => '50', 'default' => '1', 'delivery_free' => '1', 'apilo_carrier_account_id' => '3', 'o' => '2'],
]);
$repository = new TransportRepository($mockDb);
$rows = $repository->allForAdmin();
$this->assertCount(2, $rows);
$this->assertSame(0, $rows[1]['status']);
$this->assertSame(1, $rows[1]['default']);
$this->assertSame(50, $rows[1]['max_wp']);
}
}