diff --git a/src/models/Config.tsx b/src/models/Config.tsx index 407c824..adbc5b5 100644 --- a/src/models/Config.tsx +++ b/src/models/Config.tsx @@ -97,29 +97,48 @@ export class Config { * Updates template by adding placeholders for environment params */ public updateTemplateFromEnv(env: Env): void { - let templateObj: Record = {}; + // If template is empty, initialize with empty object + if (!this.template.content || !this.template.content.trim()) { + this.template = new ConfigTemplate('{}'); + } // Try to parse existing template + let templateObj: Record = {}; + let hasExistingContent = false; + try { if (this.template.content.trim()) { templateObj = JSON.parse(this.template.content); + hasExistingContent = Object.keys(templateObj).length > 0; } } catch { - // Start fresh if invalid JSON + // If invalid JSON, preserve the raw content and don't modify + return; } // Add placeholders for params that don't exist yet + let hasChanges = false; for (const param of env.params) { if (param.name && param.name.trim()) { - const placeholder = `@${param.name}@`; + const paramName = param.name.trim(); + const placeholder = `@${paramName}@`; + const templateKey = `!!! ${paramName}`; + // Check if placeholder exists anywhere in template if (!this.template.content.includes(placeholder)) { - templateObj[`!!! ${param.name}`] = placeholder; + // Only add if not already in templateObj + if (!templateObj[templateKey]) { + templateObj[templateKey] = placeholder; + hasChanges = true; + } } } } - this.template = new ConfigTemplate(JSON.stringify(templateObj, null, 4)); + // Only update if there are actual changes + if (hasChanges || !hasExistingContent) { + this.template = new ConfigTemplate(JSON.stringify(templateObj, null, 4)); + } } /** diff --git a/src/test/ConfigUpdate.test.ts b/src/test/ConfigUpdate.test.ts new file mode 100644 index 0000000..80a3463 --- /dev/null +++ b/src/test/ConfigUpdate.test.ts @@ -0,0 +1,159 @@ +import { describe, it, expect } from 'vitest'; +import { Config } from '../models/Config'; +import { Env } from '../models/Env'; +import { EnvParam } from '../models/EnvParam'; + +describe('Config Template Update', () => { + it('should preserve existing template when adding new parameter', () => { + // Arrange: Create config with existing template + const config = new Config(); + const existingTemplate = `{ + "existingKey": "existingValue", + "anotherKey": 123 +}`; + config.setTemplate(existingTemplate); + + const env = new Env(0, 'DEFAULT', [ + new EnvParam(1, 'host', 'localhost'), + new EnvParam(2, 'port', '8080') + ]); + config.setEnvs([env]); + + // Act: Update template from env (simulating adding parameters) + config.updateTemplateFromEnv(env); + + // Assert: Template should have both existing content and new placeholders + const templateContent = config.template.content; + const templateObj = JSON.parse(templateContent); + + expect(templateObj.existingKey).toBe('existingValue'); + expect(templateObj.anotherKey).toBe(123); + expect(templateObj['!!! host']).toBe('@host@'); + expect(templateObj['!!! port']).toBe('@port@'); + }); + + it('should not duplicate placeholders that already exist', () => { + // Arrange: Create config with template that already has placeholder + const config = new Config(); + const existingTemplate = `{ + "host": "@host@", + "port": 8080 +}`; + config.setTemplate(existingTemplate); + + const env = new Env(0, 'DEFAULT', [ + new EnvParam(1, 'host', 'localhost'), // Already in template + new EnvParam(2, 'port', '8080') // Already in template + ]); + config.setEnvs([env]); + + // Act: Update template + config.updateTemplateFromEnv(env); + + // Assert: Should not add !!! entries for existing placeholders + const templateContent = config.template.content; + // Count occurrences of @host@ + const hostCount = (templateContent.match(/@host@/g) || []).length; + expect(hostCount).toBe(1); // Only original, no duplicate + + // Should preserve original structure + expect(templateContent).toContain('"host": "@host@"'); + }); + + it('should initialize empty template', () => { + // Arrange: Create config with empty template + const config = new Config(); + config.setTemplate(''); + + const env = new Env(0, 'DEFAULT', [ + new EnvParam(1, 'host', 'localhost') + ]); + config.setEnvs([env]); + + // Act: Update template + config.updateTemplateFromEnv(env); + + // Assert: Should create template with placeholder + const templateContent = config.template.content; + const templateObj = JSON.parse(templateContent); + expect(templateObj['!!! host']).toBe('@host@'); + }); + + it('should not modify invalid JSON template', () => { + // Arrange: Create config with invalid JSON template + const config = new Config(); + const invalidTemplate = `{ + "key": "value" // missing comma + "another": "key" +}`; + config.setTemplate(invalidTemplate); + const originalTemplate = config.template.content; + + const env = new Env(0, 'DEFAULT', [ + new EnvParam(1, 'host', 'localhost') + ]); + config.setEnvs([env]); + + // Act: Update template (should not modify invalid JSON) + config.updateTemplateFromEnv(env); + + // Assert: Template should remain unchanged + expect(config.template.content).toBe(originalTemplate); + }); + + it('should add multiple new placeholders to existing template', () => { + // Arrange + const config = new Config(); + const existingTemplate = `{ + "name": "myapp" +}`; + config.setTemplate(existingTemplate); + + const env = new Env(0, 'DEFAULT', [ + new EnvParam(1, 'host', 'localhost'), + new EnvParam(2, 'port', '8080'), + new EnvParam(3, 'user', 'admin') + ]); + config.setEnvs([env]); + + // Act + config.updateTemplateFromEnv(env); + + // Assert + const templateObj = JSON.parse(config.template.content); + expect(templateObj.name).toBe('myapp'); + expect(templateObj['!!! host']).toBe('@host@'); + expect(templateObj['!!! port']).toBe('@port@'); + expect(templateObj['!!! user']).toBe('@user@'); + }); + + it('should only add placeholders for params not in template', () => { + // Arrange: Template has @host@ but not @port@ + const config = new Config(); + config.setTemplate(`{ + "host": "@host@", + "name": "test" +}`); + + const env = new Env(0, 'DEFAULT', [ + new EnvParam(1, 'host', 'localhost'), // In template + new EnvParam(2, 'port', '8080') // Not in template + ]); + config.setEnvs([env]); + + // Act + config.updateTemplateFromEnv(env); + + // Assert + const templateContent = config.template.content; + const templateObj = JSON.parse(templateContent); + + // Should preserve original + expect(templateObj.host).toBe('@host@'); + expect(templateObj.name).toBe('test'); + + // Should add only missing + expect(templateObj['!!! port']).toBe('@port@'); + expect(templateObj['!!! host']).toBeUndefined(); // Not added, already exists + }); +}); diff --git a/src/test/JsonValidation.test.ts b/src/test/JsonValidation.test.ts new file mode 100644 index 0000000..d6a5486 --- /dev/null +++ b/src/test/JsonValidation.test.ts @@ -0,0 +1,204 @@ +import { describe, it, expect } from 'vitest'; + +/** + * Helper function that mimics the validation logic in ConfigTemplate.tsx + * This is the improved version with comment support + */ +function validateJsonWithComments(value: string): { valid: boolean; error?: string } { + try { + if (!value.trim()) { + return { valid: true }; + } + + // Step 1: Remove /* */ multi-line comments + let sanitized = value.replace(/\/\*[\s\S]*?\*\//g, ''); + + // Step 2: Process line by line to handle // comments outside quotes + const lines = sanitized.split('\n'); + const processedLines = lines.map(line => { + let result = ''; + let inQuote = false; + let quoteChar = ''; + + for (let i = 0; i < line.length; i++) { + const char = line[i]; + const prevChar = i > 0 ? line[i - 1] : ''; + + // Check for quote start/end (not escaped) + if ((char === '"' || char === "'") && prevChar !== '\\') { + if (!inQuote) { + inQuote = true; + quoteChar = char; + } else if (char === quoteChar) { + inQuote = false; + quoteChar = ''; + } + result += char; + } + // Check for // comment start (only outside quotes) + else if (!inQuote && char === '/' && line[i + 1] === '/') { + break; // Rest of line is comment + } + else { + result += char; + } + } + + return result; + }); + + sanitized = processedLines.join('\n'); + + // Step 3: Replace @placeholders@ with dummy values + sanitized = sanitized.replace(/@[^@]+@/g, '1'); + + JSON.parse(sanitized); + return { valid: true }; + } catch (e) { + return { valid: false, error: (e as Error).message }; + } +} + +describe('JSON Validation - Comment Support', () => { + it('should remove single-line // comments outside quotes', () => { + const json = `{ + "key": "value" // this is a comment + }`; + const result = validateJsonWithComments(json); + expect(result.valid).toBe(true); + }); + + it('should preserve // inside quoted strings', () => { + const json = `{ + "url": "http://example.com/path", + "description": "This // that // other" + }`; + const result = validateJsonWithComments(json); + expect(result.valid).toBe(true); + }); + + it('should preserve URLs with // in values', () => { + const json = `{ + "apiUrl": "http://localhost:8080/api", + "cdn": "https://cdn.example.com/assets" + }`; + const result = validateJsonWithComments(json); + expect(result.valid).toBe(true); + }); + + it('should remove multi-line /* */ comments', () => { + const json = `{ + /* This is a + multi-line comment */ + "key": "value" + }`; + const result = validateJsonWithComments(json); + expect(result.valid).toBe(true); + }); + + it('should handle mixed comments and placeholders', () => { + const json = `{ + // Host configuration + "host": "@host@", + "port": @port@, // unquoted placeholder + /* API settings */ + "apiUrl": "http://@host@:@port@/api" // URL with placeholders + }`; + const result = validateJsonWithComments(json); + expect(result.valid).toBe(true); + }); + + it('should handle comment-only lines', () => { + const json = `{ + // Just a comment + "key": "value" + // Another comment + }`; + const result = validateJsonWithComments(json); + expect(result.valid).toBe(true); + }); + + it('should preserve // in string values with special characters', () => { + const json = `{ + "regex": "pattern // with slashes", + "path": "C://Program Files//App", + "comment": "text // more text // even more" + }`; + const result = validateJsonWithComments(json); + expect(result.valid).toBe(true); + }); + + it('should handle escaped quotes correctly', () => { + const json = `{ + "message": "He said \\"hello // world\\"" + }`; + const result = validateJsonWithComments(json); + expect(result.valid).toBe(true); + }); + + it('should reject invalid JSON with comments', () => { + const json = `{ + "key": "value" // missing comma + "another": "key" + }`; + const result = validateJsonWithComments(json); + expect(result.valid).toBe(false); + expect(result.error).toContain('Expected'); + }); + + it('should handle empty template', () => { + const result = validateJsonWithComments(''); + expect(result.valid).toBe(true); + }); + + it('should handle whitespace only', () => { + const result = validateJsonWithComments(' \n\t '); + expect(result.valid).toBe(true); + }); +}); + +describe('JSON Validation - Placeholder Support', () => { + it('should validate quoted placeholders', () => { + const json = `{ + "Host": "@host@", + "Port": "@port@" + }`; + const result = validateJsonWithComments(json); + expect(result.valid).toBe(true); + }); + + it('should validate unquoted placeholders', () => { + const json = `{ + "Host": "@host@", + "Port": @port@ + }`; + const result = validateJsonWithComments(json); + expect(result.valid).toBe(true); + }); + + it('should validate placeholders in URLs', () => { + const json = `{ + "ApiUrl": "http://@host@:@port@/api/v1" + }`; + const result = validateJsonWithComments(json); + expect(result.valid).toBe(true); + }); + + it('should validate complex template with comments and placeholders', () => { + const json = `{ + // Database config + "db": { + "host": "@db_host@", + "port": @db_port@, + "url": "jdbc:mysql://@db_host@:@db_port@/mydb" // connection string + }, + /* API settings */ + "api": { + "baseUrl": "https://@api_host@/v1", + "timeout": @timeout@ + } + }`; + const result = validateJsonWithComments(json); + expect(result.valid).toBe(true); + }); +});