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); + } +}