diff --git a/package-lock.json b/package-lock.json index 8db7c2f..0b042b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,7 +69,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1917,7 +1916,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.6.tgz", "integrity": "sha512-NNu0sjyNxpoiW3YuVFfNz7mxSQ+S4X2G28uqg2s+CzoqoQjLPsWSbsFFyztIAqt2vb8kfEAsJNepMGPTxFDx3Q==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2035,7 +2033,6 @@ "integrity": "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", @@ -2374,7 +2371,6 @@ "integrity": "sha512-rkoPH+RqWopVxDnCBE/ysIdfQ2A7j1eDmW8tCxxrR9nnFBa9jKf86VgsSAzxBd1x+ny0GC4JgiD3SNfRHv3pOg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/utils": "4.0.16", "fflate": "^0.8.2", @@ -2430,7 +2426,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2710,7 +2705,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2901,7 +2895,6 @@ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -3389,7 +3382,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3632,7 +3624,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -5071,7 +5062,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6094,7 +6084,6 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -6148,7 +6137,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6280,7 +6268,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -6356,7 +6343,6 @@ "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", diff --git a/src/backends/traefik/templateParser.ts b/src/backends/traefik/templateParser.ts index cd120fb..7e05ae4 100644 --- a/src/backends/traefik/templateParser.ts +++ b/src/backends/traefik/templateParser.ts @@ -77,6 +77,7 @@ export function renderTemplate(template: string, appName: string, data: XMagicPr /** * Get a value from context, supporting nested property access with dot notation. * e.g., "userData.foo" returns context.userData.foo + * Converts primitives (string, number, boolean) to strings for template substitution. */ function getContextValue(path: string): string | undefined { const parts = path.split('.'); @@ -89,7 +90,11 @@ export function renderTemplate(template: string, appName: string, data: XMagicPr value = (value as Record)[part]; } - return typeof value === 'string' ? value : undefined; + // Convert primitives to strings, reject objects/arrays/functions + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return String(value); + } + return undefined; } // Replace all {{ key }} occurrences diff --git a/src/config.ts b/src/config.ts index d727bd6..41ba613 100644 --- a/src/config.ts +++ b/src/config.ts @@ -23,7 +23,7 @@ export function getDefaultConfigFile(): string { export const DEFAULT_CONFIG_FILE = getDefaultConfigFile(); /** Valid proxy backend names */ -const VALID_BACKENDS = ['traefik'] as const; +const VALID_BACKENDS: readonly ['traefik'] = ['traefik']; /** * Load and validate a configuration file. diff --git a/test/unit/traefik/helpers.test.ts b/test/unit/traefik/helpers.test.ts new file mode 100644 index 0000000..74f7574 --- /dev/null +++ b/test/unit/traefik/helpers.test.ts @@ -0,0 +1,128 @@ +import { describe, it, expect } from 'vitest'; +import { getErrorMessage, detectCollisions } from '../../../src/backends/traefik/helpers'; + +describe('Traefik Helpers', () => { + describe('getErrorMessage', () => { + it('extracts message from Error object', () => { + const error = new Error('Something went wrong'); + expect(getErrorMessage(error)).toBe('Something went wrong'); + }); + + it('converts string to string', () => { + expect(getErrorMessage('plain string error')).toBe('plain string error'); + }); + + it('converts number to string', () => { + expect(getErrorMessage(42)).toBe('42'); + }); + + it('converts null to string', () => { + expect(getErrorMessage(null)).toBe('null'); + }); + + it('converts undefined to string', () => { + expect(getErrorMessage(undefined)).toBe('undefined'); + }); + + it('converts object to string', () => { + const obj = { code: 'ERR_UNKNOWN' }; + expect(getErrorMessage(obj)).toBe('[object Object]'); + }); + + it('handles TypeError', () => { + const error = new TypeError('Invalid type'); + expect(getErrorMessage(error)).toBe('Invalid type'); + }); + + it('handles custom Error subclass', () => { + class CustomError extends Error { + constructor(message: string) { + super(message); + this.name = 'CustomError'; + } + } + const error = new CustomError('Custom error message'); + expect(getErrorMessage(error)).toBe('Custom error message'); + }); + }); + + describe('detectCollisions', () => { + it('detects single collision', () => { + const target = { foo: 1, bar: 2 }; + const source = { foo: 3, baz: 4 }; + const collisions = detectCollisions(target, source); + expect(collisions).toEqual(['foo']); + }); + + it('detects multiple collisions', () => { + const target = { foo: 1, bar: 2, baz: 3 }; + const source = { foo: 10, bar: 20, qux: 40 }; + const collisions = detectCollisions(target, source); + expect(collisions).toContain('foo'); + expect(collisions).toContain('bar'); + expect(collisions).toHaveLength(2); + }); + + it('returns empty array when no collisions', () => { + const target = { foo: 1, bar: 2 }; + const source = { baz: 3, qux: 4 }; + const collisions = detectCollisions(target, source); + expect(collisions).toEqual([]); + }); + + it('handles empty target', () => { + const target = {}; + const source = { foo: 1, bar: 2 }; + const collisions = detectCollisions(target, source); + expect(collisions).toEqual([]); + }); + + it('handles empty source', () => { + const target = { foo: 1, bar: 2 }; + const source = {}; + const collisions = detectCollisions(target, source); + expect(collisions).toEqual([]); + }); + + it('handles both empty objects', () => { + const collisions = detectCollisions({}, {}); + expect(collisions).toEqual([]); + }); + + it('handles undefined target (default parameter)', () => { + const source = { foo: 1, bar: 2 }; + const collisions = detectCollisions(undefined, source); + expect(collisions).toEqual([]); + }); + + it('handles undefined source (default parameter)', () => { + const target = { foo: 1, bar: 2 }; + const collisions = detectCollisions(target, undefined); + expect(collisions).toEqual([]); + }); + + it('works with different value types', () => { + const target = { str: 'hello', num: 42, bool: true }; + const source = { str: 'world', obj: { nested: 'value' } }; + const collisions = detectCollisions(target, source); + expect(collisions).toEqual(['str']); + }); + + it('detects all keys when source is subset of target', () => { + const target = { a: 1, b: 2, c: 3, d: 4 }; + const source = { a: 10, c: 30 }; + const collisions = detectCollisions(target, source); + expect(collisions).toContain('a'); + expect(collisions).toContain('c'); + expect(collisions).toHaveLength(2); + }); + + it('preserves order of keys from source', () => { + const target = { a: 1, b: 2, c: 3 }; + const source = { c: 30, a: 10, b: 20 }; + const collisions = detectCollisions(target, source); + // Object.keys() order in modern JS follows insertion order + expect(collisions).toEqual(['c', 'a', 'b']); + }); + }); +});