const { APP_TEMPLATES, TEMPLATE_CATEGORIES, DIFFICULTY_LEVELS } = require('../app-templates'); describe('App Templates', () => { const templates = Object.values(APP_TEMPLATES); const templateIds = Object.keys(APP_TEMPLATES); const categoryNames = Object.keys(TEMPLATE_CATEGORIES); describe('Template Structure', () => { it('has at least 40 templates', () => { expect(templates.length).toBeGreaterThanOrEqual(40); }); it('every template has required fields: name, description, icon, category', () => { for (const tmpl of templates) { expect(tmpl).toHaveProperty('name'); expect(tmpl).toHaveProperty('description'); expect(tmpl).toHaveProperty('icon'); expect(tmpl).toHaveProperty('category'); expect(typeof tmpl.name).toBe('string'); expect(tmpl.name.length).toBeGreaterThan(0); expect(typeof tmpl.description).toBe('string'); } }); it('every Docker-based template has docker config with image', () => { for (const id of templateIds) { const tmpl = APP_TEMPLATES[id]; if (!tmpl.docker) continue; // Skip static sites and dashboard widgets expect(tmpl.docker).toHaveProperty('image'); expect(typeof tmpl.docker.image).toBe('string'); expect(tmpl.docker.image.length).toBeGreaterThan(0); } }); it('every template has subdomain property', () => { for (const id of templateIds) { const tmpl = APP_TEMPLATES[id]; expect(tmpl).toHaveProperty('subdomain'); // subdomain can be null for widgets if (tmpl.subdomain !== null) { expect(typeof tmpl.subdomain).toBe('string'); } } }); it('all Docker-based templates have valid defaultPorts (1-65535)', () => { for (const id of templateIds) { const tmpl = APP_TEMPLATES[id]; if (!tmpl.docker) continue; // Skip non-Docker templates const port = tmpl.defaultPort; expect(port).toBeGreaterThanOrEqual(1); expect(port).toBeLessThanOrEqual(65535); } }); it('all category values are in TEMPLATE_CATEGORIES', () => { for (const tmpl of templates) { expect(categoryNames).toContain(tmpl.category); } }); it('Docker images have no shell injection characters', () => { const dangerous = [';', '&', '|', '`', '$', '\n']; for (const id of templateIds) { const tmpl = APP_TEMPLATES[id]; if (!tmpl.docker) continue; const image = tmpl.docker.image; for (const char of dangerous) { expect(image).not.toContain(char); } } }); }); describe('TEMPLATE_CATEGORIES', () => { it('is a non-empty object with category entries', () => { expect(typeof TEMPLATE_CATEGORIES).toBe('object'); expect(TEMPLATE_CATEGORIES).not.toBeNull(); expect(categoryNames.length).toBeGreaterThan(0); }); it('each category has icon and color', () => { for (const name of categoryNames) { const cat = TEMPLATE_CATEGORIES[name]; expect(cat).toHaveProperty('icon'); expect(cat).toHaveProperty('color'); expect(typeof cat.color).toBe('string'); } }); }); describe('DIFFICULTY_LEVELS', () => { it('is a non-empty object with difficulty entries', () => { const levels = Object.keys(DIFFICULTY_LEVELS); expect(levels.length).toBeGreaterThan(0); }); it('each level has color and description', () => { for (const [name, level] of Object.entries(DIFFICULTY_LEVELS)) { expect(level).toHaveProperty('color'); expect(level).toHaveProperty('description'); expect(typeof level.color).toBe('string'); expect(typeof level.description).toBe('string'); } }); it('includes Easy, Intermediate, and Advanced levels', () => { expect(DIFFICULTY_LEVELS).toHaveProperty('Easy'); expect(DIFFICULTY_LEVELS).toHaveProperty('Intermediate'); expect(DIFFICULTY_LEVELS).toHaveProperty('Advanced'); }); }); describe('Specific Templates', () => { it('plex template has PLEX_CLAIM as empty string', () => { const plex = APP_TEMPLATES.plex; expect(plex).toBeDefined(); expect(plex.docker.environment).toHaveProperty('PLEX_CLAIM'); expect(plex.docker.environment.PLEX_CLAIM).toBe(''); }); it('jellyfin template exists with correct default port', () => { const jf = APP_TEMPLATES.jellyfin; expect(jf).toBeDefined(); expect(jf.defaultPort).toBe(8096); }); it('radarr template exists with correct default port', () => { const radarr = APP_TEMPLATES.radarr; expect(radarr).toBeDefined(); expect(radarr.defaultPort).toBe(7878); }); it('sonarr template exists with correct default port', () => { const sonarr = APP_TEMPLATES.sonarr; expect(sonarr).toBeDefined(); expect(sonarr.defaultPort).toBe(8989); }); it('prowlarr template exists with correct default port', () => { const prowlarr = APP_TEMPLATES.prowlarr; expect(prowlarr).toBeDefined(); expect(prowlarr.defaultPort).toBe(9696); }); it('DashCA is a static site without docker config', () => { const dashca = APP_TEMPLATES.dashca; if (dashca) { expect(dashca.isStaticSite).toBe(true); expect(dashca.docker).toBeUndefined(); } }); }); describe('Template Ports', () => { it('all templates with docker.ports have valid port mappings', () => { // Ports use template syntax like "{{PORT}}:32400" or "{{PORT}}:32400/tcp" const portPattern = /^(\{\{PORT\}\}|\d+):(\d+)(\/[a-z]+)?$/; for (const id of templateIds) { const tmpl = APP_TEMPLATES[id]; if (!tmpl.docker || !tmpl.docker.ports) continue; expect(Array.isArray(tmpl.docker.ports)).toBe(true); for (const port of tmpl.docker.ports) { expect(typeof port).toBe('string'); expect(port).toMatch(portPattern); } } }); it('no two templates share the same default port (prevent conflicts)', () => { const portMap = new Map(); for (const id of templateIds) { const port = APP_TEMPLATES[id].defaultPort; if (port !== null) { portMap.set(port, id); } } // At minimum, we should have more unique ports than 30% of templates expect(portMap.size).toBeGreaterThan(templateIds.length * 0.3); }); }); });