Add OWASP compliance mapping and security test case templates, and document version control implementation phases

This commit is contained in:
pacnpal
2025-02-07 10:51:11 -05:00
parent d353f24f9d
commit 2c4d2daf34
38 changed files with 5313 additions and 94 deletions

View 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
View 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);
}
});