mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 05:31:09 -05:00
Add OWASP compliance mapping and security test case templates, and document version control implementation phases
This commit is contained in:
217
static/js/__tests__/version-control.test.js
Normal file
217
static/js/__tests__/version-control.test.js
Normal file
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { initVersionControl, setupBranchHandlers, handleMergeConflicts } from '../version-control';
|
||||
|
||||
describe('Version Control UI', () => {
|
||||
let container;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
container.id = 'version-control-panel';
|
||||
document.body.appendChild(container);
|
||||
|
||||
// Mock HTMX
|
||||
window.htmx = {
|
||||
trigger: jest.fn(),
|
||||
ajax: jest.fn(),
|
||||
on: jest.fn()
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should initialize version control UI', () => {
|
||||
const panel = document.createElement('div');
|
||||
panel.className = 'version-control-panel';
|
||||
container.appendChild(panel);
|
||||
|
||||
initVersionControl();
|
||||
|
||||
expect(window.htmx.on).toHaveBeenCalled();
|
||||
expect(container.querySelector('.version-control-panel')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should setup branch switch handlers', () => {
|
||||
const switchButton = document.createElement('button');
|
||||
switchButton.setAttribute('data-branch-id', '1');
|
||||
switchButton.className = 'branch-switch';
|
||||
container.appendChild(switchButton);
|
||||
|
||||
setupBranchHandlers();
|
||||
switchButton.click();
|
||||
|
||||
expect(window.htmx.ajax).toHaveBeenCalledWith(
|
||||
'POST',
|
||||
'/version-control/switch-branch/',
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('branch operations', () => {
|
||||
it('should handle branch creation', () => {
|
||||
const form = document.createElement('form');
|
||||
form.id = 'create-branch-form';
|
||||
container.appendChild(form);
|
||||
|
||||
const event = new Event('submit');
|
||||
form.dispatchEvent(event);
|
||||
|
||||
expect(window.htmx.trigger).toHaveBeenCalledWith(
|
||||
form,
|
||||
'branch-created',
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should update UI after branch switch', () => {
|
||||
const response = {
|
||||
branch_name: 'feature/test',
|
||||
status: 'success'
|
||||
};
|
||||
|
||||
const event = new CustomEvent('branchSwitched', {
|
||||
detail: response
|
||||
});
|
||||
|
||||
document.dispatchEvent(event);
|
||||
|
||||
expect(container.querySelector('.current-branch')?.textContent)
|
||||
.toContain('feature/test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('merge operations', () => {
|
||||
it('should handle merge conflicts', () => {
|
||||
const conflicts = [
|
||||
{
|
||||
field: 'name',
|
||||
source_value: 'Feature Name',
|
||||
target_value: 'Main Name'
|
||||
}
|
||||
];
|
||||
|
||||
handleMergeConflicts(conflicts);
|
||||
|
||||
const conflictDialog = document.querySelector('.merge-conflict-dialog');
|
||||
expect(conflictDialog).toBeTruthy();
|
||||
expect(conflictDialog.innerHTML).toContain('name');
|
||||
expect(conflictDialog.innerHTML).toContain('Feature Name');
|
||||
expect(conflictDialog.innerHTML).toContain('Main Name');
|
||||
});
|
||||
|
||||
it('should submit merge resolution', () => {
|
||||
const resolutionForm = document.createElement('form');
|
||||
resolutionForm.id = 'merge-resolution-form';
|
||||
container.appendChild(resolutionForm);
|
||||
|
||||
const event = new Event('submit');
|
||||
resolutionForm.dispatchEvent(event);
|
||||
|
||||
expect(window.htmx.ajax).toHaveBeenCalledWith(
|
||||
'POST',
|
||||
'/version-control/resolve-conflicts/',
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should display error messages', () => {
|
||||
const errorEvent = new CustomEvent('showError', {
|
||||
detail: { message: 'Test error message' }
|
||||
});
|
||||
|
||||
document.dispatchEvent(errorEvent);
|
||||
|
||||
const errorMessage = document.querySelector('.error-message');
|
||||
expect(errorMessage).toBeTruthy();
|
||||
expect(errorMessage.textContent).toContain('Test error message');
|
||||
});
|
||||
|
||||
it('should clear error messages', () => {
|
||||
const errorMessage = document.createElement('div');
|
||||
errorMessage.className = 'error-message';
|
||||
container.appendChild(errorMessage);
|
||||
|
||||
const clearEvent = new Event('clearErrors');
|
||||
document.dispatchEvent(clearEvent);
|
||||
|
||||
expect(container.querySelector('.error-message')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('loading states', () => {
|
||||
it('should show loading indicator during operations', () => {
|
||||
const loadingEvent = new Event('versionControlLoading');
|
||||
document.dispatchEvent(loadingEvent);
|
||||
|
||||
const loader = document.querySelector('.version-control-loader');
|
||||
expect(loader).toBeTruthy();
|
||||
expect(loader.style.display).toBe('block');
|
||||
});
|
||||
|
||||
it('should hide loading indicator after operations', () => {
|
||||
const loader = document.createElement('div');
|
||||
loader.className = 'version-control-loader';
|
||||
container.appendChild(loader);
|
||||
|
||||
const doneEvent = new Event('versionControlLoaded');
|
||||
document.dispatchEvent(doneEvent);
|
||||
|
||||
expect(loader.style.display).toBe('none');
|
||||
});
|
||||
});
|
||||
|
||||
describe('UI updates', () => {
|
||||
it('should update branch list after operations', () => {
|
||||
const branchList = document.createElement('ul');
|
||||
branchList.className = 'branch-list';
|
||||
container.appendChild(branchList);
|
||||
|
||||
const updateEvent = new CustomEvent('updateBranchList', {
|
||||
detail: {
|
||||
branches: [
|
||||
{ name: 'main', active: true },
|
||||
{ name: 'feature/test', active: false }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
document.dispatchEvent(updateEvent);
|
||||
|
||||
const listItems = branchList.querySelectorAll('li');
|
||||
expect(listItems.length).toBe(2);
|
||||
expect(listItems[0].textContent).toContain('main');
|
||||
expect(listItems[1].textContent).toContain('feature/test');
|
||||
});
|
||||
|
||||
it('should highlight active branch', () => {
|
||||
const branchItems = [
|
||||
{ name: 'main', active: true },
|
||||
{ name: 'feature/test', active: false }
|
||||
].map(branch => {
|
||||
const item = document.createElement('li');
|
||||
item.textContent = branch.name;
|
||||
item.className = 'branch-item';
|
||||
if (branch.active) item.classList.add('active');
|
||||
return item;
|
||||
});
|
||||
|
||||
const branchList = document.createElement('ul');
|
||||
branchList.className = 'branch-list';
|
||||
branchList.append(...branchItems);
|
||||
container.appendChild(branchList);
|
||||
|
||||
const activeItem = branchList.querySelector('.branch-item.active');
|
||||
expect(activeItem).toBeTruthy();
|
||||
expect(activeItem.textContent).toBe('main');
|
||||
});
|
||||
});
|
||||
});
|
||||
207
static/js/error-handling.js
Normal file
207
static/js/error-handling.js
Normal file
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* Error handling and state management for version control system
|
||||
*/
|
||||
|
||||
class VersionControlError extends Error {
|
||||
constructor(message, code, details = {}) {
|
||||
super(message);
|
||||
this.name = 'VersionControlError';
|
||||
this.code = code;
|
||||
this.details = details;
|
||||
this.timestamp = new Date();
|
||||
}
|
||||
}
|
||||
|
||||
// Error boundary for version control operations
|
||||
class VersionControlErrorBoundary {
|
||||
constructor(options = {}) {
|
||||
this.onError = options.onError || this.defaultErrorHandler;
|
||||
this.errors = new Map();
|
||||
this.retryAttempts = new Map();
|
||||
this.maxRetries = options.maxRetries || 3;
|
||||
}
|
||||
|
||||
defaultErrorHandler(error) {
|
||||
console.error(`[Version Control Error]: ${error.message}`, error);
|
||||
this.showErrorNotification(error);
|
||||
}
|
||||
|
||||
showErrorNotification(error) {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = 'version-control-error notification';
|
||||
notification.innerHTML = `
|
||||
<div class="notification-content">
|
||||
<span class="error-icon">⚠️</span>
|
||||
<span class="error-message">${error.message}</span>
|
||||
<button class="close-btn">×</button>
|
||||
</div>
|
||||
${error.details.retry ? '<button class="retry-btn">Retry</button>' : ''}
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Auto-hide after 5 seconds unless it's a critical error
|
||||
if (!error.details.critical) {
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Handle retry
|
||||
const retryBtn = notification.querySelector('.retry-btn');
|
||||
if (retryBtn && error.details.retryCallback) {
|
||||
retryBtn.addEventListener('click', () => {
|
||||
notification.remove();
|
||||
error.details.retryCallback();
|
||||
});
|
||||
}
|
||||
|
||||
// Handle close
|
||||
const closeBtn = notification.querySelector('.close-btn');
|
||||
closeBtn.addEventListener('click', () => notification.remove());
|
||||
}
|
||||
|
||||
async wrapOperation(operationKey, operation) {
|
||||
try {
|
||||
// Check if operation is already in progress
|
||||
if (this.errors.has(operationKey)) {
|
||||
throw new VersionControlError(
|
||||
'Operation already in progress',
|
||||
'DUPLICATE_OPERATION'
|
||||
);
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
this.showLoading(operationKey);
|
||||
|
||||
const result = await operation();
|
||||
|
||||
// Clear any existing errors for this operation
|
||||
this.errors.delete(operationKey);
|
||||
this.retryAttempts.delete(operationKey);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const retryCount = this.retryAttempts.get(operationKey) || 0;
|
||||
|
||||
// Handle specific error types
|
||||
if (error.name === 'VersionControlError') {
|
||||
this.handleVersionControlError(error, operationKey, retryCount);
|
||||
} else {
|
||||
// Convert unknown errors to VersionControlError
|
||||
const vcError = new VersionControlError(
|
||||
'An unexpected error occurred',
|
||||
'UNKNOWN_ERROR',
|
||||
{ originalError: error }
|
||||
);
|
||||
this.handleVersionControlError(vcError, operationKey, retryCount);
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
this.hideLoading(operationKey);
|
||||
}
|
||||
}
|
||||
|
||||
handleVersionControlError(error, operationKey, retryCount) {
|
||||
this.errors.set(operationKey, error);
|
||||
|
||||
// Determine if operation can be retried
|
||||
const canRetry = retryCount < this.maxRetries;
|
||||
|
||||
error.details.retry = canRetry;
|
||||
error.details.retryCallback = canRetry ?
|
||||
() => this.retryOperation(operationKey) :
|
||||
undefined;
|
||||
|
||||
this.onError(error);
|
||||
}
|
||||
|
||||
async retryOperation(operationKey) {
|
||||
const retryCount = (this.retryAttempts.get(operationKey) || 0) + 1;
|
||||
this.retryAttempts.set(operationKey, retryCount);
|
||||
|
||||
// Exponential backoff for retries
|
||||
const backoffDelay = Math.min(1000 * Math.pow(2, retryCount - 1), 10000);
|
||||
await new Promise(resolve => setTimeout(resolve, backoffDelay));
|
||||
|
||||
// Get the original operation and retry
|
||||
const operation = this.pendingOperations.get(operationKey);
|
||||
if (operation) {
|
||||
return this.wrapOperation(operationKey, operation);
|
||||
}
|
||||
}
|
||||
|
||||
showLoading(operationKey) {
|
||||
const loadingElement = document.createElement('div');
|
||||
loadingElement.className = `loading-indicator loading-${operationKey}`;
|
||||
loadingElement.innerHTML = `
|
||||
<div class="loading-spinner"></div>
|
||||
<span class="loading-text">Processing...</span>
|
||||
`;
|
||||
document.body.appendChild(loadingElement);
|
||||
}
|
||||
|
||||
hideLoading(operationKey) {
|
||||
const loadingElement = document.querySelector(`.loading-${operationKey}`);
|
||||
if (loadingElement) {
|
||||
loadingElement.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
const errorBoundary = new VersionControlErrorBoundary({
|
||||
onError: (error) => {
|
||||
// Log to monitoring system
|
||||
if (window.monitoring) {
|
||||
window.monitoring.logError('version_control', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Export error handling utilities
|
||||
export const versionControl = {
|
||||
/**
|
||||
* Wrap version control operations with error handling
|
||||
*/
|
||||
async performOperation(key, operation) {
|
||||
return errorBoundary.wrapOperation(key, operation);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new error instance
|
||||
*/
|
||||
createError(message, code, details) {
|
||||
return new VersionControlError(message, code, details);
|
||||
},
|
||||
|
||||
/**
|
||||
* Show loading state manually
|
||||
*/
|
||||
showLoading(key) {
|
||||
errorBoundary.showLoading(key);
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide loading state manually
|
||||
*/
|
||||
hideLoading(key) {
|
||||
errorBoundary.hideLoading(key);
|
||||
},
|
||||
|
||||
/**
|
||||
* Show error notification manually
|
||||
*/
|
||||
showError(error) {
|
||||
errorBoundary.showErrorNotification(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Add global error handler for uncaught version control errors
|
||||
window.addEventListener('unhandledrejection', event => {
|
||||
if (event.reason instanceof VersionControlError) {
|
||||
event.preventDefault();
|
||||
errorBoundary.defaultErrorHandler(event.reason);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user