diff --git a/src/Controller/HistoryController.php b/src/Controller/HistoryController.php
index 23480c0df..d52b3cb6f 100644
--- a/src/Controller/HistoryController.php
+++ b/src/Controller/HistoryController.php
@@ -75,7 +75,7 @@ public function getGroupedLogsOfRun(Request $request, Context $context): JsonRes
path: '/api/_action/migration/download-logs-of-run',
name: 'api.admin.migration.download-logs-of-run',
methods: [Request::METHOD_POST],
- defaults: ['auth_required' => false, PlatformRequest::ATTRIBUTE_ACL => ['swag_migration.viewer']]
+ defaults: [PlatformRequest::ATTRIBUTE_ACL => ['swag_migration.viewer']]
)]
public function downloadLogsOfRun(Request $request, Context $context): StreamedResponse
{
diff --git a/src/Resources/app/administration/src/module/swag-migration/page/swag-migration-history-detail-errors/index.ts b/src/Resources/app/administration/src/module/swag-migration/page/swag-migration-history-detail-errors/index.ts
index 8b4ee925d..810adf436 100644
--- a/src/Resources/app/administration/src/module/swag-migration/page/swag-migration-history-detail-errors/index.ts
+++ b/src/Resources/app/administration/src/module/swag-migration/page/swag-migration-history-detail-errors/index.ts
@@ -16,7 +16,6 @@ export interface SwagMigrationHistoryDetailErrorsData {
sortDirection: string;
disableRouteParams: boolean;
limit: number;
- downloadUrl: string;
}
/**
@@ -32,6 +31,7 @@ export default Shopware.Component.wrapComponentConfig({
mixins: [
Mixin.getByName('listing'),
+ Mixin.getByName('notification'),
],
props: {
@@ -50,7 +50,6 @@ export default Shopware.Component.wrapComponentConfig({
sortDirection: 'DESC',
disableRouteParams: true,
limit: 10,
- downloadUrl: '',
};
},
@@ -123,7 +122,6 @@ export default Shopware.Component.wrapComponentConfig({
item.title = this.$tc(this.getErrorTitleSnippet(item), { entity: item.entity }, 0);
});
- this.downloadUrl = response.downloadUrl;
return this.allMigrationErrors;
});
},
@@ -156,8 +154,23 @@ export default Shopware.Component.wrapComponentConfig({
return 'swag-migration.index.error-resolution.codes.unknown';
},
- submitDownload() {
- this.$refs.downloadForm.submit();
+ async submitDownload(): Promise {
+ try {
+ const blob = await this.migrationApiService.downloadLogsOfRun(this.migrationRun.id);
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement('a');
+
+ link.href = url;
+ link.download = `migrationRunLog-${this.migrationRun.id}.txt`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(url);
+ } catch {
+ this.createNotificationError({
+ message: this.$tc('swag-migration.index.error-resolution.errors.downloadLogsFailed'),
+ });
+ }
},
},
});
diff --git a/src/Resources/app/administration/src/module/swag-migration/page/swag-migration-history-detail-errors/swag-migration-history-detail-errors.html.twig b/src/Resources/app/administration/src/module/swag-migration/page/swag-migration-history-detail-errors/swag-migration-history-detail-errors.html.twig
index bc2bd4f5a..a336a98b6 100644
--- a/src/Resources/app/administration/src/module/swag-migration/page/swag-migration-history-detail-errors/swag-migration-history-detail-errors.html.twig
+++ b/src/Resources/app/administration/src/module/swag-migration/page/swag-migration-history-detail-errors/swag-migration-history-detail-errors.html.twig
@@ -57,21 +57,6 @@
{% endblock %}
- {% block swag_migration_history_detail_errors_download_form %}
-
- {% endblock %}
-
{% block swag_migration_history_detail_errors_description %}
{{ $tc('swag-migration.history.detailPage.logDescription') }}
diff --git a/src/Resources/app/administration/src/module/swag-migration/page/swag-migration-history/index.ts b/src/Resources/app/administration/src/module/swag-migration/page/swag-migration-history/index.ts
index 34949a10c..854f339a9 100644
--- a/src/Resources/app/administration/src/module/swag-migration/page/swag-migration-history/index.ts
+++ b/src/Resources/app/administration/src/module/swag-migration/page/swag-migration-history/index.ts
@@ -24,8 +24,6 @@ export interface SwagMigrationHistoryData {
isLoading: boolean;
migrationRuns: TEntityCollection<'swag_migration_run'>;
sortDirection: string;
- logDownloadEndpoint: string | null;
- runIdForLogDownload: string | null;
oldParams: Record<
string,
{
@@ -73,8 +71,6 @@ export default Shopware.Component.wrapComponentConfig({
sortDirection: 'DESC',
oldParams: {},
context: Shopware.Context.api,
- logDownloadEndpoint: '',
- runIdForLogDownload: '',
};
},
@@ -98,10 +94,6 @@ export default Shopware.Component.wrapComponentConfig({
},
},
- created() {
- this.logDownloadEndpoint = `/api/_action/${this.migrationApiService.getApiBasePath()}/download-logs-of-run`;
- },
-
methods: {
getMigrationColumns() {
return [
@@ -201,11 +193,23 @@ export default Shopware.Component.wrapComponentConfig({
return params;
},
- onContextDownloadLogFile(runId: string) {
- this.runIdForLogDownload = runId;
- this.$nextTick(() => {
- this.$refs.downloadLogsOfRunForm.submit();
- });
+ async onContextDownloadLogFile(runId: string): Promise {
+ try {
+ const blob = await this.migrationApiService.downloadLogsOfRun(runId);
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement('a');
+
+ link.href = url;
+ link.download = `migrationRunLog-${runId}.txt`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(url);
+ } catch {
+ this.createNotificationError({
+ message: this.$tc('swag-migration.index.error-resolution.errors.downloadLogsFailed'),
+ });
+ }
},
},
});
diff --git a/src/Resources/app/administration/src/module/swag-migration/page/swag-migration-history/swag-migration-history.html.twig b/src/Resources/app/administration/src/module/swag-migration/page/swag-migration-history/swag-migration-history.html.twig
index cb0041e44..6bde5f73d 100644
--- a/src/Resources/app/administration/src/module/swag-migration/page/swag-migration-history/swag-migration-history.html.twig
+++ b/src/Resources/app/administration/src/module/swag-migration/page/swag-migration-history/swag-migration-history.html.twig
@@ -88,18 +88,6 @@
{% block swag_migration_history_router_view %}
{% endblock %}
-
- {% block swag_migration_history_log_download_form %}
-
- {% endblock %}
-
{% endblock %}
diff --git a/tests/Jest/src/core/service/api/swag-migration.api.service.spec.js b/tests/Jest/src/core/service/api/swag-migration.api.service.spec.js
index e2a7fe7fa..c1691ed57 100644
--- a/tests/Jest/src/core/service/api/swag-migration.api.service.spec.js
+++ b/tests/Jest/src/core/service/api/swag-migration.api.service.spec.js
@@ -307,6 +307,7 @@ describe('src/core/service/api/swag-migration.api.service', () => {
expect(clientMock.history.post[0].url).toBe('_action/migration/download-logs-of-run');
expect(clientMock.history.post[0].responseType).toBe(data.responseType);
+ expect(clientMock.history.post[0].headers.Authorization).toBe('Bearer test-token');
expect(clientMock.history.post[0].headers['test-header']).toBe('test-value');
});
diff --git a/tests/Jest/src/module/swag-migration/page/swag-migration-history-download.spec.js b/tests/Jest/src/module/swag-migration/page/swag-migration-history-download.spec.js
new file mode 100644
index 000000000..46e7c1cae
--- /dev/null
+++ b/tests/Jest/src/module/swag-migration/page/swag-migration-history-download.spec.js
@@ -0,0 +1,114 @@
+import { createPinia } from 'pinia';
+import { shallowMount } from '@vue/test-utils';
+import SwagMigrationHistory from 'SwagMigrationAssistant/module/swag-migration/page/swag-migration-history';
+import SwagMigrationHistoryDetailErrors from 'SwagMigrationAssistant/module/swag-migration/page/swag-migration-history-detail-errors';
+
+Shopware.Component.register('swag-migration-history', () => SwagMigrationHistory);
+Shopware.Component.register('swag-migration-history-detail-errors', () => SwagMigrationHistoryDetailErrors);
+
+describe.each([
+ [
+ 'swag-migration-history',
+ 'swag-migration.history.contextMenu.downloadLogs',
+ ],
+ [
+ 'swag-migration-history-detail-errors',
+ 'swag-migration.history.detailPage.logDownload',
+ ],
+])('%s log download', (componentName, downloadLabel) => {
+ let wrapper;
+ let downloadLogsOfRun;
+ let clickSpy;
+ const originalCreateObjectURL = window.URL.createObjectURL;
+ const originalRevokeObjectURL = window.URL.revokeObjectURL;
+
+ beforeEach(() => {
+ downloadLogsOfRun = jest.fn();
+ window.URL.createObjectURL = jest.fn(() => 'blob:migration-log');
+ window.URL.revokeObjectURL = jest.fn();
+ clickSpy = jest.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ wrapper?.unmount();
+ jest.restoreAllMocks();
+ window.URL.createObjectURL = originalCreateObjectURL;
+ window.URL.revokeObjectURL = originalRevokeObjectURL;
+ });
+
+ async function createWrapper() {
+ const runs = Object.assign([{ id: 'run-id' }], { total: 1 });
+
+ wrapper = shallowMount(await Shopware.Component.build(componentName), {
+ props: componentName === 'swag-migration-history-detail-errors' ? { migrationRun: runs[0] } : {},
+ global: {
+ plugins: [createPinia()],
+ provide: {
+ searchRankingService: {},
+ migrationApiService: {
+ downloadLogsOfRun,
+ getGroupedLogsOfRun: jest.fn().mockResolvedValue({ items: [], total: 0 }),
+ },
+ repositoryFactory: {
+ create: () => ({ search: jest.fn().mockResolvedValue(runs) }),
+ },
+ },
+ mocks: {
+ $route: {
+ name: 'swag.migration.index.history',
+ query: { page: 1, limit: 25 },
+ },
+ },
+ stubs: {
+ 'mt-card': { template: '
' },
+ 'sw-data-grid': {
+ props: ['dataSource'],
+ template: '
',
+ },
+ 'sw-context-menu-item': { template: '' },
+ 'mt-link': { template: '' },
+ 'router-view': true,
+ },
+ },
+ });
+
+ await flushPromises();
+ }
+
+ it('downloads the selected run through the API service', async () => {
+ const blob = new Blob(['migration log'], { type: 'text/plain' });
+ downloadLogsOfRun.mockResolvedValue(blob);
+ await createWrapper();
+
+ const downloadControl = wrapper.findAll('button, a').find((control) => control.text() === downloadLabel);
+ await downloadControl.trigger('click');
+ await flushPromises();
+
+ expect(downloadLogsOfRun).toHaveBeenCalledWith('run-id');
+ expect(window.URL.createObjectURL).toHaveBeenCalledWith(blob);
+ expect(clickSpy).toHaveBeenCalledTimes(1);
+ const link = clickSpy.mock.instances[0];
+ expect(link.href).toBe('blob:migration-log');
+ expect(link.download).toBe('migrationRunLog-run-id.txt');
+ expect(document.body.contains(link)).toBe(false);
+ expect(window.URL.revokeObjectURL).toHaveBeenCalledWith('blob:migration-log');
+ expect(wrapper.find('form').exists()).toBe(false);
+ });
+
+ it('shows an error notification when the download fails', async () => {
+ downloadLogsOfRun.mockRejectedValue(new Error('Download failed'));
+ await createWrapper();
+
+ const downloadControl = wrapper.findAll('button, a').find((control) => control.text() === downloadLabel);
+ await downloadControl.trigger('click');
+ await flushPromises();
+
+ expect(downloadLogsOfRun).toHaveBeenCalledWith('run-id');
+ expect(clickSpy).not.toHaveBeenCalled();
+ expect(Object.values(Shopware.Store.get('notification').notifications)).toEqual([
+ expect.objectContaining({
+ message: 'swag-migration.index.error-resolution.errors.downloadLogsFailed',
+ }),
+ ]);
+ });
+});
diff --git a/tests/Migration/Controller/HistoryControllerTest.php b/tests/Migration/Controller/HistoryControllerTest.php
index e938c4f75..1f296bd33 100644
--- a/tests/Migration/Controller/HistoryControllerTest.php
+++ b/tests/Migration/Controller/HistoryControllerTest.php
@@ -16,6 +16,7 @@
use Shopware\Core\Framework\RateLimiter\Exception\RateLimitExceededException;
use Shopware\Core\Framework\Routing\RoutingException;
use Shopware\Core\Framework\Test\RateLimiter\DisableRateLimiterCompilerPass;
+use Shopware\Core\Framework\Test\TestCaseBase\AdminApiTestBehaviour;
use Shopware\Core\Framework\Test\TestCaseBase\IntegrationTestBehaviour;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\PlatformRequest;
@@ -31,10 +32,12 @@
use SwagMigrationAssistant\Profile\Shopware\Gateway\Local\ShopwareLocalGateway;
use SwagMigrationAssistant\Profile\Shopware55\Shopware55Profile;
use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
#[Package('fundamentals@after-sales')]
class HistoryControllerTest extends TestCase
{
+ use AdminApiTestBehaviour;
use IntegrationTestBehaviour;
private const DEFAULT_MAX_LIMIT = 500;
@@ -169,6 +172,19 @@ public function testDownloadLogsOfRunWithoutUuid(): void
$this->controller->downloadLogsOfRun($request, $this->context);
}
+ public function testDownloadLogsOfRunRequiresAuthentication(): void
+ {
+ $browser = $this->createClient(authorized: false);
+ $browser->request(
+ Request::METHOD_POST,
+ '/api/_action/migration/download-logs-of-run',
+ server: ['CONTENT_TYPE' => 'application/json'],
+ content: json_encode(['runUuid' => $this->runUuid], \JSON_THROW_ON_ERROR)
+ );
+
+ static::assertSame(Response::HTTP_UNAUTHORIZED, $browser->getResponse()->getStatusCode());
+ }
+
public function testDownloadLogsOfRun(): void
{
$request = new Request([], ['runUuid' => $this->runUuid]);
@@ -177,6 +193,26 @@ public function testDownloadLogsOfRun(): void
static::assertSame('text/plain', $response->headers->get('Content-type'));
}
+ public function testDownloadLogsOfRunRequiresViewerPrivilege(): void
+ {
+ $browser = $this->createClient(permissions: []);
+ $browser->jsonRequest(Request::METHOD_POST, '/api/_action/migration/download-logs-of-run', ['runUuid' => $this->runUuid]);
+
+ static::assertSame(Response::HTTP_FORBIDDEN, $browser->getResponse()->getStatusCode());
+ }
+
+ public function testDownloadLogsOfRunWithViewerPrivilege(): void
+ {
+ $browser = $this->createClient(permissions: ['swag_migration.viewer']);
+ $browser->jsonRequest(Request::METHOD_POST, '/api/_action/migration/download-logs-of-run', ['runUuid' => $this->runUuid]);
+
+ $response = $browser->getResponse();
+ static::assertSame(Response::HTTP_OK, $response->getStatusCode());
+ static::assertSame('text/plain; charset=UTF-8', $response->headers->get('Content-Type'));
+ static::assertSame('attachment; filename=migrationRunLog-' . $this->runUuid . '.txt', $response->headers->get('Content-Disposition'));
+ static::assertStringContainsString('migration_error_1', $browser->getInternalResponse()->getContent());
+ }
+
public function testGetGroupedLogsOfRunIsRateLimited(): void
{
$request = new Request(['runUuid' => $this->runUuid]);