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]);