diff --git a/.env.local.template b/.env.template
similarity index 72%
rename from .env.local.template
rename to .env.template
index 7eda5b47f..8adca0772 100644
--- a/.env.local.template
+++ b/.env.template
@@ -7,3 +7,8 @@
API_KEY_MYPARCEL=
API_KEY_BELGIE=
API_KEY_FLESPAKKET=
+
+###
+# Docker image
+###
+IMAGE_NAME=ghcr.io/myparcelnl/pdk
diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml
index 292e87a93..9c31cb448 100644
--- a/.github/actions/setup/action.yml
+++ b/.github/actions/setup/action.yml
@@ -2,6 +2,10 @@ name: 'Setup'
description: ''
inputs:
+ token:
+ description: 'GitHub token'
+ required: true
+
php-version:
description: 'PHP version'
required: true
@@ -9,6 +13,11 @@ inputs:
runs:
using: composite
steps:
+ - uses: myparcelnl/actions/build-docker-image-reg@v4
+ id: build
+ with:
+ registry-password: ${{ inputs.token }}
+
- name: 'Handle composer cache'
uses: actions/cache@v4
with:
@@ -22,22 +31,21 @@ runs:
composer-${{ hashFiles('**/composer.json') }}-
composer-
- - uses: myparcelnl/actions/pull-docker-image@v4
- with:
- image: 'ghcr.io/myparcelnl/php-xd:${{ inputs.php-version }}-cli-alpine'
-
- name: 'Prepare environment'
shell: bash
#language=bash
run: |
- touch .env.local
+ touch .env
+ echo "COMPOSER_HOME=/root/.composer" >> .env
+ echo "COMPOSER_MEMORY_LIMIT=-1" >> .env
- name: 'Install composer dependencies'
shell: bash
+ env:
+ IMAGE_NAME: ${{ steps.build.outputs.tagged-image }}
#language=bash
run: |
docker compose run \
--volume $HOME/.composer:/root/.composer \
- --env COMPOSER_CACHE_DIR=/root/.composer \
php \
composer update --no-progress --no-scripts --no-plugins
diff --git a/.github/workflows/--analyse.yml b/.github/workflows/--analyse.yml
index 0ee77b418..f48abdb4e 100644
--- a/.github/workflows/--analyse.yml
+++ b/.github/workflows/--analyse.yml
@@ -31,6 +31,7 @@ jobs:
- uses: ./.github/actions/setup
if: steps.phpstan-cache.outputs.cache-hit != 'true'
with:
+ token: ${{ secrets.GITHUB_TOKEN }}
php-version: ${{ vars.PHP_VERSION }}
- name: 'Run PHPStan analysis'
diff --git a/.github/workflows/--quality.yml b/.github/workflows/--quality.yml
index f75199dc2..f2adfced7 100644
--- a/.github/workflows/--quality.yml
+++ b/.github/workflows/--quality.yml
@@ -37,6 +37,7 @@ jobs:
- uses: ./.github/actions/setup
if: steps.rector-cache.outputs.cache-hit != 'true'
with:
+ token: ${{ secrets.GITHUB_TOKEN }}
php-version: ${{ vars.PHP_VERSION }}
- name: 'Run quality checks in dry-run mode'
diff --git a/.github/workflows/--setup.yml b/.github/workflows/--setup.yml
index 5c031807c..8e189a29c 100644
--- a/.github/workflows/--setup.yml
+++ b/.github/workflows/--setup.yml
@@ -10,4 +10,5 @@ jobs:
- uses: ./.github/actions/setup
with:
+ token: ${{ secrets.GITHUB_TOKEN }}
php-version: ${{ vars.PHP_VERSION }}
diff --git a/.github/workflows/--test-integration.yml b/.github/workflows/--test-integration.yml
index 078a1eb9d..d33b05bdb 100644
--- a/.github/workflows/--test-integration.yml
+++ b/.github/workflows/--test-integration.yml
@@ -11,6 +11,7 @@ jobs:
- uses: ./.github/actions/setup
with:
+ token: ${{ secrets.GITHUB_TOKEN }}
php-version: ${{ vars.PHP_VERSION }}
- name: 'Run integration tests'
diff --git a/.github/workflows/--test-unit.yml b/.github/workflows/--test-unit.yml
index 0beb940b2..229c31ba8 100644
--- a/.github/workflows/--test-unit.yml
+++ b/.github/workflows/--test-unit.yml
@@ -19,6 +19,7 @@ jobs:
- uses: ./.github/actions/setup
if: steps.coverage-cache.outputs.cache-hit != 'true'
with:
+ token: ${{ secrets.GITHUB_TOKEN }}
php-version: ${{ vars.PHP_VERSION }}
- name: 'Run unit tests'
diff --git a/.gitignore b/.gitignore
index 5e209066e..fea352f4d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,7 +6,7 @@ composer.lock
clover.xml
docker-compose.override.yml
-.env.local
+.env
/.cache/
/.nx/
diff --git a/.idea/php.xml b/.idea/php.xml
index 40f9c8a18..80ba89213 100644
--- a/.idea/php.xml
+++ b/.idea/php.xml
@@ -18,36 +18,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -141,6 +111,7 @@
+
@@ -159,8 +130,8 @@
-
- /usr/local/etc/php/conf.d/docker-php-ext-pcov.ini, /usr/local/etc/php/conf.d/docker-php-ext-sodium.ini, /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini, /usr/local/etc/php/conf.d/zzz-pcov.ini, /usr/local/etc/php/conf.d/zzz-xdebug.ini
+
+ /usr/local/etc/php/conf.d/docker-php-ext-pcov.ini, /usr/local/etc/php/conf.d/docker-php-ext-sodium.ini, /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini, /usr/local/etc/php/conf.d/docker-php-ext-zip.ini, /usr/local/etc/php/conf.d/zzz-pcov.ini, /usr/local/etc/php/conf.d/zzz-xdebug.ini
@@ -205,6 +176,7 @@
+
@@ -224,6 +196,7 @@
+
@@ -276,6 +249,7 @@
+
@@ -314,7 +288,6 @@
-
@@ -341,4 +314,4 @@
-
+
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 000000000..6384e0d4c
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,7 @@
+FROM ghcr.io/myparcelnl/php-xd:7.4-fpm-alpine
+
+# install php zip extension
+RUN apk add --no-cache libzip-dev \
+ && docker-php-ext-configure zip \
+ && docker-php-ext-install zip \
+ && docker-php-ext-enable zip
diff --git a/README.md b/README.md
index 04b2616e8..3b8e767b4 100644
--- a/README.md
+++ b/README.md
@@ -35,10 +35,10 @@ View our [contribution guidelines] for information on how to contribute to the P
### Installation
-Create `.env.local`:
+Create `.env`:
```shell
-cp .env.local.template .env.local
+cp .env.template .env
```
Install Yarn dependencies:
diff --git a/composer.json b/composer.json
index f8d8fefde..4de0727e8 100644
--- a/composer.json
+++ b/composer.json
@@ -6,6 +6,7 @@
"homepage": "https://myparcel.nl",
"license": "MIT",
"require": {
+ "ext-zip": "*",
"justinrainbow/json-schema": "^5.2",
"myparcelnl/sdk": ">= 7",
"php": ">=7.1.0",
@@ -71,4 +72,4 @@
"pestphp/pest-plugin": true
}
}
-}
\ No newline at end of file
+}
diff --git a/config/actions.php b/config/actions.php
index 94c5219cd..c4c7bd0c9 100644
--- a/config/actions.php
+++ b/config/actions.php
@@ -5,6 +5,7 @@
use MyParcelNL\Pdk\App\Action\Backend\Account\DeleteAccountAction;
use MyParcelNL\Pdk\App\Action\Backend\Account\UpdateAccountAction;
use MyParcelNL\Pdk\App\Action\Backend\Account\UpdateSubscriptionFeaturesAction;
+use MyParcelNL\Pdk\App\Action\Backend\Debug\DownloadLogsAction;
use MyParcelNL\Pdk\App\Action\Backend\Order\ExportOrderAction;
use MyParcelNL\Pdk\App\Action\Backend\Order\FetchOrdersAction;
use MyParcelNL\Pdk\App\Action\Backend\Order\PostOrderNotesAction;
@@ -31,6 +32,7 @@
use MyParcelNL\Pdk\App\Request\Account\UpdateAccountEndpointRequest;
use MyParcelNL\Pdk\App\Request\Account\UpdateSubscriptionFeaturesEndpointRequest;
use MyParcelNL\Pdk\App\Request\Context\FetchContextEndpointRequest;
+use MyParcelNL\Pdk\App\Request\Debug\DownloadLogsEndpointRequest;
use MyParcelNL\Pdk\App\Request\Orders\ExportOrdersEndpointRequest;
use MyParcelNL\Pdk\App\Request\Orders\FetchOrdersEndpointRequest;
use MyParcelNL\Pdk\App\Request\Orders\PostOrderNotesEndpointRequest;
@@ -215,5 +217,13 @@
'request' => FetchWebhooksEndpointRequest::class,
'action' => FetchWebhooksAction::class,
],
+
+ /**
+ * Download logs
+ */
+ PdkBackendActions::DOWNLOAD_LOGS => [
+ 'request' => DownloadLogsEndpointRequest::class,
+ 'action' => DownloadLogsAction::class,
+ ],
],
];
diff --git a/config/pdk-services.php b/config/pdk-services.php
index 67f16b963..379f53311 100644
--- a/config/pdk-services.php
+++ b/config/pdk-services.php
@@ -32,12 +32,14 @@
use MyParcelNL\Pdk\Base\Contract\CountryServiceInterface;
use MyParcelNL\Pdk\Base\Contract\CurrencyServiceInterface;
use MyParcelNL\Pdk\Base\Contract\WeightServiceInterface;
+use MyParcelNL\Pdk\Base\Contract\ZipServiceInterface;
use MyParcelNL\Pdk\Base\FileSystem;
use MyParcelNL\Pdk\Base\FileSystemInterface;
use MyParcelNL\Pdk\Base\Pdk;
use MyParcelNL\Pdk\Base\Service\CountryService;
use MyParcelNL\Pdk\Base\Service\CurrencyService;
use MyParcelNL\Pdk\Base\Service\WeightService;
+use MyParcelNL\Pdk\Base\Service\ZipService;
use MyParcelNL\Pdk\Carrier\Contract\CarrierRepositoryInterface;
use MyParcelNL\Pdk\Carrier\Repository\CarrierRepository;
use MyParcelNL\Pdk\Context\Contract\ContextServiceInterface;
@@ -215,4 +217,9 @@
* Handles executing webhooks.
*/
PdkWebhookManagerInterface::class => autowire(PdkWebhookManager::class),
+
+ /**
+ * Handles zipping files.
+ */
+ ZipServiceInterface::class => autowire(ZipService::class),
];
diff --git a/config/platform/belgie.php b/config/platform/belgie.php
index 0246af833..0b66a7b1c 100644
--- a/config/platform/belgie.php
+++ b/config/platform/belgie.php
@@ -11,6 +11,7 @@
'name' => 'belgie',
'human' => 'SendMyParcel',
'backofficeUrl' => 'https://backoffice.sendmyparcel.be',
+ 'supportUrl' => 'https://developer.myparcel.nl/contact',
'localCountry' => CountryCodes::CC_BE,
'defaultCarrier' => Carrier::CARRIER_BPOST_NAME,
'defaultCarrierId' => Carrier::CARRIER_BPOST_ID,
diff --git a/config/platform/flespakket.php b/config/platform/flespakket.php
index e7772cb84..a158a2127 100644
--- a/config/platform/flespakket.php
+++ b/config/platform/flespakket.php
@@ -12,6 +12,7 @@
'name' => 'flespakket',
'human' => 'Flespakket',
'backofficeUrl' => 'https://backoffice.flespakket.nl',
+ 'supportUrl' => 'https://developer.myparcel.nl/contact',
'localCountry' => CountryCodes::CC_NL,
'defaultCarrier' => Carrier::CARRIER_POSTNL_NAME,
'defaultCarrierId' => Carrier::CARRIER_POSTNL_ID,
diff --git a/config/platform/myparcel.php b/config/platform/myparcel.php
index 9b7ccec33..3f30acba7 100644
--- a/config/platform/myparcel.php
+++ b/config/platform/myparcel.php
@@ -12,6 +12,7 @@
'name' => 'myparcel',
'human' => 'MyParcel',
'backofficeUrl' => 'https://backoffice.myparcel.nl',
+ 'supportUrl' => 'https://developer.myparcel.nl/contact',
'localCountry' => CountryCodes::CC_NL,
'defaultCarrier' => Carrier::CARRIER_POSTNL_NAME,
'defaultCarrierId' => Carrier::CARRIER_POSTNL_ID,
diff --git a/docker-compose.yml b/docker-compose.yml
index cd156a08a..bef1a8019 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,19 +1,24 @@
-x-image: &image ghcr.io/myparcelnl/php-xd:7.4-cli-alpine
+x-common: &common
+ build:
+ context: .
+ dockerfile: Dockerfile
+ # Set in .env
+ image: $IMAGE_NAME
services:
php:
- image: *image
- command: ['composer', 'install', '--no-interaction', '--no-progress', '--no-suggest']
+ <<: *common
init: true
+ command: ['composer', 'install', '--no-interaction', '--no-progress']
env_file:
- - .env.local
+ - .env
volumes:
- ./:/app
console:
- image: *image
- command: []
+ <<: *common
init: true
+ command: []
entrypoint: ['php', 'bin/console']
volumes:
- ./:/app
diff --git a/src/App/Action/Backend/Debug/DownloadLogsAction.php b/src/App/Action/Backend/Debug/DownloadLogsAction.php
new file mode 100644
index 000000000..c76be180a
--- /dev/null
+++ b/src/App/Action/Backend/Debug/DownloadLogsAction.php
@@ -0,0 +1,87 @@
+zipService = $zipService;
+ }
+
+ /**
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ *
+ * @return \Symfony\Component\HttpFoundation\Response
+ */
+ public function handle(Request $request): Response
+ {
+ $path = $this->createZipPath();
+
+ $this->createLogsZip($path);
+
+ $response = new BinaryFileResponse($path);
+ $disposition = HeaderUtils::makeDisposition(HeaderUtils::DISPOSITION_ATTACHMENT, basename($path));
+
+ $response->headers->set('Content-Type', 'application/zip');
+ $response->headers->set('Content-Disposition', $disposition);
+ $response->deleteFileAfterSend();
+
+ return $response;
+ }
+
+ /**
+ * @param string $path
+ *
+ * @return void
+ */
+ protected function createLogsZip(string $path): void
+ {
+ $logFiles = Logger::getLogFiles();
+
+ if (empty($logFiles)) {
+ throw new RuntimeException('No log files found');
+ }
+
+ $this->zipService->create($path);
+
+ foreach ($logFiles as $filePath) {
+ $this->zipService->addFile($filePath);
+ }
+
+ $this->zipService->close();
+ }
+
+ /**
+ * @return string
+ */
+ protected function createZipPath(): string
+ {
+ $appInfo = Pdk::getAppInfo();
+
+ $timestamp = date('Y-m-d_H-i-s');
+ $filename = "logs/{$timestamp}_{$appInfo->name}_logs.zip";
+
+ return $appInfo->createPath($filename);
+ }
+}
diff --git a/src/App/Api/Backend/PdkBackendActions.php b/src/App/Api/Backend/PdkBackendActions.php
index 1f31c7bf6..a96dd9e04 100644
--- a/src/App/Api/Backend/PdkBackendActions.php
+++ b/src/App/Api/Backend/PdkBackendActions.php
@@ -31,4 +31,6 @@ final class PdkBackendActions
public const CREATE_WEBHOOKS = 'createWebhooks';
public const DELETE_WEBHOOKS = 'deleteWebhooks';
public const FETCH_WEBHOOKS = 'fetchWebhooks';
+ // Debugging
+ public const DOWNLOAD_LOGS = 'downloadLogs';
}
diff --git a/src/App/Request/Debug/DownloadLogsEndpointRequest.php b/src/App/Request/Debug/DownloadLogsEndpointRequest.php
new file mode 100644
index 000000000..c8c2c98c9
--- /dev/null
+++ b/src/App/Request/Debug/DownloadLogsEndpointRequest.php
@@ -0,0 +1,23 @@
+ 'application/zip',
+ ];
+ }
+
+ public function getMethod(): string
+ {
+ return HttpRequest::METHOD_POST;
+ }
+}
diff --git a/src/Base/Contract/ZipServiceInterface.php b/src/Base/Contract/ZipServiceInterface.php
new file mode 100644
index 000000000..2bcabf9f0
--- /dev/null
+++ b/src/Base/Contract/ZipServiceInterface.php
@@ -0,0 +1,28 @@
+ 'string',
'url' => 'string',
];
+
+ /**
+ * @param string $path
+ *
+ * @return string
+ */
+ public function createPath(string $path): string
+ {
+ $pattern = sprintf('/\%s+/', DIRECTORY_SEPARATOR);
+
+ return preg_replace($pattern, DIRECTORY_SEPARATOR, "$this->path/$path");
+ }
}
diff --git a/src/Base/Service/ZipService.php b/src/Base/Service/ZipService.php
new file mode 100644
index 000000000..bf11764b2
--- /dev/null
+++ b/src/Base/Service/ZipService.php
@@ -0,0 +1,114 @@
+fileSystem = $fileSystem;
+ }
+
+ /**
+ * @param string $filename
+ * @param null|string $targetFilename
+ *
+ * @return void
+ * @throws \MyParcelNL\Pdk\Base\Exception\ZipException
+ */
+ public function addFile(string $filename, ?string $targetFilename = null): void
+ {
+ $this->validateHasFile();
+
+ $success = $this->currentFile->addFile($filename, $targetFilename ?? basename($filename));
+
+ if (! $success) {
+ throw new ZipException('Failed to add file to zip');
+ }
+ }
+
+ /**
+ * @param string $string
+ * @param string $targetFilename
+ *
+ * @return void
+ * @throws \MyParcelNL\Pdk\Base\Exception\ZipException
+ */
+ public function addFromString(string $string, string $targetFilename): void
+ {
+ $this->validateHasFile();
+ $this->currentFile->addFromString($targetFilename, $string);
+ }
+
+ /**
+ * @return void
+ * @throws \MyParcelNL\Pdk\Base\Exception\ZipException
+ */
+ public function close(): void
+ {
+ $this->validateHasFile();
+
+ try {
+ $this->currentFile->close();
+ $this->currentFile = null;
+ } catch (Throwable $e) {
+ throw new ZipException('Failed to close zip file', 0, $e);
+ }
+ }
+
+ /**
+ * @param string $filename
+ *
+ * @return void
+ * @throws \MyParcelNL\Pdk\Base\Exception\ZipException
+ */
+ public function create(string $filename): void
+ {
+ $zip = new ZipArchive();
+ $dirname = $this->fileSystem->dirname($filename);
+
+ $this->fileSystem->mkdir($dirname, true);
+
+ $status = $zip->open($filename, ZipArchive::CREATE);
+
+ if (true === $status) {
+ $this->currentFile = $zip;
+ } else {
+ throw new ZipException("Failed to create zip file. Error code: $status");
+ }
+ }
+
+ /**
+ * @return void
+ * @throws \MyParcelNL\Pdk\Base\Exception\ZipException
+ */
+ private function validateHasFile(): void
+ {
+ if (null !== $this->currentFile) {
+ return;
+ }
+
+ throw new ZipException('No zip file is open');
+ }
+}
diff --git a/src/Context/Model/GlobalContext.php b/src/Context/Model/GlobalContext.php
index 51213de63..cc121f228 100644
--- a/src/Context/Model/GlobalContext.php
+++ b/src/Context/Model/GlobalContext.php
@@ -79,6 +79,7 @@ public function __construct(?array $data = null)
'name',
'human',
'backofficeUrl',
+ 'supportUrl',
'localCountry',
'defaultCarrier',
'defaultCarrierId',
diff --git a/src/Facade/Logger.php b/src/Facade/Logger.php
index 460ed741c..0dcd7bb3f 100644
--- a/src/Facade/Logger.php
+++ b/src/Facade/Logger.php
@@ -19,6 +19,7 @@
* @method static void notice($message, array $context = [])
* @method static void warning($message, array $context = [])
* @method static void deprecated(string $subject, string $replacement = null, array $context = [])
+ * @method static array getLogFiles()
* @see \MyParcelNL\Pdk\Logger\Contract\PdkLoggerInterface
*/
final class Logger extends Facade
diff --git a/src/Logger/AbstractLogger.php b/src/Logger/AbstractLogger.php
index 4dc32571a..5e6117848 100644
--- a/src/Logger/AbstractLogger.php
+++ b/src/Logger/AbstractLogger.php
@@ -95,6 +95,16 @@ public function error($message, array $context = []): void
$this->createLog(LogLevel::ERROR, $message, $context);
}
+ /**
+ * @TODO: remove this default in v3.0.0, for now it's here to prevent breaking changes
+ * @return string[]
+ * @codeCoverageIgnore
+ */
+ public function getLogFiles(): array
+ {
+ return [];
+ }
+
/**
* @param string $message
* @param array $context
diff --git a/src/Logger/Contract/PdkLoggerInterface.php b/src/Logger/Contract/PdkLoggerInterface.php
index 2fd8cc900..ba3e4615b 100644
--- a/src/Logger/Contract/PdkLoggerInterface.php
+++ b/src/Logger/Contract/PdkLoggerInterface.php
@@ -15,9 +15,12 @@ interface PdkLoggerInterface extends LoggerInterface
*
* @return void
*/
- public function deprecated(
- string $subject,
- ?string $replacement = null,
- array $context = []
- ): void;
+ public function deprecated(string $subject, ?string $replacement = null, array $context = []): void;
+
+ /**
+ * Get all logs as an associative array with log levels as keys and log file paths as values.
+ *
+ * @return string[]
+ */
+ public function getLogFiles(): array;
}
diff --git a/tests/Bootstrap/MockLogger.php b/tests/Bootstrap/MockLogger.php
index 8e33d25d4..7227a5da1 100644
--- a/tests/Bootstrap/MockLogger.php
+++ b/tests/Bootstrap/MockLogger.php
@@ -4,15 +4,59 @@
namespace MyParcelNL\Pdk\Tests\Bootstrap;
+use MyParcelNL\Pdk\Base\FileSystemInterface;
+use MyParcelNL\Pdk\Facade\Pdk;
use MyParcelNL\Pdk\Logger\AbstractLogger;
+use Psr\Log\LogLevel;
+use function array_filter;
+use function array_reduce;
+use function json_encode;
class MockLogger extends AbstractLogger
{
+ private const ALL_LOG_LEVELS = [
+ LogLevel::ALERT,
+ LogLevel::CRITICAL,
+ LogLevel::DEBUG,
+ LogLevel::EMERGENCY,
+ LogLevel::ERROR,
+ LogLevel::INFO,
+ LogLevel::NOTICE,
+ LogLevel::WARNING,
+ ];
+
+ /**
+ * @var \MyParcelNL\Pdk\Base\FileSystemInterface
+ */
+ private $fileSystem;
+
/**
* @var array
*/
private $logs = [];
+ /**
+ * @var array
+ */
+ private $streams = [];
+
+ /**
+ * @param \MyParcelNL\Pdk\Base\FileSystemInterface $fileSystem
+ */
+ public function __construct(FileSystemInterface $fileSystem)
+ {
+ $this->fileSystem = $fileSystem;
+
+ $appInfo = Pdk::getAppInfo();
+ $this->fileSystem->mkdir($appInfo->createPath('logs'), true);
+
+ foreach (self::ALL_LOG_LEVELS as $level) {
+ $filename = $appInfo->createPath("logs/test_$level.log");
+
+ $this->streams[$level] = $this->fileSystem->openStream($filename, 'w');
+ }
+ }
+
/**
* @return void
*/
@@ -22,7 +66,21 @@ public function clear(): void
}
/**
- * @return void
+ * @return string[]
+ */
+ public function getLogFiles(): array
+ {
+ $appInfo = Pdk::getAppInfo();
+
+ return array_reduce(self::ALL_LOG_LEVELS, static function (array $acc, string $level) use ($appInfo) {
+ $acc[$level] = $appInfo->createPath("logs/test_$level.log");
+
+ return $acc;
+ }, []);
+ }
+
+ /**
+ * @return array
*/
public function getLogs(): array
{
@@ -43,5 +101,13 @@ public function log($level, $message, array $context = []): void
'message' => $message,
'context' => $context,
];
+
+ $formattedString = implode(' ', array_filter([
+ "!$level!",
+ $message,
+ empty($context) ? null : json_encode($context),
+ ]));
+
+ $this->fileSystem->writeToStream($this->streams[$level], $formattedString . PHP_EOL);
}
}
diff --git a/tests/Bootstrap/MockPdkConfig.php b/tests/Bootstrap/MockPdkConfig.php
index 1cd4c0aed..64246b205 100644
--- a/tests/Bootstrap/MockPdkConfig.php
+++ b/tests/Bootstrap/MockPdkConfig.php
@@ -69,7 +69,7 @@ private static function getDefaultConfig(): array
'name' => 'pest',
'title' => 'Pest',
'version' => '1.0.0',
- 'path' => 'APP_PATH',
+ 'path' => '/app/.tmp/',
'url' => 'APP_URL',
]);
}),
diff --git a/tests/Hook/DeleteTemporaryFilesHook.php b/tests/Hook/DeleteTemporaryFilesHook.php
index 6e5cd9a9d..5a97f3c50 100644
--- a/tests/Hook/DeleteTemporaryFilesHook.php
+++ b/tests/Hook/DeleteTemporaryFilesHook.php
@@ -6,43 +6,15 @@
namespace MyParcelNL\Pdk\Tests\Hook;
use PHPUnit\Runner\AfterLastTestHook;
+use function MyParcelNL\Pdk\Tests\deleteTemporaryFiles;
final class DeleteTemporaryFilesHook implements AfterLastTestHook
{
- private const TMP_DIR = __DIR__ . '/../../.tmp';
-
/**
* @return void
*/
public function executeAfterLastTest(): void
{
- $this->deleteDirectory(self::TMP_DIR);
- }
-
- /**
- * @param string $dir
- * @param bool $deleteDir
- *
- * @return void
- */
- private function deleteDirectory(string $dir, bool $deleteDir = false): void
- {
- $paths = scandir($dir);
-
- foreach ($paths as $path) {
- if ('.' === $path || '..' === $path) {
- continue;
- }
-
- if (is_dir("$dir/$path")) {
- $this->deleteDirectory("$dir/$path", true);
- } else {
- unlink("$dir/$path");
- }
- }
-
- if ($deleteDir) {
- rmdir($dir);
- }
+ deleteTemporaryFiles();
}
}
diff --git a/tests/Unit/App/Action/Backend/Debug/DownloadLogsActionTest.php b/tests/Unit/App/Action/Backend/Debug/DownloadLogsActionTest.php
new file mode 100644
index 000000000..e9cfe600a
--- /dev/null
+++ b/tests/Unit/App/Action/Backend/Debug/DownloadLogsActionTest.php
@@ -0,0 +1,75 @@
+ get(FileSystem::class),
+]));
+
+test('it downloads logs', function () {
+ // Warning and notice are not called to check if they're omitted from the created zip for being empty.
+ Logger::emergency('emergency message');
+ Logger::alert('hi');
+ Logger::critical('some string');
+ Logger::error('error message');
+ Logger::info('info message with context', ['some' => 'context']);
+ Logger::debug('debug message');
+ Logger::debug('debug message 2');
+ Logger::debug('debug message 3');
+
+ $request = new Request(['action' => PdkBackendActions::DOWNLOAD_LOGS]);
+
+ /** @var \Symfony\Component\HttpFoundation\BinaryFileResponse $response */
+ $response = Actions::execute($request);
+
+ $file = $response->getFile();
+
+ expect($response)
+ ->toBeInstanceOf(BinaryFileResponse::class)
+ ->and($response->getStatusCode())
+ ->toBe(200)
+ ->and($file->isFile())
+ ->toBeTrue()
+ ->and($file->getFilename())
+ ->toMatch('/\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}_pest_logs.zip$/');
+
+ // Check if the returned zip file contains the logs
+ $logs = readZip($file->getPathname());
+
+ expect($logs)->toEqual([
+ 'test_emergency.log' => '!emergency! [PDK]: emergency message' . PHP_EOL,
+ 'test_alert.log' => '!alert! [PDK]: hi' . PHP_EOL,
+ 'test_critical.log' => '!critical! [PDK]: some string' . PHP_EOL,
+ 'test_error.log' => '!error! [PDK]: error message' . PHP_EOL,
+ 'test_info.log' => '!info! [PDK]: info message with context {"some":"context"}' . PHP_EOL,
+ 'test_debug.log' => implode(PHP_EOL, [
+ '!debug! [PDK]: debug message',
+ '!debug! [PDK]: debug message 2',
+ '!debug! [PDK]: debug message 3',
+ ]) . PHP_EOL,
+ 'test_notice.log' => '',
+ 'test_warning.log' => '',
+ ]);
+
+ // Send the response to check if the created file is deleted after sending
+ $response->send();
+
+ expect($file->isFile())->toBeFalse();
+});
diff --git a/tests/Unit/Base/Model/AppInfoTest.php b/tests/Unit/Base/Model/AppInfoTest.php
new file mode 100644
index 000000000..31bdd949d
--- /dev/null
+++ b/tests/Unit/Base/Model/AppInfoTest.php
@@ -0,0 +1,28 @@
+ "/some/path$trailing",
+ ]);
+
+ expect($appInfo->createPath('my_file.txt'))
+ ->toBe('/some/path/my_file.txt')
+ ->and($appInfo->createPath('/with/slashes/vroom.txt'))
+ ->toBe('/some/path/with/slashes/vroom.txt')
+ ->and($appInfo->createPath('/with//some//more////slashes//yikes.txt'))
+ ->toBe('/some/path/with/some/more/slashes/yikes.txt');
+})->with([
+ 'path without trailing slash' => [''],
+ 'path with trailing slash' => ['/'],
+ 'path with a lot of trailing slashes' => ['////'],
+]);
diff --git a/tests/Unit/Base/Service/ZipServiceTest.php b/tests/Unit/Base/Service/ZipServiceTest.php
new file mode 100644
index 000000000..39f0121cf
--- /dev/null
+++ b/tests/Unit/Base/Service/ZipServiceTest.php
@@ -0,0 +1,152 @@
+createPath('test.zip');
+
+ $zipService->create($filename);
+ $zipService->addFromString('test', 'test.txt');
+ $zipService->close();
+
+ expect($fileSystem->fileExists($filename))->toBeTrue();
+
+ $contents = readZip($filename);
+
+ expect($contents)->toEqual([
+ 'test.txt' => 'test',
+ ]);
+});
+
+it('adds files to a zip', function () {
+ /** @var \MyParcelNL\Pdk\Base\Contract\ZipServiceInterface $zipService */
+ $zipService = Pdk::get(ZipServiceInterface::class);
+ /** @var \MyParcelNL\Pdk\Base\FileSystem $fileSystem */
+ $fileSystem = Pdk::get(FileSystemInterface::class);
+
+ $appInfo = Pdk::getAppInfo();
+ $filename = $appInfo->createPath('test-from-files.zip');
+
+ $fileSystem->put($appInfo->createPath('some-file.txt'), 'test some file');
+ $fileSystem->put($appInfo->createPath('some-other-file.txt'), 'test some other file');
+
+ $zipService->create($filename);
+
+ $zipService->addFile($appInfo->createPath('some-file.txt'));
+ $zipService->addFile($appInfo->createPath('some-other-file.txt'), 'nested/some-renamed-file.txt');
+
+ $zipService->close();
+
+ expect($fileSystem->fileExists($filename))->toBeTrue();
+
+ $contents = readZip($filename);
+
+ expect($contents)->toEqual([
+ 'some-file.txt' => 'test some file',
+ 'nested/some-renamed-file.txt' => 'test some other file',
+ ]);
+});
+
+it('throws error when calling method while no zip is open', function (callable $callback) {
+ /** @var \MyParcelNL\Pdk\Base\Contract\ZipServiceInterface $zipService */
+ $zipService = Pdk::get(ZipServiceInterface::class);
+ /** @var \MyParcelNL\Pdk\Base\FileSystem $fileSystem */
+ $fileSystem = Pdk::get(FileSystemInterface::class);
+
+ $callback($zipService, $fileSystem);
+})
+ ->throws(ZipException::class)
+ ->with([
+ 'addFromString' => function () {
+ return function (ZipServiceInterface $zipService) {
+ $zipService->addFromString('test', 'test.txt');
+ };
+ },
+
+ 'addFile' => function () {
+ return function (ZipServiceInterface $zipService, FileSystemInterface $fileSystem) {
+ $appInfo = Pdk::getAppInfo();
+ $path = $appInfo->createPath('some-file.txt');
+
+ $fileSystem->put($path, 'test some file');
+ $zipService->addFile($path);
+ };
+ },
+
+ 'close' => function () {
+ return function (ZipServiceInterface $zipService) {
+ $zipService->close();
+ };
+ },
+ ]);
+
+it('throws error when adding a file fails', function () {
+ $appInfo = Pdk::getAppInfo();
+ /** @var \MyParcelNL\Pdk\Base\Contract\ZipServiceInterface $zipService */
+ $zipService = Pdk::get(ZipServiceInterface::class);
+
+ $zipFilename = $appInfo->createPath('test.zip');
+
+ $zipService->create($zipFilename);
+
+ // Adding file that does not exist
+ $zipService->addFile($appInfo->createPath('some-file.txt'));
+})->throws(ZipException::class);
+
+it('throws error when fails to open zip file', function () {
+ /** @var \MyParcelNL\Pdk\Base\Contract\ZipServiceInterface $zipService */
+ $zipService = Pdk::get(ZipServiceInterface::class);
+ /** @var \MyParcelNL\Pdk\Base\FileSystem $fileSystem */
+ $fileSystem = Pdk::get(FileSystemInterface::class);
+
+ $appInfo = Pdk::getAppInfo();
+ $path = $appInfo->createPath('some-file.txt');
+
+ // Creating file in the place of the zip file
+ $fileSystem->put($path, 'test some file');
+
+ // Throws exception because file already exists
+ $zipService->create($path);
+})->throws(ZipException::class);
+
+it('throws error when closing zip file fails', function () {
+ /** @var \MyParcelNL\Pdk\Base\Contract\ZipServiceInterface $zipService */
+ $zipService = Pdk::get(ZipServiceInterface::class);
+ /** @var \MyParcelNL\Pdk\Base\FileSystem $fileSystem */
+ $fileSystem = Pdk::get(FileSystemInterface::class);
+
+ $appInfo = Pdk::getAppInfo();
+ $zipFilename = $appInfo->createPath('test.zip');
+
+ $zipService->create($zipFilename);
+
+ $filename = $appInfo->createPath('some-file.txt');
+ $fileSystem->put($filename, 'test some file');
+
+ // Add a file
+ $zipService->addFile($filename);
+ // Then delete that file before closing the zip, triggering exception on close.
+ $fileSystem->unlink($filename);
+
+ $zipService->close();
+})->throws(ZipException::class);
diff --git a/tests/Uses/UsesRealFileSystem.php b/tests/Uses/UsesRealFileSystem.php
new file mode 100644
index 000000000..d1a20ec0e
--- /dev/null
+++ b/tests/Uses/UsesRealFileSystem.php
@@ -0,0 +1,27 @@
+open($filename);
+
+ $files = [];
+
+ for ($i = 0; $i < $zip->numFiles; $i++) {
+ $stat = $zip->statIndex($i);
+ $contents = $zip->getFromIndex($i);
+
+ $files[$stat['name']] = $contents;
+ }
+
+ $zip->close();
+
+ return $files;
+}
+
+/**
+ * @param string $dir
+ * @param bool $deleteDir
+ *
+ * @return void
+ */
+function deleteTemporaryFiles(string $dir = TMP_DIR, bool $deleteDir = false): void
+{
+ $paths = scandir($dir);
+
+ foreach ($paths as $path) {
+ if ('.' === $path || '..' === $path) {
+ continue;
+ }
+
+ if (is_dir("$dir/$path")) {
+ deleteTemporaryFiles("$dir/$path", true);
+ } else {
+ unlink("$dir/$path");
+ }
+ }
+
+ if ($deleteDir) {
+ rmdir($dir);
+ }
+}