test: expand onebox integration coverage

This commit is contained in:
2026-05-24 06:00:23 +00:00
parent 08ab7fea8e
commit 150e023cf9
6 changed files with 528 additions and 90 deletions
+124 -1
View File
@@ -1,5 +1,5 @@
import assert from 'assert/strict';
import { readFileSync } from 'fs';
import { existsSync, readFileSync, readdirSync } from 'fs';
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
@@ -9,6 +9,10 @@ const readWorkspaceFile = (pathFromRoot: string) => {
return readFileSync(resolve(root, pathFromRoot), 'utf8');
};
const readWorkspaceJson = <T>(pathFromRoot: string) => {
return JSON.parse(readWorkspaceFile(pathFromRoot)) as T;
};
const assertIncludes = (filePath: string, expected: string) => {
const content = readWorkspaceFile(filePath);
assert.ok(content.includes(expected), `${filePath} should include ${expected}`);
@@ -19,6 +23,86 @@ const assertExcludes = (filePath: string, forbidden: string) => {
assert.ok(!content.includes(forbidden), `${filePath} should not include ${forbidden}`);
};
const assertPort = (valueArg: unknown, labelArg: string) => {
assert.ok(Number.isInteger(valueArg), `${labelArg} should be an integer`);
assert.ok((valueArg as number) >= 1 && (valueArg as number) <= 65535, `${labelArg} should be a valid TCP/UDP port`);
};
const assertValidPublishedPort = (portArg: any, labelArg: string) => {
assertPort(portArg.targetPort, `${labelArg}.targetPort`);
const targetEnd = portArg.targetPortEnd ?? portArg.targetPort;
assertPort(targetEnd, `${labelArg}.targetPortEnd`);
assert.ok(targetEnd >= portArg.targetPort, `${labelArg} target range should be ascending`);
const publishedStart = portArg.publishedPort ?? portArg.targetPort;
const publishedEnd = portArg.publishedPortEnd ?? (publishedStart + (targetEnd - portArg.targetPort));
assertPort(publishedStart, `${labelArg}.publishedPort`);
assertPort(publishedEnd, `${labelArg}.publishedPortEnd`);
assert.ok(publishedEnd >= publishedStart, `${labelArg} published range should be ascending`);
assert.equal(targetEnd - portArg.targetPort, publishedEnd - publishedStart, `${labelArg} target and published ranges should have the same size`);
assert.ok(['tcp', 'udp'].includes(portArg.protocol ?? 'tcp'), `${labelArg}.protocol should be tcp or udp`);
};
const assertValidAppCatalog = () => {
const catalog = readWorkspaceJson<{
schemaVersion: number;
apps: Array<{ id: string; latestVersion: string }>;
}>('appstore-apptemplates/catalog.json');
assert.equal(catalog.schemaVersion, 1, 'App catalog schema version should be 1');
const appIds = catalog.apps.map((appArg) => appArg.id);
assert.equal(new Set(appIds).size, appIds.length, 'App catalog ids should be unique');
const appDirs = readdirSync(resolve(root, 'appstore-apptemplates/apps'), { withFileTypes: true })
.filter((direntArg) => direntArg.isDirectory())
.map((direntArg) => direntArg.name)
.sort();
assert.deepEqual([...appIds].sort(), appDirs, 'Every app directory should be represented in catalog.json');
for (const catalogApp of catalog.apps) {
const appPath = `appstore-apptemplates/apps/${catalogApp.id}`;
const metaPath = `${appPath}/app.json`;
assert.ok(existsSync(resolve(root, metaPath)), `${metaPath} should exist`);
const appMeta = readWorkspaceJson<{
id: string;
latestVersion: string;
versions: string[];
}>(metaPath);
assert.equal(appMeta.id, catalogApp.id, `${metaPath} id should match its directory and catalog entry`);
assert.equal(appMeta.latestVersion, catalogApp.latestVersion, `${metaPath} latestVersion should match catalog.json`);
assert.ok(appMeta.versions.includes(appMeta.latestVersion), `${metaPath} versions should include latestVersion`);
const configPath = `${appPath}/versions/${appMeta.latestVersion}/config.json`;
assert.ok(existsSync(resolve(root, configPath)), `${configPath} should exist`);
const config = readWorkspaceJson<{
image: string;
port: number;
envVars?: Array<{ key: string; value?: string; description: string; required?: boolean }>;
volumes?: Array<string | { mountPath: string }>;
publishedPorts?: any[];
}>(configPath);
assert.ok(config.image, `${configPath} should define an image`);
assertPort(config.port, `${configPath}.port`);
const envKeys = new Set<string>();
for (const envVar of config.envVars || []) {
assert.ok(envVar.key, `${configPath} env var should define a key`);
assert.ok(!envKeys.has(envVar.key), `${configPath} env var ${envVar.key} should be unique`);
envKeys.add(envVar.key);
assert.equal(typeof envVar.description, 'string', `${configPath} env var ${envVar.key} should describe its purpose`);
}
for (const [index, volume] of (config.volumes || []).entries()) {
const mountPath = typeof volume === 'string' ? volume : volume.mountPath;
assert.ok(mountPath?.startsWith('/'), `${configPath} volume ${index} should define an absolute mountPath`);
}
for (const [index, publishedPort] of (config.publishedPorts || []).entries()) {
assertValidPublishedPort(publishedPort, `${configPath}.publishedPorts[${index}]`);
}
}
};
assertIncludes('containerarchive/rust/src/prune.rs', 'rewritten_offsets');
assertExcludes('containerarchive/rust/src/prune.rs', 'offset: entry.offset, // Note: offset in the new pack may differ');
assertIncludes('containerarchive/test/test.ts', 'partial-pack GC rewrites chunks');
@@ -35,17 +119,56 @@ assertIncludes('api/ts/classes.image.ts', 'versionString: imageVersion');
assertIncludes('cloudly/ts/manager.image/classes.imagemanager.ts', 'readFromWebstream(readable)');
assertIncludes('cloudly/ts/manager.service/classes.servicemanager.ts', 'adminIdentityGuard');
assertIncludes('cloudly/ts/manager.cluster/classes.clustermanager.ts', 'getClusterById');
assertIncludes('cloudly/ts/manager.appcatalog/classes.appcatalogmanager.ts', 'installAppCatalogApp');
assertIncludes('cloudly/ts/manager.appcatalog/classes.appcatalogmanager.ts', 'externalImageRef');
assertIncludes('cloudly/ts/manager.appcatalog/classes.appcatalogmanager.ts', 'SERVICE_DOMAIN');
assertIncludes('cloudly/ts/manager.deployment/classes.deploymentmanager.ts', 'deploymentWorkspaceExec');
assertIncludes('coreflow/ts/coreflow.classes.coreflow.ts', 'deploymentRuntimeManager');
assertIncludes('coreflow/ts/coreflow.classes.deploymentruntime.ts', 'coreflowDeploymentWorkspaceExec');
assertIncludes('coreflow/ts/coreflow.classes.clustermanager.ts', 'externalImageRef');
assertIncludes('coreflow/ts/coreflow.classes.clustermanager.ts', 'resolveEnvTemplates');
assertIncludes('corestore/ts/corestore.classes.corestore.ts', "'/archive/manifest'");
assertIncludes('corestore/ts/corestore.classes.corestore.ts', "'/archive/object/read'");
assertIncludes('corestore/ts/corestore.classes.corestore.ts', "'/archive/object/write'");
assertIncludes('platformclient/ts/email/classes.emailconnector.ts', 'return response.responseId');
assertIncludes('platformclient/ts/email/classes.letterconnector.ts', 'return response.processId');
assertIncludes('platformclient/ts/email/classes.pushnotificationconnector.ts', "typeof process !== 'undefined'");
assertIncludes('siprouter/ts/sipproxy.ts', 'return { id: callId };');
assertIncludes('siprouter/ts/storage.ts', 'MONGODB_URI');
assertIncludes('siprouter/ts/storage.ts', 'S3_ENDPOINT');
assertIncludes('siprouter/Dockerfile', 'libjpeg-dev');
assertIncludes('siprouter/Dockerfile', 'libtiff-dev');
assertIncludes('siprouter/Dockerfile', 'libjpeg-turbo');
assertIncludes('siprouter/.smartconfig.json', 'linux/amd64');
assertIncludes('siprouter/.smartconfig.json', 'linux/arm64');
assertIncludes('onebox/ts/opsserver/classes.opsserver.ts', "'/v2/*'");
assertIncludes('onebox/ts/classes/appstore.ts', 'installApp(optionsArg: IAppInstallOptions)');
assertIncludes('onebox/ts/classes/appstore.ts', 'publishedPorts: optionsArg.publishedPorts || config.publishedPorts');
assertIncludes('onebox/ts/classes/docker.ts', 'expandPublishedPorts');
assertIncludes('onebox/ts/classes/docker.ts', 'Mounts: this.getSwarmVolumeMounts(service)');
assertIncludes('onebox/ts/database/migrations/migration-016-service-volumes.ts', 'volumes TEXT');
assertIncludes('onebox/ts/database/migrations/migration-017-service-published-ports.ts', 'published_ports TEXT');
assertIncludes('onebox/ts_interfaces/requests/appstore.ts', "method: 'installAppTemplate'");
assertIncludes('onebox/ts_interfaces/requests/appstore.ts', 'publishedPorts?: data.IServicePublishedPort[]');
assertIncludes('onebox/ts/classes/managed-dcrouter.ts', 'DCROUTER_ADMIN_API_TOKEN');
assertIncludes('api/ts/classes.cloudlyapiclient.ts', 'createNodeJumpCommand');
assertIncludes('interfaces/ts/appcatalog/types.ts', 'publishedPorts?: IAppCatalogPublishedPort[]');
assertIncludes('spark/ts/spark.cli.ts', 'CLOUDLY_URL: cloudlyUrl');
assertValidAppCatalog();
const rustdeskConfig = JSON.parse(readWorkspaceFile('appstore-apptemplates/apps/rustdesk-server/versions/1.0.0/config.json'));
const relay = rustdeskConfig.envVars.find((entry: { key: string }) => entry.key === 'RELAY');
assert.equal(relay?.value, '${SERVICE_DOMAIN}:21116');
const siprouterConfig = readWorkspaceJson<any>('appstore-apptemplates/apps/siprouter/versions/1.0.0/config.json');
assert.equal(siprouterConfig.platformRequirements?.mongodb, true);
assert.equal(siprouterConfig.platformRequirements?.s3, true);
assert.ok(siprouterConfig.publishedPorts.some((portArg: any) => portArg.targetPort === 20000 && portArg.targetPortEnd === 20200 && portArg.protocol === 'udp'));
const gitopsConfig = readWorkspaceJson<any>('appstore-apptemplates/apps/gitops/versions/1.0.0/config.json');
assert.ok(gitopsConfig.volumes.includes('/data/.serve.zone/gitops'));
console.log('codebase regression checks passed');