UNPKG

ctrlshiftleft

Version:

AI-powered toolkit for embedding QA and security testing into development workflows

588 lines (574 loc) 24.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateTestDataTemplate = generateTestDataTemplate; const parser_1 = require("@babel/parser"); const traverse_1 = __importDefault(require("@babel/traverse")); const t = __importStar(require("@babel/types")); const path_1 = __importDefault(require("path")); /** * Generates a Test Data Factory for a component * @param sourcePath Path to the source component * @param sourceCode Source code of the component * @returns Test template with Test Data Factory content */ function generateTestDataTemplate(sourcePath, sourceCode) { const template = { content: '', format: 'typescript' }; // Extract component name from path const fileName = path_1.default.basename(sourcePath); const componentName = fileName.split('.')[0]; // Extract fields and their validation patterns from the component const fields = extractComponentFields(sourceCode); // Generate the Test Data Factory template.content = `/** * Test Data Factory for ${componentName} * Automatically generated by ctrl.shift.left */ export const ${componentName.toLowerCase()}Data = { // Valid test data valid: { ${generateValidTestData(fields)} }, // Invalid test data - general validation failures invalid: { ${generateInvalidTestData(fields)} }, // Security test data security: { xss: { ${generateXSSTestData(fields)} }, sqlInjection: { ${generateSQLInjectionTestData(fields)} } }, // Edge cases edgeCases: { // Boundary values ${generateEdgeCaseTestData(fields)} } }; ${generateHelperMethods(componentName, fields)} `; return template; } /** * Extract fields and their validation patterns from the component source code */ function extractComponentFields(sourceCode) { const fields = []; try { // Parse the source code const ast = (0, parser_1.parse)(sourceCode, { sourceType: 'module', plugins: ['jsx', 'typescript'] }); // Track state variables const stateVars = {}; // Track validation patterns const validations = {}; // Traverse the AST (0, traverse_1.default)(ast, { // Find state variables VariableDeclarator(path) { if (t.isIdentifier(path.node.id) && t.isCallExpression(path.node.init) && t.isIdentifier(path.node.init.callee) && path.node.init.callee.name === 'useState') { const varName = path.node.id.name; // Try to determine type from init value let varType = 'string'; if (path.node.init.arguments && path.node.init.arguments.length > 0) { const initArg = path.node.init.arguments[0]; if (t.isStringLiteral(initArg)) varType = 'string'; else if (t.isNumericLiteral(initArg)) varType = 'number'; else if (t.isBooleanLiteral(initArg)) varType = 'boolean'; else if (t.isObjectExpression(initArg)) varType = 'object'; else if (t.isArrayExpression(initArg)) varType = 'array'; } stateVars[varName] = varType; } }, // Look for validation patterns in the code CallExpression(path) { // Check for validation function calls like validateEmail, validatePassword, etc. if (t.isIdentifier(path.node.callee) && path.node.callee.name.startsWith('validate')) { const validationName = path.node.callee.name; const validationArg = path.node.arguments[0]; if (t.isIdentifier(validationArg)) { const fieldName = validationArg.name; if (!validations[fieldName]) { validations[fieldName] = {}; } if (validationName.includes('Email')) { validations[fieldName].isEmail = true; } else if (validationName.includes('Password')) { validations[fieldName].isPassword = true; } else if (validationName.includes('CreditCard') || validationName.includes('CardNumber')) { validations[fieldName].isCreditCard = true; } else if (validationName.includes('Amount') || validationName.includes('Money')) { validations[fieldName].isAmount = true; } } } // Check for pattern validation like string.match(PATTERN) if (t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.property) && path.node.callee.property.name === 'match') { const calleeObj = path.node.callee.object; const patternArg = path.node.arguments[0]; if (t.isIdentifier(calleeObj) && t.isRegExpLiteral(patternArg)) { const fieldName = calleeObj.name; const pattern = patternArg.pattern; if (!validations[fieldName]) { validations[fieldName] = {}; } validations[fieldName].pattern = pattern; // Analyze pattern to detect type if (pattern.includes('@')) { validations[fieldName].isEmail = true; } else if (pattern.includes('\\d') && pattern.includes('{')) { // Likely a credit card or numeric pattern if (pattern.includes('{16}') || pattern.includes('{13,19}')) { validations[fieldName].isCreditCard = true; } } } } }, // Find form elements and input fields from JSX JSXOpeningElement(path) { const elementName = path.node.name.type === 'JSXIdentifier' ? path.node.name.name : ''; const attributes = path.node.attributes; if (['input', 'select', 'textarea'].includes(elementName)) { let name = ''; let type = ''; let required = false; let pattern = ''; let min = undefined; let max = undefined; for (const attr of attributes) { if (attr.type === 'JSXAttribute') { // Get name attribute if (attr.name.name === 'name' && attr.value && attr.value.type === 'StringLiteral') { name = attr.value.value; } // Get id attribute as fallback if (!name && attr.name.name === 'id' && attr.value && attr.value.type === 'StringLiteral') { name = attr.value.value; } // Get data-testid as last resort if (!name && attr.name.name === 'data-testid' && attr.value && attr.value.type === 'StringLiteral') { name = attr.value.value.replace(/-input$/, ''); } // Get type attribute if (attr.name.name === 'type' && attr.value && attr.value.type === 'StringLiteral') { type = attr.value.value; } // Check for required if (attr.name.name === 'required' && !attr.value) { required = true; } // Get pattern if (attr.name.name === 'pattern' && attr.value && attr.value.type === 'StringLiteral') { pattern = attr.value.value; } // Get min/max if (attr.name.name === 'min' && attr.value) { if (t.isStringLiteral(attr.value)) { min = parseFloat(attr.value.value); } else if (t.isNumericLiteral(attr.value)) { min = attr.value.value; } else if (t.isJSXExpressionContainer(attr.value)) { // Handle JSX expression containers safely if (t.isNumericLiteral(attr.value.expression)) { min = attr.value.expression.value; } } } if (attr.name.name === 'max' && attr.value) { if (t.isStringLiteral(attr.value)) { max = parseFloat(attr.value.value); } else if (t.isNumericLiteral(attr.value)) { max = attr.value.value; } else if (t.isJSXExpressionContainer(attr.value)) { // Handle JSX expression containers safely if (t.isNumericLiteral(attr.value.expression)) { max = attr.value.expression.value; } } } } } // Determine field type based on input type let fieldType = 'string'; if (['number', 'range'].includes(type)) { fieldType = 'number'; } else if (type === 'checkbox') { fieldType = 'boolean'; } else if (['date', 'datetime-local', 'month', 'time', 'week'].includes(type)) { fieldType = 'date'; } // Infer validation type from name if not already determined if (!validations[name]) { validations[name] = {}; } if (pattern) { validations[name].pattern = pattern; } if (min !== undefined) { validations[name].min = min; } if (max !== undefined) { validations[name].max = max; } if (type === 'email' || name.includes('email')) { validations[name].isEmail = true; } else if (type === 'password' || name.includes('password')) { validations[name].isPassword = true; } else if (name.includes('card') && name.includes('number')) { validations[name].isCreditCard = true; } else if (name.includes('amount') || name.includes('price') || name.includes('cost')) { validations[name].isAmount = true; } // Add field if it has a name if (name) { fields.push({ name, type: fieldType, isRequired: required, validation: validations[name] }); } } } }); // Add state variables that don't have corresponding UI elements Object.entries(stateVars).forEach(([name, type]) => { // Check if this state var already has a UI element if (!fields.some(field => field.name === name)) { fields.push({ name, type, validation: validations[name] }); } }); } catch (error) { console.error('Error parsing component:', error); // Fallback with generic fields if parsing fails fields.push({ name: 'email', type: 'string', validation: { isEmail: true } }, { name: 'password', type: 'string', validation: { isPassword: true } }, { name: 'cardNumber', type: 'string', validation: { isCreditCard: true } }, { name: 'amount', type: 'string', validation: { isAmount: true } }); } // If we couldn't extract any fields, provide some defaults if (fields.length === 0) { fields.push({ name: 'email', type: 'string', validation: { isEmail: true } }, { name: 'password', type: 'string', validation: { isPassword: true } }, { name: 'name', type: 'string' }, { name: 'description', type: 'string' }); } return fields; } /** * Generate valid test data for each field */ function generateValidTestData(fields) { return fields.map(field => { let validValue = ''; if (field.type === 'string') { if (field.validation?.isEmail) { validValue = '"test@example.com"'; } else if (field.validation?.isPassword) { validValue = '"Password123!"'; } else if (field.validation?.isCreditCard) { validValue = '"4111111111111111"'; } else if (field.name.includes('cvv') || field.name.includes('cvc')) { validValue = '"123"'; } else if (field.name.includes('expiry') || field.name.includes('exp')) { validValue = '"12/25"'; } else if (field.validation?.isAmount) { validValue = '"100.00"'; } else if (field.name.includes('name')) { validValue = '"John Doe"'; } else if (field.name.includes('phone')) { validValue = '"555-123-4567"'; } else if (field.name.includes('address')) { validValue = '"123 Main St"'; } else if (field.name.includes('city')) { validValue = '"San Francisco"'; } else if (field.name.includes('state')) { validValue = '"CA"'; } else if (field.name.includes('zip') || field.name.includes('postal')) { validValue = '"94105"'; } else if (field.name.includes('country')) { validValue = '"USA"'; } else { validValue = '"test value"'; } } else if (field.type === 'number') { if (field.validation?.isAmount) { validValue = '100.00'; } else if (field.validation?.min !== undefined && field.validation?.max !== undefined) { // Pick a value between min and max const middle = (field.validation.min + field.validation.max) / 2; validValue = middle.toString(); } else if (field.validation?.min !== undefined) { validValue = (field.validation.min + 1).toString(); } else if (field.validation?.max !== undefined) { validValue = (field.validation.max - 1).toString(); } else { validValue = '42'; } } else if (field.type === 'boolean') { validValue = 'true'; } else if (field.type === 'date') { validValue = '"2025-01-01"'; } else { validValue = '"test value"'; } return ` ${field.name}: ${validValue},`; }).join('\n'); } /** * Generate invalid test data for each field */ function generateInvalidTestData(fields) { return fields.map(field => { let invalidValue = ''; if (field.type === 'string') { if (field.validation?.isEmail) { invalidValue = '"not-an-email"'; } else if (field.validation?.isPassword) { invalidValue = '"weak"'; } else if (field.validation?.isCreditCard) { invalidValue = '"1234"'; // Too short } else if (field.name.includes('cvv') || field.name.includes('cvc')) { invalidValue = '"abc"'; // Non-numeric } else if (field.name.includes('expiry') || field.name.includes('exp')) { invalidValue = '"invalid"'; } else if (field.validation?.isAmount) { invalidValue = '"-50"'; // Negative amount } else if (field.isRequired) { invalidValue = '""'; // Empty string for required fields } else { invalidValue = '""'; // Empty string as default invalid value } } else if (field.type === 'number') { if (field.validation?.isAmount) { invalidValue = '-50'; // Negative amount } else if (field.validation?.min !== undefined) { invalidValue = (field.validation.min - 1).toString(); // Below minimum } else if (field.validation?.max !== undefined) { invalidValue = (field.validation.max + 1).toString(); // Above maximum } else { invalidValue = 'NaN'; // Not a number } } else if (field.type === 'boolean') { invalidValue = 'null'; // null for boolean } else if (field.type === 'date') { invalidValue = '"invalid-date"'; // Invalid date string } else { invalidValue = 'null'; // null as default invalid value } return ` ${field.name}: ${invalidValue},`; }).join('\n'); } /** * Generate XSS test data for each field */ function generateXSSTestData(fields) { return fields .filter(field => field.type === 'string') .map(field => { return ` ${field.name}: ${field.name === 'email' ? '"test<script>alert(\\"XSS\\")</script>@example.com"' : '"test<script>alert(\\"XSS\\")</script>"'},`; }).join('\n'); } /** * Generate SQL injection test data for each field */ function generateSQLInjectionTestData(fields) { return fields .filter(field => field.type === 'string') .map(field => { return ` ${field.name}: "${field.name.includes('email') ? 'test\' OR 1=1 --@example.com' : 'test\' OR 1=1 --'}",`; }).join('\n'); } /** * Generate edge case test data for each field */ function generateEdgeCaseTestData(fields) { const results = []; // Add boundary values for amounts const amountFields = fields.filter(f => f.validation?.isAmount || f.name.includes('amount') || f.name.includes('price')); if (amountFields.length > 0) { results.push(' zeroAmount: "0.00",'); results.push(' largeAmount: "999999.99",'); } // Add boundary values for credit cards const cardFields = fields.filter(f => f.validation?.isCreditCard || (f.name.includes('card') && f.name.includes('number'))); if (cardFields.length > 0) { results.push(' minCardLength: "0".repeat(13), // Minimum valid length'); results.push(' maxCardLength: "0".repeat(19), // Maximum valid length'); } // Add special characters for string fields const nameFields = fields.filter(f => f.name.includes('name')); if (nameFields.length > 0) { results.push(' specialChars: {'); results.push(' nameOnCard: "O\'Connor-Smith Jr."'); results.push(' }'); } return results.join('\n'); } /** * Generate helper methods for the test data factory */ function generateHelperMethods(componentName, fields) { let methods = ''; // Generate card number helper if we have credit card fields const cardFields = fields.filter(f => f.validation?.isCreditCard || (f.name.includes('card') && f.name.includes('number'))); if (cardFields.length > 0) { methods += ` /** * Generate a random valid credit card number for testing * (For simulation only - these are not real card algorithms) */ export function generateValidCardNumber(type: 'visa' | 'mastercard' | 'amex' | 'discover' = 'visa'): string { const prefixes = { visa: '4', mastercard: '5', amex: '34', discover: '6011' }; const lengths = { visa: 16, mastercard: 16, amex: 15, discover: 16 }; const prefix = prefixes[type]; const length = lengths[type]; // Generate random digits for the rest of the card let cardNumber = prefix; const remainingDigits = length - prefix.length; for (let i = 0; i < remainingDigits; i++) { cardNumber += Math.floor(Math.random() * 10).toString(); } return cardNumber; }`; } // Generate expiry date helper if we have expiry date fields const expiryFields = fields.filter(f => f.name.includes('expiry') || f.name.includes('exp')); if (expiryFields.length > 0) { methods += ` /** * Generate a valid expiry date for testing (MM/YY format) * Always generates a date in the future */ export function generateValidExpiryDate(): string { const currentDate = new Date(); const month = Math.floor(Math.random() * 12) + 1; // 1-12 // Generate a year 1-10 years in the future const yearsToAdd = Math.floor(Math.random() * 10) + 1; const year = (currentDate.getFullYear() + yearsToAdd) % 100; // Get last 2 digits // Format with leading zeros if needed const formattedMonth = month.toString().padStart(2, '0'); const formattedYear = year.toString().padStart(2, '0'); return \`\${formattedMonth}/\${formattedYear}\`; }`; } return methods; } //# sourceMappingURL=testDataTemplate.js.map