diff --git a/Dockerfile b/Dockerfile index 49a05d5..1e0f728 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,8 +8,6 @@ LABEL author="Gustavo Freze" \ ARG FLYWAY_VERSION=10.8.1 -RUN docker-php-ext-install pdo_mysql - RUN apk --no-cache add tar curl mysql-client openjdk21-jre \ && mkdir -p /opt/flyway \ && curl -L "https://repo1.maven.org/maven2/org/flywaydb/flyway-commandline/${FLYWAY_VERSION}/flyway-commandline-${FLYWAY_VERSION}-linux-x64.tar.gz" | tar -xz --strip-components=1 -C /opt/flyway \ diff --git a/Makefile b/Makefile index 9c1d3ef..ec2f5c3 100755 --- a/Makefile +++ b/Makefile @@ -2,10 +2,11 @@ IMAGE = gustavofreze/php:8.2 DOCKER_RUN = docker run -u root --rm -it --net=host -v ${PWD}:/app -w /app ${IMAGE} DOCKER_EXEC = docker exec -it cheap-delivery +INTEGRATION_TEST = docker run -u root --rm -it --name cheap-delivery-integration-test --link cheap-delivery-adm --network=cheap-delivery_default -v ${PWD}:/app -w /app ${IMAGE} + FLYWAY = docker run --rm -v ${PWD}/db/mysql/migrations:/flyway/sql --env-file=config/local.env --link cheap-delivery-adm --network=cheap-delivery_default -e FLYWAY_EDITION="community" flyway/flyway:10.8.1 MIGRATE_DB = ${FLYWAY} -locations=filesystem:/flyway/sql -schemas=cheap_delivery_adm - -.PHONY: configure run test test-no-coverage review fix-style show-coverage clean migrate-database clean-database +MIGRATE_TEST_DB = ${FLYWAY} -locations=filesystem:/flyway/sql -schemas=cheap_delivery_adm_test configure: @docker-compose up -d --build @@ -13,17 +14,17 @@ configure: configure-local: @${DOCKER_RUN} composer update --optimize-autoloader -test: - @${DOCKER_RUN} composer run tests +test: migrate-test-database + @${INTEGRATION_TEST} composer run tests -test-no-coverage: - @${DOCKER_RUN} composer run test-no-coverage +test-no-coverage: migrate-test-database + @${INTEGRATION_TEST} composer run test-no-coverage test-unit: @${DOCKER_RUN} composer run test-unit -test-integration: - @${DOCKER_RUN} composer run test-integration +test-integration: migrate-test-database + @${INTEGRATION_TEST} composer run test-integration review: @${DOCKER_RUN} composer review @@ -43,3 +44,7 @@ migrate-database: clean-database: @${MIGRATE_DB} clean + +migrate-test-database: + @${MIGRATE_TEST_DB} clean + @${MIGRATE_TEST_DB} migrate diff --git a/composer.json b/composer.json index 5f78c96..969cec2 100644 --- a/composer.json +++ b/composer.json @@ -12,10 +12,8 @@ }, "autoload-dev": { "psr-4": { - "CheapDelivery\\": [ - "tests/Unit/", - "tests/Integration/" - ] + "Test\\": "tests/", + "CheapDelivery\\": "tests/Unit/" } }, "require": { diff --git a/phpunit.xml b/phpunit.xml index 9b0155e..89cc3fc 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,12 +1,13 @@ @@ -26,9 +27,17 @@ src - src/routes.php - src/bootstrap.php + src/Routes.php + src/Dependencies.php + + + + + + + + diff --git a/src/Query/QueryBuilder.php b/src/Query/QueryBuilder.php index c73d4c8..02e377b 100644 --- a/src/Query/QueryBuilder.php +++ b/src/Query/QueryBuilder.php @@ -6,13 +6,14 @@ final class QueryBuilder extends DoctrineQueryBuilder { - private const AND_WHERE = '%s = :%'; + private const AND_WHERE = '%s = :%s'; public function applyFilters(Filters $filters): QueryBuilder { foreach ($filters->toArray() as $column => $value) { $this->andWhere(sprintf(self::AND_WHERE, $column, $column))->setParameter($column, $value); } + return $this; } } diff --git a/src/Query/Shipment/Database/Builder.php b/src/Query/Shipment/Database/Builder.php index abddd55..f3f5764 100644 --- a/src/Query/Shipment/Database/Builder.php +++ b/src/Query/Shipment/Database/Builder.php @@ -12,7 +12,7 @@ public static function from(array $shipments): array 'carrier' => [ 'name' => $shipment['carrierName'] ], - 'createdAt' => $shipment['createdAt'] + 'createdAt' => date('c', strtotime($shipment['createdAt'])) ]; return array_map(fn(array $shipment) => $mapper(shipment: $shipment), $shipments); diff --git a/src/Query/Shipment/Database/Facade.php b/src/Query/Shipment/Database/Facade.php index 1622335..44a4b4d 100644 --- a/src/Query/Shipment/Database/Facade.php +++ b/src/Query/Shipment/Database/Facade.php @@ -21,6 +21,7 @@ public function findAll(ShipmentFilters $filters): array ]) ->from('shipment') ->applyFilters($filters) + ->orderBy('created_at', 'DESC') ->executeQuery() ->fetchAllAssociative(); diff --git a/tests/Integration/IntegrationTestCapabilities.php b/tests/Integration/IntegrationTestCapabilities.php new file mode 100644 index 0000000..6cacb4e --- /dev/null +++ b/tests/Integration/IntegrationTestCapabilities.php @@ -0,0 +1,18 @@ +sub(new DateInterval($duration)); + return $this; + } + + public function toString(): string + { + return $this->format('c'); + } +} diff --git a/tests/Integration/Query/Shipment/QueryAdapter.php b/tests/Integration/Query/Shipment/QueryAdapter.php new file mode 100644 index 0000000..80bdce6 --- /dev/null +++ b/tests/Integration/Query/Shipment/QueryAdapter.php @@ -0,0 +1,38 @@ +connection = $container->get(Connection::class); + $this->connection->beginTransaction(); + } + + public function saveShipments(array $shipments): void + { + foreach ($shipments as $shipment) { + $this->connection->executeStatement(self::INSERT_SHIPMENT, [ + 'id' => $shipment['id'], + 'cost' => $shipment['cost'], + 'createdAt' => $shipment['createdAt'], + 'carrierName' => $shipment['carrier']['name'] + ]); + } + } + + public function rollBack(): void + { + $this->connection->rollBack(); + } +} diff --git a/tests/Integration/Query/Shipment/ResourceTest.php b/tests/Integration/Query/Shipment/ResourceTest.php new file mode 100644 index 0000000..05a3603 --- /dev/null +++ b/tests/Integration/Query/Shipment/ResourceTest.php @@ -0,0 +1,118 @@ +query = new QueryAdapter(self::$container); + $this->resource = self::$container->get(Resource::class); + } + + protected function tearDown(): void + { + $this->query->rollBack(); + } + + public function testFindShipmentsWithoutFilters(): void + { + /** @Given I have some shipments */ + $shipments = [ + [ + 'id' => '03ec1cfc-5546-45ae-925d-eb36211965d9', + 'cost' => floatval(rand(100, 150)), + 'carrier' => [ + 'name' => 'DHL' + ], + 'createdAt' => IsoDate::now()->toString() + ], + [ + 'id' => '9a33a5f5-3d4b-4777-afa7-fbdadd9620d3', + 'cost' => floatval(rand(100, 150)), + 'carrier' => [ + 'name' => 'FedEx' + ], + 'createdAt' => IsoDate::now()->subtract(duration: 'PT10M')->toString() + ] + ]; + + /** @And these shipments are persisted */ + $this->query->saveShipments(shipments: $shipments); + + /** @And I have a request to fetch the persisted shipments without filters */ + $request = $this->requestWith(parameters: []); + + /** @When I make the request */ + $response = $this->resource->handle(request: $request); + + /** @Then the shipments should be returned */ + $actual = json_decode($response->getBody()->__toString(), true); + + self::assertCount(2, $actual); + self::assertEquals($shipments, $actual); + } + + public function testFindShipmentsWithFilters(): void + { + /** @Given I have some shipments */ + $shipments = [ + [ + 'id' => 'ca83b7f1-4d54-4447-85a1-6cfb59122899', + 'cost' => floatval(rand(100, 150)), + 'carrier' => [ + 'name' => 'DHL' + ], + 'createdAt' => IsoDate::now()->toString() + ], + [ + 'id' => '73440f3c-2d29-42dd-9094-130d04e98712', + 'cost' => floatval(rand(100, 150)), + 'carrier' => [ + 'name' => 'FedEx' + ], + 'createdAt' => IsoDate::now()->toString() + ] + ]; + + /** @And these shipments are persisted */ + $this->query->saveShipments(shipments: $shipments); + + /** @And I have a request to fetch the persisted shipments with filters */ + $request = $this->requestWith(parameters: ['carrierName' => 'DHL']); + + /** @When I make the request */ + $response = $this->resource->handle(request: $request); + + /** @Then the shipments should be returned */ + $actual = json_decode($response->getBody()->__toString(), true); + + self::assertCount(1, $actual); + self::assertEquals($shipments[0]['id'], $actual[0]['id']); + self::assertEquals($shipments[0]['cost'], $actual[0]['cost']); + self::assertEquals($shipments[0]['carrier'], $actual[0]['carrier']); + self::assertEquals($shipments[0]['createdAt'], $actual[0]['createdAt']); + } + + private function requestWith(array $parameters): ServerRequestInterface + { + $request = $this->getMockBuilder(ServerRequestInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $request->expects(self::once()) + ->method('getQueryParams') + ->willReturn($parameters); + + return $request; + } +}