4 Commits
Author SHA1 Message Date
haylanandClaude Bot 391db5b621 docs: match CLAUDE.md to the idempotent release and registry repack
plugin-checks / version-changelog (pull_request) Successful in 22s
plugin-checks / package (pull_request) Successful in 1m12s
plugin-checks / phpunit (pull_request) Canceled after 3m1s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 00:50:20 +02:00
haylanandClaude Bot 8afe30b476 test: assert plugin logs to injected PSR logger
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 00:50:19 +02:00
haylanandClaude Bot 5f8cc9bdad docs: refresh CLAUDE.md and compliance spec after guideline PR
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 00:49:57 +02:00
haylanandClaude Bot c08d27c8e1 fix(release): repack the zip root-level for the Composer registry; make release steps idempotent
Gitea's registry answered 400 'composer.json file is missing' for the shopware-cli zip
(single top-level folder). Repack the same prebuilt content with composer.json at the root.
Drop the tag-exists skip guard so a half-finished release can be re-run; reuse an existing
release/asset. Bump to 0.1.1.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 00:49:53 +02:00
8 changed files with 226 additions and 40 deletions

No files matched your search

+46 -25
View File
@@ -3,6 +3,10 @@ name: release
# Plugin-agnostic: owner, repo, host, package name and version come from the `github` context # Plugin-agnostic: owner, repo, host, package name and version come from the `github` context
# and composer.json. shopware-cli runs from its official Docker image, intentionally unpinned (latest). # and composer.json. shopware-cli runs from its official Docker image, intentionally unpinned (latest).
# RELEASE_TOKEN is the single token used for releases, assets and the Composer registry. # RELEASE_TOKEN is the single token used for releases, assets and the Composer registry.
#
# Every step is idempotent (release/asset reuse, registry 409 = already published), so a failed run can
# simply be re-run and picks up where it stopped. There is deliberately no "tag exists -> skip" guard:
# it made a half-finished release (tag + release but no registry package) impossible to heal.
on: on:
push: push:
@@ -23,17 +27,17 @@ jobs:
container: container:
image: ghcr.io/shopware/shopware-cli:latest image: ghcr.io/shopware/shopware-cli:latest
steps: steps:
- name: Ensure helper tools (jq, curl, unzip, git) - name: Ensure helper tools (jq, curl, unzip, zip, git)
run: | run: |
command -v jq >/dev/null && command -v curl >/dev/null && command -v unzip >/dev/null && command -v git >/dev/null && exit 0 command -v jq >/dev/null && command -v curl >/dev/null && command -v unzip >/dev/null && command -v zip >/dev/null && command -v git >/dev/null && exit 0
(apt-get update -q && apt-get install -y -q --no-install-recommends jq curl unzip git) || apk add --no-cache jq curl unzip git (apt-get update -q && apt-get install -y -q --no-install-recommends jq curl unzip zip git) || apk add --no-cache jq curl unzip zip git
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.RELEASE_TOKEN }} token: ${{ secrets.RELEASE_TOKEN }}
- name: Determine version and guard against re-release - name: Determine version, tag and file names
id: meta id: meta
run: | run: |
set -e set -e
@@ -47,21 +51,13 @@ jobs:
echo "tag=${TAG}" echo "tag=${TAG}"
echo "zip=${REPO_NAME}-${TAG}.zip" echo "zip=${REPO_NAME}-${TAG}.zip"
} >> "$GITHUB_OUTPUT" } >> "$GITHUB_OUTPUT"
if git ls-remote --tags origin "refs/tags/${TAG}" | grep -q "${TAG}"; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "Tag ${TAG} already exists, nothing to release."
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
# Reuse the zip the PR workflow already built and tested for this exact tree. # Reuse the zip the PR workflow already built and tested for this exact tree.
- name: Tree hash of this commit - name: Tree hash of this commit
if: steps.meta.outputs.skip == 'false'
id: tree id: tree
run: echo "hash=$(git rev-parse "${GITHUB_SHA}^{tree}")" >> "$GITHUB_OUTPUT" run: echo "hash=$(git rev-parse "${GITHUB_SHA}^{tree}")" >> "$GITHUB_OUTPUT"
- name: Restore the tested zip from the PR run - name: Restore the tested zip from the PR run
if: steps.meta.outputs.skip == 'false'
id: prebuilt id: prebuilt
uses: actions/cache/restore@v4 uses: actions/cache/restore@v4
with: with:
@@ -69,18 +65,16 @@ jobs:
key: plugin-zip-${{ steps.tree.outputs.hash }} key: plugin-zip-${{ steps.tree.outputs.hash }}
- name: Name the prebuilt zip - name: Name the prebuilt zip
if: steps.meta.outputs.skip == 'false' && steps.prebuilt.outputs.cache-hit == 'true' if: steps.prebuilt.outputs.cache-hit == 'true'
run: mv .build/package.zip ".build/${{ steps.meta.outputs.zip }}" run: mv .build/package.zip ".build/${{ steps.meta.outputs.zip }}"
# Packages only on a cache miss; always validates and asserts what ships (guards a bad cache entry). # Packages only on a cache miss; always validates and asserts what ships (guards a bad cache entry).
- uses: ./.gitea/actions/package - uses: ./.gitea/actions/package
if: steps.meta.outputs.skip == 'false'
with: with:
filename: ${{ steps.meta.outputs.zip }} filename: ${{ steps.meta.outputs.zip }}
build: ${{ steps.prebuilt.outputs.cache-hit != 'true' }} build: ${{ steps.prebuilt.outputs.cache-hit != 'true' }}
- name: Extract changelog section - name: Extract changelog section
if: steps.meta.outputs.skip == 'false'
run: | run: |
awk -v ver="${{ steps.meta.outputs.version }}" ' awk -v ver="${{ steps.meta.outputs.version }}" '
BEGIN { found=0 } BEGIN { found=0 }
@@ -93,26 +87,54 @@ jobs:
cat release-notes.md cat release-notes.md
- name: Create Gitea release (creates the tag) and upload the zip - name: Create Gitea release (creates the tag) and upload the zip
if: steps.meta.outputs.skip == 'false'
env: env:
TOKEN: ${{ secrets.RELEASE_TOKEN }} TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: | run: |
set -e set -e
API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}" API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
BODY=$(jq -n --arg tag "${{ steps.meta.outputs.tag }}" --arg sha "$GITHUB_SHA" --rawfile notes release-notes.md \ TAG="${{ steps.meta.outputs.tag }}"
ZIP="${{ steps.meta.outputs.zip }}"
CODE=$(curl -s -o /tmp/release.json -w "%{http_code}" -H "Authorization: token ${TOKEN}" "${API}/releases/tags/${TAG}")
if [ "$CODE" = "200" ]; then
echo "Release ${TAG} already exists, reusing it."
else
BODY=$(jq -n --arg tag "$TAG" --arg sha "$GITHUB_SHA" --rawfile notes release-notes.md \
'{tag_name:$tag, target_commitish:$sha, name:$tag, body:$notes}') '{tag_name:$tag, target_commitish:$sha, name:$tag, body:$notes}')
RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -d "$BODY" "${API}/releases" | jq -r .id) curl -sf -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -d "$BODY" "${API}/releases" > /tmp/release.json
fi
RELEASE_ID=$(jq -r .id /tmp/release.json)
echo "release id: ${RELEASE_ID}" echo "release id: ${RELEASE_ID}"
curl -sf -H "Authorization: token ${TOKEN}" \ if jq -e --arg n "$ZIP" '.assets[]? | select(.name == $n)' /tmp/release.json > /dev/null; then
-F "attachment=@.build/${{ steps.meta.outputs.zip }}" \ echo "Asset ${ZIP} already uploaded."
"${API}/releases/${RELEASE_ID}/assets?name=${{ steps.meta.outputs.zip }}" > /dev/null else
curl -sf -H "Authorization: token ${TOKEN}" -F "attachment=@.build/${ZIP}" \
"${API}/releases/${RELEASE_ID}/assets?name=${ZIP}" > /dev/null
fi
- name: Publish the same zip to the Composer registry # Gitea's Composer registry needs composer.json at the zip root, but shopware-cli zips wrap
if: steps.meta.outputs.skip == 'false' # everything in one top-level folder (the registry answers 400 "composer.json file is missing").
# Same prebuilt content, repacked root-level.
- name: Repack the zip for the Composer registry
run: |
set -e
rm -rf /tmp/repack && mkdir /tmp/repack
unzip -q ".build/${{ steps.meta.outputs.zip }}" -d /tmp/repack
if [ -f /tmp/repack/composer.json ]; then
ROOT=/tmp/repack
else
ROOT=$(find /tmp/repack -mindepth 1 -maxdepth 1 -type d | head -1)
fi
test -f "$ROOT/composer.json" || { echo "::error::no composer.json found in the packaged zip"; exit 1; }
OUT="$PWD/.build/composer-${{ steps.meta.outputs.zip }}"
(cd "$ROOT" && zip -qr "$OUT" .)
unzip -l "$OUT" | grep -E ' composer\.json$' > /dev/null || { echo "::error::repacked zip has no root-level composer.json"; exit 1; }
echo "repacked: $OUT"
- name: Publish to the Composer registry
run: | run: |
STATUS=$(curl -s -o /tmp/composer-publish.log -w "%{http_code}" \ STATUS=$(curl -s -o /tmp/composer-publish.log -w "%{http_code}" \
-u "${GITHUB_REPOSITORY_OWNER}:${{ secrets.RELEASE_TOKEN }}" \ -u "${GITHUB_REPOSITORY_OWNER}:${{ secrets.RELEASE_TOKEN }}" \
-T ".build/${{ steps.meta.outputs.zip }}" \ -T ".build/composer-${{ steps.meta.outputs.zip }}" \
"${GITHUB_SERVER_URL}/api/packages/${GITHUB_REPOSITORY_OWNER}/composer") "${GITHUB_SERVER_URL}/api/packages/${GITHUB_REPOSITORY_OWNER}/composer")
cat /tmp/composer-publish.log cat /tmp/composer-publish.log
if [ "$STATUS" != "201" ] && [ "$STATUS" != "409" ]; then if [ "$STATUS" != "201" ] && [ "$STATUS" != "409" ]; then
@@ -121,7 +143,6 @@ jobs:
fi fi
- name: Link Composer package to this repository - name: Link Composer package to this repository
if: steps.meta.outputs.skip == 'false'
run: | run: |
ENCODED_NAME=$(printf '%s' "${{ steps.meta.outputs.package_name }}" | sed 's#/#%2F#g') ENCODED_NAME=$(printf '%s' "${{ steps.meta.outputs.package_name }}" | sed 's#/#%2F#g')
STATUS=$(curl -s -o /dev/null -w "%{http_code}" -u "${GITHUB_REPOSITORY_OWNER}:${{ secrets.RELEASE_TOKEN }}" -X POST \ STATUS=$(curl -s -o /dev/null -w "%{http_code}" -u "${GITHUB_REPOSITORY_OWNER}:${{ secrets.RELEASE_TOKEN }}" -X POST \
+7
View File
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.1.1] - 2026-10-04
### Fixed
- Release pipeline: the Composer registry rejected the packaged zip (`composer.json file is missing`) because `shopware-cli` zips wrap everything in a top-level folder. The same prebuilt content is now repacked root-level for the registry upload.
- Release steps are idempotent (existing release and asset are reused, registry 409 counts as published) and the "tag exists -> skip" guard is gone, so a failed release can be re-run to completion.
## [0.1.0] - 2026-10-03 ## [0.1.0] - 2026-10-03
### Changed ### Changed
+28 -2
View File
@@ -20,7 +20,9 @@ the full build spec live under `.scratch/admin-dump-manager/` and `.scratch/dump
- Plugin store-config UI fields (e.g. `maxDumps`, `retantionDays`) are defined in - Plugin store-config UI fields (e.g. `maxDumps`, `retantionDays`) are defined in
`src/Resources/config/config.xml` and read at runtime via Shopware's SystemConfigService. `src/Resources/config/config.xml` and read at runtime via Shopware's SystemConfigService.
- `src/Resources/app/administration/` holds the admin UI module (Vue-based Shopware Administration - `src/Resources/app/administration/` holds the admin UI module (Vue-based Shopware Administration
extension), currently just the `swag-example` scaffold module. extension). The `aeon-dump-manager` module (`src/module/aeon-dump-manager/`) provides the list page
(`page/aeon-dump-manager-list`), en-GB/de-DE snippets and ACL privilege roles (`acl/`: `viewer`,
`creator`, `deleter`); `src/service/dump-api.service.ts` is the client for the dump API routes.
## Running the plugin ## Running the plugin
@@ -51,6 +53,10 @@ activates this plugin inside a real Shopware kernel for the test run (`KERNEL_CL
`Shopware\Core\Kernel` in `phpunit.xml`). This means tests require a working Shopware installation/DB `Shopware\Core\Kernel` in `phpunit.xml`). This means tests require a working Shopware installation/DB
to bootstrap against — they are not runnable as an isolated PHP package. to bootstrap against — they are not runnable as an isolated PHP package.
In CI (`phpunit` job) a Shopware 6.6 project is prepared once (`composer create-project` + phpunit,
cached monthly), the plugin is copied into `custom/plugins`, and `system:install` runs once into
`shopware_test` (MariaDB 10.11 service), so the bootstrapper only activates the plugin.
```bash ```bash
vendor/bin/phpunit # full suite vendor/bin/phpunit # full suite
vendor/bin/phpunit --filter TestClassName # single test class vendor/bin/phpunit --filter TestClassName # single test class
@@ -59,10 +65,30 @@ vendor/bin/phpunit tests/SomeTest.php # single file
## Dependencies ## Dependencies
- PHP `^8.1`, `shopware/core` `~6.6.0` (from `composer.json`); `phpunit/phpunit` `^10.0` as the only - PHP `^8.2`, `shopware/core` `~6.6.0` (from `composer.json`); `phpunit/phpunit` `^10.0` as the only
dev dependency. dev dependency.
- PSR-4 autoload: `AeonDumpManager\` → `src/`, `AeonDumpManager\Tests\` → `tests/`. - PSR-4 autoload: `AeonDumpManager\` → `src/`, `AeonDumpManager\Tests\` → `tests/`.
## CI / release pipeline
Gitea workflows in `.gitea/`, plugin-agnostic (name/version come from `composer.json`). `shopware-cli`
runs from its official Docker image, intentionally unpinned (`latest`): a red build after a CLI release
means fix the config.
- `workflows/plugin-checks.yml` (PRs into `main`) has three jobs: `version-changelog` (every PR into
`main` must bump `composer.json` version and add a matching `## [x.y.z]` CHANGELOG heading), `package`
(shared composite action `actions/package`: `shopware-cli extension package`, `extension validate` on
the zip, `scripts/ci/assert-built-assets.sh`), and `phpunit`. The package job caches the tested zip
keyed by the commit's tree hash.
- `workflows/release.yml` (push to `main`, serialized; every step is idempotent so a failed run can simply be re-run): reuses the
cached zip when the tree hash matches (else packages again, always re-validates), creates the Gitea
release + tag via API with the zip as asset, publishes a root-level repack of the zip to the Composer registry
(Gitea requires `composer.json` at the zip root), then links the
package to the repo. Uses `RELEASE_TOKEN`.
- Required checks on `main` (`plugin-checks / version-changelog|package|phpunit (pull_request)`) are set
once with `scripts/protect-main.sh` (`tea api`). Build excludes/validation live in
`.config/shopware-extension.yml`. Background: `docs/spec/shopware-guideline-compliance.md`.
## Adding functionality ## Adding functionality
- New console commands go in `src/Command/`, new scheduled tasks in `src/ScheduledTask/` — both must - New console commands go in `src/Command/`, new scheduled tasks in `src/ScheduledTask/` — both must
+1 -1
View File
@@ -3,7 +3,7 @@
"description": "Export database dumps for Shopware 6 environments.", "description": "Export database dumps for Shopware 6 environments.",
"type": "shopware-platform-plugin", "type": "shopware-platform-plugin",
"license": "MIT", "license": "MIT",
"version": "0.1.0", "version": "0.1.1",
"keywords": [ "keywords": [
"shopware", "shopware",
"shopware6", "shopware6",
+16 -12
View File
@@ -10,8 +10,9 @@ on branches `research/shopware-guideline-checklist`, `research/shopware-cli-ci-b
- Workflows are **plugin-agnostic**: no plugin name, package name, owner, repo or host is hard-coded; - Workflows are **plugin-agnostic**: no plugin name, package name, owner, repo or host is hard-coded;
all derive from `composer.json` and the `gitea` context. The same files must work unchanged for a all derive from `composer.json` and the `gitea` context. The same files must work unchanged for a
second plugin and be extractable to Gitea's shared-workflow repository later. second plugin and be extractable to Gitea's shared-workflow repository later.
- One artifact: the `shopware-cli extension package` zip (prebuilt admin/storefront JS) is used for - One artifact: the `shopware-cli extension package` zip (prebuilt admin/storefront JS) is the Gitea
both the Gitea Release asset and the Composer registry. Release asset; the Composer registry gets the same content repacked root-level (the registry needs
`composer.json` at the zip root, see section 4).
- `shopware-cli` is intentionally **unpinned** (`latest`). Known tradeoff: a CLI release can turn PRs - `shopware-cli` is intentionally **unpinned** (`latest`). Known tradeoff: a CLI release can turn PRs
red without a repo change; treat that as a signal to fix config. red without a repo change; treat that as a signal to fix config.
- `RELEASE_TOKEN` is the only token (release, assets, registry publish, link). Workflows still declare - `RELEASE_TOKEN` is the only token (release, assets, registry publish, link). Workflows still declare
@@ -34,7 +35,9 @@ on branches `research/shopware-guideline-checklist`, `research/shopware-cli-ci-b
### PR workflows ### PR workflows
Jobs (all required checks on `main`): Jobs (all required checks on `main`):
`pr-version-check.yml` (job `version-changelog`) is unchanged apart from `permissions:`; its name keeps the existing required context. New `plugin-checks.yml`: `plugin-checks.yml` (the former `pr-version-check.yml` was deleted; `version-changelog` is now a job in it):
0. `version-changelog` - version bump vs `main` and matching CHANGELOG heading.
1. `package` - `extension package` with `--git-commit "$GITHUB_SHA"`, `extension validate` on the resulting zip (hard gate; replaces a separate directory validate, which only repeats those checks), then `scripts/ci/assert-built-assets.sh` (built admin/storefront JS present when sources exist). All shopware-cli jobs run in `ghcr.io/shopware/shopware-cli:latest`; npm and shopware-cli caches via `actions/cache`. 1. `package` - `extension package` with `--git-commit "$GITHUB_SHA"`, `extension validate` on the resulting zip (hard gate; replaces a separate directory validate, which only repeats those checks), then `scripts/ci/assert-built-assets.sh` (built admin/storefront JS present when sources exist). All shopware-cli jobs run in `ghcr.io/shopware/shopware-cli:latest`; npm and shopware-cli caches via `actions/cache`.
2. `phpunit` - `composer create-project shopware/production` (6.6.x) + MariaDB service, copy the plugin into `custom/plugins`, install/activate, run phpunit. Runs in the same `shopware-cli` image (PHP and Composer included; `shivammathur/setup-php` hangs on this runner and apt installs cost ~4.5 min). MariaDB 10.11, because 11.4 broke Shopware's own migrations. 2. `phpunit` - `composer create-project shopware/production` (6.6.x) + MariaDB service, copy the plugin into `custom/plugins`, install/activate, run phpunit. Runs in the same `shopware-cli` image (PHP and Composer included; `shivammathur/setup-php` hangs on this runner and apt installs cost ~4.5 min). MariaDB 10.11, because 11.4 broke Shopware's own migrations.
@@ -51,17 +54,18 @@ before the tag exists, so a failed build leaves no orphan tag.
### Branch protection (one-time, manual) ### Branch protection (one-time, manual)
`scripts/protect-main.sh` using `tea api PATCH repos/{owner}/{repo}/branch_protections/main` to set the `scripts/protect-main.sh` using `tea api PATCH repos/{owner}/{repo}/branch_protections/main` to set the
required status-check contexts (current: `version-changelog / version-changelog (pull_request)`; add required status-check contexts (now `plugin-checks / version-changelog|package|phpunit (pull_request)`,
the new PR jobs). `tea branches protect` cannot set status checks. Not run by the pipeline. set on `main`). `tea branches protect` cannot set status checks. Not run by the pipeline.
## 4. Open items to verify while implementing ## 4. Open items (status after the first pipeline run)
- Release POST creates a missing tag (fallback: tag step after packaging). - Release POST creates a missing tag: **resolved** - the POST created the tag and release for v0.1.0 (release id 55).
- Meaning of HTTP 400 on the package-link endpoint. - Meaning of HTTP 400 on the package-link endpoint: **open** (not evidenced yet).
- Whether bare `validate` runs PHPStan/ESLint (needs PHP/Node on the runner); Node availability on the runner image. - Whether bare `validate` runs PHPStan/ESLint; Node availability on the runner image: **open** (not evidenced; the zip validate passes in the `shopware-cli` image).
- Accepted size for `metadata.icon.size`; error-vs-warning rule list of validate. - Accepted size for `metadata.icon.size`; error-vs-warning rule list of validate: **resolved** - `validate` on the zip reports "No problems found", so the icon size is accepted.
- Whether `routes.php` is flagged by validate; `shopware-cli extension fix` correctness for the YAML migration. - Whether `routes.php` is flagged by validate: **resolved** - not flagged ("No problems found"). `shopware-cli extension fix` correctness for the YAML migration: **open**.
- Exact status-check context names of the new jobs (set after the first run). - Exact status-check context names: **resolved** - final: `plugin-checks / version-changelog (pull_request)`, `plugin-checks / package (pull_request)`, `plugin-checks / phpunit (pull_request)`, set on `main`.
- **Finding:** the Composer registry rejected the `shopware-cli` zip with "composer.json file is missing"; Gitea needs `composer.json` at the zip root, while the `shopware-cli` zip nests everything in a top-level folder. Fix (implemented separately): repack the zip with the top-level folder stripped for the registry upload; the Release asset stays the `shopware-cli` zip.
## 5. Out of scope / later ## 5. Out of scope / later
@@ -0,0 +1,46 @@
<?php declare(strict_types=1);
namespace AeonDumpManager\Tests\MessageQueue\Handler;
use AeonDumpManager\MessageQueue\Handler\CreateDumpHandler;
use AeonDumpManager\MessageQueue\Message\CreateDumpMessage;
use AeonDumpManager\Service\DumpJobStatusService;
use AeonDumpManager\Service\DumpService;
use AeonDumpManager\Tests\Support\RecordingLogger;
use PHPUnit\Framework\TestCase;
class CreateDumpHandlerTest extends TestCase
{
public function testLogsStartOnSuccess(): void
{
$status = $this->createMock(DumpJobStatusService::class);
$status->expects(self::once())->method('markRunning')->with('job-1');
$status->expects(self::never())->method('markFailed');
$logger = new RecordingLogger();
(new CreateDumpHandler($this->createMock(DumpService::class), $status, $logger))(new CreateDumpMessage('job-1'));
self::assertCount(1, $logger->records);
self::assertSame('info', $logger->records[0]['level']);
self::assertSame('Dump job started.', $logger->records[0]['message']);
self::assertSame(['jobId' => 'job-1'], $logger->records[0]['context']);
}
public function testLogsErrorAndMarksFailedWhenCreateThrows(): void
{
$exception = new \RuntimeException('boom');
$service = $this->createMock(DumpService::class);
$service->method('createAsync')->willThrowException($exception);
$status = $this->createMock(DumpJobStatusService::class);
$status->expects(self::once())->method('markFailed')->with('job-2', 'boom');
$logger = new RecordingLogger();
(new CreateDumpHandler($service, $status, $logger))(new CreateDumpMessage('job-2'));
self::assertCount(2, $logger->records);
self::assertSame('error', $logger->records[1]['level']);
self::assertSame('Dump job failed.', $logger->records[1]['message']);
self::assertSame('job-2', $logger->records[1]['context']['jobId']);
self::assertSame($exception, $logger->records[1]['context']['exception']);
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php declare(strict_types=1);
namespace AeonDumpManager\Tests\Service;
use AeonDumpManager\Service\DumpBinaryLocator;
use AeonDumpManager\Service\DumpFilenameParser;
use AeonDumpManager\Service\DumpLister;
use AeonDumpManager\Service\DumpService;
use AeonDumpManager\Service\Exception\DumpNotFoundException;
use AeonDumpManager\Tests\Support\RecordingLogger;
use Doctrine\DBAL\Connection;
use League\Flysystem\FilesystemOperator;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shopware\Core\System\SystemConfig\SystemConfigService;
class DumpServiceLoggingTest extends TestCase
{
private const FILENAME = 'dump_2026-09-14_15-04-05.sql.gz';
private FilesystemOperator&MockObject $filesystem;
private RecordingLogger $logger;
private DumpService $service;
protected function setUp(): void
{
$this->filesystem = $this->createMock(FilesystemOperator::class);
$this->logger = new RecordingLogger();
$this->service = new DumpService(
$this->createMock(DumpLister::class),
$this->filesystem,
$this->createMock(DumpBinaryLocator::class),
$this->createMock(Connection::class),
$this->createMock(SystemConfigService::class),
new DumpFilenameParser(),
$this->logger,
);
}
public function testDeleteLogsInfo(): void
{
$this->filesystem->method('fileExists')->willReturn(true);
$this->filesystem->expects(self::once())->method('delete')->with(DumpLister::path(self::FILENAME));
$this->service->delete(self::FILENAME);
self::assertSame(
[['level' => 'info', 'message' => 'Dump deleted.', 'context' => ['filename' => self::FILENAME]]],
$this->logger->records
);
}
public function testDeleteMissingDumpDoesNotLog(): void
{
$this->filesystem->method('fileExists')->willReturn(false);
try {
$this->service->delete(self::FILENAME);
self::fail('Expected DumpNotFoundException.');
} catch (DumpNotFoundException) {
self::assertSame([], $this->logger->records);
}
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php declare(strict_types=1);
namespace AeonDumpManager\Tests\Support;
use Psr\Log\AbstractLogger;
class RecordingLogger extends AbstractLogger
{
/** @var list<array{level: string, message: string, context: array<mixed>}> */
public array $records = [];
public function log($level, string|\Stringable $message, array $context = []): void
{
$this->records[] = ['level' => (string) $level, 'message' => (string) $message, 'context' => $context];
}
}