UNPKG

@shopware-ag/meteor-admin-sdk

Version:

The Meteor SDK for the Shopware Administration.

571 lines 29.9 kB
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