import { assertEquals, assertThrows } from '@std/assert'; import { AppStoreManager } from '../ts/classes/appstore.ts'; import { OneboxDockerManager } from '../ts/classes/docker.ts'; import type { IAppVersionConfig } from '../ts/classes/appstore-types.ts'; import type { IService } from '../ts/types.ts'; const createAppStore = () => new AppStoreManager({} as any); const baseConfig: IAppVersionConfig = { image: 'example/app:1.0.0', port: 3000, envVars: [ { key: 'APP_PORT', value: '3000', description: 'Application port', required: true, }, ], }; const baseService: IService = { id: 1, name: 'test-service', image: 'example/app:1.0.0', envVars: {}, port: 3000, status: 'stopped', createdAt: Date.now(), updatedAt: Date.now(), }; Deno.test('appstore normalizes and validates app template runtime fields', () => { const appStore = createAppStore(); const normalizedVolumes = appStore.normalizeVolumes([ '/data/app', { mountPath: '/config', readOnly: true }, ]); assertEquals(normalizedVolumes, [ { mountPath: '/data/app' }, { mountPath: '/config', readOnly: true }, ]); appStore.validateAppVersionConfig({ ...baseConfig, volumes: normalizedVolumes, publishedPorts: [ { targetPort: 3000, publishedPort: 3000, protocol: 'tcp' }, { targetPort: 20000, targetPortEnd: 20002, publishedPort: 20000, publishedPortEnd: 20002, protocol: 'udp' }, ], }); }); Deno.test('appstore rejects invalid template ports and volumes', () => { const appStore = createAppStore(); assertThrows( () => appStore.validateAppVersionConfig({ ...baseConfig, port: 70000 }), Error, 'Invalid app config port', ); assertThrows( () => appStore.normalizeVolumes([{ mountPath: 'relative/path' }]), Error, 'mountPath must be an absolute path', ); assertThrows( () => appStore.validateAppVersionConfig({ ...baseConfig, publishedPorts: [ { targetPort: 3000, targetPortEnd: 3002, publishedPort: 3000, publishedPortEnd: 3001, protocol: 'tcp' }, ], }), Error, 'ranges must have the same size', ); }); Deno.test('appstore resolves repo manifests and docker digest-tracked latest images', async () => { const catalogBaseUrl = 'https://catalog.example.test'; const manifestUrl = 'https://code.example.test/cloudly/servezone.catalog.json'; const digest = 'sha256:1234567890abcdef'; const fakeFetch: typeof fetch = async (input, init) => { const url = input instanceof Request ? input.url : input.toString(); const method = init?.method || 'GET'; if (url === `${catalogBaseUrl}/catalog.resolved.json`) { return new Response('not found', { status: 404 }); } if (url === `${catalogBaseUrl}/catalog.json`) { return Response.json({ schemaVersion: 1, updatedAt: '2026-05-24T00:00:00Z', apps: [ { id: 'cloudly', name: 'Cloudly', description: 'Central metadata can stay curated.', category: 'Dev Tools', latestVersion: '1.0.0', source: { type: 'repoManifest', url: manifestUrl, ref: 'main', }, }, ], }); } if (url === manifestUrl) { return Response.json({ schemaVersion: 1, app: { id: 'cloudly', name: 'Cloudly', description: 'Manifest-owned app metadata.', category: 'Dev Tools', maintainer: 'serve.zone', }, latestVersion: 'latest', source: { type: 'dockerImage', image: 'registry.example.test/serve.zone/cloudly:latest', tracking: 'digest', }, runtime: { image: 'registry.example.test/serve.zone/cloudly:latest', port: 80, }, }); } if ( url === 'https://registry.example.test/v2/serve.zone/cloudly/manifests/latest' && method === 'HEAD' ) { return new Response(null, { status: 200, headers: { 'docker-content-digest': digest }, }); } return new Response(`unexpected ${method} ${url}`, { status: 500 }); }; const appStore = new AppStoreManager({} as any, { repoBaseUrl: catalogBaseUrl, fetch: fakeFetch, }); const catalog = await appStore.getCatalog(); assertEquals(catalog.apps[0].latestVersion, `latest@${digest}`); assertEquals(catalog.apps[0].resolvedSource?.manifestHash?.length, 64); assertEquals(catalog.apps[0].upgradeStrategy, 'dockerDigest'); const appMeta = await appStore.getAppMeta('cloudly'); assertEquals(appMeta.latestVersion, `latest@${digest}`); assertEquals(appMeta.versions, [`latest@${digest}`]); const config = await appStore.getAppVersionConfig('cloudly', appMeta.latestVersion); assertEquals(config.image, 'registry.example.test/serve.zone/cloudly:latest'); assertEquals(config.catalogVersion, `latest@${digest}`); assertEquals(config.resolvedImageDigest, digest); }); Deno.test('docker service spec validation rejects unsafe volume and port declarations', () => { const dockerManager = new OneboxDockerManager(); dockerManager.validateServiceSpec({ ...baseService, volumes: [{ mountPath: '/data/app' }], publishedPorts: [{ targetPort: 3000, publishedPort: 3000, protocol: 'tcp' }], }); assertThrows( () => dockerManager.validateServiceSpec({ ...baseService, volumes: [{ mountPath: 'relative/path' }], }), Error, 'must be an absolute path', ); assertThrows( () => dockerManager.validateServiceSpec({ ...baseService, publishedPorts: [ { targetPort: 3001, publishedPort: 3000, hostIp: '127.0.0.1', protocol: 'tcp' }, { targetPort: 3000, publishedPort: 3000, protocol: 'tcp' }, ], }), Error, 'Duplicate published port', ); });