@shopware-ag/meteor-admin-sdk
Version:
The Meteor SDK for the Shopware Administration.
571 lines • 29.9 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import * as modal from '../../ui/modal';
import * as location from '../../location';
import * as context from '../../context';
import * as data from '../../data';
import { jwtDecode } from 'jwt-decode';
/* Mock dependencies */
jest.mock('../../ui/modal');
jest.mock('../../location');
jest.mock('../../context');
jest.mock('../../data', () => ({
repository: jest.fn(),
Classes: {
Criteria: jest.fn().mockImplementation(function () {
this.addFilter = jest.fn();
return this;
}),
},
}));
jest.mock('jwt-decode', () => ({
jwtDecode: jest.fn(),
}));
/* Set up BroadcastChannel mock before importing the module */
const mockBroadcastChannelInstance = {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
postMessage: jest.fn(),
close: jest.fn(),
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
global.BroadcastChannel = jest.fn().mockImplementation(() => {
return mockBroadcastChannelInstance;
});
import { addPaymentIframe, decodeLicense, MESSAGE_EVENT_TYPE, startPaymentFlow } from './index';
describe('Private Service Payment', () => {
let mockModalClose;
let mockLocationStartAutoResizer;
let mockGetAppInformation;
let mockGetShopId;
let eventListeners;
let mockModalOpen;
beforeEach(() => {
mockModalOpen = jest.fn().mockResolvedValue(undefined);
modal.open = mockModalOpen;
/* Track event listeners for testing */
eventListeners = new Map();
const originalAddEventListener = window.addEventListener.bind(window);
const originalRemoveEventListener = window.removeEventListener.bind(window);
/* Mock window.addEventListener for testing */
jest.spyOn(window, 'addEventListener').mockImplementation((type, listener) => {
if (!eventListeners.has(type)) {
eventListeners.set(type, new Set());
}
const listenerSet = eventListeners.get(type);
if (listenerSet) {
listenerSet.add(listener);
}
originalAddEventListener(type, listener);
});
/* Mock window.removeEventListener for testing */
jest.spyOn(window, 'removeEventListener').mockImplementation((type, listener) => {
var _a;
(_a = eventListeners.get(type)) === null || _a === void 0 ? void 0 : _a.delete(listener);
originalRemoveEventListener(type, listener);
});
// Mock modal functions
mockModalClose = jest.fn().mockResolvedValue(undefined);
modal.close = mockModalClose;
// Mock location functions
mockLocationStartAutoResizer = jest.fn();
location.startAutoResizer = mockLocationStartAutoResizer;
// Mock context functions
mockGetAppInformation = jest.fn().mockResolvedValue({
name: 'test-app',
version: '1.0.0',
});
context.getAppInformation = mockGetAppInformation;
// Mock context.getShopId
mockGetShopId = jest.fn().mockResolvedValue('shop-id-123');
context.getShopId = mockGetShopId;
// Clear all mocks
jest.clearAllMocks();
mockBroadcastChannelInstance.addEventListener.mockClear();
mockBroadcastChannelInstance.removeEventListener.mockClear();
mockBroadcastChannelInstance.postMessage.mockClear();
mockBroadcastChannelInstance.close.mockClear();
});
afterEach(() => {
// Clean up event listeners
eventListeners.clear();
jest.restoreAllMocks();
});
describe('addPaymentIframe', () => {
describe('iframe creation', () => {
let container;
const baseUrl = 'https://example.com';
const options = {
shopUrl: 'https://shop.example.com',
swVersion: '6.5.0',
swUserLanguage: 'en-GB',
shopPlan: 'beyond',
};
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
if (container.parentNode) {
document.body.removeChild(container);
}
});
it('should create an iframe with correct attributes and URL when no valid license', () => __awaiter(void 0, void 0, void 0, function* () {
const result = yield addPaymentIframe(container, baseUrl, options);
expect(mockGetAppInformation).toHaveBeenCalledTimes(1);
expect(result.iframeEl).toBeInstanceOf(HTMLIFrameElement);
expect(result.iframeEl.src).toBe('https://example.com/payment?service-name=test-app&service-version=1.0.0&shop-url=https://shop.example.com&sw-version=6.5.0&sw-user-language=en-GB&shop-plan=beyond&shop-id=shop-id-123');
expect(container.contains(result.iframeEl)).toBe(true);
}));
it('should create an iframe with shop-url and shop-plan from license when license is valid', () => __awaiter(void 0, void 0, void 0, function* () {
const futureExp = Math.floor(Date.now() / 1000) + 3600;
const mockSearch = jest.fn().mockResolvedValue({
first: () => ({ configurationValue: 'valid-jwt-token' }),
});
data.repository.mockReturnValue({ search: mockSearch });
jwtDecode.mockReturnValue({
aud: 'https://license-aud.shop.example.com',
'plan-name': 'premium',
exp: futureExp,
iat: 1000000000,
iss: 'shopware',
nbf: 1000000000,
'license-toggles': {},
'plan-usage': 'commercial',
'plan-variant': 'default',
swemp: 'example',
});
const result = yield addPaymentIframe(container, baseUrl, options);
expect(result.iframeEl.src).toBe('https://example.com/payment?service-name=test-app&service-version=1.0.0&shop-url=https://license-aud.shop.example.com&sw-version=6.5.0&sw-user-language=en-GB&shop-plan=premium&shop-id=shop-id-123');
}));
it('should default shop-id to an empty value when getShopId returns null', () => __awaiter(void 0, void 0, void 0, function* () {
mockGetShopId.mockResolvedValue(null);
const result = yield addPaymentIframe(container, baseUrl, options);
expect(mockGetShopId).toHaveBeenCalledTimes(1);
expect(result.iframeEl.src.endsWith('&shop-id=')).toBe(true);
}));
it('should append iframe to the provided element', () => __awaiter(void 0, void 0, void 0, function* () {
const result = yield addPaymentIframe(container, baseUrl, options);
expect(container.contains(result.iframeEl)).toBe(true);
expect(container.children.length).toBe(1);
expect(container.children[0]).toBe(result.iframeEl);
}));
it('should add event listener for message events', () => __awaiter(void 0, void 0, void 0, function* () {
var _a;
yield addPaymentIframe(container, baseUrl, options);
expect(window.addEventListener).toHaveBeenCalledWith('message', expect.any(Function));
expect((_a = eventListeners.get('message')) === null || _a === void 0 ? void 0 : _a.size).toBeGreaterThan(0);
}));
});
describe('event handling', () => {
let container;
const baseUrl = 'https://example.com';
const options = {
shopUrl: 'https://shop.example.com',
swVersion: '6.5.0',
swUserLanguage: 'en-GB',
};
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
if (container.parentNode) {
document.body.removeChild(container);
}
});
it('should handle SYNC_HEIGHT event and update iframe height', () => __awaiter(void 0, void 0, void 0, function* () {
const result = yield addPaymentIframe(container, baseUrl, options);
const messageListeners = eventListeners.get('message');
expect(messageListeners === null || messageListeners === void 0 ? void 0 : messageListeners.size).toBeGreaterThan(0);
const messageListenersArray = messageListeners ? Array.from(messageListeners) : [];
const handleEvent = messageListenersArray[0];
const mockEvent = new MessageEvent('message', {
data: {
type: MESSAGE_EVENT_TYPE.SYNC_HEIGHT,
payload: 500,
},
});
handleEvent(mockEvent);
expect(result.iframeEl.style.height).toBe('500px');
expect(mockLocationStartAutoResizer).toHaveBeenCalledTimes(1);
}));
it('should handle PAYMENT_CLOSE event and close modal', () => __awaiter(void 0, void 0, void 0, function* () {
yield addPaymentIframe(container, baseUrl, options);
const messageListeners = eventListeners.get('message');
const messageListenersArray = messageListeners ? Array.from(messageListeners) : [];
const handleEvent = messageListenersArray[0];
const mockEvent = new MessageEvent('message', {
data: {
type: MESSAGE_EVENT_TYPE.PAYMENT_CLOSE,
payload: null,
},
});
handleEvent(mockEvent);
expect(window.removeEventListener).toHaveBeenCalledWith('message', handleEvent);
expect(mockModalClose).toHaveBeenCalledWith({
locationId: 'sw-service-payment-modal',
});
}));
it('should handle PAYMENT_SUCCESS event and post to BroadcastChannel', () => __awaiter(void 0, void 0, void 0, function* () {
yield addPaymentIframe(container, baseUrl, options);
const messageListeners = eventListeners.get('message');
const messageListenersArray = messageListeners ? Array.from(messageListeners) : [];
const handleEvent = messageListenersArray[0];
const eventData = {
type: MESSAGE_EVENT_TYPE.PAYMENT_SUCCESS,
payload: { transactionId: '123' },
};
const mockEvent = new MessageEvent('message', {
data: eventData,
});
handleEvent(mockEvent);
expect(mockBroadcastChannelInstance.postMessage).toHaveBeenCalledWith(eventData);
}));
it('should not handle unknown event types', () => __awaiter(void 0, void 0, void 0, function* () {
const result = yield addPaymentIframe(container, baseUrl, options);
const messageListeners = eventListeners.get('message');
const messageListenersArray = messageListeners ? Array.from(messageListeners) : [];
const handleEvent = messageListenersArray[0];
const mockEvent = new MessageEvent('message', {
data: {
type: 'unknown_event',
payload: null,
},
});
const initialHeight = result.iframeEl.style.height;
handleEvent(mockEvent);
expect(result.iframeEl.style.height).toBe(initialHeight);
expect(mockModalClose).not.toHaveBeenCalled();
expect(mockBroadcastChannelInstance.postMessage).not.toHaveBeenCalled();
}));
it('should handle PAYMENT_ERROR event (no special handling)', () => __awaiter(void 0, void 0, void 0, function* () {
yield addPaymentIframe(container, baseUrl, options);
const messageListeners = eventListeners.get('message');
const messageListenersArray = messageListeners ? Array.from(messageListeners) : [];
const handleEvent = messageListenersArray[0];
const mockEvent = new MessageEvent('message', {
data: {
type: MESSAGE_EVENT_TYPE.PAYMENT_ERROR,
payload: { error: 'Payment failed' },
},
});
// Should not throw and should not call any special handlers
expect(() => handleEvent(mockEvent)).not.toThrow();
expect(mockModalClose).not.toHaveBeenCalled();
expect(mockBroadcastChannelInstance.postMessage).not.toHaveBeenCalled();
}));
it('should handle multiple SYNC_HEIGHT events and update height each time', () => __awaiter(void 0, void 0, void 0, function* () {
const result = yield addPaymentIframe(container, baseUrl, options);
const messageListeners = eventListeners.get('message');
const messageListenersArray = messageListeners ? Array.from(messageListeners) : [];
const handleEvent = messageListenersArray[0];
const firstEvent = new MessageEvent('message', {
data: {
type: MESSAGE_EVENT_TYPE.SYNC_HEIGHT,
payload: 300,
},
});
const secondEvent = new MessageEvent('message', {
data: {
type: MESSAGE_EVENT_TYPE.SYNC_HEIGHT,
payload: 600,
},
});
handleEvent(firstEvent);
expect(result.iframeEl.style.height).toBe('300px');
handleEvent(secondEvent);
expect(result.iframeEl.style.height).toBe('600px');
}));
});
describe('unmount function', () => {
let container;
const baseUrl = 'https://example.com';
const options = {
shopUrl: 'https://shop.example.com',
swVersion: '6.5.0',
swUserLanguage: 'en-GB',
};
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
if (container.parentNode) {
document.body.removeChild(container);
}
});
it('should provide unmount function that cleans up', () => __awaiter(void 0, void 0, void 0, function* () {
const result = yield addPaymentIframe(container, baseUrl, options);
const messageListeners = eventListeners.get('message');
const messageListenersArray = messageListeners ? Array.from(messageListeners) : [];
const handleEvent = messageListenersArray[0];
result.unmount();
expect(window.removeEventListener).toHaveBeenCalledWith('message', handleEvent);
expect(mockBroadcastChannelInstance.close).toHaveBeenCalledTimes(1);
}));
it('should remove event listener when unmount is called', () => __awaiter(void 0, void 0, void 0, function* () {
var _a, _b;
const result = yield addPaymentIframe(container, baseUrl, options);
const initialListenerCount = ((_a = eventListeners.get('message')) === null || _a === void 0 ? void 0 : _a.size) || 0;
result.unmount();
expect((_b = eventListeners.get('message')) === null || _b === void 0 ? void 0 : _b.size).toBeLessThan(initialListenerCount);
}));
});
});
describe('startPaymentFlow', () => {
it('should open modal with correct options and return flow', () => __awaiter(void 0, void 0, void 0, function* () {
const flow = yield startPaymentFlow();
expect(mockModalOpen).toHaveBeenCalledWith({
locationId: 'sw-service-payment-modal',
variant: 'small',
showHeader: false,
showFooter: false,
closable: false,
});
expect(flow).toBeDefined();
expect(flow.subscribe).toBeInstanceOf(Function);
}));
it('should return a flow with subscribe method', () => __awaiter(void 0, void 0, void 0, function* () {
const flow = yield startPaymentFlow();
expect(typeof flow.subscribe).toBe('function');
}));
describe('Flow subscribe', () => {
let flow;
let mockModalOpen;
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
mockModalOpen = jest.fn().mockResolvedValue(undefined);
modal.open = mockModalOpen;
flow = yield startPaymentFlow();
}));
it('should subscribe to events and call callback when event matches', () => {
const callback = jest.fn();
const { unsubscribe } = flow.subscribe(MESSAGE_EVENT_TYPE.PAYMENT_SUCCESS, callback);
expect(mockBroadcastChannelInstance.addEventListener).toHaveBeenCalledWith('message', expect.any(Function));
// Simulate message event
const mockCalls = mockBroadcastChannelInstance.addEventListener.mock.calls;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (!mockCalls[0] || !mockCalls[0][1]) {
throw new Error('Event handler not found');
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const eventHandler = mockCalls[0][1];
const mockEvent = {
data: {
type: MESSAGE_EVENT_TYPE.PAYMENT_SUCCESS,
payload: null,
},
};
eventHandler(mockEvent);
expect(callback).toHaveBeenCalledTimes(1);
unsubscribe();
});
it('should not call callback when event type does not match', () => {
const callback = jest.fn();
const { unsubscribe } = flow.subscribe(MESSAGE_EVENT_TYPE.PAYMENT_SUCCESS, callback);
const mockCalls = mockBroadcastChannelInstance.addEventListener.mock.calls;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (!mockCalls[0] || !mockCalls[0][1]) {
throw new Error('Event handler not found');
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const eventHandler = mockCalls[0][1];
const mockEvent = {
data: {
type: MESSAGE_EVENT_TYPE.PAYMENT_ERROR,
payload: null,
},
};
eventHandler(mockEvent);
expect(callback).not.toHaveBeenCalled();
unsubscribe();
});
it('should unsubscribe from events correctly', () => {
const callback = jest.fn();
const { unsubscribe } = flow.subscribe(MESSAGE_EVENT_TYPE.PAYMENT_SUCCESS, callback);
const mockCalls = mockBroadcastChannelInstance.addEventListener.mock.calls;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (!mockCalls[0] || !mockCalls[0][1]) {
throw new Error('Event handler not found');
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const eventHandler = mockCalls[0][1];
unsubscribe();
expect(mockBroadcastChannelInstance.removeEventListener).toHaveBeenCalledWith('message', eventHandler);
});
it('should handle multiple subscriptions independently', () => {
const callback1 = jest.fn();
const callback2 = jest.fn();
const { unsubscribe: unsubscribe1 } = flow.subscribe(MESSAGE_EVENT_TYPE.PAYMENT_SUCCESS, callback1);
const { unsubscribe: unsubscribe2 } = flow.subscribe(MESSAGE_EVENT_TYPE.PAYMENT_ERROR, callback2);
expect(mockBroadcastChannelInstance.addEventListener).toHaveBeenCalledTimes(2);
// Trigger first subscription
const mockCalls = mockBroadcastChannelInstance.addEventListener.mock.calls;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (!mockCalls[0] || !mockCalls[0][1]) {
throw new Error('Event handler not found');
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const eventHandler1 = mockCalls[0][1];
eventHandler1({
data: {
type: MESSAGE_EVENT_TYPE.PAYMENT_SUCCESS,
payload: null,
},
});
expect(callback1).toHaveBeenCalledTimes(1);
expect(callback2).not.toHaveBeenCalled();
// Trigger second subscription
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (!mockCalls[1] || !mockCalls[1][1]) {
throw new Error('Event handler not found');
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const eventHandler2 = mockCalls[1][1];
eventHandler2({
data: {
type: MESSAGE_EVENT_TYPE.PAYMENT_ERROR,
payload: null,
},
});
expect(callback1).toHaveBeenCalledTimes(1);
expect(callback2).toHaveBeenCalledTimes(1);
unsubscribe1();
unsubscribe2();
});
it('should handle all event types correctly', () => {
const eventTypes = [
MESSAGE_EVENT_TYPE.PAYMENT_CLOSE,
MESSAGE_EVENT_TYPE.PAYMENT_SUCCESS,
MESSAGE_EVENT_TYPE.PAYMENT_ERROR,
MESSAGE_EVENT_TYPE.SYNC_HEIGHT,
];
eventTypes.forEach((eventType) => {
const callback = jest.fn();
const { unsubscribe } = flow.subscribe(eventType, callback);
const mockCalls = mockBroadcastChannelInstance.addEventListener.mock.calls;
const lastCallIndex = mockCalls.length - 1;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (!mockCalls[lastCallIndex] || !mockCalls[lastCallIndex][1]) {
throw new Error('Event handler not found');
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const eventHandler = mockCalls[lastCallIndex][1];
eventHandler({
data: {
type: eventType,
payload: null,
},
});
expect(callback).toHaveBeenCalledTimes(1);
unsubscribe();
});
});
it('should allow multiple subscriptions to the same event type', () => {
const callback1 = jest.fn();
const callback2 = jest.fn();
const { unsubscribe: unsubscribe1 } = flow.subscribe(MESSAGE_EVENT_TYPE.PAYMENT_SUCCESS, callback1);
const { unsubscribe: unsubscribe2 } = flow.subscribe(MESSAGE_EVENT_TYPE.PAYMENT_SUCCESS, callback2);
const mockCalls = mockBroadcastChannelInstance.addEventListener.mock.calls;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (!mockCalls[0] || !mockCalls[0][1]) {
throw new Error('Event handler not found');
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const eventHandler1 = mockCalls[0][1];
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (!mockCalls[1] || !mockCalls[1][1]) {
throw new Error('Event handler not found');
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const eventHandler2 = mockCalls[1][1];
const mockEvent = {
data: {
type: MESSAGE_EVENT_TYPE.PAYMENT_SUCCESS,
payload: null,
},
};
eventHandler1(mockEvent);
eventHandler2(mockEvent);
expect(callback1).toHaveBeenCalledTimes(1);
expect(callback2).toHaveBeenCalledTimes(1);
unsubscribe1();
unsubscribe2();
});
});
});
describe('decodeLicense', () => {
const mockTokenPayload = {
'license-toggles': { 'feature-a': true },
'plan-name': 'beyond',
'plan-usage': 'commercial',
'plan-variant': 'default',
aud: 'https://shop.example.com',
exp: 9999999999,
iat: 1000000000,
iss: 'shopware',
nbf: 1000000000,
swemp: 'example',
};
beforeEach(() => {
const mockSearch = jest.fn().mockResolvedValue({
first: () => ({ configurationValue: 'valid-jwt-token' }),
});
const mockRepository = { search: mockSearch };
data.repository.mockReturnValue(mockRepository);
jwtDecode.mockReturnValue(mockTokenPayload);
});
it('should return decoded token when system config has license key and token is valid', () => __awaiter(void 0, void 0, void 0, function* () {
const result = yield decodeLicense();
expect(data.repository).toHaveBeenCalledWith('system_config');
expect(result).toEqual(mockTokenPayload);
expect(jwtDecode).toHaveBeenCalledWith('valid-jwt-token');
}));
it('should return null when no license key in system config (empty token)', () => __awaiter(void 0, void 0, void 0, function* () {
const mockSearch = jest.fn().mockResolvedValue({
first: () => ({ configurationValue: '' }),
});
data.repository.mockReturnValue({ search: mockSearch });
jwtDecode.mockImplementation(() => {
throw new Error('Invalid token');
});
const result = yield decodeLicense();
expect(jwtDecode).toHaveBeenCalledWith('');
expect(result).toBeNull();
}));
it('should return null when repository search throws', () => __awaiter(void 0, void 0, void 0, function* () {
const mockSearch = jest.fn().mockRejectedValue(new Error('Network error'));
data.repository.mockReturnValue({ search: mockSearch });
const result = yield decodeLicense();
expect(result).toBeNull();
expect(jwtDecode).not.toHaveBeenCalled();
}));
it('should return null when res.first() returns undefined (no config found)', () => __awaiter(void 0, void 0, void 0, function* () {
const mockSearch = jest.fn().mockResolvedValue({
first: () => undefined,
});
data.repository.mockReturnValue({ search: mockSearch });
jwtDecode.mockImplementation(() => {
throw new Error('Invalid token');
});
const result = yield decodeLicense();
expect(jwtDecode).toHaveBeenCalledWith('');
expect(result).toBeNull();
}));
it('should return null when jwtDecode throws (invalid token)', () => __awaiter(void 0, void 0, void 0, function* () {
jwtDecode.mockImplementation(() => {
throw new Error('Invalid token');
});
const result = yield decodeLicense();
expect(result).toBeNull();
}));
});
});
//# sourceMappingURL=index.spec.js.map