Files
runtipi-appstore/__tests__/apps.test.ts
T
git-hermes e2b6a35b26
Test / test (pull_request) Canceled after 0s
fix(n8n-sandbox): remove unused port selector
2026-09-03 10:37:49 +02:00

146 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { expect, test, describe } from "bun:test";
import { appInfoSchema, dynamicComposeSchema } from '@runtipi/common/schemas'
import { fromError } from 'zod-validation-error';
import fs from 'node:fs'
import path from 'node:path'
import YAML from 'yaml'
const getApps = async () => {
const appsDir = await fs.promises.readdir(path.join(process.cwd(), 'apps'))
const appDirs = appsDir.filter((app) => {
const stat = fs.statSync(path.join(process.cwd(), 'apps', app))
return stat.isDirectory()
})
return appDirs
};
const getFile = async (app: string, file: string) => {
const filePath = path.join(process.cwd(), 'apps', app, file)
try {
const file = await fs.promises.readFile(filePath, 'utf-8')
return file
} catch (err) {
return null
}
}
describe("each app should have the required files", async () => {
const apps = await getApps()
for (const app of apps) {
const files = ['config.json', 'metadata/logo.jpg', 'metadata/description.md']
for (const file of files) {
test(`app ${app} should have ${file}`, async () => {
const fileContent = await getFile(app, file)
expect(fileContent).not.toBeNull()
})
}
test(`app ${app} should have a compose file`, async () => {
const legacyCompose = await getFile(app, 'docker-compose.json')
const modernCompose = await getFile(app, 'docker-compose.yml')
expect(legacyCompose || modernCompose).not.toBeNull()
})
}
})
describe("each app should have a valid config.json", async () => {
const apps = await getApps()
for (const app of apps) {
test(`app ${app} should have a valid config.json`, async () => {
const fileContent = await getFile(app, 'config.json')
const parsed = appInfoSchema.omit({ urn: true }).safeParse(JSON.parse(fileContent || '{}'))
if (!parsed.success) {
const validationError = fromError(parsed.error);
console.error(`Error parsing config.json for app ${app}:`, validationError.toString());
}
expect(parsed.success).toBe(true)
})
}
})
describe("n8n-sandbox installation secrets", () => {
test("exposes the shared API key as a password field", async () => {
const fileContent = await getFile('n8n-sandbox', 'config.json')
const config = JSON.parse(fileContent || '{}')
const apiKey = config.form_fields?.find((field: { env_variable?: string }) => field.env_variable === 'SANDBOX_API_KEYS')
expect(apiKey?.type).toBe('password')
expect(apiKey?.required).toBe(true)
expect(apiKey?.min).toBeGreaterThanOrEqual(48)
expect(config.port).toBeUndefined()
expect(config.tipi_version).toBeGreaterThanOrEqual(5)
})
})
describe("n8n-sandbox post-install documentation", () => {
test("documents the actual n8n override and optional SearXNG JSON setup", async () => {
const description = await getFile('n8n-sandbox', 'metadata/description.md')
expect(description).toContain('services:\n n8n-2:\n environment:')
expect(description).toContain('N8N_SANDBOX_SERVICE_URL=http://sandbox-api:8080')
expect(description).toContain("Runtipi ne demande aucun port pour cette app")
expect(description).not.toContain('sandbox-api:<PORT')
expect(description).toContain('N8N_SANDBOX_SERVICE_API_KEY=<clé choisie lors de linstallation>')
expect(description).toContain('/opt/runtipi/app-data/migrated/searxng/data/settings.yml')
expect(description).toContain('- json')
})
})
describe("modern compose files preserve runtime semantics", () => {
test("n8n-sandbox keeps its certificate bootstrap healthy for Runtipi", async () => {
const fileContent = await getFile('n8n-sandbox', 'docker-compose.yml')
expect(fileContent).not.toBeNull()
const parsed = YAML.parse(fileContent || '')
const certs = parsed.services?.['sandbox-certs']
const api = parsed.services?.['sandbox-api']
expect(parsed['x-runtipi']?.schema_version).toBe(2)
const certCommand = certs?.command?.join(' ') || ''
expect(certs?.restart).toBe('unless-stopped')
expect(certCommand).toContain('rm -f /tmp/certs-ready')
expect(certCommand).toContain('touch /tmp/certs-ready')
expect(certCommand).toContain('tail -f /dev/null')
expect(certCommand.indexOf('rm -f /tmp/certs-ready')).toBeLessThan(certCommand.indexOf('bootstrap-mtls.sh'))
expect(certs?.healthcheck?.test).toContain('test -f /tmp/certs-ready')
expect(api?.depends_on?.['sandbox-certs']?.condition).toBe('service_healthy')
expect(api?.['x-runtipi']?.is_main).toBe(true)
expect(parsed.services?.['sandbox-runner-1']?.environment?.SANDBOX_RUNNER_HTTP_BASE_URL).toBe('https://sandbox-runner-1:8080')
expect(parsed.services?.['sandbox-runner-1']?.healthcheck?.test).toContain('https://localhost:8080/readyz')
})
})
describe("each app should have a valid compose file", async () => {
const apps = await getApps()
for (const app of apps) {
test(`app ${app} should have a valid compose file`, async () => {
const legacyCompose = await getFile(app, 'docker-compose.json')
const modernCompose = await getFile(app, 'docker-compose.yml')
if (modernCompose) {
const parsed = YAML.parse(modernCompose)
expect(parsed['x-runtipi']?.schema_version).toBeTypeOf('number')
expect(parsed.services).toBeTypeOf('object')
return
}
const parsed = dynamicComposeSchema.safeParse(JSON.parse(legacyCompose || '{}'))
if (!parsed.success) {
const validationError = fromError(parsed.error);
console.error(`Error parsing compose file for app ${app}:`, validationError.toString());
}
expect(parsed.success).toBe(true)
})
}
});