diff --git a/e2e/environment.spec.ts b/e2e/environment.spec.ts index adc783e..99d3c37 100644 --- a/e2e/environment.spec.ts +++ b/e2e/environment.spec.ts @@ -119,6 +119,56 @@ test.describe('Environment Management', () => { expect(hostPlaceholderCount).toBe(2); }); + test('should validate template with unquoted placeholders', async ({ page }) => { + await page.goto('/'); + await page.click('button:has-text("Create new")'); + await page.waitForTimeout(500); + + // Add a parameter + await page.click('button:has-text("✚")'); + await page.waitForTimeout(300); + const nameInput = page.locator('input[placeholder="name"]'); + const valueInput = page.locator('input[placeholder="value"]'); + const addButton = page.locator('button:has-text("✓")'); + await nameInput.fill('port'); + await valueInput.fill('8080'); + await addButton.click(); + await page.waitForTimeout(500); + + // Go to Content Template and edit with unquoted placeholder + await page.click('a:has-text("Content Template")'); + await page.waitForTimeout(300); + await page.click('button:has-text("Edit")'); + await page.waitForTimeout(300); + + // Fill template with unquoted @port@ placeholder + const textarea = page.locator('textarea'); + await textarea.fill('{\n "Host": "@host@",\n "Port": @port@,\n "Url": "http://@host@:@port@/api"\n}'); + await page.waitForTimeout(300); + + // Check that Save button is enabled (validation passed) + const saveButton = page.locator('button:has-text("Save")'); + await expect(saveButton).toBeEnabled(); + + // Check that there's no JSON error + const errorAlert = page.locator('.alert-danger'); + await expect(errorAlert).not.toBeVisible(); + + // Save the template + await saveButton.click(); + await page.waitForTimeout(500); + + // Verify it was saved - should be in view mode with Edit button visible + const editButton = page.locator('button:has-text("Edit")'); + await expect(editButton).toBeVisible(); + + // Verify the template content is displayed correctly + const codeContent = page.locator('code'); + await expect(codeContent).toBeVisible(); + const content = await codeContent.textContent(); + expect(content).toContain('@port@'); + }); + test('should download config file with correct filename', async ({ page }) => { await page.goto('/'); await page.click('button:has-text("Create new")'); diff --git a/src/componets/content/ConfigTemplate.tsx b/src/componets/content/ConfigTemplate.tsx index 1055f3c..cb5b102 100644 --- a/src/componets/content/ConfigTemplate.tsx +++ b/src/componets/content/ConfigTemplate.tsx @@ -35,9 +35,10 @@ export function ConfigTemplate(props: ConfigTemplateProps) { } function handleSave() { - // Validate JSON before saving + // Validate JSON before saving (with placeholder support) try { - JSON.parse(draftContent); + const sanitizedValue = draftContent.replace(/@[^@]+@/g, '1'); + JSON.parse(sanitizedValue); setJsonError(null); props.onSaved(draftContent); setMode('view'); @@ -52,12 +53,8 @@ export function ConfigTemplate(props: ConfigTemplateProps) { try { if (value.trim()) { // Replace @placeholders@ with valid JSON values for validation - // Handle both quoted "@placeholder@" and unquoted @placeholder@ - let sanitizedValue = value - // Replace quoted placeholders: "@host@" -> "__PLACEHOLDER__" - .replace(/"@[^"]+@"/g, '"__PLACEHOLDER__"') - // Replace unquoted placeholders between : and , or } : @port@ -> "__PLACEHOLDER__" - .replace(/:\s*@[^,\s}]+@/g, ': "__PLACEHOLDER__"'); + // Strategy: Replace ALL @...@ patterns with "1" (valid for both string and numeric contexts) + const sanitizedValue = value.replace(/@[^@]+@/g, '1'); JSON.parse(sanitizedValue); setJsonError(null); diff --git a/src/test/ConfigTemplate.test.ts b/src/test/ConfigTemplate.test.ts new file mode 100644 index 0000000..1ea9554 --- /dev/null +++ b/src/test/ConfigTemplate.test.ts @@ -0,0 +1,108 @@ +import { describe, it, expect } from 'vitest'; + +describe('JSON Validation with @placeholders@', () => { + /** + * Helper function that mimics the validation logic in ConfigTemplate.tsx + */ + function validateJsonWithPlaceholders(value: string): { valid: boolean; error?: string } { + try { + if (!value.trim()) { + return { valid: true }; + } + + // This is the current implementation from ConfigTemplate.tsx + // Replace ALL @...@ patterns with "1" (valid for both string and numeric contexts) + const sanitizedValue = value.replace(/@[^@]+@/g, '1'); + + JSON.parse(sanitizedValue); + return { valid: true }; + } catch (e) { + return { valid: false, error: (e as Error).message }; + } + } + + it('should validate quoted placeholders', () => { + const json = `{ + "Host": "@host@", + "Port": "@port@" + }`; + + const result = validateJsonWithPlaceholders(json); + expect(result.valid).toBe(true); + }); + + it('should validate unquoted placeholders', () => { + const json = `{ + "Host": "@host@", + "Port": @port@ + }`; + + const result = validateJsonWithPlaceholders(json); + expect(result.valid).toBe(true); + }); + + it('should validate mixed quoted and unquoted placeholders', () => { + const json = `{ + "Host": "@host@", + "Port": @port@, + "ApiPath": "@host@:@port@/v1/data", + "MessageBroker": { + "hosts": @MessageBrokerHosts@ + } + }`; + + const result = validateJsonWithPlaceholders(json); + expect(result.valid).toBe(true); + }); + + it('should validate placeholders inside strings (URLs)', () => { + const json = `{ + "ApiUrl": "http://@host@:@port@/api" + }`; + + const result = validateJsonWithPlaceholders(json); + expect(result.valid).toBe(true); + }); + + it('should validate complex real-world template', () => { + const json = `{ + "Host": "@host@", + "Port": @port@, + + "ApiPath": "@host@:@port@/v1/data", + + "MessageBroker": { + "hosts": @MessageBrokerHosts@ + }, + + "basePath": "./@env_name@/in", + + "NoParam": "@no_param@" + }`; + + const result = validateJsonWithPlaceholders(json); + expect(result.valid).toBe(true); + }); + + it('should reject invalid JSON structure', () => { + const json = `{ + "Host": "@host@", + "Port": @port@ + "Missing": "comma" + }`; + + const result = validateJsonWithPlaceholders(json); + expect(result.valid).toBe(false); + expect(result.error).toContain('Expected'); + }); + + it('should handle empty value', () => { + const result = validateJsonWithPlaceholders(''); + expect(result.valid).toBe(true); + }); + + it('should handle whitespace only', () => { + const result = validateJsonWithPlaceholders(' \n\t '); + expect(result.valid).toBe(true); + }); +});