Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { flushPromises } from '@vue/test-utils';
import { handle, publish } from '@shopware-ag/meteor-admin-sdk/es/channel';
import { handle, publish, setExtensions } from '@shopware-ag/meteor-admin-sdk/es/channel';
import { ExtensionStoreChannelService } from 'SwagExtensionStore/module/sw-extension-store/service/extension-store-channel.service';
import extensionStoreContextStore from 'SwagExtensionStore/module/sw-extension-store/store/extension-store-context.store';
import extensionStorePurchaseConfirmationStore from 'SwagExtensionStore/module/sw-extension-store/store/extension-store-purchase-confirmation.store';
import type { ExtensionStoreBasket } from 'SwagExtensionStore/module/sw-extension-store/types/extension-store-basket.types';

jest.mock('@shopware-ag/meteor-admin-sdk/es/channel', () => ({
handle: jest.fn(() => jest.fn()),
publish: jest.fn(),
setExtensions: jest.fn(),
}));

jest.mock('SwagExtensionStore/util/telemetry', () => ({
Expand All @@ -15,6 +17,13 @@ jest.mock('SwagExtensionStore/util/telemetry', () => ({

const handleMock = handle as jest.MockedFunction<typeof handle>;
const publishMock = publish as jest.MockedFunction<typeof publish>;
const setExtensionsMock = setExtensions as jest.MockedFunction<typeof setExtensions>;

const STORE_ORIGIN = 'https://sky-bridge-store.production.shopware.in';

type ChannelInformation = {
_event_: MessageEvent<string>;
};

const buildCart = (type: 'app' | 'plugin' = 'plugin') => ({
positions: [
Expand Down Expand Up @@ -52,6 +61,7 @@ describe('SwagExtensionStore/module/sw-extension-store/service/extension-store-c
let extensionStorePreferencesService: { state: { installAfterPurchase: boolean } };
let createNotificationSpy: jest.SpyInstance;
let router: { push: jest.Mock; afterEach: jest.Mock; currentRoute: { value: { params: object; query: object } } };
let channelWindow: Window;

const createService = (cart: ExtensionStoreBasket) => {
extensionStoreActionService = {
Expand Down Expand Up @@ -93,15 +103,28 @@ describe('SwagExtensionStore/module/sw-extension-store/service/extension-store-c
Shopware.Context.app.config.bundles = bundles as never;
service.register();

const channelHandler = handleMock.mock.calls[0][1] as (data: unknown) => Promise<{ isDemoShop: boolean }>;

return channelHandler({ action: 'handshake', sessionToken: 'session-token', version: '1.0.0' });
const channelHandler = handleMock.mock.calls[0][1] as (
data: unknown,
additionalInformation: ChannelInformation
) => Promise<{ isDemoShop: boolean }>;

return channelHandler(
{ action: 'handshake', sessionToken: 'session-token', version: '1.0.0' },
{
_event_: {
source: channelWindow,
origin: STORE_ORIGIN,
} as MessageEvent<string>,
},
);
};

/** Runs the purchase action through the channel and returns the confirm callback result. */
const performPurchase = async () => {
service.register();

await performHandshake();

const channelHandler = handleMock.mock.calls[0][1] as (data: unknown) => Promise<unknown>;
await channelHandler(purchaseActionData);

Expand All @@ -111,10 +134,25 @@ describe('SwagExtensionStore/module/sw-extension-store/service/extension-store-c
return onConfirm!();
};

const expectPublishedToStore = (data: object) => {
expect(publishMock).toHaveBeenCalledWith(
'swag-extension-store-channel',
data,
[expect.objectContaining({
source: channelWindow,
origin: STORE_ORIGIN,
sdkVersion: '1.0.0',
})],
);
};

beforeEach(() => {
handleMock.mockClear();
publishMock.mockClear();
setExtensionsMock.mockClear();
extensionStorePurchaseConfirmationStore().$reset();
channelWindow = { postMessage: jest.fn() } as unknown as Window;
extensionStoreContextStore().iframeUrl = `${STORE_ORIGIN}/`;
createNotificationSpy = jest.spyOn(Shopware.Store.get('notification'), 'createNotification')
.mockImplementation(() => null);
service = createService(buildCart());
Expand Down Expand Up @@ -145,6 +183,34 @@ describe('SwagExtensionStore/module/sw-extension-store/service/extension-store-c
});
});

describe('channel registration', () => {
it('should register and target the store iframe after its handshake', async () => {
await performHandshake();

expect(setExtensionsMock).toHaveBeenCalledWith({
'swag-extension-store-sky-bridge': {
baseUrl: STORE_ORIGIN,
permissions: {},
},
});
});

it('should remove the same popstate listener that was registered', () => {
const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');

service.register();

const popstateListener = addEventListenerSpy.mock.calls
.find(([eventName]) => eventName === 'popstate')?.[1];

service.unregister();

expect(popstateListener).toBeDefined();
expect(removeEventListenerSpy).toHaveBeenCalledWith('popstate', popstateListener);
});
});

describe('routeTo', () => {
it('should navigate to the named route with its parameters', async () => {
service.register();
Expand Down Expand Up @@ -182,7 +248,7 @@ describe('SwagExtensionStore/module/sw-extension-store/service/extension-store-c

expect(extensionHelperService.downloadAndActivateExtension).toHaveBeenCalledWith('SwagExtension', 'plugin');
expect(result).toEqual({ success: true, requiresReload: true });
expect(publishMock).toHaveBeenCalledWith('swag-extension-store-channel', {
expectPublishedToStore({
action: 'purchaseResult',
sessionToken: 'session-token',
success: true,
Expand Down Expand Up @@ -223,6 +289,7 @@ describe('SwagExtensionStore/module/sw-extension-store/service/extension-store-c
}));

service.register();
await performHandshake();
const channelHandler = handleMock.mock.calls[0][1] as (data: unknown) => Promise<unknown>;
await channelHandler(purchaseActionData);

Expand All @@ -239,7 +306,7 @@ describe('SwagExtensionStore/module/sw-extension-store/service/extension-store-c
await confirmed;

expect(publishMock).toHaveBeenCalledTimes(1);
expect(publishMock).toHaveBeenCalledWith('swag-extension-store-channel', {
expectPublishedToStore({
action: 'purchaseResult',
sessionToken: 'session-token',
success: true,
Expand Down Expand Up @@ -270,8 +337,9 @@ describe('SwagExtensionStore/module/sw-extension-store/service/extension-store-c
const result = await performPurchase();

expect(result).toEqual({ success: true, requiresReload: false });
expect(extensionStoreActionService.getMyExtensions).not.toHaveBeenCalled();
expect(publishMock).toHaveBeenCalledWith('swag-extension-store-channel', {
// The handshake loads the extensions once. A failed install must not look the version up again.
expect(extensionStoreActionService.getMyExtensions).toHaveBeenCalledTimes(1);
expectPublishedToStore({
action: 'purchaseResult',
sessionToken: 'session-token',
success: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { handle, publish } from '@shopware-ag/meteor-admin-sdk/es/channel';
import { handle, publish, setExtensions } from '@shopware-ag/meteor-admin-sdk/es/channel';
import type ExtensionStoreActionService from 'src/module/sw-extension/service/extension-store-action.service';
import type ShopwareExtensionService from 'src/module/sw-extension/service/shopware-extension.service';
import type ExtensionStoreLicensesService from './extension-store-licenses.service';
Expand Down Expand Up @@ -88,6 +88,12 @@ type PurchaseResultData = {
extensionVersion?: string | null;
};

type StoreChannelSource = {
source: Window;
origin: string;
sdkVersion: string;
};

type CopyToClipboardActionData = StoreChannelActionData & {
text: string;
};
Expand Down Expand Up @@ -117,6 +123,8 @@ export class ExtensionStoreChannelService {
private removeRouterAfterEachHook?: () => void;
private unsubscribeFunction?: () => void;
private registeredAt?: number;
private channelSource?: StoreChannelSource;
private readonly onPopState = (): void => this.publishRouterSync();

constructor(
private readonly extensionStoreActionService: ExtensionStoreActionService,
Expand All @@ -136,19 +144,26 @@ export class ExtensionStoreChannelService {

this.registeredAt = performance.now();

this.unsubscribeFunction = handle('swag-extension-store-channel' as keyof ShopwareMessageTypes, (data: unknown) => {
try {
return this.handleAction(data);
} catch (err) {
return {
success: false,
};
}
});
this.unsubscribeFunction = handle(
'swag-extension-store-channel' as keyof ShopwareMessageTypes,
(data: unknown, additionalInformation) => {
try {
if (this.isHandshakeActionData(data)) {
this.registerChannelSource(additionalInformation._event_, data.version);
}

return this.handleAction(data);
} catch (err) {
return {
success: false,
};
}
},
);

this.removeRouterAfterEachHook = this.router.afterEach((to, from) => this.publishReturnToExtensionStore(to, from));

window.addEventListener('popstate', () => this.publishRouterSync());
window.addEventListener('popstate', this.onPopState);
}

unregister(): void {
Expand All @@ -160,9 +175,10 @@ export class ExtensionStoreChannelService {
this.removeRouterAfterEachHook?.();
this.removeRouterAfterEachHook = undefined;

window.removeEventListener('popstate', () => this.publishRouterSync());
window.removeEventListener('popstate', this.onPopState);

this.registeredAt = undefined;
this.channelSource = undefined;
}

private publishReturnToExtensionStore(to: RouteLocationNormalizedGeneric, from: RouteLocationNormalizedLoadedGeneric): void {
Expand Down Expand Up @@ -398,10 +414,7 @@ export class ExtensionStoreChannelService {
...(extensionVersion !== undefined ? { extensionVersion } : {}),
};

publish(
'swag-extension-store-channel' as keyof ShopwareMessageTypes,
resultData,
);
this.publishToExtensionStore(resultData);
}

private publishRouterSync(): void {
Expand All @@ -413,9 +426,49 @@ export class ExtensionStoreChannelService {
currentRouteQuery,
};

this.publishToExtensionStore(data);
}

private registerChannelSource(event: MessageEvent<string>, sdkVersion: string): void {
if (!event.source || !this.isStoreOrigin(event.origin)) {
return;
}

const source = event.source as Window;

setExtensions({
'swag-extension-store-sky-bridge': {
baseUrl: event.origin,
permissions: {},
},
});

this.channelSource = {
source,
origin: event.origin,
sdkVersion,
};
}

private isStoreOrigin(origin: string): boolean {
const iframeUrl = extensionStoreContextStore().iframeUrl;

if (!iframeUrl) {
return false;
}

return new URL(iframeUrl).origin === origin;
}

private publishToExtensionStore(data: PurchaseResultData | RouterSyncData): void {
if (!this.channelSource) {
return;
}

publish(
'swag-extension-store-channel' as keyof ShopwareMessageTypes,
data,
[this.channelSource],
);
}

Expand Down
Loading