Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Controller/HistoryController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export interface SwagMigrationHistoryDetailErrorsData {
sortDirection: string;
disableRouteParams: boolean;
limit: number;
downloadUrl: string;
}

/**
Expand All @@ -32,6 +31,7 @@ export default Shopware.Component.wrapComponentConfig({

mixins: [
Mixin.getByName('listing'),
Mixin.getByName('notification'),
],

props: {
Expand All @@ -50,7 +50,6 @@ export default Shopware.Component.wrapComponentConfig({
sortDirection: 'DESC',
disableRouteParams: true,
limit: 10,
downloadUrl: '',
};
},

Expand Down Expand Up @@ -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;
});
},
Expand Down Expand Up @@ -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<void> {
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'),
});
}
},
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,6 @@
</p>
{% endblock %}

{% block swag_migration_history_detail_errors_download_form %}
<form
ref="downloadForm"
class="swag-migration-history-detail-errors__download-form"
:action="downloadUrl"
method="POST"
>
<input
type="hidden"
name="runUuid"
:value="migrationRun.id"
/>
</form>
{% endblock %}

{% block swag_migration_history_detail_errors_description %}
<p class="swag-migration-history-detail-errors__description">
<span>{{ $tc('swag-migration.history.detailPage.logDescription') }}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
{
Expand Down Expand Up @@ -73,8 +71,6 @@ export default Shopware.Component.wrapComponentConfig({
sortDirection: 'DESC',
oldParams: {},
context: Shopware.Context.api,
logDownloadEndpoint: '',
runIdForLogDownload: '',
};
},

Expand All @@ -98,10 +94,6 @@ export default Shopware.Component.wrapComponentConfig({
},
},

created() {
this.logDownloadEndpoint = `/api/_action/${this.migrationApiService.getApiBasePath()}/download-logs-of-run`;
},

methods: {
getMigrationColumns() {
return [
Expand Down Expand Up @@ -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<void> {
try {
const blob = await this.migrationApiService.downloadLogsOfRun(runId);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Downloading the logs of a run with 50k log entries fails for me after 30 s with "Failed downloading log file.", while the server was still streaming the file. Could the admin client's 30 s timeout cause this? The form post had no client-side limit, core sets it here:

https://github.com/shopware/shopware/blob/f7e74efb144b27a3595fb5ddadf234b671b2dfc8/src/Administration/Resources/app/administration/src/core/factory/http.factory.js#L46

Until that error appeared, nothing showed that the download was in progress.

Seed for a run with 50k log entries
SET @conn = UNHEX('0199c8a0000070008000000000000001');
SET @run = UNHEX('0199c8a0000070008000000000000002');

DELETE FROM swag_migration_logging WHERE run_id = @run;
DELETE FROM swag_migration_run WHERE id = @run;
DELETE FROM swag_migration_connection WHERE id = @conn;

INSERT INTO swag_migration_connection (id, name, profile_name, gateway_name, created_at)
VALUES (@conn, 'Log download repro', 'shopware55', 'local', NOW(3));

INSERT INTO swag_migration_run (id, connection_id, environment_information, step, created_at)
VALUES (@run, @conn, '{"sourceSystemName":"Shopware","sourceSystemVersion":"5.7.18"}', 'finished', NOW(3));

INSERT INTO swag_migration_logging (id, level, code, run_id, created_at, profile_name, gateway_name, user_fixable, entity_name, source_data)
SELECT UNHEX(REPLACE(UUID(), '-', '')), 'error', 'SWAG_MIGRATION__LOG_DOWNLOAD_REPRO', @run, NOW(3), 'shopware55', 'local', 0, 'customer',
    JSON_OBJECT(
        'column_1', 'some source value', 'column_2', 'some source value', 'column_3', 'some source value', 'column_4', 'some source value',
        'column_5', 'some source value', 'column_6', 'some source value', 'column_7', 'some source value', 'column_8', 'some source value',
        'column_9', 'some source value', 'column_10', 'some source value', 'column_11', 'some source value', 'column_12', 'some source value',
        'column_13', 'some source value', 'column_14', 'some source value', 'column_15', 'some source value', 'column_16', 'some source value',
        'column_17', 'some source value', 'column_18', 'some source value', 'column_19', 'some source value', 'column_20', 'some source value'
    )
FROM (SELECT 0 d UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4) a
CROSS JOIN (SELECT 0 d UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) b
CROSS JOIN (SELECT 0 d UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) c
CROSS JOIN (SELECT 0 d UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) e
CROSS JOIN (SELECT 0 d UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) f;

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'),
});
}
},
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -88,18 +88,6 @@
{% block swag_migration_history_router_view %}
<router-view />
{% endblock %}

{% block swag_migration_history_log_download_form %}
<form ref="downloadLogsOfRunForm"
class="swag-migration-history-detail-errors__download-form"
:action="logDownloadEndpoint"
method="POST">
<input type="hidden"
name="runUuid"
:value="runIdForLogDownload">
</form>
{% endblock %}

</template>
</mt-card>
{% endblock %}
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});

Expand Down
Original file line number Diff line number Diff line change
@@ -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: '<div><slot name="grid" /></div>' },
'sw-data-grid': {
props: ['dataSource'],
template: '<div><slot name="actions" :item="dataSource[0]" /></div>',
},
'sw-context-menu-item': { template: '<button><slot /></button>' },
'mt-link': { template: '<a href="#"><slot /></a>' },
'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',
}),
]);
});
});
36 changes: 36 additions & 0 deletions tests/Migration/Controller/HistoryControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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]);
Expand All @@ -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]);
Expand Down
Loading