fix: DockerSecretsResolver - don't normalize absolute paths like /var/www/html/...
Some checks failed
Deploy Application / deploy (push) Has been cancelled
Some checks failed
Deploy Application / deploy (push) Has been cancelled
This commit is contained in:
448
tests/JavaScript/ActionHandler.test.js
Normal file
448
tests/JavaScript/ActionHandler.test.js
Normal file
@@ -0,0 +1,448 @@
|
||||
/**
|
||||
* Tests for ActionHandler Module
|
||||
*/
|
||||
|
||||
import { ActionHandler } from '../../resources/js/modules/common/ActionHandler.js';
|
||||
import { dockerContainerHandler, genericApiHandler } from '../../resources/js/modules/common/ActionHandlers.js';
|
||||
|
||||
describe('ActionHandler', () => {
|
||||
let container;
|
||||
let handler;
|
||||
let mockToast;
|
||||
let mockConfirm;
|
||||
let mockRefresh;
|
||||
|
||||
beforeEach(() => {
|
||||
// Setup DOM
|
||||
document.body.innerHTML = '';
|
||||
container = document.createElement('div');
|
||||
container.className = 'test-container';
|
||||
document.body.appendChild(container);
|
||||
|
||||
// Mock functions
|
||||
mockToast = jest.fn();
|
||||
mockConfirm = jest.fn(() => true);
|
||||
mockRefresh = jest.fn();
|
||||
|
||||
// Create handler
|
||||
handler = new ActionHandler('.test-container', {
|
||||
toastHandler: mockToast,
|
||||
confirmationHandler: mockConfirm,
|
||||
refreshHandler: mockRefresh,
|
||||
autoRefresh: true
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
describe('Event Delegation', () => {
|
||||
test('should handle click events on buttons with data-action', () => {
|
||||
const button = document.createElement('button');
|
||||
button.setAttribute('data-action', 'test');
|
||||
button.setAttribute('data-action-url', '/api/test');
|
||||
container.appendChild(button);
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve({ success: true })
|
||||
})
|
||||
);
|
||||
|
||||
button.click();
|
||||
|
||||
expect(global.fetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should not handle clicks outside buttons', () => {
|
||||
const div = document.createElement('div');
|
||||
container.appendChild(div);
|
||||
|
||||
global.fetch = jest.fn();
|
||||
|
||||
div.click();
|
||||
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Handler Registration', () => {
|
||||
test('should register handler', () => {
|
||||
handler.registerHandler('test-handler', {
|
||||
urlTemplate: '/api/test/{id}/{action}'
|
||||
});
|
||||
|
||||
expect(handler.handlers.has('test-handler')).toBe(true);
|
||||
});
|
||||
|
||||
test('should use registered handler for actions', () => {
|
||||
handler.registerHandler('docker-container', dockerContainerHandler);
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.setAttribute('data-action', 'start');
|
||||
button.setAttribute('data-action-handler', 'docker-container');
|
||||
button.setAttribute('data-action-param-id', '123');
|
||||
container.appendChild(button);
|
||||
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve({ success: true })
|
||||
})
|
||||
);
|
||||
|
||||
button.click();
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/admin/infrastructure/docker/api/containers/123/start'),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('URL Template Processing', () => {
|
||||
test('should replace {action} placeholder', () => {
|
||||
handler.registerHandler('test', {
|
||||
urlTemplate: '/api/{action}'
|
||||
});
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.setAttribute('data-action', 'delete');
|
||||
button.setAttribute('data-action-handler', 'test');
|
||||
container.appendChild(button);
|
||||
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve({ success: true })
|
||||
})
|
||||
);
|
||||
|
||||
button.click();
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/api/delete'),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
test('should replace {id} placeholder', () => {
|
||||
handler.registerHandler('test', {
|
||||
urlTemplate: '/api/users/{id}/{action}'
|
||||
});
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.setAttribute('data-action', 'delete');
|
||||
button.setAttribute('data-action-handler', 'test');
|
||||
button.setAttribute('data-action-param-id', '123');
|
||||
container.appendChild(button);
|
||||
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve({ success: true })
|
||||
})
|
||||
);
|
||||
|
||||
button.click();
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/api/users/123/delete'),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
test('should replace {param:name} placeholders', () => {
|
||||
handler.registerHandler('test', {
|
||||
urlTemplate: '/api/{param:entity}/{param:id}/{action}'
|
||||
});
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.setAttribute('data-action', 'update');
|
||||
button.setAttribute('data-action-handler', 'test');
|
||||
button.setAttribute('data-action-param-entity', 'users');
|
||||
button.setAttribute('data-action-param-id', '123');
|
||||
container.appendChild(button);
|
||||
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve({ success: true })
|
||||
})
|
||||
);
|
||||
|
||||
button.click();
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/api/users/123/update'),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CSRF Token Handling', () => {
|
||||
test('should extract token from data-live-component', () => {
|
||||
const component = document.createElement('div');
|
||||
component.setAttribute('data-live-component', 'test-component');
|
||||
component.setAttribute('data-csrf-token', 'test-token');
|
||||
document.body.appendChild(component);
|
||||
container.appendChild(component);
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.setAttribute('data-action', 'test');
|
||||
button.setAttribute('data-action-url', '/api/test');
|
||||
container.appendChild(button);
|
||||
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve({ success: true })
|
||||
})
|
||||
);
|
||||
|
||||
button.click();
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
'X-CSRF-Token': 'test-token'
|
||||
}),
|
||||
body: expect.stringContaining('"token":"test-token"')
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should extract token from meta tag', () => {
|
||||
const meta = document.createElement('meta');
|
||||
meta.setAttribute('name', 'csrf-token');
|
||||
meta.setAttribute('content', 'meta-token');
|
||||
document.head.appendChild(meta);
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.setAttribute('data-action', 'test');
|
||||
button.setAttribute('data-action-url', '/api/test');
|
||||
container.appendChild(button);
|
||||
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve({ success: true })
|
||||
})
|
||||
);
|
||||
|
||||
button.click();
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
'X-CSRF-Token': 'meta-token'
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
document.head.removeChild(meta);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Loading States', () => {
|
||||
test('should set loading state on button', async () => {
|
||||
const button = document.createElement('button');
|
||||
button.textContent = 'Click Me';
|
||||
button.setAttribute('data-action', 'test');
|
||||
button.setAttribute('data-action-url', '/api/test');
|
||||
container.appendChild(button);
|
||||
|
||||
global.fetch = jest.fn(() =>
|
||||
new Promise(resolve => setTimeout(() => {
|
||||
resolve({
|
||||
json: () => Promise.resolve({ success: true })
|
||||
});
|
||||
}, 100))
|
||||
);
|
||||
|
||||
button.click();
|
||||
|
||||
expect(button.disabled).toBe(true);
|
||||
expect(button.classList.contains('action-loading')).toBe(true);
|
||||
});
|
||||
|
||||
test('should reset loading state after request', async () => {
|
||||
const button = document.createElement('button');
|
||||
button.textContent = 'Click Me';
|
||||
button.setAttribute('data-action', 'test');
|
||||
button.setAttribute('data-action-url', '/api/test');
|
||||
container.appendChild(button);
|
||||
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve({ success: true })
|
||||
})
|
||||
);
|
||||
|
||||
await button.click();
|
||||
|
||||
// Wait for async operations
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
expect(button.disabled).toBe(false);
|
||||
expect(button.classList.contains('action-loading')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Confirmations', () => {
|
||||
test('should show confirmation before action', () => {
|
||||
handler.registerHandler('test', {
|
||||
urlTemplate: '/api/{action}',
|
||||
confirmations: {
|
||||
delete: 'Are you sure?'
|
||||
}
|
||||
});
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.setAttribute('data-action', 'delete');
|
||||
button.setAttribute('data-action-handler', 'test');
|
||||
container.appendChild(button);
|
||||
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve({ success: true })
|
||||
})
|
||||
);
|
||||
|
||||
button.click();
|
||||
|
||||
expect(mockConfirm).toHaveBeenCalledWith('Are you sure?');
|
||||
});
|
||||
|
||||
test('should not execute action if confirmation cancelled', () => {
|
||||
mockConfirm.mockReturnValue(false);
|
||||
|
||||
handler.registerHandler('test', {
|
||||
urlTemplate: '/api/{action}',
|
||||
confirmations: {
|
||||
delete: 'Are you sure?'
|
||||
}
|
||||
});
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.setAttribute('data-action', 'delete');
|
||||
button.setAttribute('data-action-handler', 'test');
|
||||
container.appendChild(button);
|
||||
|
||||
global.fetch = jest.fn();
|
||||
|
||||
button.click();
|
||||
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Toast Integration', () => {
|
||||
test('should show success toast on success', async () => {
|
||||
const button = document.createElement('button');
|
||||
button.setAttribute('data-action', 'test');
|
||||
button.setAttribute('data-action-url', '/api/test');
|
||||
container.appendChild(button);
|
||||
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve({ success: true })
|
||||
})
|
||||
);
|
||||
|
||||
await button.click();
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockToast).toHaveBeenCalledWith(
|
||||
expect.stringContaining('successfully'),
|
||||
'success'
|
||||
);
|
||||
});
|
||||
|
||||
test('should show error toast on error', async () => {
|
||||
handler.registerHandler('test', {
|
||||
urlTemplate: '/api/{action}',
|
||||
errorMessages: {
|
||||
test: 'Test failed'
|
||||
}
|
||||
});
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.setAttribute('data-action', 'test');
|
||||
button.setAttribute('data-action-handler', 'test');
|
||||
container.appendChild(button);
|
||||
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve({ success: false, message: 'Error' })
|
||||
})
|
||||
);
|
||||
|
||||
await button.click();
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockToast).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Test failed'),
|
||||
'error'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Auto Refresh', () => {
|
||||
test('should refresh after successful action', async () => {
|
||||
const button = document.createElement('button');
|
||||
button.setAttribute('data-action', 'test');
|
||||
button.setAttribute('data-action-url', '/api/test');
|
||||
container.appendChild(button);
|
||||
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve({ success: true })
|
||||
})
|
||||
);
|
||||
|
||||
await button.click();
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockRefresh).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should not refresh on error', async () => {
|
||||
const button = document.createElement('button');
|
||||
button.setAttribute('data-action', 'test');
|
||||
button.setAttribute('data-action-url', '/api/test');
|
||||
container.appendChild(button);
|
||||
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve({ success: false })
|
||||
})
|
||||
);
|
||||
|
||||
await button.click();
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockRefresh).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
test('should handle network errors', async () => {
|
||||
const button = document.createElement('button');
|
||||
button.setAttribute('data-action', 'test');
|
||||
button.setAttribute('data-action-url', '/api/test');
|
||||
container.appendChild(button);
|
||||
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.reject(new Error('Network error'))
|
||||
);
|
||||
|
||||
await button.click();
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockToast).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Network error'),
|
||||
'error'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user