feat: tests

This commit is contained in:
2023-04-21 12:08:35 +02:00
parent 8cb9285462
commit d8c79256c9
140 changed files with 2100 additions and 2693 deletions

5
.c8rc.json Normal file
View File

@@ -0,0 +1,5 @@
{
"all": true,
"src": "./src",
"reporter": ["text", "text-summary", "cobertura", "html"]
}

5
.mocharc.json Normal file
View File

@@ -0,0 +1,5 @@
{
"extension": ["ts"],
"node-option": ["loader=ts-node/esm"],
"spec": ["test/**/*.spec.ts"]
}

View File

@@ -1,33 +1,18 @@
const path = require("path"); const path = require("path");
const merge = require("deepmerge"); const merge = require("deepmerge");
const additionalDeps = {
'@openstapps/eslint-config': require('./configuration/eslint-config/package.json'),
'@openstapps/prettier-config': require('./configuration/prettier-config/package.json'),
}
function readPackage(pkg, context) { function readPackage(pkg, context) {
const eslintDeps = require('./configuration/eslint-config/package.json').peerDependencies; for (const dep in additionalDeps) {
const prettierDeps = require('./configuration/prettier-config/package.json').peerDependencies; if (dep in pkg.devDependencies) {
Object.assign(pkg.devDependencies, additionalDeps[dep].peerDependencies)
pkg.devDependencies = { }
...eslintDeps,
...prettierDeps,
...(pkg.devDependencies || {}),
} }
// const targetConfig = defaultConfig
// .provideFields
// ?.map(it => it.split('.'))
// .reduce((acc, curr) => {
// let target = acc;
// let from = defaultConfig;
// for (const fragment of curr.slice(0, -1)) {
// target[fragment] = target[fragment] || {}
// target = target[fragment]
// from = from[fragment]
// }
// const fragment = curr[curr.length - 1]
// target[fragment] = from[fragment];
// return acc;
// }, {}) ?? {}
// return merge(targetConfig, pkg);
return pkg return pkg
} }

View File

@@ -20,8 +20,8 @@
"scripts": { "scripts": {
"build": "tsup --dts", "build": "tsup --dts",
"dev": "tsup --watch", "dev": "tsup --watch",
"format": "prettier .", "format": "prettier . --ignore-path ../../.gitignore",
"format:fix": "prettier --write .", "format:fix": "prettier --write . --ignore-path ../../.gitignore",
"lint": "eslint --ext .ts src/", "lint": "eslint --ext .ts src/",
"lint:fix": "eslint --fix --ext .ts src/", "lint:fix": "eslint --fix --ext .ts src/",
"start": "NODE_CONFIG_ENV=elasticsearch ALLOW_NO_TRANSPORT=true node ./lib/cli.js", "start": "NODE_CONFIG_ENV=elasticsearch ALLOW_NO_TRANSPORT=true node ./lib/cli.js",

View File

@@ -13,9 +13,10 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {ConfigFile} from '../src/common.js';
const config: ConfigFile = { // ESM is not supported, and cts is not detected, so we use type-checked cjs instead.
/** @type {import('../src/common').ConfigFile} */
const configFile = {
activeVersions: ['1\\.0\\.\\d+', '2\\.0\\.\\d+'], activeVersions: ['1\\.0\\.\\d+', '2\\.0\\.\\d+'],
hiddenRoutes: ['/bulk'], hiddenRoutes: ['/bulk'],
logFormat: 'default', logFormat: 'default',
@@ -31,4 +32,4 @@ const config: ConfigFile = {
}, },
}; };
export default config; export default configFile;

View File

@@ -17,11 +17,11 @@
"bin": "app.js", "bin": "app.js",
"scripts": { "scripts": {
"build": "tsup --dts", "build": "tsup --dts",
"format": "prettier .", "format": "prettier . --ignore-path ../../.gitignore",
"format:fix": "prettier --write .", "format:fix": "prettier --write . --ignore-path ../../.gitignore",
"lint": "eslint --ext .ts src/", "lint": "eslint --ext .ts src/",
"lint:fix": "eslint --fix --ext .ts src/", "lint:fix": "eslint --fix --ext .ts src/",
"test": "nyc mocha 'test/**/*.spec.ts'" "test": "c8 mocha"
}, },
"dependencies": { "dependencies": {
"@openstapps/logger": "workspace:*", "@openstapps/logger": "workspace:*",
@@ -33,38 +33,36 @@
"dockerode": "3.3.5", "dockerode": "3.3.5",
"is-cidr": "4.0.2", "is-cidr": "4.0.2",
"mustache": "4.2.0", "mustache": "4.2.0",
"node-port-scanner": "3.0.1", "semver": "7.3.8",
"semver": "7.3.8" "typescript": "4.8.4"
}, },
"devDependencies": { "devDependencies": {
"@openstapps/eslint-config": "workspace:*", "@openstapps/eslint-config": "workspace:*",
"@openstapps/nyc-config": "workspace:*", "@openstapps/nyc-config": "workspace:*",
"@openstapps/prettier-config": "workspace:*", "@openstapps/prettier-config": "workspace:*",
"@openstapps/tsconfig": "workspace:*", "@openstapps/tsconfig": "workspace:*",
"@testdeck/mocha": "0.3.3",
"@types/chai": "4.3.5", "@types/chai": "4.3.5",
"@types/chai-spies": "1.0.3", "@types/sinon-chai": "3.2.9",
"@types/config": "3.3.0", "@types/config": "3.3.0",
"@types/sinon": "10.0.14",
"@types/dockerode": "3.3.14", "@types/dockerode": "3.3.14",
"@types/mustache": "4.2.2", "@types/mustache": "4.2.2",
"@types/mocha": "10.0.1",
"@types/node": "18.15.3", "@types/node": "18.15.3",
"@types/proxyquire": "1.3.28", "@types/proxyquire": "1.3.28",
"@types/semver": "7.3.13", "@types/semver": "7.3.13",
"@types/sha1": "1.1.3", "@types/sha1": "1.1.3",
"chai": "4.3.7", "chai": "4.3.7",
"chai-spies": "1.0.0", "sinon": "15.0.4",
"sinon-chai": "3.7.0",
"mocha": "10.2.0", "mocha": "10.2.0",
"nyc": "15.1.0", "c8": "7.13.0",
"proxyquire": "2.1.3",
"rimraf": "3.0.2",
"ts-node": "10.9.1", "ts-node": "10.9.1",
"tsup": "6.7.0", "tsup": "6.7.0",
"typedoc": "0.23.28", "typedoc": "0.23.28"
"typescript": "4.8.4"
}, },
"tsup": { "tsup": {
"entry": [ "entry": [
"src/app.ts",
"src/app.ts" "src/app.ts"
], ],
"sourcemap": true, "sourcemap": true,
@@ -77,8 +75,5 @@
"extends": [ "extends": [
"@openstapps" "@openstapps"
] ]
},
"nyc": {
"extends": "@openstapps/nyc-config"
} }
} }

View File

@@ -15,10 +15,11 @@
*/ */
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import {execSync} from 'child_process'; import {execSync} from 'child_process';
import * as Dockerode from 'dockerode'; import type {ContainerInfo} from 'dockerode';
import mustache from 'mustache'; import mustache from 'mustache';
import {asyncReadFile, asyncWriteFile} from './common.js';
import {getContainers, getTemplateView} from './main.js'; import {getContainers, getTemplateView} from './main.js';
import {readFile, writeFile} from 'fs/promises';
import {configFile} from './common.js';
/* eslint-disable unicorn/prefer-module */ /* eslint-disable unicorn/prefer-module */
@@ -39,14 +40,11 @@ async function updateNginxConfig() {
const containers = await getContainers(); const containers = await getContainers();
const containerHash = containers const containerHash = containers
.map((container: Dockerode.ContainerInfo) => { .map((container: ContainerInfo) => {
return container.Id; return container.Id;
}) })
.join(','); .join(',');
delete require.cache[require.resolve('config')];
// eslint-disable-next-line @typescript-eslint/no-var-requires
const configFile = require('config');
const configHash = JSON.stringify(configFile); const configHash = JSON.stringify(configFile);
// if containers changed -> write config file, reload nginx // if containers changed -> write config file, reload nginx
@@ -57,7 +55,7 @@ async function updateNginxConfig() {
// render nginx config file // render nginx config file
const nginxConfig = mustache.render( const nginxConfig = mustache.render(
await asyncReadFile('nginx.conf.template', 'utf8'), await readFile('nginx.conf.template', 'utf8'),
await getTemplateView(containers), await getTemplateView(containers),
); );
@@ -67,7 +65,7 @@ async function updateNginxConfig() {
Logger.log(`Writing new config file "${configFile.output}"`); Logger.log(`Writing new config file "${configFile.output}"`);
// overwrite nginx config file with our rendered one // overwrite nginx config file with our rendered one
await asyncWriteFile(configFile.output, nginxConfig, 'utf8'); await writeFile(configFile.output, nginxConfig, 'utf8');
Logger.log('Executing "nginx -s reload" to tell nginx to reload the configuration file'); Logger.log('Executing "nginx -s reload" to tell nginx to reload the configuration file');

View File

@@ -15,15 +15,11 @@
*/ */
import {Logger, SMTP} from '@openstapps/logger'; import {Logger, SMTP} from '@openstapps/logger';
import config from 'config'; import config from 'config';
import {existsSync, readFile, writeFile} from 'fs'; import {existsSync} from 'fs';
import {promisify} from 'util';
// set transport on logger // set transport on logger
Logger.setTransport(SMTP.getInstance()); Logger.setTransport(SMTP.getInstance());
export const asyncReadFile = promisify(readFile);
export const asyncWriteFile = promisify(writeFile);
/** /**
* A representation of the file paths of the needed ssl certificates * A representation of the file paths of the needed ssl certificates
*/ */

View File

@@ -20,8 +20,7 @@ import mustache from 'mustache';
import path from 'path'; import path from 'path';
import * as semver from 'semver'; import * as semver from 'semver';
import { import {
asyncReadFile, configFile,
ConfigFile,
isFileType, isFileType,
protocolHardeningParameters, protocolHardeningParameters,
SSLFilePaths, SSLFilePaths,
@@ -29,10 +28,9 @@ import {
SupportedLogFormats, SupportedLogFormats,
TemplateView, TemplateView,
} from './common.js'; } from './common.js';
// @ts-expect-error missing type defs import {readFile} from 'fs/promises';
import nodePortScanner from 'node-port-scanner'; import PortScanner from './port-scanner.js';
/* eslint-disable unicorn/prefer-module */
/* eslint-disable unicorn/no-await-expression-member */ /* eslint-disable unicorn/no-await-expression-member */
/** /**
@@ -90,9 +88,7 @@ Please expose a port if the container should be accessible by NGINX.`);
// Get a routable network connection // Get a routable network connection
for (const network in container.NetworkSettings.Networks) { for (const network in container.NetworkSettings.Networks) {
const scan = await nodePortScanner(container.NetworkSettings.Networks[network].IPAddress, [port]); if (await PortScanner.isPortFree(port, container.NetworkSettings.Networks[network].IPAddress)) {
if ((scan.ports.open as Array<number>).includes(port)) {
Logger.info( Logger.info(
`${container.Names[0]} reachable via ${container.NetworkSettings.Networks[network].IPAddress}:${port}`, `${container.Names[0]} reachable via ${container.NetworkSettings.Networks[network].IPAddress}:${port}`,
); );
@@ -260,7 +256,7 @@ export async function generateMetricsServer(logFormat: string, enableMetrics?: b
* @param view Data to render template with * @param view Data to render template with
*/ */
async function renderTemplate(path: string, view: unknown): Promise<string> { async function renderTemplate(path: string, view: unknown): Promise<string> {
const content = await asyncReadFile(path, 'utf8'); const content = await readFile(path, 'utf8');
return mustache.render(content, view); return mustache.render(content, view);
} }
@@ -283,12 +279,7 @@ function generateRateLimitAllowList(entries: string[]): string {
* @param containers List of container info * @param containers List of container info
*/ */
export async function getTemplateView(containers: Dockerode.ContainerInfo[]): Promise<TemplateView> { export async function getTemplateView(containers: Dockerode.ContainerInfo[]): Promise<TemplateView> {
delete require.cache[require.resolve('config')]; const cors = await readFile('./fixtures/cors.template', 'utf8');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const config = require('config');
const configFile = config as ConfigFile;
const cors = await asyncReadFile('./fixtures/cors.template', 'utf8');
const visibleRoutesPromises = ['/'].map(async route => { const visibleRoutesPromises = ['/'].map(async route => {
return renderTemplate(path.join('fixtures', 'visibleRoute.template'), { return renderTemplate(path.join('fixtures', 'visibleRoute.template'), {

View File

@@ -0,0 +1,22 @@
import {createServer, Server} from 'net';
/**
* Checks if a port is in use
*/
async function isPortFree(port: number, hostname?: string): Promise<boolean> {
return new Promise((resolve, reject) => {
const server: Server = createServer()
.once('error', error => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((error as any).code === 'EADDRINUSE') {
resolve(true);
} else {
reject(error);
}
})
.once('listening', () => server.once('close', () => resolve(false)).close())
.listen(port, hostname);
});
}
export default {isPortFree};

View File

@@ -13,17 +13,12 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
// tslint:disable:no-implicit-dependencies
// tslint:disable:no-magic-numbers
// tslint:disable:completed-docs
// tslint:disable:prefer-function-over-method
// tslint:disable:newline-per-chained-call
import {suite, test} from '@testdeck/mocha';
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import {expect} from 'chai'; import {expect} from 'chai';
import {mkdirSync, writeFileSync, unlinkSync, rmdirSync} from 'fs'; import {mkdirSync, writeFileSync, unlinkSync, rmdirSync} from 'fs';
import {resolve} from 'path'; import path from 'path';
import {isFileType} from '../src/common.js'; import {isFileType} from '../src/common.js';
import {fileURLToPath} from 'url';
process.on('unhandledRejection', async error => { process.on('unhandledRejection', async error => {
await Logger.error(error); await Logger.error(error);
@@ -31,22 +26,20 @@ process.on('unhandledRejection', async error => {
process.exit(1); process.exit(1);
}); });
@suite describe('common', function () {
export class CommonSpec { it('should use ssl certs', function () {
@test const testCertDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'certs');
async testSSLCert() { mkdirSync(testCertDirectory);
const testCertDir = resolve(__dirname, 'certs'); const notAnExpectedFileTypeFilePath = path.resolve(testCertDirectory, 'notAnExpectedFileType.txt');
mkdirSync(testCertDir); const anExpectedFileTypeFilePath = path.resolve(testCertDirectory, 'notARealCert.crt');
const notAnExptectedFileTypeFilePath = resolve(testCertDir, 'notAnExptectedFileType.txt'); writeFileSync(notAnExpectedFileTypeFilePath, 'Test');
const anExptectedFileTypeFilePath = resolve(testCertDir, 'notARealCert.crt'); writeFileSync(anExpectedFileTypeFilePath, 'Test');
writeFileSync(notAnExptectedFileTypeFilePath, 'Test');
writeFileSync(anExptectedFileTypeFilePath, 'Test');
expect(isFileType(notAnExptectedFileTypeFilePath, 'crt')).to.equal(false); expect(isFileType(notAnExpectedFileTypeFilePath, 'crt')).to.equal(false);
expect(isFileType(anExptectedFileTypeFilePath, 'crt')).to.equal(true); expect(isFileType(anExpectedFileTypeFilePath, 'crt')).to.equal(true);
unlinkSync(notAnExptectedFileTypeFilePath); unlinkSync(notAnExpectedFileTypeFilePath);
unlinkSync(anExptectedFileTypeFilePath); unlinkSync(anExpectedFileTypeFilePath);
rmdirSync(testCertDir); rmdirSync(testCertDirectory);
} });
} });

View File

@@ -0,0 +1,44 @@
/* eslint-disable unicorn/no-null */
import type {ContainerInfo} from 'dockerode';
export const anyContainerWithExposedPorts: ContainerInfo = {
Command: 'sh',
Created: 1_524_669_882,
HostConfig: {
NetworkMode: 'default',
},
Id: 'e3d3f4d18aceac2780bdb95523845d066ed25c04fc65168a5ddbd37a85671bb7',
Image: 'ubuntu:4',
ImageID: 'sha256:ef9f0c8c4b6f99dd208948c7aae1d042590aa18e05ebeae4f586e4b4beebeac9',
Labels: {},
Mounts: [],
Names: ['/container_name_1'],
NetworkSettings: {
Networks: {
bridge: {
Aliases: null,
EndpointID: 'da17549a086ff2c9f622e80de833e6f334afda52c8f07080428640c1716dcd14',
Gateway: '172.18.0.1',
GlobalIPv6Address: '',
GlobalIPv6PrefixLen: 0,
IPAMConfig: null,
IPAddress: '172.18.0.3',
IPPrefixLen: 16,
IPv6Gateway: '',
Links: null,
MacAddress: '03:41:ac:11:00:23',
NetworkID: '947ea5247cc7429e1fdebd5404fa4d15f7c05e6765f2b93ddb3bdb6aaffd1193',
},
},
},
Ports: [
{
IP: '0.0.0.0',
PrivatePort: 80,
PublicPort: 80,
Type: 'tcp',
},
],
State: 'running',
Status: 'Up 3 minutes',
};

View File

@@ -0,0 +1,52 @@
/* eslint-disable unicorn/no-null */
import type {ContainerInfo} from 'dockerode';
export const backendContainerWithExposedPorts: ContainerInfo = {
Command: 'node ./bin/www',
Created: 1_524_669_882,
HostConfig: {
NetworkMode: 'deployment_default',
},
Id: 'e3d3f4d18aceac2780bdb95523845d066ed25c04fc65168a5ddbd37a85671bb7',
Image: 'registry.gitlab.com/openstapps/backend/b-tu-typescript-refactor-for-new-tslint-config',
ImageID: 'sha256:ef9f0c8c4b6f99dd208948c7aae1d042590aa18e05ebeae4f586e4b4beebeac9',
Labels: {
'com.docker.compose.config-hash': '91c6e0cebad15951824162c93392b6880b69599692f07798ae8de659c1616a03',
'com.docker.compose.container-number': '1',
'com.docker.compose.oneoff': 'False',
'com.docker.compose.project': 'deployment',
'com.docker.compose.service': 'backend',
'com.docker.compose.version': '1.21.0',
'stapps.version': '1.0.0',
},
Mounts: [],
Names: ['/deployment_backend_1'],
NetworkSettings: {
Networks: {
deployment_default: {
Aliases: null,
EndpointID: 'da17549a086ff2c9f622e80de833e6f334afda52c8f07080428640c1716dcd14',
Gateway: '172.18.0.1',
GlobalIPv6Address: '',
GlobalIPv6PrefixLen: 0,
IPAMConfig: null,
IPAddress: '172.18.0.3',
IPPrefixLen: 16,
IPv6Gateway: '',
Links: null,
MacAddress: '03:41:ac:11:00:23',
NetworkID: '947ea5247cc7429e1fdebd5404fa4d15f7c05e6765f2b93ddb3bdb6aaffd1193',
},
},
},
Ports: [
{
IP: '127.0.0.1',
PrivatePort: 3000,
PublicPort: 3000,
Type: 'tcp',
},
],
State: 'running',
Status: 'Up 3 minutes',
};

View File

@@ -0,0 +1,52 @@
/* eslint-disable unicorn/no-null */
import type {ContainerInfo} from 'dockerode';
export const swarmBackendContainerWithExposedPorts: ContainerInfo = {
Command: 'node ./bin/www',
Created: 1_524_669_882,
HostConfig: {
NetworkMode: 'swarm_default',
},
Id: 'e3d3f4d18aceac2780bdb95523845d066ed25c04fc65168a5ddbd37a85671bb7',
Image: 'registry.gitlab.com/openstapps/backend/b-tu-typescript-refactor-for-new-tslint-config',
ImageID: 'sha256:ef9f0c8c4b6f99dd208948c7aae1d042590aa18e05ebeae4f586e4b4beebeac9',
Labels: {
'com.docker.compose.config-hash': '91c6e0cebad15951824162c93392b6880b69599692f07798ae8de659c1616a03',
'com.docker.compose.container-number': '1',
'com.docker.compose.oneoff': 'False',
'com.docker.stack.namespace': 'deployment',
'com.docker.swarm.service.name': 'deployment_backend',
'com.docker.compose.version': '1.21.0',
'stapps.version': '1.0.0',
},
Mounts: [],
Names: ['/deployment_backend_1'],
NetworkSettings: {
Networks: {
ingress: {
Aliases: null,
EndpointID: 'da17549a086ff2c9f622e80de833e6f334afda52c8f07080428640c1716dcd14',
Gateway: '172.18.0.1',
GlobalIPv6Address: '',
GlobalIPv6PrefixLen: 0,
IPAMConfig: null,
IPAddress: '172.18.0.3',
IPPrefixLen: 16,
IPv6Gateway: '',
Links: null,
MacAddress: '03:41:ac:11:00:23',
NetworkID: '947ea5247cc7429e1fdebd5404fa4d15f7c05e6765f2b93ddb3bdb6aaffd1193',
},
},
},
Ports: [
{
IP: 'delete me',
PrivatePort: 3000,
PublicPort: 3000,
Type: 'tcp',
},
],
State: 'running',
Status: 'Up 3 minutes',
};

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* /*
* Copyright (C) 2019 StApps * Copyright (C) 2019 StApps
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@@ -13,18 +14,16 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
// tslint:disable:no-implicit-dependencies
// tslint:disable:no-magic-numbers
// tslint:disable:completed-docs
// tslint:disable:prefer-function-over-method
// tslint:disable:newline-per-chained-call
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import chai from 'chai'; import chai from 'chai';
import {expect} from 'chai'; import {expect} from 'chai';
import chaiSpies from 'chai-spies';
import {ContainerInfo} from 'dockerode'; import {ContainerInfo} from 'dockerode';
import {slow, suite, test, timeout} from '@testdeck/mocha'; import {
import {sslHardeningParameters, protocolHardeningParameters, SSLFilePaths} from './../src/common.js'; sslHardeningParameters,
protocolHardeningParameters,
SSLFilePaths,
configFile,
} from './../src/common.js';
import { import {
containerMatchesRegex, containerMatchesRegex,
generateUpstreamMap, generateUpstreamMap,
@@ -34,11 +33,15 @@ import {
generateMetricsServer, generateMetricsServer,
getContainers, getContainers,
} from '../src/main.js'; } from '../src/main.js';
import {resolve} from 'path'; import path from 'path';
import {mkdirSync, writeFileSync, unlinkSync, rmdirSync} from 'fs'; import {mkdirSync, writeFileSync, unlinkSync, rmdirSync} from 'fs';
import proxyquire from 'proxyquire'; import {anyContainerWithExposedPorts} from './containers/any-with-exposed-ports.js';
import {backendContainerWithExposedPorts} from './containers/backend-with-exposed-ports.js';
proxyquire.callThru().preserveCache(); import {swarmBackendContainerWithExposedPorts} from './containers/swarm-backend-with-exposed-ports.js';
import {fileURLToPath} from 'url';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import portScannerModule from '../src/port-scanner.js';
process.on('unhandledRejection', async error => { process.on('unhandledRejection', async error => {
await Logger.error(error); await Logger.error(error);
@@ -47,196 +50,47 @@ process.on('unhandledRejection', async error => {
}); });
chai.should(); chai.should();
chai.use(chaiSpies); chai.use(sinonChai);
@suite(timeout(1000), slow(500)) console.log(configFile);
export class MainSpec {
static 'anyContainerWithExposedPorts': ContainerInfo = {
Command: 'sh',
Created: 1_524_669_882,
HostConfig: {
NetworkMode: 'default',
},
Id: 'e3d3f4d18aceac2780bdb95523845d066ed25c04fc65168a5ddbd37a85671bb7',
Image: 'ubuntu:4',
ImageID: 'sha256:ef9f0c8c4b6f99dd208948c7aae1d042590aa18e05ebeae4f586e4b4beebeac9',
Labels: {},
Mounts: [],
Names: ['/container_name_1'],
NetworkSettings: {
Networks: {
bridge: {
Aliases: null,
EndpointID: 'da17549a086ff2c9f622e80de833e6f334afda52c8f07080428640c1716dcd14',
Gateway: '172.18.0.1',
GlobalIPv6Address: '',
GlobalIPv6PrefixLen: 0,
IPAMConfig: null,
IPAddress: '172.18.0.3',
IPPrefixLen: 16,
IPv6Gateway: '',
Links: null,
MacAddress: '03:41:ac:11:00:23',
NetworkID: '947ea5247cc7429e1fdebd5404fa4d15f7c05e6765f2b93ddb3bdb6aaffd1193',
},
},
},
Ports: [
{
IP: '0.0.0.0',
PrivatePort: 80,
PublicPort: 80,
Type: 'tcp',
},
],
State: 'running',
Status: 'Up 3 minutes',
};
static 'backendContainerWithExposedPorts': ContainerInfo = { describe('main', function () {
Command: 'node ./bin/www', this.timeout(1000);
Created: 1524669882, this.slow(500);
HostConfig: {
NetworkMode: 'deployment_default',
},
Id: 'e3d3f4d18aceac2780bdb95523845d066ed25c04fc65168a5ddbd37a85671bb7',
Image: 'registry.gitlab.com/openstapps/backend/b-tu-typescript-refactor-for-new-tslint-config',
ImageID: 'sha256:ef9f0c8c4b6f99dd208948c7aae1d042590aa18e05ebeae4f586e4b4beebeac9',
Labels: {
'com.docker.compose.config-hash': '91c6e0cebad15951824162c93392b6880b69599692f07798ae8de659c1616a03',
'com.docker.compose.container-number': '1',
'com.docker.compose.oneoff': 'False',
'com.docker.compose.project': 'deployment',
'com.docker.compose.service': 'backend',
'com.docker.compose.version': '1.21.0',
'stapps.version': '1.0.0',
},
Mounts: [],
Names: ['/deployment_backend_1'],
NetworkSettings: {
Networks: {
deployment_default: {
Aliases: null,
EndpointID: 'da17549a086ff2c9f622e80de833e6f334afda52c8f07080428640c1716dcd14',
Gateway: '172.18.0.1',
GlobalIPv6Address: '',
GlobalIPv6PrefixLen: 0,
IPAMConfig: null,
IPAddress: '172.18.0.3',
IPPrefixLen: 16,
IPv6Gateway: '',
Links: null,
MacAddress: '03:41:ac:11:00:23',
NetworkID: '947ea5247cc7429e1fdebd5404fa4d15f7c05e6765f2b93ddb3bdb6aaffd1193',
},
},
},
Ports: [
{
IP: '127.0.0.1',
PrivatePort: 3000,
PublicPort: 3000,
Type: 'tcp',
},
],
State: 'running',
Status: 'Up 3 minutes',
};
static 'swarmBackendContainerWithExposedPorts': ContainerInfo = { const sandbox = sinon.createSandbox();
Command: 'node ./bin/www',
Created: 1524669882,
HostConfig: {
NetworkMode: 'swarm_default',
},
Id: 'e3d3f4d18aceac2780bdb95523845d066ed25c04fc65168a5ddbd37a85671bb7',
Image: 'registry.gitlab.com/openstapps/backend/b-tu-typescript-refactor-for-new-tslint-config',
ImageID: 'sha256:ef9f0c8c4b6f99dd208948c7aae1d042590aa18e05ebeae4f586e4b4beebeac9',
Labels: {
'com.docker.compose.config-hash': '91c6e0cebad15951824162c93392b6880b69599692f07798ae8de659c1616a03',
'com.docker.compose.container-number': '1',
'com.docker.compose.oneoff': 'False',
'com.docker.stack.namespace': 'deployment',
'com.docker.swarm.service.name': 'deployment_backend',
'com.docker.compose.version': '1.21.0',
'stapps.version': '1.0.0',
},
Mounts: [],
Names: ['/deployment_backend_1'],
NetworkSettings: {
Networks: {
ingress: {
Aliases: null,
EndpointID: 'da17549a086ff2c9f622e80de833e6f334afda52c8f07080428640c1716dcd14',
Gateway: '172.18.0.1',
GlobalIPv6Address: '',
GlobalIPv6PrefixLen: 0,
IPAMConfig: null,
IPAddress: '172.18.0.3',
IPPrefixLen: 16,
IPv6Gateway: '',
Links: null,
MacAddress: '03:41:ac:11:00:23',
NetworkID: '947ea5247cc7429e1fdebd5404fa4d15f7c05e6765f2b93ddb3bdb6aaffd1193',
},
},
},
Ports: [
{
IP: 'delete me',
PrivatePort: 3000,
PublicPort: 3000,
Type: 'tcp',
},
],
State: 'running',
Status: 'Up 3 minutes',
};
static 'sandbox' = chai.spy.sandbox(); beforeEach(function () {
sandbox.restore();
});
'before'() { it('should check if container does not match any container', function () {
MainSpec.sandbox.restore(); expect(containerMatchesRegex('anyName', new RegExp('d+'), anyContainerWithExposedPorts)).to.be.equal(
} false,
);
});
@test it('should check if container does not match if version is incorrect', function () {
'check if container does not match any container'() {
expect( expect(
containerMatchesRegex('anyName', new RegExp('d+'), MainSpec.anyContainerWithExposedPorts), containerMatchesRegex('backend', new RegExp('1\\.4\\.\\d+'), backendContainerWithExposedPorts),
).to.be.equal(false); ).to.be.equal(false);
} });
@test it('should check if container matches', function () {
'check if container does not match if version is incorrect'() {
expect( expect(
containerMatchesRegex('backend', new RegExp('1\\.4\\.\\d+'), MainSpec.backendContainerWithExposedPorts), containerMatchesRegex('backend', new RegExp('1\\.0\\.\\d+'), backendContainerWithExposedPorts),
).to.be.equal(false);
}
@test
'check if container matches'() {
expect(
containerMatchesRegex('backend', new RegExp('1\\.0\\.\\d+'), MainSpec.backendContainerWithExposedPorts),
).to.be.equal(true); ).to.be.equal(true);
expect( expect(
containerMatchesRegex( containerMatchesRegex('backend', new RegExp('1\\.0\\.\\d+'), swarmBackendContainerWithExposedPorts),
'backend',
new RegExp('1\\.0\\.\\d+'),
MainSpec.swarmBackendContainerWithExposedPorts,
),
).to.be.equal(true); ).to.be.equal(true);
} });
@test it('should get gateway of any container with exposed ports', async function () {
async 'get gateway of any container with exposed ports'() { expect(await getGatewayOfStAppsBackend(anyContainerWithExposedPorts)).to.be.equal('0.0.0.0:80');
expect(await getGatewayOfStAppsBackend(MainSpec.anyContainerWithExposedPorts)).to.be.equal('0.0.0.0:80'); });
}
@test it('should get gateway of backend container', async function () {
async 'get gateway of backend container'() { const spy = sandbox.stub(console, 'error');
const spy = MainSpec.sandbox.on(console, 'error', () => {
// noop
});
const containerWithoutPorts: Partial<ContainerInfo> = { const containerWithoutPorts: Partial<ContainerInfo> = {
Id: 'Foo', Id: 'Foo',
@@ -245,112 +99,68 @@ export class MainSpec {
}; };
expect(await getGatewayOfStAppsBackend(containerWithoutPorts as ContainerInfo)).to.be.equal(''); expect(await getGatewayOfStAppsBackend(containerWithoutPorts as ContainerInfo)).to.be.equal('');
expect(spy.__spy.calls[0][0]).to.contain('Container /container_name_1 does not advertise any port.'); expect(spy).to.have.been.calledWithMatch('Container /container_name_1 does not advertise any port.');
} });
@test it('should get gateway of backend container without ports', async function () {
async 'get gateway of backend container without ports'() { expect(await getGatewayOfStAppsBackend(backendContainerWithExposedPorts)).to.be.equal('127.0.0.1:3000');
expect(await getGatewayOfStAppsBackend(MainSpec.backendContainerWithExposedPorts)).to.be.equal( });
'127.0.0.1:3000',
);
}
@test it('should get gateway of backend container within docker swarm', async function () {
async 'get gateway of backend container within docker swarm'() { const backendContainer = swarmBackendContainerWithExposedPorts as any;
const backendContainer = MainSpec.swarmBackendContainerWithExposedPorts as any;
delete backendContainer.Ports[0].IP; delete backendContainer.Ports[0].IP;
const main = proxyquire('../src/main', { const spy = sandbox.stub(portScannerModule, 'isPortFree').resolves(true);
'node-port-scanner': (_host: unknown, _ports: unknown) => { expect(await getGatewayOfStAppsBackend(backendContainer)).to.be.equal('172.18.0.3:3000');
return new Promise((resolve, _reject) => { expect(spy).to.have.been.called;
resolve({ });
ports: {
open: [3000],
},
});
});
},
});
expect(await main.getGatewayOfStAppsBackend(backendContainer)).to.be.equal('172.18.0.3:3000');
}
@test it('should fail to get gateway of backend container if unreachable', async function () {
async 'fail to get gateway of backend container if unreachable'() { const backendContainer = swarmBackendContainerWithExposedPorts as any;
const backendContainer = MainSpec.swarmBackendContainerWithExposedPorts as any;
delete backendContainer.Ports[0].IP; delete backendContainer.Ports[0].IP;
const spy = MainSpec.sandbox.on(console, 'error', () => { const spy = sandbox.stub(console, 'error');
// noop
});
const main = proxyquire('../src/main', { const scanner = sandbox.stub(portScannerModule, 'isPortFree').resolves(false);
'node-port-scanner': (_host: unknown, _ports: unknown) => {
return new Promise((resolve, _reject) => {
resolve({
ports: {
open: [],
},
});
});
},
});
expect(await main.getGatewayOfStAppsBackend(MainSpec.swarmBackendContainerWithExposedPorts)).to.be.equal( expect(await getGatewayOfStAppsBackend(swarmBackendContainerWithExposedPorts)).to.be.equal('');
'', expect(scanner.calledOnce).to.be.true;
); expect(spy).to.have.been.calledWithMatch(
expect(spy.__spy.calls[0][0]).to.contain(
"It's possible your current Docker network setup isn't supported yet.", "It's possible your current Docker network setup isn't supported yet.",
); );
} });
@test it('should fail to get gateway of backend container network mode is unsupported', async function () {
async 'fail to get gateway of backend container network mode is unsupported'() { const backendContainer = swarmBackendContainerWithExposedPorts as any;
const backendContainer = MainSpec.swarmBackendContainerWithExposedPorts as any;
delete backendContainer.Ports[0].IP; delete backendContainer.Ports[0].IP;
delete backendContainer.Ports[0].PublicPort; delete backendContainer.Ports[0].PublicPort;
delete backendContainer.Ports[0].PrivatePort; delete backendContainer.Ports[0].PrivatePort;
const spy = MainSpec.sandbox.on(console, 'error', () => { const spy = sandbox.stub(console, 'error');
// noop
});
expect(await getGatewayOfStAppsBackend(MainSpec.swarmBackendContainerWithExposedPorts)).to.be.equal(''); expect(await getGatewayOfStAppsBackend(swarmBackendContainerWithExposedPorts)).to.be.equal('');
expect(spy.__spy.calls[0][0]).to.contain( expect(spy).to.have.been.calledWithMatch(
"It's possible your current Docker network setup isn't supported yet.", "It's possible your current Docker network setup isn't supported yet.",
); );
} });
@test it('should upstream map calls logger error when no matching container is found', async function () {
async 'upstream map calls logger error when no matching container is found'() { const spy = sandbox.stub(console, 'warn');
const spy = MainSpec.sandbox.on(console, 'warn', () => {
// noop
});
expect( expect(await generateUpstreamMap(['0\\.8\\.\\d+'], ['1\\.1\\.\\d+'], [backendContainerWithExposedPorts]))
await generateUpstreamMap( .to.be.equal(`map $http_x_stapps_version $proxyurl {
['0\\.8\\.\\d+'],
['1\\.1\\.\\d+'],
[MainSpec.backendContainerWithExposedPorts],
),
).to.be.equal(`map $http_x_stapps_version $proxyurl {
default unsupported; default unsupported;
"~0\\.8\\.\\d+" unavailable; "~0\\.8\\.\\d+" unavailable;
"~1\\.1\\.\\d+" outdated; "~1\\.1\\.\\d+" outdated;
} }
`); `);
expect(spy.__spy.calls[0][0]).to.contain('No backend for version'); expect(spy).to.have.been.calledWithMatch('No backend for version');
} });
@test it('should upstream map with one active version and no outdated ones', async function () {
async 'upstream map with one active version and no outdated ones'() { expect(await generateUpstreamMap(['1\\.0\\.\\d+'], ['0\\.8\\.\\d+'], [backendContainerWithExposedPorts]))
expect( .to.be.equal(`map $http_x_stapps_version $proxyurl {
await generateUpstreamMap(
['1\\.0\\.\\d+'],
['0\\.8\\.\\d+'],
[MainSpec.backendContainerWithExposedPorts],
),
).to.be.equal(`map $http_x_stapps_version $proxyurl {
default unsupported; default unsupported;
"~1\\.0\\.\\d+" 1__0___d_; "~1\\.0\\.\\d+" 1__0___d_;
"~0\\.8\\.\\d+" outdated; "~0\\.8\\.\\d+" outdated;
@@ -359,17 +169,16 @@ upstream 1__0___d_ {
server 127.0.0.1:3000; server 127.0.0.1:3000;
} }
`); `);
} });
@test it('should get containers', async function () {
async 'get containers'() {
try { try {
await getContainers(); await getContainers();
return false; return false;
} catch (e) { } catch (error) {
if ((e as Error).message.startsWith('No')) { if ((error as Error).message.startsWith('No')) {
// Result, if docker is installed // Result, if docker is installed
expect((e as Error).message).to.equal(`No running docker containers found. expect((error as Error).message).to.equal(`No running docker containers found.
Please check if docker is running and Node.js can access the docker socket (/var/run/docker.sock)`); Please check if docker is running and Node.js can access the docker socket (/var/run/docker.sock)`);
} else { } else {
@@ -377,41 +186,34 @@ Please check if docker is running and Node.js can access the docker socket (/var
expect([ expect([
new Error(`connect ENOENT /var/run/docker.sock`).message, new Error(`connect ENOENT /var/run/docker.sock`).message,
new Error('connect EACCES /var/run/docker.sock').message, new Error('connect EACCES /var/run/docker.sock').message,
]).to.include((e as Error).message); ]).to.include((error as Error).message);
} }
} }
return true; return true;
} });
@test it('should get template view', async function () {
async 'get template view'() {
try { try {
let containersWithSameVersion = [ const containersWithSameVersion = [backendContainerWithExposedPorts, backendContainerWithExposedPorts];
MainSpec.backendContainerWithExposedPorts,
MainSpec.backendContainerWithExposedPorts,
];
await getTemplateView(containersWithSameVersion); await getTemplateView(containersWithSameVersion);
return false; return false;
} catch (e) { } catch (error) {
expect((e as Error).message).to.equal(`Multiple backends for one version found.`); expect((error as Error).message).to.equal(`Multiple backends for one version found.`);
} }
return true; return true;
} });
@test it('should include metrics config', async function () {
async 'include metrics config'() {
expect(await generateMetricsServer('test', true)).length.to.be.greaterThan(1); expect(await generateMetricsServer('test', true)).length.to.be.greaterThan(1);
} });
@test it('should omit metrics config', async function () {
async 'omit metrics config'() {
expect(await generateMetricsServer('test', false)).to.equal(''); expect(await generateMetricsServer('test', false)).to.equal('');
} });
@test it('should create listener faulty config', async function () {
'create listener faulty config'() {
expect( expect(
generateListener({ generateListener({
certificate: 'faultyTest', certificate: 'faultyTest',
@@ -423,17 +225,16 @@ Please check if docker is running and Node.js can access the docker socket (/var
${protocolHardeningParameters} ${protocolHardeningParameters}
`); `);
} });
@test it('should create listener correct config', async function () {
'create listener correct config'() { const testCertDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'certs');
const testCertDir = resolve(__dirname, 'certs'); mkdirSync(testCertDirectory);
mkdirSync(testCertDir);
const certificateFile = resolve(testCertDir, 'ssl.crt'); const certificateFile = path.resolve(testCertDirectory, 'ssl.crt');
const certificateKeyFile = resolve(testCertDir, 'ssl.key'); const certificateKeyFile = path.resolve(testCertDirectory, 'ssl.key');
const certificateChainFile = resolve(testCertDir, 'chain.crt'); const certificateChainFile = path.resolve(testCertDirectory, 'chain.crt');
const dhparamFile = resolve(testCertDir, 'dhparam.pem'); const dhparamFile = path.resolve(testCertDirectory, 'dhparam.pem');
writeFileSync(certificateFile, 'Test'); writeFileSync(certificateFile, 'Test');
writeFileSync(certificateKeyFile, 'Test'); writeFileSync(certificateKeyFile, 'Test');
@@ -461,6 +262,6 @@ ${protocolHardeningParameters}
unlinkSync(certificateKeyFile); unlinkSync(certificateKeyFile);
unlinkSync(certificateChainFile); unlinkSync(certificateChainFile);
unlinkSync(dhparamFile); unlinkSync(dhparamFile);
rmdirSync(testCertDir); rmdirSync(testCertDirectory);
} });
} });

View File

@@ -1,4 +1,4 @@
{ {
"extends": "@openstapps/tsconfig", "extends": "@openstapps/tsconfig",
"exclude": ["./config/", "./lib/", "./test/"] "exclude": ["./config/", "./lib/"]
} }

View File

@@ -3,18 +3,9 @@
"branches": 90, "branches": 90,
"check-coverage": true, "check-coverage": true,
"exclude": [], "exclude": [],
"extension": [ "extension": [".ts"],
".ts"
],
"functions": 95, "functions": 95,
"include": [ "include": ["src/**/*.ts"],
"src/protocol/route.ts",
"src/things/abstract/thing.ts",
"src/things/abstract/thing-that-can-be-offered.ts",
"src/things/abstract/thing-with-categories.ts",
"src/translator.ts",
"src/guards.ts"
],
"lines": 95, "lines": 95,
"per-file": true, "per-file": true,
"reporter": [ "reporter": [
@@ -22,8 +13,6 @@
"html", "html",
"text-summary" "text-summary"
], ],
"require": [ "require": ["ts-node/register"],
"ts-node/register"
],
"statements": 95 "statements": 95
} }

View File

@@ -20,11 +20,11 @@
}, },
"scripts": { "scripts": {
"build": "tsup --dts", "build": "tsup --dts",
"format": "prettier .", "format": "prettier . --ignore-path ../../.gitignore",
"format:fix": "prettier --write .", "format:fix": "prettier --write . --ignore-path ../../.gitignore",
"lint": "eslint --ext .ts src/", "lint": "eslint --ext .ts src/",
"lint:fix": "eslint --fix --ext .ts src/", "lint:fix": "eslint --fix --ext .ts src/",
"test": "nyc mocha 'test/**/*.spec.ts'" "test": "c8 mocha"
}, },
"dependencies": { "dependencies": {
"@openstapps/gitlab-api": "workspace:*", "@openstapps/gitlab-api": "workspace:*",
@@ -33,15 +33,12 @@
"commander": "10.0.0", "commander": "10.0.0",
"date-fns": "2.29.3", "date-fns": "2.29.3",
"glob": "10.2.1", "glob": "10.2.1",
"mustache": "4.2.0", "mustache": "4.2.0"
"tmp": "0.2.1"
}, },
"devDependencies": { "devDependencies": {
"@openstapps/eslint-config": "workspace:*", "@openstapps/eslint-config": "workspace:*",
"@openstapps/nyc-config": "workspace:*",
"@openstapps/prettier-config": "workspace:*", "@openstapps/prettier-config": "workspace:*",
"@openstapps/tsconfig": "workspace:*", "@openstapps/tsconfig": "workspace:*",
"@testdeck/mocha": "0.3.3",
"@types/chai": "4.3.4", "@types/chai": "4.3.4",
"@types/chai-as-promised": "7.1.5", "@types/chai-as-promised": "7.1.5",
"@types/glob": "8.0.1", "@types/glob": "8.0.1",
@@ -52,7 +49,7 @@
"chai": "4.3.7", "chai": "4.3.7",
"chai-as-promised": "7.1.1", "chai-as-promised": "7.1.1",
"mocha": "10.2.0", "mocha": "10.2.0",
"nyc": "15.1.0", "c8": "7.13.0",
"ts-node": "10.9.1", "ts-node": "10.9.1",
"tsup": "6.7.0", "tsup": "6.7.0",
"typescript": "4.8.4" "typescript": "4.8.4"
@@ -72,8 +69,5 @@
"extends": [ "extends": [
"@openstapps" "@openstapps"
] ]
},
"nyc": {
"extends": "@openstapps/nyc-config"
} }
} }

View File

@@ -13,9 +13,7 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Api} from '@openstapps/gitlab-api'; import {Api} from '@openstapps/gitlab-api';
import {Logger} from '@openstapps/logger'; import {Logger, AddLogLevel, Colorize} from '@openstapps/logger';
import {AddLogLevel} from '@openstapps/logger/lib/transformations/add-log-level.js';
import {Colorize} from '@openstapps/logger/lib/transformations/colorize.js';
import {Command} from 'commander'; import {Command} from 'commander';
import {existsSync, readFileSync} from 'fs'; import {existsSync, readFileSync} from 'fs';
import path from 'path'; import path from 'path';

View File

@@ -12,7 +12,7 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Label} from '@openstapps/gitlab-api/lib/types.js'; import {Label} from '@openstapps/gitlab-api';
import setHours from 'date-fns/setHours'; import setHours from 'date-fns/setHours';
import nextThursday from 'date-fns/nextThursday'; import nextThursday from 'date-fns/nextThursday';
import previousThursday from 'date-fns/previousThursday'; import previousThursday from 'date-fns/previousThursday';

View File

@@ -12,15 +12,15 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Api} from '@openstapps/gitlab-api';
import { import {
Api,
AccessLevel, AccessLevel,
MembershipScope, MembershipScope,
MergeRequestMergeStatus, MergeRequestMergeStatus,
MergeRequestState, MergeRequestState,
Scope, Scope,
User, User,
} from '@openstapps/gitlab-api/lib/types.js'; } from '@openstapps/gitlab-api';
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import {WebClient} from '@slack/web-api'; import {WebClient} from '@slack/web-api';
import {GROUPS, MAX_DEPTH_FOR_REMINDER, NOTE_PREFIX, SLACK_CHANNEL} from '../configuration.js'; import {GROUPS, MAX_DEPTH_FOR_REMINDER, NOTE_PREFIX, SLACK_CHANNEL} from '../configuration.js';

View File

@@ -12,15 +12,15 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Api} from '@openstapps/gitlab-api';
import { import {
Api,
Issue, Issue,
IssueState, IssueState,
MembershipScope, MembershipScope,
MergeRequestState, MergeRequestState,
Project, Project,
User, User,
} from '@openstapps/gitlab-api/lib/types.js'; } from '@openstapps/gitlab-api';
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import mustache from 'mustache'; import mustache from 'mustache';
import path from 'path'; import path from 'path';

View File

@@ -12,8 +12,8 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Api} from '@openstapps/gitlab-api';
import { import {
Api,
AccessLevel, AccessLevel,
IssueState, IssueState,
MembershipScope, MembershipScope,
@@ -21,7 +21,7 @@ import {
Milestone, Milestone,
Project, Project,
Scope, Scope,
} from '@openstapps/gitlab-api/lib/types.js'; } from '@openstapps/gitlab-api';
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import {getProjects} from '../common.js'; import {getProjects} from '../common.js';
import { import {

View File

@@ -12,8 +12,7 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Api} from '@openstapps/gitlab-api'; import {Api, IssueState, Scope} from '@openstapps/gitlab-api';
import {IssueState, Scope} from '@openstapps/gitlab-api/lib/types.js';
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import {GROUPS, LAST_MEETING, NOTE_PREFIX} from '../configuration.js'; import {GROUPS, LAST_MEETING, NOTE_PREFIX} from '../configuration.js';
import isBefore from 'date-fns/isBefore'; import isBefore from 'date-fns/isBefore';

View File

@@ -1,43 +1,26 @@
import * as chai from 'chai'; import * as chai from 'chai';
import {expect} from 'chai'; import {expect} from 'chai';
import chaiAsPromised from 'chai-as-promised'; import chaiAsPromised from 'chai-as-promised';
import {execSync} from 'child_process'; import path from 'path';
import {suite, test} from '@testdeck/mocha';
import {join} from 'path';
import {dirSync} from 'tmp';
import {getUsedVersion, getUsedVersionMajorMinor} from '../src/tasks/get-used-version.js'; import {getUsedVersion, getUsedVersionMajorMinor} from '../src/tasks/get-used-version.js';
chai.use(chaiAsPromised); chai.use(chaiAsPromised);
chai.should(); chai.should();
@suite() describe('Verify Versions', function () {
export class GetUsedVersionsSpec { it('should not depend on core', async function () {
@test await getUsedVersion(process.cwd(), '@openstapps/core').should.eventually.be.rejected;
async 'does not depend on core'() { });
return getUsedVersion(join(__dirname, '..'), '@openstapps/core').should.eventually.be.rejected;
}
@test it('should not be a Node.js project', async function () {
async 'not a Node.js project'() { await getUsedVersion(path.resolve(process.cwd(), '..'), '@openstapps/core').should.eventually.be.rejected;
return getUsedVersion(__dirname, '@openstapps/core').should.eventually.be.rejected; });
}
@test it('should get used version', async function () {
async 'has no dependencies'() { expect(await getUsedVersion(process.cwd(), 'mustache')).to.be.equal('4.2.0');
const temporaryDirectory = dirSync(); });
execSync(`cd ${temporaryDirectory.name}; npm init -y`); it('should get used version major minor', async function () {
expect(await getUsedVersionMajorMinor(process.cwd(), 'mustache')).to.be.equal('4.2');
await getUsedVersion(temporaryDirectory.name, '@openstapps/core').should.eventually.be.rejected; });
} });
@test
async 'get used version'() {
expect(await getUsedVersion(join(__dirname, '..'), '@krlwlfrt/async-pool')).to.be.equal('0.4.1');
}
@test
async 'get used version major minor'() {
expect(await getUsedVersionMajorMinor(join(__dirname, '..'), '@krlwlfrt/async-pool')).to.be.equal('0.4');
}
}

View File

@@ -25,5 +25,8 @@
"strict": true, "strict": true,
"target": "ES2021" "target": "ES2021"
}, },
"exclude": ["../../../app.js", "../../../lib/", "../../../test/"] "ts-node": {
"transpileOnly": true
},
"exclude": ["../../../app.js", "../../../lib/"]
} }

View File

@@ -16,8 +16,8 @@
"types": "lib/index.d.ts", "types": "lib/index.d.ts",
"scripts": { "scripts": {
"build": "tsup --dts", "build": "tsup --dts",
"format": "prettier .", "format": "prettier . --ignore-path ../../.gitignore",
"format:fix": "prettier --write .", "format:fix": "prettier --write . --ignore-path ../../.gitignore",
"lint": "eslint --ext .ts src/", "lint": "eslint --ext .ts src/",
"lint:fix": "eslint --fix --ext .ts src/", "lint:fix": "eslint --fix --ext .ts src/",
"test": "nyc mocha 'test/**/*.spec.ts'" "test": "nyc mocha 'test/**/*.spec.ts'"

View File

@@ -12,8 +12,8 @@
"bin": "app.js", "bin": "app.js",
"scripts": { "scripts": {
"build": "tsup --dts", "build": "tsup --dts",
"format": "prettier .", "format": "prettier . --ignore-path ../../.gitignore",
"format:fix": "prettier --write .", "format:fix": "prettier --write . --ignore-path ../../.gitignore",
"lint": "eslint --ext .ts src/", "lint": "eslint --ext .ts src/",
"lint:fix": "eslint --fix --ext .ts src/", "lint:fix": "eslint --fix --ext .ts src/",
"start": "node lib/cli.js" "start": "node lib/cli.js"

View File

@@ -1,70 +1,70 @@
[ [
{ {
"pkg": "@capacitor/app", "pkg": "@capacitor/app",
"classpath": "com.capacitorjs.plugins.app.AppPlugin" "classpath": "com.capacitorjs.plugins.app.AppPlugin"
}, },
{ {
"pkg": "@capacitor/browser", "pkg": "@capacitor/browser",
"classpath": "com.capacitorjs.plugins.browser.BrowserPlugin" "classpath": "com.capacitorjs.plugins.browser.BrowserPlugin"
}, },
{ {
"pkg": "@capacitor/device", "pkg": "@capacitor/device",
"classpath": "com.capacitorjs.plugins.device.DevicePlugin" "classpath": "com.capacitorjs.plugins.device.DevicePlugin"
}, },
{ {
"pkg": "@capacitor/dialog", "pkg": "@capacitor/dialog",
"classpath": "com.capacitorjs.plugins.dialog.DialogPlugin" "classpath": "com.capacitorjs.plugins.dialog.DialogPlugin"
}, },
{ {
"pkg": "@capacitor/filesystem", "pkg": "@capacitor/filesystem",
"classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin" "classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin"
}, },
{ {
"pkg": "@capacitor/geolocation", "pkg": "@capacitor/geolocation",
"classpath": "com.capacitorjs.plugins.geolocation.GeolocationPlugin" "classpath": "com.capacitorjs.plugins.geolocation.GeolocationPlugin"
}, },
{ {
"pkg": "@capacitor/haptics", "pkg": "@capacitor/haptics",
"classpath": "com.capacitorjs.plugins.haptics.HapticsPlugin" "classpath": "com.capacitorjs.plugins.haptics.HapticsPlugin"
}, },
{ {
"pkg": "@capacitor/keyboard", "pkg": "@capacitor/keyboard",
"classpath": "com.capacitorjs.plugins.keyboard.KeyboardPlugin" "classpath": "com.capacitorjs.plugins.keyboard.KeyboardPlugin"
}, },
{ {
"pkg": "@capacitor/local-notifications", "pkg": "@capacitor/local-notifications",
"classpath": "com.capacitorjs.plugins.localnotifications.LocalNotificationsPlugin" "classpath": "com.capacitorjs.plugins.localnotifications.LocalNotificationsPlugin"
}, },
{ {
"pkg": "@capacitor/network", "pkg": "@capacitor/network",
"classpath": "com.capacitorjs.plugins.network.NetworkPlugin" "classpath": "com.capacitorjs.plugins.network.NetworkPlugin"
}, },
{ {
"pkg": "@capacitor/preferences", "pkg": "@capacitor/preferences",
"classpath": "com.capacitorjs.plugins.preferences.PreferencesPlugin" "classpath": "com.capacitorjs.plugins.preferences.PreferencesPlugin"
}, },
{ {
"pkg": "@capacitor/share", "pkg": "@capacitor/share",
"classpath": "com.capacitorjs.plugins.share.SharePlugin" "classpath": "com.capacitorjs.plugins.share.SharePlugin"
}, },
{ {
"pkg": "@capacitor/splash-screen", "pkg": "@capacitor/splash-screen",
"classpath": "com.capacitorjs.plugins.splashscreen.SplashScreenPlugin" "classpath": "com.capacitorjs.plugins.splashscreen.SplashScreenPlugin"
}, },
{ {
"pkg": "@capacitor/status-bar", "pkg": "@capacitor/status-bar",
"classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin" "classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin"
}, },
{ {
"pkg": "@hugotomazi/capacitor-navigation-bar", "pkg": "@hugotomazi/capacitor-navigation-bar",
"classpath": "br.com.tombus.capacitor.plugin.navigationbar.NavigationBarPlugin" "classpath": "br.com.tombus.capacitor.plugin.navigationbar.NavigationBarPlugin"
}, },
{ {
"pkg": "@transistorsoft/capacitor-background-fetch", "pkg": "@transistorsoft/capacitor-background-fetch",
"classpath": "com.transistorsoft.bgfetch.capacitor.BackgroundFetchPlugin" "classpath": "com.transistorsoft.bgfetch.capacitor.BackgroundFetchPlugin"
}, },
{ {
"pkg": "capacitor-secure-storage-plugin", "pkg": "capacitor-secure-storage-plugin",
"classpath": "com.whitestein.securestorage.SecureStoragePluginPlugin" "classpath": "com.whitestein.securestorage.SecureStoragePluginPlugin"
} }
] ]

View File

@@ -1,116 +1,116 @@
{ {
"images" : [ "images": [
{ {
"size" : "20x20", "size": "20x20",
"idiom" : "iphone", "idiom": "iphone",
"filename" : "AppIcon-20x20@2x.png", "filename": "AppIcon-20x20@2x.png",
"scale" : "2x" "scale": "2x"
}, },
{ {
"size" : "20x20", "size": "20x20",
"idiom" : "iphone", "idiom": "iphone",
"filename" : "AppIcon-20x20@3x.png", "filename": "AppIcon-20x20@3x.png",
"scale" : "3x" "scale": "3x"
}, },
{ {
"size" : "29x29", "size": "29x29",
"idiom" : "iphone", "idiom": "iphone",
"filename" : "AppIcon-29x29@2x-1.png", "filename": "AppIcon-29x29@2x-1.png",
"scale" : "2x" "scale": "2x"
}, },
{ {
"size" : "29x29", "size": "29x29",
"idiom" : "iphone", "idiom": "iphone",
"filename" : "AppIcon-29x29@3x.png", "filename": "AppIcon-29x29@3x.png",
"scale" : "3x" "scale": "3x"
}, },
{ {
"size" : "40x40", "size": "40x40",
"idiom" : "iphone", "idiom": "iphone",
"filename" : "AppIcon-40x40@2x.png", "filename": "AppIcon-40x40@2x.png",
"scale" : "2x" "scale": "2x"
}, },
{ {
"size" : "40x40", "size": "40x40",
"idiom" : "iphone", "idiom": "iphone",
"filename" : "AppIcon-40x40@3x.png", "filename": "AppIcon-40x40@3x.png",
"scale" : "3x" "scale": "3x"
}, },
{ {
"size" : "60x60", "size": "60x60",
"idiom" : "iphone", "idiom": "iphone",
"filename" : "AppIcon-60x60@2x.png", "filename": "AppIcon-60x60@2x.png",
"scale" : "2x" "scale": "2x"
}, },
{ {
"size" : "60x60", "size": "60x60",
"idiom" : "iphone", "idiom": "iphone",
"filename" : "AppIcon-60x60@3x.png", "filename": "AppIcon-60x60@3x.png",
"scale" : "3x" "scale": "3x"
}, },
{ {
"size" : "20x20", "size": "20x20",
"idiom" : "ipad", "idiom": "ipad",
"filename" : "AppIcon-20x20@1x.png", "filename": "AppIcon-20x20@1x.png",
"scale" : "1x" "scale": "1x"
}, },
{ {
"size" : "20x20", "size": "20x20",
"idiom" : "ipad", "idiom": "ipad",
"filename" : "AppIcon-20x20@2x-1.png", "filename": "AppIcon-20x20@2x-1.png",
"scale" : "2x" "scale": "2x"
}, },
{ {
"size" : "29x29", "size": "29x29",
"idiom" : "ipad", "idiom": "ipad",
"filename" : "AppIcon-29x29@1x.png", "filename": "AppIcon-29x29@1x.png",
"scale" : "1x" "scale": "1x"
}, },
{ {
"size" : "29x29", "size": "29x29",
"idiom" : "ipad", "idiom": "ipad",
"filename" : "AppIcon-29x29@2x.png", "filename": "AppIcon-29x29@2x.png",
"scale" : "2x" "scale": "2x"
}, },
{ {
"size" : "40x40", "size": "40x40",
"idiom" : "ipad", "idiom": "ipad",
"filename" : "AppIcon-40x40@1x.png", "filename": "AppIcon-40x40@1x.png",
"scale" : "1x" "scale": "1x"
}, },
{ {
"size" : "40x40", "size": "40x40",
"idiom" : "ipad", "idiom": "ipad",
"filename" : "AppIcon-40x40@2x-1.png", "filename": "AppIcon-40x40@2x-1.png",
"scale" : "2x" "scale": "2x"
}, },
{ {
"size" : "76x76", "size": "76x76",
"idiom" : "ipad", "idiom": "ipad",
"filename" : "AppIcon-76x76@1x.png", "filename": "AppIcon-76x76@1x.png",
"scale" : "1x" "scale": "1x"
}, },
{ {
"size" : "76x76", "size": "76x76",
"idiom" : "ipad", "idiom": "ipad",
"filename" : "AppIcon-76x76@2x.png", "filename": "AppIcon-76x76@2x.png",
"scale" : "2x" "scale": "2x"
}, },
{ {
"size" : "83.5x83.5", "size": "83.5x83.5",
"idiom" : "ipad", "idiom": "ipad",
"filename" : "AppIcon-83.5x83.5@2x.png", "filename": "AppIcon-83.5x83.5@2x.png",
"scale" : "2x" "scale": "2x"
}, },
{ {
"size" : "1024x1024", "size": "1024x1024",
"idiom" : "ios-marketing", "idiom": "ios-marketing",
"filename" : "AppIcon-512@2x.png", "filename": "AppIcon-512@2x.png",
"scale" : "1x" "scale": "1x"
} }
], ],
"info" : { "info": {
"version" : 1, "version": 1,
"author" : "xcode" "author": "xcode"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"info" : { "info": {
"version" : 1, "version": 1,
"author" : "xcode" "author": "xcode"
} }
} }

View File

@@ -1,23 +1,23 @@
{ {
"images" : [ "images": [
{ {
"idiom" : "universal", "idiom": "universal",
"filename" : "splash-2732x2732-2.png", "filename": "splash-2732x2732-2.png",
"scale" : "1x" "scale": "1x"
}, },
{ {
"idiom" : "universal", "idiom": "universal",
"filename" : "splash-2732x2732-1.png", "filename": "splash-2732x2732-1.png",
"scale" : "2x" "scale": "2x"
}, },
{ {
"idiom" : "universal", "idiom": "universal",
"filename" : "splash-2732x2732.png", "filename": "splash-2732x2732.png",
"scale" : "3x" "scale": "3x"
} }
], ],
"info" : { "info": {
"version" : 1, "version": 1,
"author" : "xcode" "author": "xcode"
} }
} }

View File

@@ -31,8 +31,8 @@
"docker:run:android": "sudo docker run -v $PWD:/app --privileged -v /dev/bus/usb:/dev/bus/usb --net=host -it registry.gitlab.com/openstapps/app bash -c \"npm run run:android\"", "docker:run:android": "sudo docker run -v $PWD:/app --privileged -v /dev/bus/usb:/dev/bus/usb --net=host -it registry.gitlab.com/openstapps/app bash -c \"npm run run:android\"",
"docker:serve": "sudo docker run -p 8100:8100 -p 35729:35729 -p 53703:53703 -v $PWD:/app -it registry.gitlab.com/openstapps/app bash -c \"npm run start:external\"", "docker:serve": "sudo docker run -p 8100:8100 -p 35729:35729 -p 53703:53703 -v $PWD:/app -it registry.gitlab.com/openstapps/app bash -c \"npm run start:external\"",
"e2e": "ng e2e", "e2e": "ng e2e",
"format": "prettier .", "format": "prettier . --ignore-path ../../.gitignore",
"format:fix": "prettier --write .", "format:fix": "prettier --write . --ignore-path ../../.gitignore",
"licenses": "license-checker --json > src/assets/about/licenses.json && ts-node ./scripts/accumulate-licenses.ts && git add src/assets/about/licenses.json", "licenses": "license-checker --json > src/assets/about/licenses.json && ts-node ./scripts/accumulate-licenses.ts && git add src/assets/about/licenses.json",
"lint": "ng lint", "lint": "ng lint",
"lint:fix": "eslint --fix -c .eslintrc.json --ignore-path .eslintignore --ext .ts,.html src/", "lint:fix": "eslint --fix -c .eslintrc.json --ignore-path .eslintignore --ext .ts,.html src/",

View File

@@ -11,7 +11,8 @@
"lint:fix": "dotenv -c -- turbo run lint:fix", "lint:fix": "dotenv -c -- turbo run lint:fix",
"publish-packages": "dotenv -c -- turbo run build format lint test && changeset version && changeset publish", "publish-packages": "dotenv -c -- turbo run build format lint test && changeset version && changeset publish",
"syncpack": "syncpack list-mismatches && syncpack lint-semver-ranges", "syncpack": "syncpack list-mismatches && syncpack lint-semver-ranges",
"syncpack:fix": "node sync.mjs && syncpack format && syncpack fix-mismatches" "syncpack:fix": "node sync.mjs && syncpack format && syncpack fix-mismatches",
"test": "dotenv -c -- turbo run test"
}, },
"devDependencies": { "devDependencies": {
"@changesets/cli": "2.26.0", "@changesets/cli": "2.26.0",

View File

@@ -22,11 +22,11 @@
}, },
"scripts": { "scripts": {
"build": "tsup --dts", "build": "tsup --dts",
"format": "prettier .", "format": "prettier . --ignore-path ../../.gitignore",
"format:fix": "prettier --write .", "format:fix": "prettier --write . --ignore-path ../../.gitignore",
"lint": "eslint --ext .ts src/", "lint": "eslint --ext .ts src/",
"lint:fix": "eslint --fix --ext .ts src/", "lint:fix": "eslint --fix --ext .ts src/",
"test": "nyc mocha 'test/**/*.spec.ts'" "test": "c8 mocha"
}, },
"dependencies": { "dependencies": {
"@krlwlfrt/async-pool": "0.7.0", "@krlwlfrt/async-pool": "0.7.0",
@@ -58,7 +58,6 @@
"@openstapps/nyc-config": "workspace:*", "@openstapps/nyc-config": "workspace:*",
"@openstapps/prettier-config": "workspace:*", "@openstapps/prettier-config": "workspace:*",
"@openstapps/tsconfig": "workspace:*", "@openstapps/tsconfig": "workspace:*",
"@testdeck/mocha": "0.3.3",
"@types/body-parser": "1.19.2", "@types/body-parser": "1.19.2",
"@types/chai": "4.3.5", "@types/chai": "4.3.5",
"@types/chai-as-promised": "7.1.5", "@types/chai-as-promised": "7.1.5",
@@ -75,7 +74,7 @@
"fs-extra": "10.1.0", "fs-extra": "10.1.0",
"mocha": "10.2.0", "mocha": "10.2.0",
"nock": "13.3.1", "nock": "13.3.1",
"nyc": "15.1.0", "c8": "7.13.0",
"ts-node": "10.9.1", "ts-node": "10.9.1",
"tsup": "6.7.0", "tsup": "6.7.0",
"typedoc": "0.23.28", "typedoc": "0.23.28",

View File

@@ -1,10 +1,10 @@
export * from './bulk.js' export * from './bulk.js';
export * from './client.js' export * from './client.js';
export * from './connector-client.js' export * from './connector-client.js';
export * from './copy.js' export * from './copy.js';
export * from './e2e.js' export * from './e2e.js';
export * from './errors.js' export * from './errors.js';
export * from './http-client.js' export * from './http-client.js';
export * from './http-client-interface.js' export * from './http-client-interface.js';
export * from './plugin.js' export * from './plugin.js';
export * from './plugin-client.js' export * from './plugin-client.js';

View File

@@ -24,29 +24,27 @@ import {expect} from 'chai';
import chai from 'chai'; import chai from 'chai';
import chaiAsPromised from 'chai-as-promised'; import chaiAsPromised from 'chai-as-promised';
import chaiSpies from 'chai-spies'; import chaiSpies from 'chai-spies';
import {suite, test} from '@testdeck/mocha';
import moment from 'moment'; import moment from 'moment';
import {Bulk} from '../src/bulk.js'; import {HttpClient, Bulk, Client, BulkWithMultipleTypesError} from '../src/index.js';
import {Client} from '../src/client.js';
import {BulkWithMultipleTypesError} from '../src/errors.js';
import {HttpClient} from '../src/http-client.js';
chai.should(); chai.should();
chai.use(chaiSpies); chai.use(chaiSpies);
chai.use(chaiAsPromised); chai.use(chaiAsPromised);
const sandbox = chai.spy.sandbox(); describe('Bulk', function () {
const sandbox = chai.spy.sandbox();
const bulkAddRoute = new SCBulkAddRoute(); const bulkAddRoute = new SCBulkAddRoute();
const bulkDoneRoute = new SCBulkDoneRoute(); const bulkDoneRoute = new SCBulkDoneRoute();
const httpClient = new HttpClient(); const httpClient = new HttpClient();
const client = new Client(httpClient, 'http://localhost'); const client = new Client(httpClient, 'http://localhost');
@suite() afterEach(function () {
export class BulkSpec { sandbox.restore();
@test })
async add() {
it('should add', async function () {
sandbox.on(client, 'invokeRoute', () => { sandbox.on(client, 'invokeRoute', () => {
return {}; return {};
}); });
@@ -82,10 +80,9 @@ export class BulkSpec {
}, },
dish, dish,
); );
} });
@test it('should fail add', async function () {
async addFails() {
const bulk = new Bulk(SCThingType.Dish, client, { const bulk = new Bulk(SCThingType.Dish, client, {
expiration: moment().add(3600, 'seconds').format(), expiration: moment().add(3600, 'seconds').format(),
source: 'foo', source: 'foo',
@@ -108,15 +105,10 @@ export class BulkSpec {
uid: 'foo', uid: 'foo',
}; };
return bulk.add(message).should.be.rejectedWith(BulkWithMultipleTypesError); await bulk.add(message).should.be.rejectedWith(BulkWithMultipleTypesError);
} });
async after() { it('should construct', function () {
sandbox.restore();
}
@test
async construct() {
expect(() => { expect(() => {
return new Bulk(SCThingType.Dish, client, { return new Bulk(SCThingType.Dish, client, {
expiration: moment().add(3600, 'seconds').format(), expiration: moment().add(3600, 'seconds').format(),
@@ -126,10 +118,9 @@ export class BulkSpec {
uid: 'bar', uid: 'bar',
}); });
}).not.to.throw(); }).not.to.throw();
} });
@test it('should done', async function () {
async done() {
sandbox.on(client, 'invokeRoute', () => { sandbox.on(client, 'invokeRoute', () => {
return {}; return {};
}); });
@@ -149,5 +140,5 @@ export class BulkSpec {
expect(client.invokeRoute).to.have.been.first.called.with(bulkDoneRoute, { expect(client.invokeRoute).to.have.been.first.called.with(bulkDoneRoute, {
UID: 'bar', UID: 'bar',
}); });
} });
} });

View File

@@ -28,11 +28,7 @@ import {expect} from 'chai';
import chai from 'chai'; import chai from 'chai';
import chaiAsPromised from 'chai-as-promised'; import chaiAsPromised from 'chai-as-promised';
import chaiSpies from 'chai-spies'; import chaiSpies from 'chai-spies';
import {suite, test} from '@testdeck/mocha'; import {ApiError, OutOfRangeError, Client, HttpClient, HttpClientResponse} from '../src/index.js';
import {Client} from '../src/client.js';
import {ApiError, OutOfRangeError} from '../src/errors.js';
import {HttpClient} from '../src/http-client.js';
import {HttpClientResponse} from '../src/http-client-interface.js';
chai.should(); chai.should();
chai.use(chaiSpies); chai.use(chaiSpies);
@@ -84,21 +80,18 @@ async function invokeIndexRouteFails(): Promise<RecursivePartial<HttpClientRespo
}; };
} }
@suite() describe('Client', function () {
export class ClientSpec { afterEach(function () {
async after() {
sandbox.restore(); sandbox.restore();
} });
@test it('should construct', function () {
async construct() {
expect(() => { expect(() => {
return new Client(httpClient, 'http://localhost'); return new Client(httpClient, 'http://localhost');
}).not.to.throw(); }).not.to.throw();
} });
@test it('should construct with headers', async function () {
async constructWithHeaders() {
sandbox.on(httpClient, 'request', invokeIndexRoute); sandbox.on(httpClient, 'request', invokeIndexRoute);
expect(httpClient.request).not.to.have.been.first.called(); expect(httpClient.request).not.to.have.been.first.called();
@@ -115,10 +108,9 @@ export class ClientSpec {
method: indexRoute.method, method: indexRoute.method,
url: new URL('http://localhost' + indexRoute.getUrlPath()), url: new URL('http://localhost' + indexRoute.getUrlPath()),
}); });
} });
@test it('should get thing', async function () {
async getThing() {
const message: SCMessage = { const message: SCMessage = {
audiences: ['employees'], audiences: ['employees'],
categories: ['news'], categories: ['news'],
@@ -174,10 +166,9 @@ export class ClientSpec {
method: searchRoute.method, method: searchRoute.method,
url: new URL('http://localhost' + searchRoute.getUrlPath()), url: new URL('http://localhost' + searchRoute.getUrlPath()),
}); });
} });
@test it('should fail getThing by empty response', async function () {
async getThingFailsByEmptyResponse() {
sandbox.on(httpClient, 'request', async (): Promise<HttpClientResponse<SCSearchResponse>> => { sandbox.on(httpClient, 'request', async (): Promise<HttpClientResponse<SCSearchResponse>> => {
return { return {
body: { body: {
@@ -202,10 +193,9 @@ export class ClientSpec {
const client = new Client(httpClient, 'http://localhost'); const client = new Client(httpClient, 'http://localhost');
return client.getThing('bar').should.be.rejected; return client.getThing('bar').should.be.rejected;
} });
@test it('should fail getThing by uid', async function () {
async getThingFailsByUid() {
const message: SCMessage = { const message: SCMessage = {
audiences: ['employees'], audiences: ['employees'],
categories: ['news'], categories: ['news'],
@@ -244,10 +234,9 @@ export class ClientSpec {
const client = new Client(httpClient, 'http://localhost'); const client = new Client(httpClient, 'http://localhost');
return client.getThing('bar').should.be.rejected; return client.getThing('bar').should.be.rejected;
} });
@test it('should handshake', async function () {
async handshake() {
sandbox.on(httpClient, 'request', invokeIndexRoute); sandbox.on(httpClient, 'request', invokeIndexRoute);
expect(httpClient.request).not.to.have.been.first.called(); expect(httpClient.request).not.to.have.been.first.called();
@@ -263,10 +252,9 @@ export class ClientSpec {
method: indexRoute.method, method: indexRoute.method,
url: new URL('http://localhost' + indexRoute.getUrlPath()), url: new URL('http://localhost' + indexRoute.getUrlPath()),
}); });
} });
@test it('should fail handshake', async function () {
async handshakeFails() {
sandbox.on(httpClient, 'request', invokeIndexRoute); sandbox.on(httpClient, 'request', invokeIndexRoute);
expect(httpClient.request).not.to.have.been.first.called(); expect(httpClient.request).not.to.have.been.first.called();
@@ -274,10 +262,9 @@ export class ClientSpec {
const client = new Client(httpClient, 'http://localhost'); const client = new Client(httpClient, 'http://localhost');
return client.handshake('bar.bar.dummy').should.be.rejectedWith(ApiError); return client.handshake('bar.bar.dummy').should.be.rejectedWith(ApiError);
} });
@test it('should invoke plugin', async function () {
async invokePlugin() {
sandbox.on( sandbox.on(
httpClient, httpClient,
'request', 'request',
@@ -303,13 +290,12 @@ export class ClientSpec {
await client.invokePlugin('unsupportedPlugin').should.be.rejectedWith(ApiError, /.*supportedPlugin.*/gim); await client.invokePlugin('unsupportedPlugin').should.be.rejectedWith(ApiError, /.*supportedPlugin.*/gim);
// again with cached feature definitions // again with cached feature definitions
return client await client
.invokePlugin('supportedPlugin') .invokePlugin('supportedPlugin')
.should.not.be.rejectedWith(ApiError, /.*supportedPlugin.*/gim); .should.not.be.rejectedWith(ApiError, /.*supportedPlugin.*/gim);
} });
@test it('should invoke unavailable plugin', async function () {
async invokePluginUnavailable() {
sandbox.on( sandbox.on(
httpClient, httpClient,
'request', 'request',
@@ -350,10 +336,9 @@ export class ClientSpec {
); );
// again with cached feature definitions // again with cached feature definitions
return client.invokePlugin('supportedPlugin').should.be.rejectedWith(ApiError, /.*supportedPlugin.*/gim); return client.invokePlugin('supportedPlugin').should.be.rejectedWith(ApiError, /.*supportedPlugin.*/gim);
} });
@test it('should invoke route', async function () {
async invokeRoute() {
sandbox.on(httpClient, 'request', invokeIndexRoute); sandbox.on(httpClient, 'request', invokeIndexRoute);
expect(httpClient.request).not.to.have.been.first.called(); expect(httpClient.request).not.to.have.been.first.called();
@@ -369,10 +354,9 @@ export class ClientSpec {
method: indexRoute.method, method: indexRoute.method,
url: new URL('http://localhost' + indexRoute.getUrlPath()), url: new URL('http://localhost' + indexRoute.getUrlPath()),
}); });
} });
@test it('should fail to invoke route', async function () {
async invokeRouteFails() {
sandbox.on(httpClient, 'request', invokeIndexRouteFails); sandbox.on(httpClient, 'request', invokeIndexRouteFails);
expect(httpClient.request).not.to.have.been.first.called(); expect(httpClient.request).not.to.have.been.first.called();
@@ -380,10 +364,9 @@ export class ClientSpec {
const client = new Client(httpClient, 'http://localhost'); const client = new Client(httpClient, 'http://localhost');
return client.invokeRoute(indexRoute).should.be.rejectedWith(ApiError); return client.invokeRoute(indexRoute).should.be.rejectedWith(ApiError);
} });
@test it('should multi search', async function () {
async multiSearch() {
sandbox.on(httpClient, 'request', async (): Promise<HttpClientResponse<SCMultiSearchResponse>> => { sandbox.on(httpClient, 'request', async (): Promise<HttpClientResponse<SCMultiSearchResponse>> => {
return { return {
body: { body: {
@@ -430,10 +413,9 @@ export class ClientSpec {
method: multiSearchRoute.method, method: multiSearchRoute.method,
url: new URL('http://localhost' + multiSearchRoute.getUrlPath()), url: new URL('http://localhost' + multiSearchRoute.getUrlPath()),
}); });
} });
@test it('should multi search with preflight', async function () {
async multiSearchWithPreflight() {
sandbox.on(httpClient, 'request', async (): Promise<HttpClientResponse<SCMultiSearchResponse>> => { sandbox.on(httpClient, 'request', async (): Promise<HttpClientResponse<SCMultiSearchResponse>> => {
return { return {
body: { body: {
@@ -488,10 +470,9 @@ export class ClientSpec {
method: multiSearchRoute.method, method: multiSearchRoute.method,
url: new URL('http://localhost' + multiSearchRoute.getUrlPath()), url: new URL('http://localhost' + multiSearchRoute.getUrlPath()),
}); });
} });
@test it('should next window', async function () {
nextWindow() {
let searchRequest: SCSearchRequest = {size: 30}; let searchRequest: SCSearchRequest = {size: 30};
const searchResponse: SCSearchResponse = { const searchResponse: SCSearchResponse = {
data: [], data: [],
@@ -515,10 +496,9 @@ export class ClientSpec {
expect(() => { expect(() => {
Client.nextWindow(searchRequest, searchResponse); Client.nextWindow(searchRequest, searchResponse);
}).to.throw(OutOfRangeError); }).to.throw(OutOfRangeError);
} });
@test it('should search', async function () {
async search() {
sandbox.on(httpClient, 'request', async (): Promise<HttpClientResponse<SCSearchResponse>> => { sandbox.on(httpClient, 'request', async (): Promise<HttpClientResponse<SCSearchResponse>> => {
return { return {
body: { body: {
@@ -551,10 +531,9 @@ export class ClientSpec {
method: searchRoute.method, method: searchRoute.method,
url: new URL('http://localhost' + searchRoute.getUrlPath()), url: new URL('http://localhost' + searchRoute.getUrlPath()),
}); });
} });
@test it('should search next', async function () {
async searchNext() {
const searchResponse: SCSearchResponse = { const searchResponse: SCSearchResponse = {
data: [], data: [],
facets: [], facets: [],
@@ -589,10 +568,9 @@ export class ClientSpec {
method: searchRoute.method, method: searchRoute.method,
url: new URL('http://localhost' + searchRoute.getUrlPath()), url: new URL('http://localhost' + searchRoute.getUrlPath()),
}); });
} });
@test it('should search with preflight', async function () {
async searchWithPreflight() {
sandbox.on(httpClient, 'request', async (): Promise<HttpClientResponse<SCSearchResponse>> => { sandbox.on(httpClient, 'request', async (): Promise<HttpClientResponse<SCSearchResponse>> => {
return { return {
body: { body: {
@@ -633,5 +611,5 @@ export class ClientSpec {
method: searchRoute.method, method: searchRoute.method,
url: new URL('http://localhost' + searchRoute.getUrlPath()), url: new URL('http://localhost' + searchRoute.getUrlPath()),
}); });
} });
} });

View File

@@ -1,3 +1,4 @@
/* eslint-disable unicorn/no-null */
/* /*
* Copyright (C) 2018 StApps * Copyright (C) 2018 StApps
* This program is free software: you can redistribute it and/or modify it * This program is free software: you can redistribute it and/or modify it
@@ -12,7 +13,6 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {asyncPool} from '@krlwlfrt/async-pool/lib/async-pool';
import { import {
isThing, isThing,
SCBulkAddResponse, SCBulkAddResponse,
@@ -33,16 +33,12 @@ import {expect} from 'chai';
import chaiAsPromised from 'chai-as-promised'; import chaiAsPromised from 'chai-as-promised';
import chaiSpies from 'chai-spies'; import chaiSpies from 'chai-spies';
import clone = require('rfdc'); import clone = require('rfdc');
import {readdir, readFile} from 'fs';
import {suite, test} from '@testdeck/mocha';
import moment from 'moment'; import moment from 'moment';
import {join, resolve} from 'path';
import traverse from 'traverse'; import traverse from 'traverse';
import {promisify} from 'util'; import {ConnectorClient, EmptyBulkError, NamespaceNotDefinedError, HttpClient, HttpClientRequest, HttpClientResponse} from '../src/index.js';
import {ConnectorClient} from '../src/connector-client.js'; import path from "path";
import {EmptyBulkError, NamespaceNotDefinedError} from '../src/errors.js'; import {fileURLToPath} from "url";
import {HttpClient} from '../src/http-client.js'; import {readdir, readFile} from "fs/promises";
import {HttpClientRequest, HttpClientResponse} from '../src/http-client-interface.js';
chai.should(); chai.should();
chai.use(chaiSpies); chai.use(chaiSpies);
@@ -55,9 +51,6 @@ const bulkDoneRoute = new SCBulkDoneRoute();
const bulkRoute = new SCBulkRoute(); const bulkRoute = new SCBulkRoute();
const thingUpdateRoute = new SCThingUpdateRoute(); const thingUpdateRoute = new SCThingUpdateRoute();
const readdirPromisified = promisify(readdir);
const readFilePromisified = promisify(readFile);
const httpClient = new HttpClient(); const httpClient = new HttpClient();
/** /**
@@ -76,14 +69,12 @@ function doesContainThings<T extends SCThingWithoutReferences>(thing: T): boolea
}, false); }, false);
} }
@suite() describe('ConnectorClient', function () {
export class ConnectorClientSpec { afterEach(function () {
async after() {
sandbox.restore(); sandbox.restore();
} });
@test it('should bulk', async function () {
async bulk() {
sandbox.on(httpClient, 'request', async (): Promise<HttpClientResponse<SCBulkResponse>> => { sandbox.on(httpClient, 'request', async (): Promise<HttpClientResponse<SCBulkResponse>> => {
return { return {
body: { body: {
@@ -115,10 +106,9 @@ export class ConnectorClientSpec {
method: bulkRoute.method, method: bulkRoute.method,
url: new URL('http://localhost' + bulkRoute.getUrlPath()), url: new URL('http://localhost' + bulkRoute.getUrlPath()),
}); });
} });
@test it('should bulk without timeout', async function () {
async bulkWithoutTimeout() {
sandbox.on(httpClient, 'request', async (): Promise<HttpClientResponse<SCBulkResponse>> => { sandbox.on(httpClient, 'request', async (): Promise<HttpClientResponse<SCBulkResponse>> => {
return { return {
body: { body: {
@@ -150,10 +140,9 @@ export class ConnectorClientSpec {
method: bulkRoute.method, method: bulkRoute.method,
url: new URL('http://localhost' + bulkRoute.getUrlPath()), url: new URL('http://localhost' + bulkRoute.getUrlPath()),
}); });
} });
@test it('should index', async function () {
async index() {
const messages: SCMessage[] = [ const messages: SCMessage[] = [
{ {
audiences: ['employees'], audiences: ['employees'],
@@ -240,16 +229,14 @@ export class ConnectorClientSpec {
method: bulkRoute.method, method: bulkRoute.method,
url: new URL('http://localhost' + bulkRoute.getUrlPath()), url: new URL('http://localhost' + bulkRoute.getUrlPath()),
}); });
} });
@test it('should fail to index', async function () {
async indexFails() {
const connectorClient = new ConnectorClient(httpClient, 'http://localhost'); const connectorClient = new ConnectorClient(httpClient, 'http://localhost');
return connectorClient.index([]).should.be.rejectedWith(EmptyBulkError); await connectorClient.index([]).should.be.rejectedWith(EmptyBulkError);
} });
@test it('should index without source', async function () {
async indexWithoutSource() {
const messages: SCMessage[] = [ const messages: SCMessage[] = [
{ {
audiences: ['employees'], audiences: ['employees'],
@@ -336,28 +323,25 @@ export class ConnectorClientSpec {
method: bulkRoute.method, method: bulkRoute.method,
url: new URL('http://localhost' + bulkRoute.getUrlPath()), url: new URL('http://localhost' + bulkRoute.getUrlPath()),
}); });
} });
@test it('should make uuid', async function () {
makeUuid() {
const uuid = ConnectorClient.makeUUID('foo', 'b-tu'); const uuid = ConnectorClient.makeUUID('foo', 'b-tu');
expect(uuid).to.be.equal('abad271e-d9e9-5802-b7bc-96d8a647b451'); expect(uuid).to.be.equal('abad271e-d9e9-5802-b7bc-96d8a647b451');
expect(ConnectorClient.makeUUID('bar', 'b-tu')).not.to.be.equal(uuid); expect(ConnectorClient.makeUUID('bar', 'b-tu')).not.to.be.equal(uuid);
expect(ConnectorClient.makeUUID('foo', 'f-u')).not.to.be.equal(uuid); expect(ConnectorClient.makeUUID('foo', 'f-u')).not.to.be.equal(uuid);
} });
@test it('should fail making a uuid', async function (){
makeUuidFails() {
expect(() => { expect(() => {
ConnectorClient.makeUUID('foo', 'b-u'); ConnectorClient.makeUUID('foo', 'b-u');
}).to.throw(NamespaceNotDefinedError); }).to.throw(NamespaceNotDefinedError);
} });
@test it('should remove references', async function () {
async removeReferences() { const pathToTestFiles = path.resolve(
const pathToTestFiles = resolve( path.dirname(fileURLToPath(import.meta.url)),
__dirname,
'..', '..',
'node_modules', 'node_modules',
'@openstapps', '@openstapps',
@@ -367,14 +351,14 @@ export class ConnectorClientSpec {
'indexable', 'indexable',
); );
const testFiles = await readdirPromisified(pathToTestFiles); const testFiles = await readdir(pathToTestFiles);
const testInstances = await asyncPool(5, testFiles, async testFile => { const testInstances = await Promise.all(testFiles.map(async testFile => {
const buffer = await readFilePromisified(join(pathToTestFiles, testFile)); const buffer = await readFile(path.join(pathToTestFiles, testFile));
const content = JSON.parse(buffer.toString()); const content = JSON.parse(buffer.toString());
return content.instance; return content.instance;
}); }));
for (const testInstance of testInstances) { for (const testInstance of testInstances) {
const checkInstance = clone()(testInstance); const checkInstance = clone()(testInstance);
@@ -384,6 +368,7 @@ export class ConnectorClientSpec {
false, false,
JSON.stringify([testInstance, testInstanceWithoutReferences], null, 2), JSON.stringify([testInstance, testInstanceWithoutReferences], null, 2),
); );
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((testInstanceWithoutReferences as any).origin).to.be.equal( expect((testInstanceWithoutReferences as any).origin).to.be.equal(
undefined, undefined,
JSON.stringify([testInstance, testInstanceWithoutReferences], null, 2), JSON.stringify([testInstance, testInstanceWithoutReferences], null, 2),
@@ -393,10 +378,9 @@ export class ConnectorClientSpec {
'Removing the references of a thing could have side effects because no deep copy is used', 'Removing the references of a thing could have side effects because no deep copy is used',
); );
} }
} });
@test it('should remove undefined properties', async function () {
async removeUndefinedProperties() {
const objectWithUndefinedProperties = { const objectWithUndefinedProperties = {
value: 'foo', value: 'foo',
novalue: undefined, novalue: undefined,
@@ -417,10 +401,9 @@ export class ConnectorClientSpec {
objectWithoutUndefinedProperties, objectWithoutUndefinedProperties,
JSON.stringify([objectWithUndefinedProperties, objectWithoutUndefinedProperties], null, 2), JSON.stringify([objectWithUndefinedProperties, objectWithoutUndefinedProperties], null, 2),
); );
} });
@test it('should update', async function () {
async update() {
const message: SCMessage = { const message: SCMessage = {
audiences: ['employees'], audiences: ['employees'],
categories: ['news'], categories: ['news'],
@@ -462,5 +445,5 @@ export class ConnectorClientSpec {
}), }),
), ),
}); });
} });
} });

View File

@@ -27,12 +27,9 @@ import {
import chai from 'chai'; import chai from 'chai';
import chaiAsPromised from 'chai-as-promised'; import chaiAsPromised from 'chai-as-promised';
import chaiSpies from 'chai-spies'; import chaiSpies from 'chai-spies';
import {suite, test} from '@testdeck/mocha';
import moment from 'moment'; import moment from 'moment';
import {copy} from '../src/copy.js'; import {copy, ApiError, HttpClient, RequestOptions, Response} from '../src/index.js';
import {ApiError} from '../src/errors.js'; import {RecursivePartial} from './client.spec.js';
import {HttpClient, RequestOptions, Response} from '../src/http-client.js';
import {RecursivePartial} from './client.spec';
chai.should(); chai.should();
chai.use(chaiSpies); chai.use(chaiSpies);
@@ -47,14 +44,12 @@ const searchRoute = new SCSearchRoute();
const httpClient = new HttpClient(); const httpClient = new HttpClient();
@suite() describe('Copy', function () {
export class CopySpec { afterEach(function () {
async after() {
sandbox.restore(); sandbox.restore();
} });
@test it('should copy', async function () {
async copy() {
type responses = Response<SCBulkAddResponse | SCBulkDoneResponse | SCBulkResponse | SCSearchResponse>; type responses = Response<SCBulkAddResponse | SCBulkDoneResponse | SCBulkResponse | SCSearchResponse>;
sandbox.on( sandbox.on(
@@ -133,10 +128,9 @@ export class CopySpec {
type: SCThingType.Dish, type: SCThingType.Dish,
version: 'foo.bar.foobar', version: 'foo.bar.foobar',
}); });
} });
@test it('should fail to copy', async function () {
async copyShouldFail() {
type responses = Response<SCBulkAddResponse | SCBulkDoneResponse | SCBulkResponse | SCSearchResponse>; type responses = Response<SCBulkAddResponse | SCBulkDoneResponse | SCBulkResponse | SCSearchResponse>;
sandbox.on( sandbox.on(
@@ -206,7 +200,7 @@ export class CopySpec {
}, },
); );
return copy(httpClient, { await copy(httpClient, {
batchSize: 5, batchSize: 5,
from: 'http://foo.bar', from: 'http://foo.bar',
source: 'stapps-copy', source: 'stapps-copy',
@@ -214,5 +208,5 @@ export class CopySpec {
type: SCThingType.Dish, type: SCThingType.Dish,
version: 'foo.bar.foobar', version: 'foo.bar.foobar',
}).should.be.rejectedWith(ApiError); }).should.be.rejectedWith(ApiError);
} });
} });

View File

@@ -12,9 +12,7 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
// eslint-disable-next-line unicorn/prevent-abbreviations
// tslint:disable-next-line: max-line-length
// tslint:disable: completed-docs no-implicit-dependencies prefer-function-over-method newline-per-chained-call member-ordering
import { import {
SCBulkAddResponse, SCBulkAddResponse,
SCBulkAddRoute, SCBulkAddRoute,
@@ -32,12 +30,12 @@ import chaiSpies from 'chai-spies';
import clone = require('rfdc'); import clone = require('rfdc');
import {existsSync, mkdirSync, rmdirSync, unlinkSync} from 'fs'; import {existsSync, mkdirSync, rmdirSync, unlinkSync} from 'fs';
import {createFileSync} from 'fs-extra'; import {createFileSync} from 'fs-extra';
import {suite, test} from '@testdeck/mocha'; // eslint-disable-next-line unicorn/prevent-abbreviations
import {join} from 'path'; import {e2eRun, getItemsFromSamples, ApiError, HttpClient, RequestOptions, Response} from '../src/index.js';
import {e2eRun, getItemsFromSamples} from '../src/e2e.js'; import {RecursivePartial} from './client.spec.js';
import {ApiError} from '../src/errors.js'; import {expect} from "chai";
import {HttpClient, RequestOptions, Response} from '../src/http-client.js'; import path from "path";
import {RecursivePartial} from './client.spec'; import {fileURLToPath} from "url";
chai.should(); chai.should();
chai.use(chaiSpies); chai.use(chaiSpies);
@@ -55,26 +53,21 @@ const httpClient = new HttpClient();
const storedThings: Map<string, SCThings> = new Map(); const storedThings: Map<string, SCThings> = new Map();
@suite describe('e2e Connector', function () {
export class E2EConnectorSpec { afterEach(function () {
async after() {
sandbox.restore(); sandbox.restore();
} });
@test it('should get core test samples', async function () {
async getCoreTestSamples() {
const items = await getItemsFromSamples('./node_modules/@openstapps/core/test/resources'); const items = await getItemsFromSamples('./node_modules/@openstapps/core/test/resources');
// tslint:disable-next-line: no-unused-expression expect(items).to.not.be.empty;
chai.expect(items).to.not.be.empty; });
}
@test it('should fail to get core test samples', async function () {
async getCoreTestSamplesShouldFail() { await chai.expect(getItemsFromSamples('./non-existent-directory')).to.be.rejectedWith(Error);
await chai.expect(getItemsFromSamples('./nonexistantdirectory')).to.be.rejectedWith(Error); });
}
@test it('should run e2e simulation', async function () {
async e2eRunSimulation() {
type responses = Response<SCBulkAddResponse | SCBulkDoneResponse | SCBulkResponse | SCSearchResponse>; type responses = Response<SCBulkAddResponse | SCBulkDoneResponse | SCBulkResponse | SCSearchResponse>;
let failOnCompare = false; let failOnCompare = false;
@@ -167,10 +160,9 @@ export class E2EConnectorSpec {
to: 'http://localhost', to: 'http://localhost',
samplesLocation: './node_modules/@openstapps/core/test/resources', samplesLocation: './node_modules/@openstapps/core/test/resources',
}).should.be.rejectedWith('Unexpected difference'); }).should.be.rejectedWith('Unexpected difference');
} });
@test it('should fail to index', async function () {
async indexShouldFail() {
type responses = Response<SCBulkAddResponse | SCBulkDoneResponse | SCBulkResponse>; type responses = Response<SCBulkAddResponse | SCBulkDoneResponse | SCBulkResponse>;
sandbox.on(httpClient, 'request', async (): Promise<RecursivePartial<responses>> => { sandbox.on(httpClient, 'request', async (): Promise<RecursivePartial<responses>> => {
@@ -185,33 +177,31 @@ export class E2EConnectorSpec {
to: 'http://localhost', to: 'http://localhost',
samplesLocation: './node_modules/@openstapps/core/test/resources', samplesLocation: './node_modules/@openstapps/core/test/resources',
}).should.be.rejectedWith(ApiError); }).should.be.rejectedWith(ApiError);
} });
@test it('should fail to index directory without data', async function () {
async indexShouldFailDirectoryWithoutData() { const emptyDirectoryPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'emptyDir');
const emptyDirPath = join(__dirname, 'emptyDir'); if (!existsSync(emptyDirectoryPath)) {
if (!existsSync(emptyDirPath)) { mkdirSync(emptyDirectoryPath);
mkdirSync(emptyDirPath);
} }
await e2eRun(httpClient, {to: 'http://localhost', samplesLocation: emptyDirPath}).should.be.rejectedWith( await e2eRun(httpClient, {to: 'http://localhost', samplesLocation: emptyDirectoryPath}).should.be.rejectedWith(
'Could not index samples. None were retrieved from the file system.', 'Could not index samples. None were retrieved from the file system.',
); );
rmdirSync(emptyDirPath); rmdirSync(emptyDirectoryPath);
} });
@test it('should fail to index directory without json data', async function () {
async indexShouldFailDirectoryWithoutJsonData() { const somewhatFilledDirectoryPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'somewhatFilledDir');
const somewhatFilledDirPath = join(__dirname, 'somewhatFilledDir'); if (!existsSync(somewhatFilledDirectoryPath)) {
if (!existsSync(somewhatFilledDirPath)) { mkdirSync(somewhatFilledDirectoryPath);
mkdirSync(somewhatFilledDirPath);
} }
const nonJsonFile = join(somewhatFilledDirPath, 'nonjson.txt'); const nonJsonFile = path.join(somewhatFilledDirectoryPath, 'nonjson.txt');
createFileSync(nonJsonFile); createFileSync(nonJsonFile);
await e2eRun(httpClient, { await e2eRun(httpClient, {
to: 'http://localhost', to: 'http://localhost',
samplesLocation: somewhatFilledDirPath, samplesLocation: somewhatFilledDirectoryPath,
}).should.be.rejectedWith('Could not index samples. None were retrieved from the file system.'); }).should.be.rejectedWith('Could not index samples. None were retrieved from the file system.');
unlinkSync(nonJsonFile); unlinkSync(nonJsonFile);
rmdirSync(somewhatFilledDirPath); rmdirSync(somewhatFilledDirectoryPath);
} });
} });

View File

@@ -16,8 +16,7 @@ import chai from 'chai';
import {expect} from 'chai'; import {expect} from 'chai';
import chaiAsPromised from 'chai-as-promised'; import chaiAsPromised from 'chai-as-promised';
import chaiSpies from 'chai-spies'; import chaiSpies from 'chai-spies';
import {suite, test} from '@testdeck/mocha'; import {ApiError} from '../src/index.js';
import {ApiError} from '../src/errors.js';
chai.should(); chai.should();
chai.use(chaiSpies); chai.use(chaiSpies);
@@ -25,36 +24,32 @@ chai.use(chaiAsPromised);
const sandbox = chai.spy.sandbox(); const sandbox = chai.spy.sandbox();
@suite() describe('Errors', function () {
export class ErrorsSpec { afterEach(function () {
async after() {
sandbox.restore(); sandbox.restore();
} });
@test it('should add additional data', function () {
async shouldAddAdditionalData() {
const error = new ApiError({ const error = new ApiError({
additionalData: 'Lorem ipsum', additionalData: 'Lorem ipsum',
}); });
expect(error.toString()).to.contain('Lorem ipsum'); expect(error.toString()).to.contain('Lorem ipsum');
} });
@test it('should add remote stack-trace', async function () {
async shouldAddRemoteStackTrace() {
const error = new ApiError({ const error = new ApiError({
stack: 'Lorem ipsum', stack: 'Lorem ipsum',
}); });
expect(error.toString()).to.contain('Lorem ipsum'); expect(error.toString()).to.contain('Lorem ipsum');
} });
@test it('should set name', async function () {
async shouldSetName() {
const error = new ApiError({ const error = new ApiError({
name: 'Foo', name: 'Foo',
}); });
expect(error.name).to.be.equal('Foo'); expect(error.name).to.be.equal('Foo');
} });
} });

View File

@@ -13,27 +13,23 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {expect} from 'chai'; import {expect} from 'chai';
import {suite, test} from '@testdeck/mocha';
import nock from 'nock'; import nock from 'nock';
import {HttpClient} from '../src/http-client.js'; import {HttpClient} from '../src/index.js';
// TODO: use after each to clean up the nock (then there is no need for numerated resource links) // TODO: use after each to clean up the nock (then there is no need for numerated resource links)
@suite() describe('HttpClient', function () {
export class HttpClientSpec { afterEach(function () {
@test nock.cleanAll();
async construct() { })
it('should construct', function () {
expect(() => { expect(() => {
return new HttpClient(); return new HttpClient();
}).not.to.throw(); }).not.to.throw();
} });
async after() { it('should request', async function () {
nock.cleanAll();
}
@test
async request() {
const client = new HttpClient(); const client = new HttpClient();
nock('http://www.example.com').get('/resource').reply(200, 'foo'); nock('http://www.example.com').get('/resource').reply(200, 'foo');
@@ -43,10 +39,9 @@ export class HttpClientSpec {
}); });
expect(response.body).to.be.equal('foo'); expect(response.body).to.be.equal('foo');
} });
@test it('should request with body', async function () {
async requestWithBody() {
const client = new HttpClient(); const client = new HttpClient();
nock('http://www.example.com').get('/resource').reply(200, 'foo'); nock('http://www.example.com').get('/resource').reply(200, 'foo');
@@ -56,12 +51,11 @@ export class HttpClientSpec {
}); });
expect(response.body).to.be.equal('foo'); expect(response.body).to.be.equal('foo');
} });
@test it('should request with error', async function () {
async requestWithError() {
const client = new HttpClient(); const client = new HttpClient();
let caughtErr; let caughtError;
nock('http://www.example.com').get('/resource').replyWithError('foo'); nock('http://www.example.com').get('/resource').replyWithError('foo');
@@ -72,15 +66,14 @@ export class HttpClientSpec {
}, },
url: new URL('http://www.example.com/resource'), url: new URL('http://www.example.com/resource'),
}); });
} catch (err) { } catch (error) {
caughtErr = err; caughtError = error;
} }
expect(caughtErr).not.to.be.undefined; expect(caughtError).not.to.be.undefined;
} });
@test it('should request with headers', async function () {
async requestWithHeaders() {
const client = new HttpClient(); const client = new HttpClient();
nock('http://www.example.com').get('/resource').reply(200, 'foo'); nock('http://www.example.com').get('/resource').reply(200, 'foo');
@@ -93,10 +86,9 @@ export class HttpClientSpec {
}); });
expect(response.body).to.be.equal('foo'); expect(response.body).to.be.equal('foo');
} });
@test it('should request with method GET', async function () {
async requestWithMethodGet() {
const client = new HttpClient(); const client = new HttpClient();
nock('http://www.example.com').get('/resource').reply(200, 'foo'); nock('http://www.example.com').get('/resource').reply(200, 'foo');
@@ -107,10 +99,9 @@ export class HttpClientSpec {
}); });
expect(response.body).to.be.equal('foo'); expect(response.body).to.be.equal('foo');
} });
@test it('should request with method POST', async function () {
async requestWithMethodPost() {
const client = new HttpClient(); const client = new HttpClient();
nock('http://www.example.com').post('/resource').reply(200, 'foo'); nock('http://www.example.com').post('/resource').reply(200, 'foo');
@@ -121,5 +112,5 @@ export class HttpClientSpec {
}); });
expect(response.body).to.be.equal('foo'); expect(response.body).to.be.equal('foo');
} });
} });

View File

@@ -16,10 +16,7 @@ import {SCPluginRegisterRequest, SCPluginRegisterResponse, SCPluginRegisterRoute
import chai from 'chai'; import chai from 'chai';
import {expect} from 'chai'; import {expect} from 'chai';
import chaiSpies from 'chai-spies'; import chaiSpies from 'chai-spies';
import {suite, test, timeout} from '@testdeck/mocha'; import {HttpClient, HttpClientResponse, PluginClient} from '../src/index.js';
import {HttpClient} from '../src/http-client.js';
import {HttpClientResponse} from '../src/http-client-interface.js';
import {PluginClient} from '../src/plugin-client.js';
import {TestPlugin} from './plugin-resources/test-plugin.js'; import {TestPlugin} from './plugin-resources/test-plugin.js';
chai.use(chaiSpies); chai.use(chaiSpies);
@@ -27,21 +24,16 @@ chai.use(chaiSpies);
const sandbox = chai.spy.sandbox(); const sandbox = chai.spy.sandbox();
const httpClient = new HttpClient(); const httpClient = new HttpClient();
const pluginRegisterRoute = new SCPluginRegisterRoute(); const pluginRegisterRoute = new SCPluginRegisterRoute();
const pluginClient = new PluginClient(httpClient, 'http://localhost'); const pluginClient = new PluginClient(httpClient, 'http://localhost');
@suite(timeout(10000)) describe('PluginClient', function () {
export class PluginClientSpec { this.timeout(10_000);
static plugin: TestPlugin;
static async after() { let plugin: TestPlugin;
await this.plugin.close();
}
static async before() { beforeEach(async function () {
this.plugin = new TestPlugin( plugin = new TestPlugin(
4000, 4000,
'', '',
'', '',
@@ -51,19 +43,19 @@ export class PluginClientSpec {
getSchema: () => { getSchema: () => {
/***/ /***/
}, },
} as any, } as never,
'', '',
'', '',
'', '',
); );
} });
async after() { afterEach(async function () {
await plugin.close();
sandbox.restore(); sandbox.restore();
} });
@test it('should register the plugin', async function () {
async registerPlugin() {
sandbox.on(httpClient, 'request', async (): Promise<HttpClientResponse<SCPluginRegisterResponse>> => { sandbox.on(httpClient, 'request', async (): Promise<HttpClientResponse<SCPluginRegisterResponse>> => {
return { return {
body: { body: {
@@ -76,16 +68,16 @@ export class PluginClientSpec {
expect(httpClient.request).not.to.have.been.called(); expect(httpClient.request).not.to.have.been.called();
await pluginClient.registerPlugin(PluginClientSpec.plugin); await pluginClient.registerPlugin(plugin);
const request: SCPluginRegisterRequest = { const request: SCPluginRegisterRequest = {
action: 'add', action: 'add',
plugin: { plugin: {
address: PluginClientSpec.plugin.fullUrl, address: plugin.fullUrl,
name: PluginClientSpec.plugin.name, name: plugin.name,
requestSchema: PluginClientSpec.plugin.requestSchema, requestSchema: plugin.requestSchema,
responseSchema: PluginClientSpec.plugin.responseSchema, responseSchema: plugin.responseSchema,
route: PluginClientSpec.plugin.route, route: plugin.route,
}, },
}; };
@@ -97,10 +89,9 @@ export class PluginClientSpec {
method: pluginRegisterRoute.method, method: pluginRegisterRoute.method,
url: new URL(`http://localhost${pluginRegisterRoute.getUrlPath()}`), url: new URL(`http://localhost${pluginRegisterRoute.getUrlPath()}`),
}); });
} });
@test it('should unregister the plugin', async function () {
async unregisterPlugin() {
sandbox.on(httpClient, 'request', async (): Promise<HttpClientResponse<SCPluginRegisterResponse>> => { sandbox.on(httpClient, 'request', async (): Promise<HttpClientResponse<SCPluginRegisterResponse>> => {
return { return {
body: { body: {
@@ -113,11 +104,11 @@ export class PluginClientSpec {
expect(httpClient.request).not.to.have.been.called(); expect(httpClient.request).not.to.have.been.called();
await pluginClient.unregisterPlugin(PluginClientSpec.plugin); await pluginClient.unregisterPlugin(plugin);
const request: SCPluginRegisterRequest = { const request: SCPluginRegisterRequest = {
action: 'remove', action: 'remove',
route: PluginClientSpec.plugin.route, route: plugin.route,
}; };
expect(httpClient.request).to.have.been.first.called.with({ expect(httpClient.request).to.have.been.first.called.with({
@@ -128,5 +119,5 @@ export class PluginClientSpec {
method: pluginRegisterRoute.method, method: pluginRegisterRoute.method,
url: new URL(`http://localhost${pluginRegisterRoute.getUrlPath()}`), url: new URL(`http://localhost${pluginRegisterRoute.getUrlPath()}`),
}); });
} });
} });

View File

@@ -13,36 +13,35 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Converter} from '@openstapps/core-tools/lib/schema'; import {Converter} from '@openstapps/core-tools';
import chai from 'chai'; import chai from 'chai';
import {expect} from 'chai'; import {expect} from 'chai';
import chaiSpies from 'chai-spies'; import chaiSpies from 'chai-spies';
import {readFileSync} from 'fs'; import {HttpClient} from '../src/index.js';
import {suite, test, timeout} from '@testdeck/mocha';
import {resolve} from 'path';
import {HttpClient} from '../src/http-client.js';
import {TestPlugin} from './plugin-resources/test-plugin.js'; import {TestPlugin} from './plugin-resources/test-plugin.js';
import path from "path";
import {readFile} from "fs/promises";
import {fileURLToPath} from "url";
chai.use(chaiSpies); chai.use(chaiSpies);
process.on('unhandledRejection', err => { process.on('unhandledRejection', error => {
throw err; throw error;
}); });
const sandbox = chai.spy.sandbox(); const sandbox = chai.spy.sandbox();
const httpClient = new HttpClient(); const httpClient = new HttpClient();
@suite(timeout(20000)) const dirname = path.dirname(fileURLToPath(import.meta.url));
export class PluginSpec {
static testPlugin: TestPlugin;
static async after() { describe('Plugin', function () {
PluginSpec.testPlugin.close(); this.timeout(20_000);
}
static async before() { let testPlugin: TestPlugin;
PluginSpec.testPlugin = new TestPlugin(
beforeEach(function () {
testPlugin = new TestPlugin(
4000, 4000,
'', '',
'', '',
@@ -52,22 +51,22 @@ export class PluginSpec {
getSchema: () => { getSchema: () => {
/***/ /***/
}, },
} as any, } as never,
'', '',
'', '',
'', '',
); );
} })
async after() { afterEach(async function () {
await testPlugin.close();
sandbox.restore(); sandbox.restore();
} })
@test it('should construct', async function () {
async construct() {
const converter = new Converter( const converter = new Converter(
__dirname, dirname,
resolve(__dirname, 'plugin-resources', 'test-plugin-response.ts'), path.resolve(dirname, 'plugin-resources', 'test-plugin-response.ts'),
); );
sandbox.on(converter, 'getSchema', schemaName => { sandbox.on(converter, 'getSchema', schemaName => {
@@ -80,20 +79,19 @@ export class PluginSpec {
'http://B', 'http://B',
'/C', // this doesn't matter for our tests, it's only something that affects the backend '/C', // this doesn't matter for our tests, it's only something that affects the backend
'http://D', 'http://D',
// @ts-ignore fake converter is not a converter
converter, converter,
'PluginTestRequest', 'PluginTestRequest',
'PluginTestResponse', 'PluginTestResponse',
JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json')).toString()).version, JSON.parse(await readFile(path.resolve(dirname, '..', 'package.json'), 'utf8')).version,
); );
expect(constructTestPlugin.port).to.be.equal(4001); expect(constructTestPlugin.port).to.be.equal(4001);
expect(constructTestPlugin.name).to.be.equal('A'); expect(constructTestPlugin.name).to.be.equal('A');
expect(constructTestPlugin.url).to.be.equal('http://B'); expect(constructTestPlugin.url).to.be.equal('http://B');
expect(constructTestPlugin.route).to.be.equal('/C'); expect(constructTestPlugin.route).to.be.equal('/C');
// @ts-ignore backendUrl is protected // @ts-expect-error private property
expect(constructTestPlugin.backendUrl).to.be.equal('http://D'); expect(constructTestPlugin.backendUrl).to.be.equal('http://D');
// schemas are already covered, together with the directory and version // schemas are already covered, together with the directory and version
// @ts-ignore active is private // @ts-expect-error private property
expect(constructTestPlugin.active).to.be.equal(false); expect(constructTestPlugin.active).to.be.equal(false);
expect(constructTestPlugin.requestSchema.$id).to.be.equal('PluginTestRequest'); expect(constructTestPlugin.requestSchema.$id).to.be.equal('PluginTestRequest');
expect(constructTestPlugin.responseSchema.$id).to.be.equal('PluginTestResponse'); expect(constructTestPlugin.responseSchema.$id).to.be.equal('PluginTestResponse');
@@ -103,15 +101,14 @@ export class PluginSpec {
url: new URL('http://localhost:4001'), url: new URL('http://localhost:4001'),
}); });
// onRouteInvoke is a protected method, but we need to access it from the outside to test it // onRouteInvoke is a protected method, but we need to access it from the outside to test it
// @ts-ignore // @ts-expect-error protected method
expect(constructTestPlugin.onRouteInvoke).not.to.have.been.called(); expect(constructTestPlugin.onRouteInvoke).not.to.have.been.called();
await constructTestPlugin.close(); await constructTestPlugin.close();
sandbox.restore(constructTestPlugin, 'onRouteInvoke'); sandbox.restore(constructTestPlugin, 'onRouteInvoke');
} });
@test it('should have full url', async function () {
async fullUrl() {
const constructTestPlugin = new TestPlugin( const constructTestPlugin = new TestPlugin(
4001, 4001,
'', '',
@@ -122,37 +119,35 @@ export class PluginSpec {
getSchema: () => { getSchema: () => {
/***/ /***/
}, },
} as any, } as never,
'', '',
'', '',
'', '',
); );
expect(constructTestPlugin.fullUrl).to.be.equal('http://B:4001'); expect(constructTestPlugin.fullUrl).to.be.equal('http://B:4001');
await constructTestPlugin.close(); await constructTestPlugin.close();
} });
@test it('should start', async function () {
async start() { testPlugin.start();
PluginSpec.testPlugin.start();
sandbox.on(PluginSpec.testPlugin, 'onRouteInvoke'); sandbox.on(testPlugin, 'onRouteInvoke');
await httpClient.request({ await httpClient.request({
url: new URL('http://localhost:4000'), url: new URL('http://localhost:4000'),
}); });
// onRouteInvoke is a protected method, but we need to access it from the outside to test it // onRouteInvoke is a protected method, but we need to access it from the outside to test it
// @ts-ignore // @ts-expect-error protected method
expect(PluginSpec.testPlugin.onRouteInvoke).to.have.been.called(); expect(testPlugin.onRouteInvoke).to.have.been.called();
} });
@test it('should stop', async function () {
async stop() {
// simulate a normal use case by first starting the plugin and then stopping it // simulate a normal use case by first starting the plugin and then stopping it
PluginSpec.testPlugin.start(); testPlugin.start();
PluginSpec.testPlugin.stop(); testPlugin.stop();
sandbox.on(PluginSpec.testPlugin, 'onRouteInvoke'); sandbox.on(testPlugin, 'onRouteInvoke');
const response = await httpClient.request({ const response = await httpClient.request({
url: new URL('http://localhost:4000'), url: new URL('http://localhost:4000'),
@@ -160,7 +155,7 @@ export class PluginSpec {
await expect(response.statusCode).to.be.equal(404); await expect(response.statusCode).to.be.equal(404);
// onRouteInvoke is a protected method, but we need to access it from the outside to test it // onRouteInvoke is a protected method, but we need to access it from the outside to test it
// @ts-ignore // @ts-expect-error protected method
expect(PluginSpec.testPlugin.onRouteInvoke).not.to.have.been.called(); expect(testPlugin.onRouteInvoke).not.to.have.been.called();
} });
} });

View File

@@ -6,20 +6,20 @@
"main": "lib/index.js", "main": "lib/index.js",
"scripts": { "scripts": {
"build": "tsup --dts", "build": "tsup --dts",
"format": "prettier .", "format": "prettier . --ignore-path ../../.gitignore",
"format:fix": "prettier --write .", "format:fix": "prettier --write . --ignore-path ../../.gitignore",
"lint": "eslint --ext .ts src/", "lint": "eslint --ext .ts src/",
"lint:fix": "eslint --fix --ext .ts src/", "lint:fix": "eslint --fix --ext .ts src/",
"test": "nyc mocha 'test/**/*.spec.ts'" "test": "c8 mocha"
}, },
"devDependencies": { "devDependencies": {
"@openstapps/eslint-config": "workspace:*", "@openstapps/eslint-config": "workspace:*",
"@openstapps/nyc-config": "workspace:*",
"@openstapps/prettier-config": "workspace:*", "@openstapps/prettier-config": "workspace:*",
"@openstapps/tsconfig": "workspace:*", "@openstapps/tsconfig": "workspace:*",
"@types/node": "18.15.3",
"@types/chai": "4.3.4", "@types/chai": "4.3.4",
"@types/mocha": "10.0.1", "@types/mocha": "10.0.1",
"@types/node": "18.15.3", "c8": "7.13.0",
"chai": "4.3.7", "chai": "4.3.7",
"mocha": "10.2.0", "mocha": "10.2.0",
"ts-node": "10.9.1", "ts-node": "10.9.1",
@@ -41,8 +41,5 @@
"@openstapps" "@openstapps"
] ]
}, },
"nyc": {
"extends": "@openstapps/nyc-config"
},
"exports": "./lib/index.js" "exports": "./lib/index.js"
} }

View File

@@ -12,8 +12,7 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {chunk} from '../src/index.js';
import {chunk} from '../src/chunk.js';
import {expect} from 'chai'; import {expect} from 'chai';
describe('chunk', function () { describe('chunk', function () {

View File

@@ -12,7 +12,7 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {differenceBy} from '../src/difference.js'; import {differenceBy} from '../src/index.js';
import {expect} from 'chai'; import {expect} from 'chai';
describe('differenceBy', function () { describe('differenceBy', function () {

View File

@@ -12,7 +12,7 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {get} from '../src/get.js'; import {get} from '../src/index.js';
import {expect} from 'chai'; import {expect} from 'chai';
describe('get', function () { describe('get', function () {

View File

@@ -12,7 +12,7 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {groupBy, groupByStable, groupByProperty} from '../src/group-by.js'; import {groupBy, groupByStable, groupByProperty} from '../src/index.js';
import {expect} from 'chai'; import {expect} from 'chai';
describe('groupBy', () => { describe('groupBy', () => {

View File

@@ -12,7 +12,7 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {keyBy} from '../src/key-by.js'; import {keyBy} from '../src/index.js';
import {expect} from 'chai'; import {expect} from 'chai';
describe('keyBy', function () { describe('keyBy', function () {

View File

@@ -12,7 +12,7 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {mapValues} from '../src/map-values.js'; import {mapValues} from '../src/index.js';
import {expect} from 'chai'; import {expect} from 'chai';
describe('map-values', () => { describe('map-values', () => {

View File

@@ -12,7 +12,7 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {minBy} from '../src/min.js'; import {minBy} from '../src/index.js';
import {expect} from 'chai'; import {expect} from 'chai';
describe('minBy', function () { describe('minBy', function () {

View File

@@ -12,7 +12,7 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {omit} from '../src/omit.js'; import {omit} from '../src/index.js';
import {expect} from 'chai'; import {expect} from 'chai';
describe('omit', function () { describe('omit', function () {

View File

@@ -12,7 +12,7 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {partition} from '../src/partition.js'; import {partition} from '../src/index.js';
import {expect} from 'chai'; import {expect} from 'chai';
describe('partition', function () { describe('partition', function () {

View File

@@ -12,7 +12,7 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {pick} from '../src/pick.js'; import {pick} from '../src/index.js';
import {expect} from 'chai'; import {expect} from 'chai';
describe('pick', function () { describe('pick', function () {

View File

@@ -12,7 +12,7 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {shuffle} from '../src/shuffle.js'; import {shuffle} from '../src/index.js';
import {expect} from 'chai'; import {expect} from 'chai';
describe('shuffle', function () { describe('shuffle', function () {

View File

@@ -12,7 +12,7 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {stringSort, stringSortBy} from '../src/string-sort.js'; import {stringSort, stringSortBy} from '../src/index.js';
import {expect} from 'chai'; import {expect} from 'chai';
describe('stringSort', () => { describe('stringSort', () => {

View File

@@ -13,7 +13,7 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {expect} from 'chai'; import {expect} from 'chai';
import {sum, sumBy} from '../src/sum.js'; import {sum, sumBy} from '../src/index.js';
describe('sum', () => { describe('sum', () => {
it('should return the sum of all elements in the collection', () => { it('should return the sum of all elements in the collection', () => {

View File

@@ -13,7 +13,7 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {expect} from 'chai'; import {expect} from 'chai';
import {Tree, treeGroupBy} from '../src/tree-group.js'; import {Tree, treeGroupBy} from '../src/index.js';
interface TestItem { interface TestItem {
id: number; id: number;

View File

@@ -13,7 +13,7 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {expect} from 'chai'; import {expect} from 'chai';
import {uniqBy} from '../src/uniq.js'; import {uniqBy} from '../src/index.js';
describe('uniq', function () { describe('uniq', function () {
it('should return an array with unique values', function () { it('should return an array with unique values', function () {

View File

@@ -13,7 +13,7 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {expect} from 'chai'; import {expect} from 'chai';
import {zip} from '../src/zip.js'; import {zip} from '../src/index.js';
describe('zip', function () { describe('zip', function () {
it('should zip arrays together', function () { it('should zip arrays together', function () {

View File

@@ -46,7 +46,6 @@ Inside of a script in `package.json` or if the npm package is installed globally
openstapps-core-tools schema src/core lib/schema openstapps-core-tools schema src/core lib/schema
``` ```
## How to use the validator? ## How to use the validator?
### Using the validator programatically ### Using the validator programatically

View File

@@ -27,26 +27,25 @@
}, },
"scripts": { "scripts": {
"build": "tsup --dts", "build": "tsup --dts",
"format": "prettier .", "format": "prettier . --ignore-path ../../.gitignore",
"format:fix": "prettier --write .", "format:fix": "prettier --write . --ignore-path ../../.gitignore",
"lint": "eslint --ext .ts src/", "lint": "eslint --ext .ts src/",
"lint:fix": "eslint --fix --ext .ts src/", "lint:fix": "eslint --fix --ext .ts src/",
"plantuml-restart": "docker restart plantuml-server", "plantuml-restart": "docker restart plantuml-server",
"plantuml-start": "docker run --name plantuml-server -d -p 8080:8080 registry.gitlab.com/openstapps/core-tools:latest", "plantuml-start": "docker run --name plantuml-server -d -p 8080:8080 registry.gitlab.com/openstapps/core-tools:latest",
"plantuml-stop": "docker stop plantuml-server", "plantuml-stop": "docker stop plantuml-server",
"test": "nyc mocha 'test/**/*.spec.ts'" "test": "c8 mocha"
}, },
"dependencies": { "dependencies": {
"@openstapps/collection-utils": "workspace:*", "@openstapps/collection-utils": "workspace:*",
"@openstapps/logger": "workspace:*",
"@openstapps/easy-ast": "workspace:*", "@openstapps/easy-ast": "workspace:*",
"@openstapps/logger": "workspace:*",
"ajv": "8.12.0", "ajv": "8.12.0",
"re2": "1.18.0",
"better-ajv-errors": "1.2.0", "better-ajv-errors": "1.2.0",
"chai": "4.3.7",
"commander": "10.0.0", "commander": "10.0.0",
"deepmerge": "4.3.1", "deepmerge": "4.3.1",
"del": "6.1.1", "del": "6.1.1",
"eslint": "8.33.0",
"flatted": "3.2.7", "flatted": "3.2.7",
"fs-extra": "10.1.0", "fs-extra": "10.1.0",
"glob": "10.2.1", "glob": "10.2.1",
@@ -56,18 +55,14 @@
"mustache": "4.2.0", "mustache": "4.2.0",
"openapi-types": "12.1.0", "openapi-types": "12.1.0",
"plantuml-encoder": "1.4.0", "plantuml-encoder": "1.4.0",
"re2": "1.18.0",
"toposort": "2.0.2", "toposort": "2.0.2",
"ts-json-schema-generator": "1.2.0", "ts-json-schema-generator": "1.2.0"
"ts-node": "10.9.1",
"typescript": "4.8.4"
}, },
"devDependencies": { "devDependencies": {
"@openstapps/eslint-config": "workspace:*", "@openstapps/eslint-config": "workspace:*",
"@openstapps/nyc-config": "workspace:*", "@openstapps/nyc-config": "workspace:*",
"@openstapps/prettier-config": "workspace:*", "@openstapps/prettier-config": "workspace:*",
"@openstapps/tsconfig": "workspace:*", "@openstapps/tsconfig": "workspace:*",
"@testdeck/mocha": "0.3.3",
"@types/chai": "4.3.4", "@types/chai": "4.3.4",
"@types/fs-extra": "9.0.13", "@types/fs-extra": "9.0.13",
"@types/glob": "8.0.1", "@types/glob": "8.0.1",
@@ -75,10 +70,13 @@
"@types/mocha": "10.0.1", "@types/mocha": "10.0.1",
"@types/mustache": "4.2.2", "@types/mustache": "4.2.2",
"@types/node": "18.15.3", "@types/node": "18.15.3",
"chai": "4.3.7",
"mocha": "10.2.0", "mocha": "10.2.0",
"c8": "7.13.0",
"nock": "13.3.0", "nock": "13.3.0",
"tsup": "6.7.0", "tsup": "6.7.0",
"typedoc": "0.23.28" "ts-node": "10.9.1",
"typescript": "4.8.4"
}, },
"tsup": { "tsup": {
"entry": [ "entry": [
@@ -99,8 +97,5 @@
"eslintIgnore": [ "eslintIgnore": [
"resources", "resources",
"openapi" "openapi"
], ]
"nyc": {
"extends": "@openstapps/nyc-config"
}
} }

View File

@@ -17,7 +17,6 @@ import {Command} from 'commander';
import {existsSync, readFileSync, writeFileSync} from 'fs'; import {existsSync, readFileSync, writeFileSync} from 'fs';
import {copy} from 'fs-extra'; import {copy} from 'fs-extra';
import path from 'path'; import path from 'path';
import {mkdirPromisified, readFilePromisified} from './common.js';
import {lightweightDefinitionsFromPath, lightweightProjectFromPath} from '@openstapps/easy-ast'; import {lightweightDefinitionsFromPath, lightweightProjectFromPath} from '@openstapps/easy-ast';
import {openapi3Template} from './resources/openapi-303-template.js'; import {openapi3Template} from './resources/openapi-303-template.js';
import {gatherRouteInformation, generateOpenAPIForRoute} from './routes.js'; import {gatherRouteInformation, generateOpenAPIForRoute} from './routes.js';
@@ -27,6 +26,7 @@ import {UMLConfig} from './uml/uml-config.js';
import {capitalize} from './util/string.js'; import {capitalize} from './util/string.js';
import {validateFiles, writeReport} from './validate.js'; import {validateFiles, writeReport} from './validate.js';
import {fileURLToPath} from 'url'; import {fileURLToPath} from 'url';
import {mkdir, readFile} from 'fs/promises';
// handle unhandled promise rejections // handle unhandled promise rejections
process.on('unhandledRejection', async (reason: unknown) => { process.on('unhandledRejection', async (reason: unknown) => {
@@ -42,7 +42,7 @@ const commander = new Command('openstapps-core-tools');
// eslint-disable-next-line unicorn/prefer-module // eslint-disable-next-line unicorn/prefer-module
commander.version( commander.version(
JSON.parse( JSON.parse(
readFileSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json')).toString(), readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json')).toString(),
).version, ).version,
); );
@@ -100,7 +100,7 @@ commander
// copy schema json schema files // copy schema json schema files
try { try {
if (!existsSync(outDirectorySchemasPath)) { if (!existsSync(outDirectorySchemasPath)) {
await mkdirPromisified(outDirectorySchemasPath, { await mkdir(outDirectorySchemasPath, {
recursive: true, recursive: true,
}); });
} }
@@ -134,7 +134,7 @@ commander.command('schema <srcPath> <schemaPath>').action(async (relativeSourceP
Logger.info(`Found ${validatableTypes.length} type(s) to generate schemas for.`); Logger.info(`Found ${validatableTypes.length} type(s) to generate schemas for.`);
await mkdirPromisified(schemaPath, { await mkdir(schemaPath, {
recursive: true, recursive: true,
}); });
@@ -150,7 +150,7 @@ commander.command('schema <srcPath> <schemaPath>').action(async (relativeSourceP
Logger.info(`Using ${corePackageJsonPath} to determine version for schemas.`); Logger.info(`Using ${corePackageJsonPath} to determine version for schemas.`);
const buffer = await readFilePromisified(corePackageJsonPath); const buffer = await readFile(corePackageJsonPath);
const corePackageJson = JSON.parse(buffer.toString()); const corePackageJson = JSON.parse(buffer.toString());
const coreVersion = corePackageJson.version; const coreVersion = corePackageJson.version;

View File

@@ -13,17 +13,8 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import {existsSync, mkdir, readFile, unlink, writeFile} from 'fs';
import glob from 'glob';
import {platform} from 'os';
import {promisify} from 'util';
import path from 'path'; import path from 'path';
import {existsSync} from 'fs';
export const globPromisified = promisify(glob.Glob);
export const mkdirPromisified = promisify(mkdir);
export const readFilePromisified = promisify(readFile);
export const writeFilePromisified = promisify(writeFile);
export const unlinkPromisified = promisify(unlink);
/** /**
* Get path that contains a tsconfig.json * Get path that contains a tsconfig.json
@@ -33,21 +24,15 @@ export const unlinkPromisified = promisify(unlink);
export function getTsconfigPath(startPath: string): string { export function getTsconfigPath(startPath: string): string {
let tsconfigPath = startPath; let tsconfigPath = startPath;
// see https://stackoverflow.com/questions/9652043/identifying-the-file-system-root-with-node-js
const root = platform() === 'win32' ? process.cwd().split(path.sep)[0] : '/';
// repeat until a tsconfig.json is found
while (!existsSync(path.join(tsconfigPath, 'tsconfig.json'))) { while (!existsSync(path.join(tsconfigPath, 'tsconfig.json'))) {
if (tsconfigPath === root) { const parent = path.resolve(tsconfigPath, '..');
if (tsconfigPath === parent) {
throw new Error( throw new Error(
`Reached file system root ${root} while searching for 'tsconfig.json' in ${startPath}!`, `Reached file system root ${parent} while searching for 'tsconfig.json' in ${startPath}!`,
); );
} }
// pop last directory tsconfigPath = parent;
const tsconfigPathParts = tsconfigPath.split(path.sep);
tsconfigPathParts.pop();
tsconfigPath = tsconfigPathParts.join(path.sep);
} }
Logger.info(`Using 'tsconfig.json' from ${tsconfigPath}.`); Logger.info(`Using 'tsconfig.json' from ${tsconfigPath}.`);

View File

@@ -1,11 +1,11 @@
export * from './validate.js' export * from './validate.js';
export * from './types/validator.js' export * from './types/validator.js';
export * from './uml/uml-config.js' export * from './uml/uml-config.js';
export * from './uml/create-diagram.js' export * from './uml/create-diagram.js';
export * from './routes.js' export * from './routes.js';
export * from './types/routes.js' export * from './types/routes.js';
export * from './schema.js' export * from './schema.js';
export * from './types/schema.js' export * from './types/schema.js';

View File

@@ -13,7 +13,11 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {OpenAPIV3} from 'openapi-types'; import {OpenAPIV3} from 'openapi-types';
import {isLightweightClass, lightweightProjectFromPath, LightweightProjectWithIndex} from '@openstapps/easy-ast'; import {
isLightweightClass,
lightweightProjectFromPath,
LightweightProjectWithIndex,
} from '@openstapps/easy-ast';
import {RouteInstanceWithMeta, RouteWithMetaInformation} from './types/routes.js'; import {RouteInstanceWithMeta, RouteWithMetaInformation} from './types/routes.js';
import {rejectNil} from './util/collections.js'; import {rejectNil} from './util/collections.js';
import {capitalize} from './util/string.js'; import {capitalize} from './util/string.js';

View File

@@ -22,7 +22,7 @@ import {getTsconfigPath} from './common.js';
import {definitionsOf, lightweightProjectFromPath} from '@openstapps/easy-ast'; import {definitionsOf, lightweightProjectFromPath} from '@openstapps/easy-ast';
import {isSchemaWithDefinitions} from './util/guards.js'; import {isSchemaWithDefinitions} from './util/guards.js';
import path from 'path'; import path from 'path';
import re2 from './types/re2.js'; import re2 from 're2';
/** /**
* StAppsCore converter * StAppsCore converter
@@ -64,7 +64,7 @@ export class Converter {
this.generator = new SchemaGenerator(program, createParser(program, config), createFormatter(config)); this.generator = new SchemaGenerator(program, createParser(program, config), createFormatter(config));
// create Ajv instance // create Ajv instance
this.schemaValidator = new Ajv.default({code: {regExp: re2}}); this.schemaValidator = new Ajv.default({code: {regExp: re2 as never}});
} }
/** /**

View File

@@ -1,6 +0,0 @@
import re2 from 're2';
type Re2 = typeof re2 & {code: string};
(re2 as Re2).code = 'require("lib/types/re2").default';
export default re2 as Re2;

View File

@@ -13,7 +13,6 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import {createWriteStream} from 'fs';
import * as request from 'got'; import * as request from 'got';
import { import {
expandTypeValue, expandTypeValue,
@@ -22,9 +21,10 @@ import {
LightweightClassDefinition, LightweightClassDefinition,
LightweightDefinition, LightweightDefinition,
LightweightProperty, LightweightProperty,
LightweightType LightweightType,
} from '@openstapps/easy-ast'; } from '@openstapps/easy-ast';
import {UMLConfig} from './uml-config.js'; import {UMLConfig} from './uml-config.js';
import {writeFile} from 'fs/promises';
/** /**
* Converts the lightweight class/enum definitions according to the configuration, * Converts the lightweight class/enum definitions according to the configuration,
@@ -81,8 +81,8 @@ export async function createDiagramFromString(
plantUmlBaseURL: string, plantUmlBaseURL: string,
outputFile = `Diagram-${new Date().toISOString()}`, outputFile = `Diagram-${new Date().toISOString()}`,
) { ) {
// eslint-disable-next-line @typescript-eslint/no-var-requires,unicorn/prefer-module // @ts-expect-error no declarations
const plantumlEncoder = require('plantuml-encoder'); const plantumlEncoder = await import('plantuml-encoder');
const plantUMLCode = plantumlEncoder.encode(`@startuml\n${modelPlantUMLCode}\n@enduml`); const plantUMLCode = plantumlEncoder.encode(`@startuml\n${modelPlantUMLCode}\n@enduml`);
const url = `${plantUmlBaseURL}/svg/${plantUMLCode}`; const url = `${plantUmlBaseURL}/svg/${plantUMLCode}`;
let response; let response;
@@ -100,13 +100,10 @@ export async function createDiagramFromString(
throw error; throw error;
} }
// attach file extension // attach file extension
const fileName = `${outputFile}.svg`; const fileName = `${outputFile.replace(/[^\w-]/g, '_')}.svg`;
try {
createWriteStream(fileName).write(response.body); await writeFile(fileName, response.body);
Logger.log(`Writen data to file: ${fileName}`); Logger.log(`Writen data to file: ${fileName}`);
} catch {
throw new Error('Could not write file. Are you missing permissions?');
}
return fileName; return fileName;
} }

View File

@@ -17,7 +17,7 @@
*/ */
export function rejectNil<T>(array: Array<T | undefined | null>): T[] { export function rejectNil<T>(array: Array<T | undefined | null>): T[] {
// eslint-disable-next-line unicorn/no-null // eslint-disable-next-line unicorn/no-null
return array.filter(it => it == null) as T[]; return array.filter(it => it != null) as T[];
} }
/** /**

View File

@@ -15,16 +15,17 @@
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import Ajv from 'ajv'; import Ajv from 'ajv';
import betterAjvErrors, {IOutputError} from 'better-ajv-errors'; import betterAjvErrors, {IOutputError} from 'better-ajv-errors';
import {PathLike} from 'fs'; import type {PathLike} from 'fs';
import {readFile, writeFile} from 'fs/promises';
import {JSONSchema7} from 'json-schema'; import {JSONSchema7} from 'json-schema';
import mustache from 'mustache'; import mustache from 'mustache';
import {Schema} from 'ts-json-schema-generator'; import {Schema} from 'ts-json-schema-generator';
import {globPromisified, readFilePromisified, writeFilePromisified} from './common.js';
import {ExpectedValidationErrors, ValidationError, ValidationResult} from './types/validator.js'; import {ExpectedValidationErrors, ValidationError, ValidationResult} from './types/validator.js';
import {isThingWithType} from './util/guards.js'; import {isThingWithType} from './util/guards.js';
import path from 'path'; import path from 'path';
import re2 from './types/re2.js'; import re2 from 're2';
import {toPosixPath} from './util/posix-path.js'; import {glob} from 'glob';
import {fileURLToPath} from 'url';
/** /**
* StAppsCore validator * StAppsCore validator
@@ -35,13 +36,13 @@ export class Validator {
*/ */
private readonly ajv = new Ajv.default({ private readonly ajv = new Ajv.default({
verbose: true, verbose: true,
code: {regExp: re2}, code: {regExp: re2 as never},
}); });
/** /**
* Map of schema names to schemas * Map of schema names to schemas
*/ */
private readonly schemas: {[type: string]: Schema} = {}; private readonly schemas: { [type: string]: Schema } = {};
/** /**
* A wrapper function for Ajv that transforms the error into the compatible old error * A wrapper function for Ajv that transforms the error into the compatible old error
@@ -58,8 +59,9 @@ export class Validator {
* *
* @param schemaDirectory Path to directory that contains schema files * @param schemaDirectory Path to directory that contains schema files
*/ */
public async addSchemas(schemaDirectory: PathLike): Promise<string[]> { public async addSchemas(schemaDirectory: string): Promise<string[]> {
const schemaFiles = await globPromisified(path.posix.join(toPosixPath(schemaDirectory), '*.json')); const searchGlob = path.posix.join(schemaDirectory.replaceAll(path.sep, path.posix.sep), '*.json');
const schemaFiles = await glob(searchGlob);
if (schemaFiles.length === 0) { if (schemaFiles.length === 0) {
throw new Error(`No schema files in ${schemaDirectory.toString()}!`); throw new Error(`No schema files in ${schemaDirectory.toString()}!`);
@@ -70,7 +72,7 @@ export class Validator {
await Promise.all( await Promise.all(
schemaFiles.map(async (file: string) => { schemaFiles.map(async (file: string) => {
// read schema file // read schema file
const buffer = await readFilePromisified(file); const buffer = await readFile(file);
// add schema to map // add schema to map
this.schemas[path.basename(file, '.json')] = JSON.parse(buffer.toString()); this.schemas[path.basename(file, '.json')] = JSON.parse(buffer.toString());
@@ -92,7 +94,7 @@ export class Validator {
if (schema === undefined) { if (schema === undefined) {
if (isThingWithType(instance)) { if (isThingWithType(instance)) {
// schema name can be inferred from type string // schema name can be inferred from type string
const schemaSuffix = (instance as {type: string}).type const schemaSuffix = (instance as { type: string }).type
.split(' ') .split(' ')
.map((part: string) => { .map((part: string) => {
return part.slice(0, 1).toUpperCase() + part.slice(1); return part.slice(0, 1).toUpperCase() + part.slice(1);
@@ -175,8 +177,8 @@ export async function validateFiles(
const v = new Validator(); const v = new Validator();
await v.addSchemas(schemaDirectory); await v.addSchemas(schemaDirectory);
// get list of files to test // get a list of files to test
const testFiles = await globPromisified(path.join(resourcesDirectory, '*.json')); const testFiles = await glob(path.posix.join(resourcesDirectory.replaceAll(path.sep, path.posix.sep), '*.json'), {absolute: true});
if (testFiles.length === 0) { if (testFiles.length === 0) {
throw new Error(`No test files in ${resourcesDirectory}!`); throw new Error(`No test files in ${resourcesDirectory}!`);
@@ -191,7 +193,7 @@ export async function validateFiles(
testFiles.map(async (testFile: string) => { testFiles.map(async (testFile: string) => {
const testFileName = path.basename(testFile); const testFileName = path.basename(testFile);
const buffer = await readFilePromisified(path.join(resourcesDirectory, testFileName)); const buffer = await readFile(testFile);
// read test description from file // read test description from file
const testDescription = JSON.parse(buffer.toString()); const testDescription = JSON.parse(buffer.toString());
@@ -260,12 +262,14 @@ export async function validateFiles(
* @param errors Errors that occurred in validation * @param errors Errors that occurred in validation
*/ */
export async function writeReport(reportPath: PathLike, errors: ExpectedValidationErrors): Promise<void> { export async function writeReport(reportPath: PathLike, errors: ExpectedValidationErrors): Promise<void> {
// eslint-disable-next-line unicorn/prefer-module let buffer = await readFile(
let buffer = await readFilePromisified(path.resolve(__dirname, '..', 'resources', 'file.html.mustache')); path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'resources', 'file.html.mustache'),
);
const fileTemplate = buffer.toString(); const fileTemplate = buffer.toString();
// eslint-disable-next-line unicorn/prefer-module buffer = await readFile(
buffer = await readFilePromisified(path.resolve(__dirname, '..', 'resources', 'error.html.mustache')); path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'resources', 'error.html.mustache'),
);
const errorTemplate = buffer.toString(); const errorTemplate = buffer.toString();
let output = ''; let output = '';
@@ -295,11 +299,12 @@ export async function writeReport(reportPath: PathLike, errors: ExpectedValidati
}); });
} }
// eslint-disable-next-line unicorn/prefer-module buffer = await readFile(
buffer = await readFilePromisified(path.resolve(__dirname, '..', 'resources', 'report.html.mustache')); path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'resources', 'report.html.mustache'),
);
const reportTemplate = buffer.toString(); const reportTemplate = buffer.toString();
await writeFilePromisified( await writeFile(
reportPath, reportPath,
mustache.render(reportTemplate, { mustache.render(reportTemplate, {
report: output, report: output,

View File

@@ -15,9 +15,9 @@
*/ */
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import {expect} from 'chai'; import {expect} from 'chai';
import {slow, suite, test, timeout} from '@testdeck/mocha';
import {cwd} from 'process';
import {getTsconfigPath} from '../src/common.js'; import {getTsconfigPath} from '../src/common.js';
import path from 'path';
import {fileURLToPath} from 'url';
process.on('unhandledRejection', (reason: unknown): void => { process.on('unhandledRejection', (reason: unknown): void => {
if (reason instanceof Error) { if (reason instanceof Error) {
@@ -26,10 +26,10 @@ process.on('unhandledRejection', (reason: unknown): void => {
process.exit(1); process.exit(1);
}); });
@suite(timeout(20_000), slow(10_000)) describe('common', function () {
export class CommonSpec { describe('getTsconfigPath', function () {
@test it('should get tsconfig path', function () {
async getTsconfigPath() { expect(getTsconfigPath(path.dirname(fileURLToPath(import.meta.url)))).to.be.equal(process.cwd());
expect(getTsconfigPath(__dirname)).to.be.equal(cwd()); });
} });
} });

View File

@@ -14,36 +14,32 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {expect} from 'chai'; import {expect} from 'chai';
import {existsSync, unlinkSync} from 'fs'; import {unlink} from 'fs/promises';
import {slow, suite, test, timeout} from '@testdeck/mocha'; import {createDiagram, createDiagramFromString} from '../src/index.js';
import {createDiagram, createDiagramFromString} from '../src/uml/create-diagram.js'; import {UMLConfig} from '../src/index.js';
import {UMLConfig} from '../src/uml/uml-config.js'; import {lightweightDefinitionsFromPath} from '@openstapps/easy-ast';
import {LightweightDefinition, lightweightDefinitionsFromPath} from '@openstapps/easy-ast';
import nock = require('nock'); import nock = require('nock');
import path from 'path'; import path from 'path';
import {existsSync} from 'fs';
import {fileURLToPath} from 'url';
@suite(timeout(15_000), slow(5000)) describe('CreateDiagram', function () {
export class CreateDiagramSpec { this.timeout(15_000);
plantUmlConfig: UMLConfig; this.slow(5000);
definitions: LightweightDefinition[]; const plantUmlConfig: UMLConfig = {
definitions: [],
showAssociations: true,
showEnumValues: true,
showInheritance: true,
showInheritedProperties: true,
showOptionalProperties: true,
showProperties: true,
};
constructor() { const definitions = lightweightDefinitionsFromPath('./test/model');
this.plantUmlConfig = {
definitions: [],
showAssociations: true,
showEnumValues: true,
showInheritance: true,
showInheritedProperties: true,
showOptionalProperties: true,
showProperties: true,
};
this.definitions = lightweightDefinitionsFromPath('./test/model'); it('should refuse request', async function () {
}
@test
async shouldRefuseRequest() {
const testPlantUmlCode = 'class Test{\n}'; const testPlantUmlCode = 'class Test{\n}';
try { try {
await createDiagramFromString(testPlantUmlCode, 'http://plantuml:8080'); await createDiagramFromString(testPlantUmlCode, 'http://plantuml:8080');
@@ -54,7 +50,7 @@ export class CreateDiagramSpec {
new Error('getaddrinfo ENOTFOUND plantuml').message, new Error('getaddrinfo ENOTFOUND plantuml').message,
]).to.include((error as NodeJS.ErrnoException).message); ]).to.include((error as NodeJS.ErrnoException).message);
} }
} });
/** /**
* This test will only test the functionality of the method * This test will only test the functionality of the method
@@ -63,26 +59,25 @@ export class CreateDiagramSpec {
* - Writing the response to a file * - Writing the response to a file
* This test will not check the file content * This test will not check the file content
*/ */
@test it('should create diagrams', async function () {
async shouldCreateDiagrams() {
nock('http://plantuml:8080') nock('http://plantuml:8080')
.persist() .persist()
.get(() => true) .get(() => true)
.reply(200, 'This will be the file content'); .reply(200, 'This will be the file content');
let fileName = await createDiagram(this.definitions, this.plantUmlConfig, 'http://plantuml:8080'); let fileName = await createDiagram(definitions, plantUmlConfig, 'http://plantuml:8080');
let filePath = path.resolve(__dirname, '..', fileName); let filePath = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', fileName);
expect(await existsSync(filePath)).to.equal(true); expect(existsSync(filePath)).to.be.true;
await unlinkSync(fileName); await unlink(fileName);
this.plantUmlConfig.showAssociations = false; plantUmlConfig.showAssociations = false;
this.plantUmlConfig.showInheritance = false; plantUmlConfig.showInheritance = false;
fileName = await createDiagram(this.definitions, this.plantUmlConfig, 'http://plantuml:8080'); fileName = await createDiagram(definitions, plantUmlConfig, 'http://plantuml:8080');
filePath = path.resolve(__dirname, '..', fileName); filePath = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', fileName);
expect(await existsSync(filePath)).to.equal(true); expect(existsSync(filePath)).to.be.true;
await unlinkSync(fileName); await unlink(fileName);
nock.cleanAll(); nock.cleanAll();
} });
} });

View File

@@ -15,9 +15,9 @@
*/ */
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import {expect} from 'chai'; import {expect} from 'chai';
import {slow, suite, test, timeout} from '@testdeck/mocha';
import {Converter} from '../src/schema.js'; import {Converter} from '../src/schema.js';
import path from 'path'; import path from 'path';
import {fileURLToPath} from 'url';
process.on('unhandledRejection', (error: unknown) => { process.on('unhandledRejection', (error: unknown) => {
if (error instanceof Error) { if (error instanceof Error) {
@@ -26,11 +26,14 @@ process.on('unhandledRejection', (error: unknown) => {
process.exit(1); process.exit(1);
}); });
@suite(timeout(40_000), slow(10_000)) describe('Schema', function () {
export class SchemaSpec { this.timeout(40_000);
@test this.slow(10_000);
async getSchema() {
const converter = new Converter(path.join(__dirname, '..', 'src', 'resources')); it('should create schema', function () {
const converter = new Converter(
path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'src', 'resources'),
);
const schema = converter.getSchema('Foo', '0.0.1'); const schema = converter.getSchema('Foo', '0.0.1');
expect(schema).to.be.deep.equal({ expect(schema).to.be.deep.equal({
@@ -77,5 +80,5 @@ export class SchemaSpec {
required: ['lorem', 'type'], required: ['lorem', 'type'],
type: 'object', type: 'object',
}); });
} });
} });

View File

@@ -15,14 +15,14 @@
*/ */
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import {expect} from 'chai'; import {expect} from 'chai';
import {existsSync, mkdirSync, writeFileSync} from 'fs'; import {existsSync} from 'fs';
import {JSONSchema7 as Schema} from 'json-schema'; import {JSONSchema7 as Schema} from 'json-schema';
import {slow, suite, test, timeout} from '@testdeck/mocha';
import rimraf from 'rimraf';
import {Foo} from '../src/resources/foo.js'; import {Foo} from '../src/resources/foo.js';
import {Converter} from '../src/schema.js';
import {Validator} from '../src/validate.js'; import {Validator} from '../src/validate.js';
import path from 'path'; import path from 'path';
import {fileURLToPath} from 'url';
import {rm, mkdir, writeFile} from 'fs/promises';
import {Converter} from '../src/index.js';
process.on('unhandledRejection', (error: unknown) => { process.on('unhandledRejection', (error: unknown) => {
if (error instanceof Error) { if (error instanceof Error) {
@@ -31,57 +31,55 @@ process.on('unhandledRejection', (error: unknown) => {
process.exit(1); process.exit(1);
}); });
const tmpdir = path.join(__dirname, 'tmp'); const tmpdir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'tmp');
const fooInstance: Foo = { const fooInstance: Foo = {
lorem: 'ipsum', lorem: 'ipsum',
type: 'Foo', type: 'Foo',
}; };
@suite(timeout(40_000), slow(5000)) describe('Validator', function () {
export class ValidateSpec { this.timeout(40_000);
static converter: Converter; this.slow(5000);
static schema: Schema; let schema: Schema;
let converter: Converter;
static before() { beforeEach(async function () {
this.converter = new Converter(path.join(__dirname, '..', 'src', 'resources')); converter = new Converter(
this.schema = this.converter.getSchema('Foo', '0.0.1'); path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'src', 'resources'),
);
schema = converter.getSchema('Foo', '0.0.1');
if (!existsSync(tmpdir)) { if (!existsSync(tmpdir)) {
mkdirSync(tmpdir); await mkdir(tmpdir);
} }
writeFileSync(path.join(tmpdir, 'SCFoo.json'), JSON.stringify(this.schema, undefined, 2)); await writeFile(path.join(tmpdir, 'SCFoo.json'), JSON.stringify(schema, undefined, 2));
} });
static after() { afterEach(async function () {
rimraf(tmpdir, error => { try {
// tslint:disable-next-line: no-unused-expression await rm(tmpdir, {recursive: true});
} catch (error) {
expect(error, `Unable to remove temporary directory for tests at: ${tmpdir}`).to.be.null; expect(error, `Unable to remove temporary directory for tests at: ${tmpdir}`).to.be.null;
}); }
} });
@test it('should validate by schema identifying string', async function () {
async validateBySchemaIdentifyingString() {
const validator = new Validator(); const validator = new Validator();
await validator.addSchemas(tmpdir); await validator.addSchemas(tmpdir);
const validationResult = validator.validate(fooInstance, 'SCFoo'); const validationResult = validator.validate(fooInstance, 'SCFoo');
// tslint:disable-next-line: no-unused-expression
expect(validationResult.errors, JSON.stringify(validationResult.errors, undefined, 2)).to.be.empty; expect(validationResult.errors, JSON.stringify(validationResult.errors, undefined, 2)).to.be.empty;
} });
@test it('should validate by schema instance', async function () {
async validateBySchemaInstance() {
const validator = new Validator(); const validator = new Validator();
const validationResult = validator.validate(fooInstance, ValidateSpec.schema); const validationResult = validator.validate(fooInstance, schema);
// tslint:disable-next-line: no-unused-expression
expect(validationResult.errors, JSON.stringify(validationResult.errors, undefined, 2)).to.be.empty; expect(validationResult.errors, JSON.stringify(validationResult.errors, undefined, 2)).to.be.empty;
} });
@test it('should validate intrinsic', async function () {
async validateIntrinsic() {
const validator = new Validator(); const validator = new Validator();
await validator.addSchemas(tmpdir); await validator.addSchemas(tmpdir);
const validationResult = validator.validate(fooInstance); const validationResult = validator.validate(fooInstance);
// tslint:disable-next-line: no-unused-expression
expect(validationResult.errors, JSON.stringify(validationResult.errors, undefined, 2)).to.be.empty; expect(validationResult.errors, JSON.stringify(validationResult.errors, undefined, 2)).to.be.empty;
} });
} });

View File

@@ -26,15 +26,15 @@
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"scripts": { "scripts": {
"build": "tsup --dts", "build": "tsup --dts && pnpm run mappings && pnpm run schema",
"format": "prettier .", "format": "prettier . --ignore-path ../../.gitignore",
"format:fix": "prettier --write .", "format:fix": "prettier --write . --ignore-path ../../.gitignore",
"lint": "eslint --ext .ts src/", "lint": "eslint --ext .ts src/",
"lint:fix": "eslint --fix --ext .ts src/", "lint:fix": "eslint --fix --ext .ts src/",
"mappings": "openstapps-es-mapping-generator mapping ../core/src -i minlength,pattern,see,tjs-format -m lib/mappings/mappings.json -a lib/mappings/aggregations.json", "mappings": "openstapps-es-mapping-generator mapping ../core/src -i minlength,pattern,see,tjs-format -m lib/mappings/mappings.json -a lib/mappings/aggregations.json",
"mappings-integration": "openstapps-es-mapping-generator put-es-templates lib/mappings/mappings.json http://elasticsearch:9200/", "mappings-integration": "openstapps-es-mapping-generator put-es-templates lib/mappings/mappings.json http://elasticsearch:9200/",
"schema": "node --max-old-space-size=8192 --stack-size=10240 ./node_modules/@openstapps/core-tools/lib/cli.js schema src lib/schema", "schema": "node --max-old-space-size=8192 --stack-size=10240 ./node_modules/@openstapps/core-tools/lib/app.js schema src lib/schema",
"test": "nyc mocha --recursive 'test/*.spec.ts'" "test": "c8 mocha"
}, },
"dependencies": { "dependencies": {
"@openstapps/core-tools": "workspace:*", "@openstapps/core-tools": "workspace:*",
@@ -51,7 +51,7 @@
"@openstapps/logger": "workspace:*", "@openstapps/logger": "workspace:*",
"@openstapps/prettier-config": "workspace:*", "@openstapps/prettier-config": "workspace:*",
"@openstapps/tsconfig": "workspace:*", "@openstapps/tsconfig": "workspace:*",
"@testdeck/mocha": "0.3.3", "@openstapps/easy-ast": "workspace:*",
"@types/chai": "4.3.4", "@types/chai": "4.3.4",
"@types/json-patch": "0.0.30", "@types/json-patch": "0.0.30",
"@types/json-schema": "7.0.11", "@types/json-schema": "7.0.11",
@@ -62,8 +62,7 @@
"chai": "4.3.7", "chai": "4.3.7",
"conditional-type-checks": "1.0.6", "conditional-type-checks": "1.0.6",
"mocha": "10.2.0", "mocha": "10.2.0",
"nyc": "15.1.0", "c8": "7.13.0",
"rimraf": "4.4.0",
"source-map-support": "0.5.21", "source-map-support": "0.5.21",
"surge": "0.23.1", "surge": "0.23.1",
"ts-node": "10.9.1", "ts-node": "10.9.1",
@@ -115,12 +114,10 @@
"resources", "resources",
"openapi" "openapi"
], ],
"nyc": {
"extends": "@openstapps/nyc-config"
},
"openstapps-configuration": { "openstapps-configuration": {
"overrides": [ "overrides": [
"lint" "lint",
"build"
] ]
} }
} }

View File

@@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import equal from 'fast-deep-equal/es6'; import equal from 'fast-deep-equal/es6/index.js';
import clone from 'rfdc'; import clone from 'rfdc';
import {SCLanguageCode} from './general/i18n.js'; import {SCLanguageCode} from './general/i18n.js';
import {isThing} from './guards.js'; import {isThing} from './guards.js';

View File

@@ -13,41 +13,31 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {lightweightProjectFromPath} from '@openstapps/core-tools/lib/easy-ast/easy-ast'; import {lightweightProjectFromPath, LightweightProject} from '@openstapps/easy-ast';
import {LightweightProject} from '@openstapps/core-tools/lib/easy-ast/types/lightweight-project';
import {expect} from 'chai'; import {expect} from 'chai';
import {reduce} from 'lodash';
process.on('unhandledRejection', err => { process.on('unhandledRejection', error => {
throw err; throw error;
}); });
describe('Mapping Compatibility', () => { describe('Mapping Compatibility', () => {
let project: LightweightProject; let project: LightweightProject;
before(function () { before(function () {
this.timeout(15000); this.timeout(15_000);
this.slow(10000); this.slow(10_000);
project = lightweightProjectFromPath('src'); project = lightweightProjectFromPath('src');
}); });
it('non-exported definitions should not have duplicate names across files', () => { it('non-exported definitions should not have duplicate names across files', () => {
reduce( const names = new Set<string>();
project,
(result, file) => for (const file in project) {
reduce( for (const definition in project[file]) {
file, expect(names).not.to.include(definition);
(result2, _, key) => { names.add(definition);
expect(result2[key]).to.be.undefined; }
return { }
[key]: true,
...result2,
};
},
result,
),
{} as Record<string, boolean>,
);
}); });
}); });

View File

@@ -0,0 +1,33 @@
import {SCBuildingWithoutReferences, SCThingType} from '../../src/index.js';
export const building: SCBuildingWithoutReferences = {
address: {
addressCountry: 'base-address.addressCountry',
addressLocality: 'base-address.addressLocality',
postalCode: 'base-address.postalCode',
streetAddress: 'base-address.streetAddress',
},
categories: ['office', 'education'],
floors: ['base-floor0', 'base-floor1'],
geo: {
point: {
coordinates: [12, 13],
type: 'Point',
},
},
name: 'base-space-name',
translations: {
de: {
address: {
addressCountry: 'de-address.addressCountry',
addressLocality: 'de-address.addressLocality',
postalCode: 'de-address.postalCode',
streetAddress: 'de-address.streetAddress',
},
floors: ['de-floor0', 'de-floor1'],
name: 'de-space-name',
},
},
type: SCThingType.Building,
uid: '540862f3-ea30-5b8f-8678-56b4dc217140',
};

View File

@@ -0,0 +1,9 @@
import {SCBulkResponse, SCThingType} from '../../src/index.js';
export const bulkResponse: SCBulkResponse = {
expiration: '2009-06-30T18:30:00+02:00 ',
source: 'bar',
state: 'done',
type: SCThingType.Dish,
uid: 'foo',
};

View File

@@ -0,0 +1,25 @@
import {SCSearchResponse} from '../../src/index.js';
import {dishWithTranslation} from './dish-with-translation.js';
export const dishWithTranslationSearchResponse: SCSearchResponse = {
data: [dishWithTranslation],
facets: [
{
buckets: [
{
count: 1,
key: 'key',
},
],
field: 'field',
},
],
pagination: {
count: 1,
offset: 0,
total: 1,
},
stats: {
time: 1,
},
};

View File

@@ -0,0 +1,17 @@
import {SCDish, SCThingOriginType, SCThingType} from '../../src/index.js';
export const dishWithTranslation: SCDish = {
categories: ['appetizer'],
name: 'foo',
origin: {
created: '',
type: SCThingOriginType.User,
},
translations: {
de: {
name: 'Foo',
},
},
type: SCThingType.Dish,
uid: 'bar',
};

View File

@@ -0,0 +1,35 @@
import {SCDish, SCThingOriginType, SCThingType} from '../../src/index.js';
import {building} from './building.js';
export const dish: SCDish = {
categories: ['main dish', 'dessert'],
characteristics: [{name: 'base-characteristic0'}, {name: 'base-characteristic1'}],
name: 'base-dish-name',
offers: [
{
availability: 'in stock',
inPlace: building,
prices: {
default: 23.42,
},
provider: {
name: 'base-provider',
type: SCThingType.Organization,
uid: '540862f3-ea30-5b8f-8678-56b4dc217141',
},
},
],
origin: {
indexed: '1970-01-01T00:00:00.000Z',
name: 'dish-connector',
type: SCThingOriginType.Remote,
},
translations: {
de: {
characteristics: [{name: 'de-characteristic0'}, {name: 'de-characteristic1'}],
name: 'de-dish-name',
},
},
type: SCThingType.Dish,
uid: '540862f3-ea30-5b8f-8678-56b4dc217140',
};

View File

@@ -0,0 +1,13 @@
import {SCThingOriginType} from '../../src/index.js';
import {SCDish} from '../../lib/index.js';
export const notADish: Omit<SCDish, 'type'> & {type: 'foobar'} = {
categories: ['appetizer'],
name: 'foo',
origin: {
created: '',
type: SCThingOriginType.User,
},
type: 'foobar',
uid: 'bar',
};

View File

@@ -0,0 +1,18 @@
import {SCSetting, SCSettingInputType, SCThingOriginType, SCThingType} from '../../src/index.js';
export const setting: SCSetting = {
categories: ['profile'],
defaultValue: 'student',
description: 'base-description',
inputType: SCSettingInputType.SingleChoice,
name: 'group',
order: 1,
origin: {
indexed: '2018-11-11T14:30:00Z',
name: 'Dummy',
type: SCThingOriginType.Remote,
},
type: SCThingType.Setting,
uid: '2c97aa36-4aa2-43de-bc5d-a2b2cb3a530e',
values: ['student', 'employee', true, 42],
};

View File

@@ -16,18 +16,17 @@ import {
isLightweightClass, isLightweightClass,
isLightweightEnum, isLightweightEnum,
isUnionType, isUnionType,
} from '@openstapps/core-tools/lib/easy-ast/ast-util'; LightweightAliasDefinition,
import {LightweightAliasDefinition} from '@openstapps/core-tools/lib/easy-ast/types/lightweight-alias-definition'; LightweightProjectWithIndex,
import {LightweightProjectWithIndex} from '@openstapps/core-tools/lib/easy-ast/types/lightweight-project'; LightweightType,
import {LightweightType} from '@openstapps/core-tools/lib/easy-ast/types/lightweight-type'; LightweightClassDefinition,
import {LightweightClassDefinition} from '@openstapps/core-tools/src/easy-ast/types/lightweight-class-definition'; LightweightDefinition,
import {LightweightDefinition} from '@openstapps/core-tools/src/easy-ast/types/lightweight-definition'; LightweightProperty,
import {LightweightProperty} from '@openstapps/core-tools/src/easy-ast/types/lightweight-property'; } from '@openstapps/easy-ast';
import {expect} from 'chai'; import {expect} from 'chai';
import {assign, chain, clone, flatMap, isNil, reduce, reject, some} from 'lodash';
process.on('unhandledRejection', err => { process.on('unhandledRejection', error => {
throw err; throw error;
}); });
describe('Features', () => { describe('Features', () => {
@@ -37,8 +36,8 @@ describe('Features', () => {
let thingsWithoutReferences: LightweightClassDefinition[]; let thingsWithoutReferences: LightweightClassDefinition[];
before(function () { before(function () {
this.timeout(15000); this.timeout(15_000);
this.slow(10000); this.slow(10_000);
project = new LightweightProjectWithIndex('src'); project = new LightweightProjectWithIndex('src');
@@ -51,9 +50,7 @@ describe('Features', () => {
referenceName: 'SCDiff', referenceName: 'SCDiff',
}); });
expect( expect(thingsReflection.type?.specificationTypes?.map(it => it.referenceName)).not.to.include(undefined);
thingsReflection.type?.specificationTypes?.every(it => typeof it.referenceName !== 'undefined'),
).to.be.true;
thingNames = thingsReflection.type?.specificationTypes?.map(type => type.referenceName!) ?? []; thingNames = thingsReflection.type?.specificationTypes?.map(type => type.referenceName!) ?? [];
things = thingNames.map(it => project.definitions[it]).filter(isLightweightClass); things = thingNames.map(it => project.definitions[it]).filter(isLightweightClass);
thingsWithoutReferences = thingNames thingsWithoutReferences = thingNames
@@ -64,15 +61,22 @@ describe('Features', () => {
const inheritedProperties = function ( const inheritedProperties = function (
classLike: LightweightClassDefinition, classLike: LightweightClassDefinition,
): Record<string, LightweightProperty> | undefined { ): Record<string, LightweightProperty> | undefined {
return reduce( const extendClause = [
[...(classLike.implementedDefinitions ?? []), ...(classLike.extendedDefinitions ?? [])], ...(classLike.implementedDefinitions ?? []),
(obj, extension) => { ...(classLike.extendedDefinitions ?? []),
const object = project.definitions[extension.referenceName ?? '']; ];
const properties = {...classLike.properties};
return assign(obj, isLightweightClass(object) ? inheritedProperties(object) : obj); for (const definition of extendClause) {
}, const object = project.definitions[definition.referenceName!];
clone(classLike.properties), if (isLightweightClass(object)) {
); Object.assign(properties, inheritedProperties(object));
} else {
Object.assign(properties, object);
}
}
return properties;
}; };
it('should have an origin', () => { it('should have an origin', () => {
@@ -82,42 +86,36 @@ describe('Features', () => {
}); });
it('should not have duplicate names', () => { it('should not have duplicate names', () => {
reduce( const names = new Set<string>();
project.files,
(fileResult, file) =>
reduce(
file,
(definitionResult, definition: LightweightDefinition) => {
expect(definitionResult[definition.name]).to.be.undefined;
definitionResult[definition.name] = true; // something that's not undefined
return definitionResult; for (const fileName in project.files) {
}, const file = project.files[fileName];
fileResult, for (const definition in file) {
), const definitionName = file[definition].name;
{} as Record<string, true>, expect(names).not.to.include(definitionName);
); names.add(definitionName);
}
}
}); });
it('should not have properties referencing SCThing', () => { it('should not have properties referencing SCThing', () => {
const allPropertyReferenceNames: (property: LightweightProperty) => string[] = property => const allPropertyReferenceNames: (property: LightweightProperty) => string[] = property =>
reject( [
[property.type.referenceName!, ...flatMap(property.properties, allPropertyReferenceNames)], property.type.referenceName!,
isNil, ...Object.values(property.properties ?? []).flatMap(allPropertyReferenceNames),
); ].filter(it => !!it);
const typeHasSCThingReferences: (type?: LightweightType) => boolean = type => const typeHasSCThingReferences: (type?: LightweightType) => boolean = type =>
type?.referenceName type?.referenceName
? hasSCThingReferences(project.definitions[type.referenceName]) ? hasSCThingReferences(project.definitions[type.referenceName])
: some(type?.specificationTypes, typeHasSCThingReferences); : type?.specificationTypes?.some(typeHasSCThingReferences) === true;
const hasSCThingReferences: (definition?: LightweightDefinition) => boolean = definition => const hasSCThingReferences: (definition?: LightweightDefinition) => boolean = definition =>
isLightweightClass(definition) isLightweightClass(definition)
? chain(inheritedProperties(definition)) ? Object.values(inheritedProperties(definition) ?? [])
.flatMap(it => flatMap(it.properties, allPropertyReferenceNames)) .flatMap(it => Object.values(it.properties ?? []).flatMap(allPropertyReferenceNames))
.map(it => project.definitions[it] as LightweightDefinition) .map(it => project.definitions[it] as LightweightDefinition)
.some(it => it.name === 'SCThing' || hasSCThingReferences(it)) .some(it => it.name === 'SCThing' || hasSCThingReferences(it))
.value()
: definition : definition
? typeHasSCThingReferences(definition.type) ? typeHasSCThingReferences(definition.type)
: false; : false;
@@ -127,16 +125,18 @@ describe('Features', () => {
} }
}); });
/**
* Checks if a definition is an SCThing
*/
function extendsSCThing(definition?: LightweightDefinition): boolean { function extendsSCThing(definition?: LightweightDefinition): boolean {
return isLightweightClass(definition) return isLightweightClass(definition)
? chain([ ? [
...((definition as LightweightClassDefinition).extendedDefinitions ?? []), ...((definition as LightweightClassDefinition).extendedDefinitions ?? []),
...((definition as LightweightClassDefinition).implementedDefinitions ?? []), ...((definition as LightweightClassDefinition).implementedDefinitions ?? []),
]) ]
.map(it => it.referenceName) .map(it => it.referenceName)
.reject(isNil) .filter(it => !!it)
.some(it => it === 'SCThing' || extendsSCThing(project.definitions[it!])) .some(it => it === 'SCThing' || extendsSCThing(project.definitions[it!]))
.value()
: false; : false;
} }

View File

@@ -1,3 +1,4 @@
/* eslint-disable unicorn/no-null */
/* /*
* Copyright (C) 2018, 2019 StApps * Copyright (C) 2018, 2019 StApps
* This program is free software: you can redistribute it and/or modify it * This program is free software: you can redistribute it and/or modify it
@@ -12,12 +13,7 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {slow, suite, test, timeout} from '@testdeck/mocha'; import {SCMultiSearchResponse} from '../src/index.js';
import {SCBulkResponse} from '../src/protocol/routes/bulk-request.js';
import {SCMultiSearchResponse} from '../src/protocol/routes/search-multi.js';
import {SCSearchResponse} from '../src/protocol/routes/search.js';
import {SCThingOriginType, SCThingType} from '../src/things/abstract/thing.js';
import {SCDish} from '../src/things/dish.js';
import {expect} from 'chai'; import {expect} from 'chai';
import { import {
isBulkResponse, isBulkResponse,
@@ -26,117 +22,104 @@ import {
isThing, isThing,
isThingWithTranslations, isThingWithTranslations,
} from '../src/guards.js'; } from '../src/guards.js';
import {bulkResponse} from './dummy/bulk-response.js';
import {dishWithTranslation} from './dummy/dish-with-translation.js';
import {dishWithTranslationSearchResponse} from './dummy/dish-with-translation-search-response.js';
import {notADish} from './dummy/not-a-dish.js';
import {SCBulkResponse} from '../src/protocol/routes/bulk-request.js'; import {SCBulkResponse} from '../src/protocol/routes/bulk-request.js';
import {SCSearchResponse} from '../src/protocol/routes/search.js'; import {SCSearchResponse} from '../src/protocol/routes/search.js';
import {SCMultiSearchResponse} from '../src/protocol/routes/search-multi.js'; import {SCMultiSearchResponse} from '../src/protocol/routes/search-multi.js';
import {SCThingOriginType, SCThingType} from '../src/things/abstract/thing.js'; import {SCThingOriginType, SCThingType} from '../src/things/abstract/thing.js';
import {SCDish} from '../src/things/dish'; import {SCDish} from '../src/things/dish.js';
@suite(timeout(10000), slow(5000)) describe('Guards', function () {
export class GuardsSpec { this.timeout(10_000);
static bulkResponse: SCBulkResponse = { this.slow(5000);
expiration: '2009-06-30T18:30:00+02:00 ',
source: 'bar',
state: 'done',
type: SCThingType.Dish,
uid: 'foo',
};
static dishWithTranslation: SCDish = { describe('isBulkResponse', function () {
categories: ['appetizer'], it(`should not accept nullish values`, function () {
name: 'foo', expect(isBulkResponse(null)).to.be.false;
origin: { });
created: '',
type: SCThingOriginType.User,
},
translations: {
de: {
name: 'Foo',
},
},
type: SCThingType.Dish,
uid: 'bar',
};
static notADish = { it('should not accept a dish', function () {
categories: ['appetizer'], expect(isBulkResponse(dishWithTranslation)).to.be.false;
name: 'foo', });
origin: {
created: '',
type: SCThingOriginType.User,
},
type: 'foobar',
uid: 'bar',
};
static searchResponse: SCSearchResponse = { it('should accept a bulk', function () {
data: [GuardsSpec.dishWithTranslation], expect(isBulkResponse(bulkResponse)).to.be.true;
facets: [ });
{ });
buckets: [
{
count: 1,
key: 'key',
},
],
field: 'field',
},
],
pagination: {
count: 1,
offset: 0,
total: 1,
},
stats: {
time: 1,
},
};
@test describe('isMultiSearchResponse', function () {
public isBulkResponse() {
expect(isBulkResponse(null)).to.be.equal(false);
expect(isBulkResponse(GuardsSpec.dishWithTranslation)).to.be.equal(false);
expect(isBulkResponse(GuardsSpec.bulkResponse)).to.be.equal(true);
}
@test
public isMultiSearchResponse() {
const multiSearchResponse: SCMultiSearchResponse = { const multiSearchResponse: SCMultiSearchResponse = {
foo: GuardsSpec.searchResponse, foo: dishWithTranslationSearchResponse,
}; };
expect(isMultiSearchResponse(multiSearchResponse)).to.be.equal(true);
const notAMultiSearchResponse = {...multiSearchResponse, ...{bar: 'baz'}};
expect(isMultiSearchResponse(notAMultiSearchResponse)).to.be.equal(false);
delete multiSearchResponse.foo;
expect(isMultiSearchResponse(multiSearchResponse)).to.be.equal(false);
}
@test it('should accept a multi search response', function () {
public isSearchResponse() { expect(isMultiSearchResponse(multiSearchResponse)).to.be.true;
const notASearchResponse = {...GuardsSpec.searchResponse}; });
// @ts-ignore
delete notASearchResponse.pagination;
expect(isSearchResponse(notASearchResponse)).to.be.equal(false);
// @ts-ignore
delete notASearchResponse.data;
expect(isSearchResponse(notASearchResponse)).to.be.equal(false);
expect(isSearchResponse(null)).to.be.equal(false);
expect(isSearchResponse(GuardsSpec.searchResponse)).to.be.equal(true);
}
@test it('should not accept a multi search response with invalid search requests', function () {
public isThing() { const notAMultiSearchResponse = {...multiSearchResponse, bar: 'baz'};
expect(isThing('foo')).to.be.equal(false); expect(isMultiSearchResponse(notAMultiSearchResponse)).to.be.false;
expect(isThing({type: 'foo'})).to.be.equal(false); });
expect(isThing(GuardsSpec.notADish)).to.be.equal(false);
expect(isThing(GuardsSpec.dishWithTranslation)).to.be.equal(true);
}
@test it('should not accept empty responses', function () {
public isThingWithTranslations() { expect(isMultiSearchResponse({})).to.be.false;
const dishWithoutTranslation = {...GuardsSpec.dishWithTranslation}; });
delete dishWithoutTranslation.translations; });
expect(isThingWithTranslations(dishWithoutTranslation)).to.be.equal(false);
expect(isThingWithTranslations(GuardsSpec.dishWithTranslation)).to.be.equal(true); describe('isSearchResponse', function () {
} it('should accept a search response', function () {
} expect(isSearchResponse(dishWithTranslationSearchResponse)).to.be.true;
});
it('should not accept nullish values', function () {
expect(isSearchResponse(null)).to.be.false;
});
it('should not accept a response without pagination', function () {
const response = {...dishWithTranslationSearchResponse};
// @ts-expect-error this is on purpose of course
delete response.pagination;
expect(isSearchResponse(response)).to.be.false;
});
it('should not accept a response without data', function () {
const response = {...dishWithTranslationSearchResponse};
// @ts-expect-error this is on purpose of course
delete response.data;
expect(isSearchResponse(response)).to.be.false;
});
});
describe('isThing', function () {
it('should not accept strings', function () {
expect(isThing('foo')).to.be.false;
});
it('should not accept objects with arbitrary type values', function () {
expect(isThing({type: 'foo'})).to.be.false;
});
it('should not accept things with missing props', function () {
expect(isThing(notADish)).to.be.false;
});
it('should accept valid things', function () {
expect(isThing(dishWithTranslation)).to.be.true;
});
});
describe('isThingWithTranslations', function () {
it('should not accept things without translations', function () {
const dishWithoutTranslation = {...dishWithTranslation};
delete dishWithoutTranslation.translations;
expect(isThingWithTranslations(dishWithoutTranslation)).to.be.false;
});
it('should accept things with translations', function () {
expect(isThingWithTranslations(dishWithTranslation)).to.be.true;
});
});
});

View File

@@ -14,69 +14,51 @@
*/ */
import {slow, suite, test, timeout} from '@testdeck/mocha'; import {slow, suite, test, timeout} from '@testdeck/mocha';
import {expect} from 'chai'; import {expect} from 'chai';
import {slow, suite, test, timeout} from '@testdeck/mocha'; import {SCBulkRoute} from '../src/index.js';
import {SCBulkRoute} from '../src/protocol/routes/bulk-request.js'; import {SCBulkAddRoute} from '../src/index.js';
import {SCBulkAddRoute} from '../src/protocol/routes/bulk-add.js'; import {SCThingUpdateRoute} from '../src/index.js';
import {SCThingUpdateRoute} from '../src/protocol/routes/thing-update.js';
@suite(timeout(10000), slow(5000)) describe('Routes', function () {
export class RoutesSpec { this.timeout(10_000);
@test this.slow(5000);
public bulkAddRouteUrlPath() {
const bulkAddRoute = new SCBulkAddRoute();
it('should produce correct BulkAddRoute url path', function () {
expect( expect(
bulkAddRoute.getUrlPath({ new SCBulkAddRoute().getUrlPath({
UID: '540862f3-ea30-5b8f-8678-56b4dc217140', UID: '540862f3-ea30-5b8f-8678-56b4dc217140',
}), }),
).to.equal('/bulk/540862f3-ea30-5b8f-8678-56b4dc217140'); ).to.equal('/bulk/540862f3-ea30-5b8f-8678-56b4dc217140');
} });
@test it('should produce correct BlukRoute url path', function () {
public bulkRouteUrlPath() { expect(new SCBulkRoute().getUrlPath()).to.equal('/bulk');
const bulkRoute = new SCBulkRoute(); });
expect(bulkRoute.getUrlPath()).to.equal('/bulk');
}
@test
public thingUpdateRouteUrlPath() {
const thingUpdateRoute = new SCThingUpdateRoute();
it('should produce correct ThingUpdateRoute url path', function () {
expect( expect(
thingUpdateRoute.getUrlPath({ new SCThingUpdateRoute().getUrlPath({
TYPE: 'dish', TYPE: 'dish',
UID: '540862f3-ea30-5b8f-8678-56b4dc217140', UID: '540862f3-ea30-5b8f-8678-56b4dc217140',
}), }),
).to.equal('/dish/540862f3-ea30-5b8f-8678-56b4dc217140'); ).to.equal('/dish/540862f3-ea30-5b8f-8678-56b4dc217140');
} });
@test it('should throw an error if too many parameters are provided', function () {
public tooManyParameters() { expect(() =>
const thingUpdateRoute = new SCThingUpdateRoute(); new SCThingUpdateRoute().getUrlPath({
const fn = () => {
thingUpdateRoute.getUrlPath({
FOO: 'bar', FOO: 'bar',
TYPE: 'dish', TYPE: 'dish',
UID: '540862f3-ea30-5b8f-8678-56b4dc217140', UID: '540862f3-ea30-5b8f-8678-56b4dc217140',
}); }),
}; ).to.throw('Extraneous parameters provided.');
});
expect(fn).to.throw('Extraneous parameters provided.'); it('should throw an error if wrong parameters are provided', function () {
} expect(() =>
new SCThingUpdateRoute().getUrlPath({
@test
public wrongParameters() {
const thingUpdateRoute = new SCThingUpdateRoute();
const fn = () => {
thingUpdateRoute.getUrlPath({
TYPO: 'dish', TYPO: 'dish',
UID: '540862f3-ea30-5b8f-8678-56b4dc217140', UID: '540862f3-ea30-5b8f-8678-56b4dc217140',
}); }),
}; ).to.throw("Parameter 'TYPE' not provided.");
});
expect(fn).to.throw("Parameter 'TYPE' not provided."); });
}
}

View File

@@ -1,29 +1,22 @@
import {validateFiles, writeReport} from '@openstapps/core-tools/lib/validate'; import {validateFiles, writeReport} from '@openstapps/core-tools';
import {slow, suite, test, timeout} from '@testdeck/mocha';
import {expect} from 'chai'; import {expect} from 'chai';
import {mkdirSync} from 'fs'; import {mkdir} from 'fs/promises';
import {join, resolve} from 'path'; import path from 'path';
@suite(timeout(15000), slow(10000)) describe('Schema', function () {
export class SchemaSpec { this.timeout(15_000);
@test this.slow(10_000);
async 'validate against test files'() {
const errorsPerFile = {
...(await validateFiles(resolve('lib', 'schema'), resolve('test', 'resources'))),
...(await validateFiles(resolve('lib', 'schema'), resolve('test', 'resources', 'indexable'))),
};
let unexpected = false; it('should validate against test files', async function () {
Object.keys(errorsPerFile).forEach(file => { const errorsPerFile = await validateFiles(path.resolve('lib', 'schema'), path.resolve('test', 'resources'));
unexpected = unexpected || errorsPerFile[file].some(error => !error.expected);
});
mkdirSync('report', { await mkdir('report', {recursive: true});
recursive: true, await writeReport(path.join('report', 'index.html'), errorsPerFile);
});
await writeReport(join('report', 'index.html'), errorsPerFile); for (const file of Object.keys(errorsPerFile)) {
for (const error of errorsPerFile[file]) {
expect(unexpected).to.be.equal(false); expect(error.expected).to.be.true;
} }
} }
});
});

View File

@@ -15,93 +15,11 @@
import {slow, suite, test, timeout} from '@testdeck/mocha'; import {slow, suite, test, timeout} from '@testdeck/mocha';
import {expect} from 'chai'; import {expect} from 'chai';
import clone from 'rfdc'; import clone from 'rfdc';
import {SCThingOriginType, SCThingRemoteOrigin, SCThingType} from '../src/things/abstract/thing.js'; import {SCThingRemoteOrigin} from '../src/index.js';
import {SCBuildingWithoutReferences} from '../src/things/building.js'; import {SCDishMeta} from '../src/index.js';
import {SCDish, SCDishMeta} from '../src/things/dish.js'; import {SCThingTranslator} from '../src/index.js';
import {SCSetting, SCSettingInputType} from '../src/things/setting.js'; import {dish} from './dummy/dish.js';
import {SCThingTranslator} from '../src/translator.js'; import {setting} from './dummy/setting.js';
const building: SCBuildingWithoutReferences = {
address: {
addressCountry: 'base-address.addressCountry',
addressLocality: 'base-address.addressLocality',
postalCode: 'base-address.postalCode',
streetAddress: 'base-address.streetAddress',
},
categories: ['office', 'education'],
floors: ['base-floor0', 'base-floor1'],
geo: {
point: {
coordinates: [12.0, 13.0],
type: 'Point',
},
},
name: 'base-space-name',
translations: {
de: {
address: {
addressCountry: 'de-address.addressCountry',
addressLocality: 'de-address.addressLocality',
postalCode: 'de-address.postalCode',
streetAddress: 'de-address.streetAddress',
},
floors: ['de-floor0', 'de-floor1'],
name: 'de-space-name',
},
},
type: SCThingType.Building,
uid: '540862f3-ea30-5b8f-8678-56b4dc217140',
};
const dish: SCDish = {
categories: ['main dish', 'dessert'],
characteristics: [{name: 'base-characteristic0'}, {name: 'base-characteristic1'}],
name: 'base-dish-name',
offers: [
{
availability: 'in stock',
inPlace: building,
prices: {
default: 23.42,
},
provider: {
name: 'base-provider',
type: SCThingType.Organization,
uid: '540862f3-ea30-5b8f-8678-56b4dc217141',
},
},
],
origin: {
indexed: '1970-01-01T00:00:00.000Z',
name: 'dish-connector',
type: SCThingOriginType.Remote,
},
translations: {
de: {
characteristics: [{name: 'de-characteristic0'}, {name: 'de-characteristic1'}],
name: 'de-dish-name',
},
},
type: SCThingType.Dish,
uid: '540862f3-ea30-5b8f-8678-56b4dc217140',
};
const setting: SCSetting = {
categories: ['profile'],
defaultValue: 'student',
description: 'base-description',
inputType: SCSettingInputType.SingleChoice,
name: 'group',
order: 1,
origin: {
indexed: '2018-11-11T14:30:00Z',
name: 'Dummy',
type: SCThingOriginType.Remote,
},
type: SCThingType.Setting,
uid: '2c97aa36-4aa2-43de-bc5d-a2b2cb3a530e',
values: ['student', 'employee', true, 42],
};
const translator = new SCThingTranslator('de'); const translator = new SCThingTranslator('de');
const translatorEN = new SCThingTranslator('en'); const translatorEN = new SCThingTranslator('en');
@@ -111,242 +29,207 @@ const translatorWithFallback = new SCThingTranslator('tt');
const translatedThingDE = translator.translate(dish); const translatedThingDE = translator.translate(dish);
const translatedThingFallback = translatorWithFallback.translate(dish); const translatedThingFallback = translatorWithFallback.translate(dish);
@suite(timeout(10000), slow(5000)) describe('Translator', function () {
export class TranslationSpecInplace { this.timeout(10_000);
@test this.slow(5000);
public directEnumSingleValue() {
expect(translator.translatedAccess(setting).inputType()).to.equal('einfache Auswahl');
}
@test describe('direct', function () {
public directStringLiteralType() { it('should translate enum single value', function () {
expect(translator.translatedAccess(dish).type()).to.equal('Essen'); expect(translator.translatedAccess(setting).inputType).to.equal('einfache Auswahl');
expect(translatedThingDE.type).to.equal('Essen');
}
@test
public directStringProperty() {
expect(translator.translatedAccess(dish).name()).to.equal('de-dish-name');
expect(translatedThingDE.name).to.equal('de-dish-name');
}
@test
public directArrayOfString() {
expect(translator.translatedAccess(dish).characteristics()).to.deep.equal([
{name: 'de-characteristic0'},
{name: 'de-characteristic1'},
]);
expect(translatedThingDE.characteristics).to.deep.equal([
{name: 'de-characteristic0'},
{name: 'de-characteristic1'},
]);
}
@test
public directArrayOfStringSubscript() {
expect(translator.translatedAccess(dish).characteristics[1]()).to.deep.equal({
name: 'de-characteristic1',
}); });
expect(translatedThingDE.characteristics![1]).to.deep.equal({name: 'de-characteristic1'});
}
@test it('should translate string literal type', function () {
public directMetaArrayOfString() { expect(translator.translatedAccess(dish).type).to.equal('Essen');
expect(translator.translatedAccess(dish).categories()).to.deep.equal(['Hauptgericht', 'Nachtisch']); expect(translatedThingDE.type).to.equal('Essen');
expect(translatedThingDE.categories).to.deep.equal(['Hauptgericht', 'Nachtisch']);
}
@test
public directMetaArrayOfStringSubscript() {
expect(translator.translatedAccess(dish).categories[1]()).to.equal('Nachtisch');
expect(translatedThingDE.categories[1]).to.equal('Nachtisch');
}
@test
public nestedStringLiteralType() {
expect(translator.translatedAccess(dish).offers[0].inPlace.type()).to.equal('Gebäude');
expect(translatedThingDE.offers![0].inPlace!.type).to.equal('Gebäude');
}
@test
public nestedStringProperty() {
expect(translator.translatedAccess(dish).offers[0].inPlace.name()).to.equal('de-space-name');
expect(translatedThingDE.offers![0].inPlace!.name).to.equal('de-space-name');
}
@test
public nestedMetaArrayOfString() {
expect(translator.translatedAccess(dish).offers[0].inPlace.categories()).to.deep.equal([
'Büro',
'Bildung',
]);
expect(translatedThingDE.offers![0].inPlace!.categories).to.deep.equal(['Büro', 'Bildung']);
}
@test
public nestedMetaArrayOfStringSubscript() {
expect(translator.translatedAccess(dish).offers[0].inPlace.categories[1]()).to.equal('Bildung');
expect(translatedThingDE.offers![0].inPlace!.categories[1]).to.equal('Bildung');
}
@test
public directStringLiteralTypeFallback() {
expect(translatorWithFallback.translatedAccess(dish).type()).to.equal('dish');
expect(translatedThingFallback.type).to.equal('dish');
}
@test
public directStringPropertyFallback() {
expect(translatorWithFallback.translatedAccess(dish).name()).to.equal('base-dish-name');
expect(translatedThingFallback.name).to.equal('base-dish-name');
}
@test
public directArrayOfStringSubscriptFallback() {
expect(translatorWithFallback.translatedAccess(dish).characteristics[1]()).to.deep.equal({
name: 'base-characteristic1',
}); });
expect(translatedThingFallback.characteristics![1]).to.deep.equal({name: 'base-characteristic1'});
}
@test it('should translate string property', function () {
public directMetaArrayOfStringFallback() { expect(translator.translatedAccess(dish).name).to.equal('de-dish-name');
expect(translatorWithFallback.translatedAccess(dish).categories()).to.deep.equal([ expect(translatedThingDE.name).to.equal('de-dish-name');
'main dish', });
'dessert',
]);
expect(translatedThingFallback.categories).to.deep.equal(['main dish', 'dessert']);
}
@test it('should translate array of strings', function () {
public directMetaArrayOfStringSubscriptFallback() { expect(translator.translatedAccess(dish).characteristics).to.deep.equal([
expect(translatorWithFallback.translatedAccess(dish).categories[1]()).to.equal('dessert'); {name: 'de-characteristic0'},
expect(translatedThingFallback.categories[1]).to.equal('dessert'); {name: 'de-characteristic1'},
} ]);
expect(translatedThingDE.characteristics).to.deep.equal([
{name: 'de-characteristic0'},
{name: 'de-characteristic1'},
]);
});
@test it('should translate array of strings subscript', function () {
public nestedStringLiteralTypeFallback() { expect(translator.translatedAccess(dish).characteristics?.[1]).to.deep.equal({
expect(translatorWithFallback.translatedAccess(dish).offers[0].inPlace.type()).to.equal('building'); name: 'de-characteristic1',
expect(translatedThingFallback.offers![0].inPlace!.type).to.equal('building'); });
} expect(translatedThingDE.characteristics![1]).to.deep.equal({name: 'de-characteristic1'});
});
@test it('should translate meta array of string', function () {
public nestedStringPropertyFallback() { expect(translator.translatedAccess(dish).categories).to.deep.equal(['Hauptgericht', 'Nachtisch']);
expect(translatorWithFallback.translatedAccess(dish).offers[0].inPlace.name()).to.equal( expect(translatedThingDE.categories).to.deep.equal(['Hauptgericht', 'Nachtisch']);
'base-space-name', });
);
expect(translatedThingFallback.offers![0].inPlace!.name).to.equal('base-space-name');
}
@test it('should translate meta array of strings subscript', function () {
public nestedMetaArrayOfStringFallback() { expect(translator.translatedAccess(dish).categories[1]).to.equal('Nachtisch');
expect(translatorWithFallback.translatedAccess(dish).offers[0].inPlace.categories()).to.deep.equal([ expect(translatedThingDE.categories[1]).to.equal('Nachtisch');
'office', });
'education', });
]);
expect(translatedThingFallback.offers![0].inPlace!.categories).to.deep.equal(['office', 'education']);
}
@test describe('nested', function () {
public nestedMetaArrayOfStringSubscriptFallback() { it('should translate string literal type', function () {
expect(translatorWithFallback.translatedAccess(dish).offers[0].inPlace.categories[1]()).to.equal( expect(translator.translatedAccess(dish).offers?.[0].inPlace?.type).to.equal('Gebäude');
'education', expect(translatedThingDE.offers![0].inPlace!.type).to.equal('Gebäude');
); });
expect(translatedThingFallback.offers![0].inPlace!.categories[1]).to.equal('education');
}
@test it('should translate nested string property', function () {
public directStringLiteralTypeUndefined() { expect(translator.translatedAccess(dish).offers?.[0].inPlace?.name).to.equal('de-space-name');
const undefinedThing = eval('(x) => undefined;'); expect(translatedThingDE.offers![0].inPlace!.name).to.equal('de-space-name');
expect(translator.translatedAccess(undefinedThing())('defaultValue')).to.equal('defaultValue'); });
expect(translator.translatedAccess(dish).name('defaultValue')).to.not.equal('defaultValue');
}
@test it('should translate meta array of strings', function () {
public nestedMetaArrayOfStringSubscriptUndefined() { expect(translator.translatedAccess(dish).offers?.[0].inPlace?.categories).to.deep.equal([
const workingTranslation = eval( 'Büro',
"translator.translatedAccess(dish).offers[0].inPlace.categories[1]('printer');", 'Bildung',
); ]);
const defaultValueTranslation = eval( expect(translatedThingDE.offers![0].inPlace!.categories).to.deep.equal(['Büro', 'Bildung']);
"translator.translatedAccess(dish).offers[0].inPlace.categories[1234]('printer');", });
);
expect(defaultValueTranslation).to.equal('printer'); it('should translate meta array of strings subscript', function () {
expect(workingTranslation).to.not.equal('printer'); expect(translator.translatedAccess(dish).offers?.[0].inPlace?.categories[1]).to.equal('Bildung');
} expect(translatedThingDE.offers![0].inPlace!.categories[1]).to.equal('Bildung');
});
});
@test describe('direct (fallback)', function () {
public reaccessWithChangedSourceOmitsLRUCache() { it('should translate string literal types', function () {
expect(translatorWithFallback.translatedAccess(dish).type).to.equal('dish');
expect(translatedThingFallback.type).to.equal('dish');
});
it('should translate string property', function () {
expect(translatorWithFallback.translatedAccess(dish).name).to.equal('base-dish-name');
expect(translatedThingFallback.name).to.equal('base-dish-name');
});
it('should translate array of strings subscript', function () {
expect(translatorWithFallback.translatedAccess(dish).characteristics?.[1]).to.deep.equal({
name: 'base-characteristic1',
});
expect(translatedThingFallback.characteristics![1]).to.deep.equal({name: 'base-characteristic1'});
});
it('should translate meta array of strings', function () {
expect(translatorWithFallback.translatedAccess(dish).categories).to.deep.equal([
'main dish',
'dessert',
]);
expect(translatedThingFallback.categories).to.deep.equal(['main dish', 'dessert']);
});
it('should translate meta array of string subscript', function () {
expect(translatorWithFallback.translatedAccess(dish).categories[1]).to.equal('dessert');
expect(translatedThingFallback.categories[1]).to.equal('dessert');
});
});
describe('nested (fallback)', function () {
it('should translate string literal type', function () {
expect(translatorWithFallback.translatedAccess(dish).offers?.[0].inPlace?.type).to.equal('building');
expect(translatedThingFallback.offers![0].inPlace!.type).to.equal('building');
});
it('should translate string property', function () {
expect(translatorWithFallback.translatedAccess(dish).offers?.[0].inPlace?.name).to.equal(
'base-space-name',
);
expect(translatedThingFallback.offers![0].inPlace!.name).to.equal('base-space-name');
});
it('should translate meta array of string', function () {
expect(translatorWithFallback.translatedAccess(dish).offers?.[0].inPlace?.categories).to.deep.equal([
'office',
'education',
]);
expect(translatedThingFallback.offers![0].inPlace!.categories).to.deep.equal(['office', 'education']);
});
it('should translate meta array of strings subscript', function () {
expect(translatorWithFallback.translatedAccess(dish).offers?.[0].inPlace?.categories[1]).to.equal(
'education',
);
expect(translatedThingFallback.offers![0].inPlace!.categories[1]).to.equal('education');
});
});
it('should omit LRU cache with changed source', function () {
const translatorDE = new SCThingTranslator('de'); const translatorDE = new SCThingTranslator('de');
const dishCopy = clone()(dish); const dishCopy = clone()(dish);
const translatedDish = translatorDE.translatedAccess(dish); const translatedDish = translatorDE.translatedAccess(dish);
const distructivelyTranslatedDish = translatorDE.translate(dish); const destructivelyTranslatedDish = translatorDE.translate(dish);
(dishCopy.origin as SCThingRemoteOrigin).name = 'tranlator.spec'; (dishCopy.origin as SCThingRemoteOrigin).name = 'translator.spec';
expect(translatorDE.translatedAccess(dishCopy)).not.to.deep.equal(translatedDish); expect(translatorDE.translatedAccess(dishCopy)).not.to.deep.equal(translatedDish);
expect(translatorDE.translate(dishCopy)).not.to.equal(distructivelyTranslatedDish); expect(translatorDE.translate(dishCopy)).not.to.equal(destructivelyTranslatedDish);
} });
@test it('should flush its LRU cache with changed translator language', function () {
public changingTranslatorLanguageFlushesItsLRUCache() {
const translatorDE = new SCThingTranslator('de'); const translatorDE = new SCThingTranslator('de');
expect(translatorDE.translatedAccess(dish).name()).to.equal('de-dish-name'); expect(translatorDE.translatedAccess(dish).name).to.equal('de-dish-name');
expect(translatorDE.translate(dish).name).to.equal('de-dish-name'); expect(translatorDE.translate(dish).name).to.equal('de-dish-name');
translatorDE.language = 'en'; translatorDE.language = 'en';
expect(translatorDE.translatedAccess(dish).name()).to.equal('base-dish-name'); expect(translatorDE.translatedAccess(dish).name).to.equal('base-dish-name');
expect(translatorDE.translate(dish).name).to.equal('base-dish-name'); expect(translatorDE.translate(dish).name).to.equal('base-dish-name');
} });
@test it('should force translator LRU cache to overflow', function () {
public forceTranslatorLRUCacheToOverflow() {
const translatorDE = new SCThingTranslator('de'); const translatorDE = new SCThingTranslator('de');
// Make sure to add more elements to the translator cache than the maximum cache capacity. See Translator.ts // Make sure to add more elements to the translator cache than the maximum cache capacity. See Translator.ts
for (let i = 0; i < 201; i++) { for (let i = 0; i < 201; i++) {
const anotherDish = Object.assign({}, dish); const anotherDish = Object.assign({}, dish);
anotherDish.uid = String(i); anotherDish.uid = String(i);
expect(translatorDE.translatedAccess(anotherDish).name()).to.equal('de-dish-name'); expect(translatorDE.translatedAccess(anotherDish).name).to.equal('de-dish-name');
} }
} });
} });
@suite(timeout(10000), slow(5000)) describe('MetaTranslator', function () {
export class MetaTranslationSpec { this.timeout(10_000);
@test this.slow(5000);
public consistencyWithMetaClass() {
it('should have consistency with meta class', function () {
const dishMetaTranslationsDE = translator.translatedPropertyNames(dish.type); const dishMetaTranslationsDE = translator.translatedPropertyNames(dish.type);
const dishMetaTranslationsEN = translatorEN.translatedPropertyNames(dish.type); const dishMetaTranslationsEN = translatorEN.translatedPropertyNames(dish.type);
expect(dishMetaTranslationsEN).to.not.deep.equal(dishMetaTranslationsDE); expect(dishMetaTranslationsEN).to.not.deep.equal(dishMetaTranslationsDE);
expect(dishMetaTranslationsDE).to.deep.equal(new SCDishMeta().fieldTranslations.de); expect(dishMetaTranslationsDE).to.deep.equal(new SCDishMeta().fieldTranslations.de);
expect(dishMetaTranslationsEN).to.deep.equal(new SCDishMeta().fieldTranslations.en); expect(dishMetaTranslationsEN).to.deep.equal(new SCDishMeta().fieldTranslations.en);
} });
@test it('should retrieve translated property value type', function () {
public retrieveTranslatedPropertyValueType() {
const dishTypeDE = translator.translatedPropertyValue(dish.type, 'type'); const dishTypeDE = translator.translatedPropertyValue(dish.type, 'type');
const dishTypeEN = translatorEN.translatedPropertyValue(dish.type, 'type', undefined); const dishTypeEN = translatorEN.translatedPropertyValue(dish.type, 'type', undefined);
const dishTypeBASE = translatorWithFallback.translatedPropertyValue(dish.type, 'type'); const dishTypeBASE = translatorWithFallback.translatedPropertyValue(dish.type, 'type');
expect(dishTypeDE).to.deep.equal(new SCDishMeta().fieldValueTranslations.de.type); expect(dishTypeDE).to.deep.equal(new SCDishMeta().fieldValueTranslations.de.type);
expect(dishTypeEN).to.deep.equal(new SCDishMeta().fieldValueTranslations.en.type); expect(dishTypeEN).to.deep.equal(new SCDishMeta().fieldValueTranslations.en.type);
expect(dishTypeBASE).to.deep.equal(new SCDishMeta().fieldValueTranslations.en.type); expect(dishTypeBASE).to.deep.equal(new SCDishMeta().fieldValueTranslations.en.type);
} });
@test it('should retrieve translated property value nested', function () {
public retrieveTranslatedPropertyValueNested() {
const dishTypeDE = translator.translatedPropertyValue(dish.type, 'categories', 'main dish'); const dishTypeDE = translator.translatedPropertyValue(dish.type, 'categories', 'main dish');
const dishTypeEN = translatorEN.translatedPropertyValue(dish.type, 'categories', 'main dish'); const dishTypeEN = translatorEN.translatedPropertyValue(dish.type, 'categories', 'main dish');
const dishTypeBASE = translatorWithFallback.translatedPropertyValue(dish.type, 'categories', 'main dish'); const dishTypeBASE = translatorWithFallback.translatedPropertyValue(dish.type, 'categories', 'main dish');
expect(dishTypeDE).to.deep.equal(new SCDishMeta().fieldValueTranslations.de.categories['main dish']); expect(dishTypeDE).to.deep.equal(new SCDishMeta().fieldValueTranslations.de.categories['main dish']);
expect(dishTypeEN).to.deep.equal(dish.categories[0]); expect(dishTypeEN).to.deep.equal(dish.categories[0]);
expect(dishTypeBASE).to.deep.equal(dish.categories[0]); expect(dishTypeBASE).to.deep.equal(dish.categories[0]);
} });
@test it('should translate thing without meta class', function () {
public thingWithoutMetaClass() {
const dishCopy = clone()(dish); const dishCopy = clone()(dish);
const typeNonExistant = eval("(x) => x + 'typeNonExistant';"); const typeNonExistent = eval("(x) => x + 'typeNonExistent';");
// this will assign a non existant SCThingType to dishCopy // this will assign a non-existent SCThingType to dishCopy
dishCopy.type = typeNonExistant(); dishCopy.type = typeNonExistent();
const dishMetaTranslationsDE = translator.translatedPropertyNames(dishCopy.type); const dishMetaTranslationsDE = translator.translatedPropertyNames(dishCopy.type);
expect(dishMetaTranslationsDE).to.be.undefined; expect(dishMetaTranslationsDE).to.be.undefined;
} });
} });

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* /*
* Copyright (C) 2019 StApps * Copyright (C) 2019 StApps
* This program is free software: you can redistribute it and/or modify it * This program is free software: you can redistribute it and/or modify it
@@ -13,35 +14,35 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {assert, Has, IsAny, IsNever, NotHas} from 'conditional-type-checks'; import {assert, Has, IsAny, IsNever, NotHas} from 'conditional-type-checks';
import {SCThing, SCThingWithoutReferences} from '../src/things/abstract/thing.js'; import {SCThing, SCThingWithoutReferences} from '../src/index.js';
import {SCAcademicEvent, SCAcademicEventWithoutReferences} from '../src/things/academic-event.js'; import {SCAcademicEvent, SCAcademicEventWithoutReferences} from '../src/index.js';
import {SCArticle, SCArticleWithoutReferences} from '../src/things/article.js'; import {SCArticle, SCArticleWithoutReferences} from '../src/index.js';
import {SCAssessment, SCAssessmentWithoutReferences} from '../src/things/assessment.js'; import {SCAssessment, SCAssessmentWithoutReferences} from '../src/index.js';
import {SCBook, SCBookWithoutReferences} from '../src/things/book.js'; import {SCBook, SCBookWithoutReferences} from '../src/index.js';
import {SCBuilding, SCBuildingWithoutReferences} from '../src/things/building.js'; import {SCBuilding, SCBuildingWithoutReferences} from '../src/index.js';
import {SCCatalog, SCCatalogWithoutReferences} from '../src/things/catalog.js'; import {SCCatalog, SCCatalogWithoutReferences} from '../src/index.js';
import {SCContactPoint, SCContactPointWithoutReferences} from '../src/things/contact-point.js'; import {SCContactPoint, SCContactPointWithoutReferences} from '../src/index.js';
import {SCCourseOfStudy, SCCourseOfStudyWithoutReferences} from '../src/things/course-of-study.js'; import {SCCourseOfStudy, SCCourseOfStudyWithoutReferences} from '../src/index.js';
import {SCDateSeries, SCDateSeriesWithoutReferences} from '../src/things/date-series.js'; import {SCDateSeries, SCDateSeriesWithoutReferences} from '../src/index.js';
import {SCDiff, SCDiffWithoutReferences} from '../src/things/diff.js'; import {SCDiff, SCDiffWithoutReferences} from '../src/index.js';
import {SCDish, SCDishWithoutReferences} from '../src/things/dish.js'; import {SCDish, SCDishWithoutReferences} from '../src/index.js';
import {SCFavorite, SCFavoriteWithoutReferences} from '../src/things/favorite.js'; import {SCFavorite, SCFavoriteWithoutReferences} from '../src/index.js';
import {SCFloor, SCFloorWithoutReferences} from '../src/things/floor.js'; import {SCFloor, SCFloorWithoutReferences} from '../src/index.js';
import {SCMessage, SCMessageWithoutReferences} from '../src/things/message.js'; import {SCMessage, SCMessageWithoutReferences} from '../src/index.js';
import {SCOrganization, SCOrganizationWithoutReferences} from '../src/things/organization.js'; import {SCOrganization, SCOrganizationWithoutReferences} from '../src/index.js';
import {SCPerson, SCPersonWithoutReferences} from '../src/things/person.js'; import {SCPerson, SCPersonWithoutReferences} from '../src/index.js';
import {SCPointOfInterest, SCPointOfInterestWithoutReferences} from '../src/things/point-of-interest.js'; import {SCPointOfInterest, SCPointOfInterestWithoutReferences} from '../src/index.js';
import {SCRoom, SCRoomWithoutReferences} from '../src/things/room.js'; import {SCRoom, SCRoomWithoutReferences} from '../src/index.js';
import {SCSemester, SCSemesterWithoutReferences} from '../src/things/semester.js'; import {SCSemester, SCSemesterWithoutReferences} from '../src/index.js';
import {SCSetting, SCSettingWithoutReferences} from '../src/things/setting.js'; import {SCSetting, SCSettingWithoutReferences} from '../src/index.js';
import {SCSportCourse, SCSportCourseWithoutReferences} from '../src/things/sport-course.js'; import {SCSportCourse, SCSportCourseWithoutReferences} from '../src/index.js';
import {SCStudyModule, SCStudyModuleWithoutReferences} from '../src/things/study-module.js'; import {SCStudyModule, SCStudyModuleWithoutReferences} from '../src/index.js';
import {SCTicket, SCTicketWithoutReferences} from '../src/things/ticket.js'; import {SCTicket, SCTicketWithoutReferences} from '../src/index.js';
import {SCToDo, SCToDoWithoutReferences} from '../src/things/todo.js'; import {SCToDo, SCToDoWithoutReferences} from '../src/index.js';
import {SCTour, SCTourWithoutReferences} from '../src/things/tour.js'; import {SCTour, SCTourWithoutReferences} from '../src/index.js';
import {SCVideo, SCVideoWithoutReferences} from '../src/things/video.js'; import {SCVideo, SCVideoWithoutReferences} from '../src/index.js';
import {SCPeriodical, SCPeriodicalWithoutReferences} from '../src/things/periodical.js'; import {SCPeriodical, SCPeriodicalWithoutReferences} from '../src/index.js';
import {SCPublicationEvent, SCPublicationEventWithoutReferences} from '../src/things/publication-event.js'; import {SCPublicationEvent, SCPublicationEventWithoutReferences} from '../src/index.js';
/** /**
* Check if E extends T * Check if E extends T

View File

@@ -10,34 +10,33 @@
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"scripts": { "scripts": {
"build": "tsup --dts", "build": "tsup --dts",
"format": "prettier .", "format": "prettier . --ignore-path ../../.gitignore",
"format:fix": "prettier --write .", "format:fix": "prettier --write . --ignore-path ../../.gitignore",
"lint": "eslint --ext .ts src/", "lint": "eslint --ext .ts src/",
"lint:fix": "eslint --fix --ext .ts src/", "lint:fix": "eslint --fix --ext .ts src/",
"test": "nyc mocha 'test/**/*.spec.ts'" "test": "c8 mocha"
}, },
"dependencies": { "dependencies": {
"@openstapps/collection-utils": "workspace:*", "@openstapps/collection-utils": "workspace:*",
"@openstapps/logger": "workspace:*", "@openstapps/logger": "workspace:*",
"glob": "10.2.1",
"typescript": "4.8.4" "typescript": "4.8.4"
}, },
"devDependencies": { "devDependencies": {
"@openstapps/eslint-config": "workspace:*", "@openstapps/eslint-config": "workspace:*",
"@openstapps/nyc-config": "workspace:*",
"@openstapps/prettier-config": "workspace:*", "@openstapps/prettier-config": "workspace:*",
"@openstapps/tsconfig": "workspace:*", "@openstapps/tsconfig": "workspace:*",
"@testdeck/mocha": "0.3.3",
"@types/chai": "4.3.4", "@types/chai": "4.3.4",
"@types/fs-extra": "9.0.13",
"@types/mocha": "10.0.1", "@types/mocha": "10.0.1",
"@types/node": "18.15.3", "@types/node": "18.15.3",
"c8": "7.13.0",
"chai": "4.3.7",
"mocha": "10.2.0", "mocha": "10.2.0",
"nock": "13.3.0", "ts-node": "10.9.1",
"tsup": "6.7.0" "tsup": "6.7.0"
}, },
"tsup": { "tsup": {
"entry": [ "entry": [
"src/app.ts",
"src/index.ts" "src/index.ts"
], ],
"sourcemap": true, "sourcemap": true,
@@ -54,8 +53,5 @@
"eslintIgnore": [ "eslintIgnore": [
"resources", "resources",
"openapi" "openapi"
], ]
"nyc": {
"extends": "@openstapps/nyc-config"
}
} }

View File

@@ -12,7 +12,7 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as ts from 'typescript'; import ts from 'typescript';
import {cleanupEmpty} from './util.js'; import {cleanupEmpty} from './util.js';
import {LightweightComment} from './types/lightweight-comment.js'; import {LightweightComment} from './types/lightweight-comment.js';
@@ -33,7 +33,7 @@ export function extractComment(node: ts.Node): LightweightComment | undefined {
? undefined ? undefined
: cleanupEmpty({ : cleanupEmpty({
shortSummary: comment?.[0], shortSummary: comment?.[0],
description: comment?.[comment.length - 1], description: comment?.slice(1).join('\n\n'),
tags: jsDocument?.tags?.map(tag => tags: jsDocument?.tags?.map(tag =>
cleanupEmpty({ cleanupEmpty({
name: tag.tagName?.escapedText ?? 'UNRESOLVED_NAME', name: tag.tagName?.escapedText ?? 'UNRESOLVED_NAME',

View File

@@ -14,7 +14,7 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import ts from 'typescript'; import ts from 'typescript';
import {cleanupEmpty, mapNotNil, rejectNil, expandPathToFilesSync} from './util.js'; import {cleanupEmpty, expandPathToFilesSync, mapNotNil, rejectNil} from './util.js';
import { import {
extractComment, extractComment,
filterChildrenTo, filterChildrenTo,
@@ -77,7 +77,7 @@ class LightweightDefinitionBuilder {
constructor(sourcePath: string | string[], readonly includeComments: boolean) { constructor(sourcePath: string | string[], readonly includeComments: boolean) {
const rootNames = Array.isArray(sourcePath) const rootNames = Array.isArray(sourcePath)
? sourcePath ? sourcePath
: expandPathToFilesSync(path.resolve(sourcePath), file => file.endsWith('ts')); : expandPathToFilesSync(path.resolve(sourcePath), it => it.endsWith('.ts'));
this.program = ts.createProgram({ this.program = ts.createProgram({
rootNames: rootNames, rootNames: rootNames,
@@ -121,7 +121,7 @@ class LightweightDefinitionBuilder {
classLike: ts.ClassDeclaration | ts.InterfaceDeclaration, classLike: ts.ClassDeclaration | ts.InterfaceDeclaration,
): LightweightClassDefinition { ): LightweightClassDefinition {
const heritages = mapValues( const heritages = mapValues(
groupBy([...classLike.heritageClauses!], it => it.token.toString()), groupBy([...(classLike.heritageClauses || [])], it => it.token.toString()),
heritages => heritages.flatMap(it => it.types), heritages => heritages.flatMap(it => it.types),
); );
@@ -162,23 +162,25 @@ class LightweightDefinitionBuilder {
collectProperties( collectProperties(
members: ts.NodeArray<ts.ClassElement | ts.TypeElement>, members: ts.NodeArray<ts.ClassElement | ts.TypeElement>,
): Record<string, LightweightProperty> { ): Record<string, LightweightProperty> | undefined {
return keyBy( return members
filterNodeTo(members as ts.NodeArray<ts.ClassElement | ts.TypeElement>, isProperty).map(property => ? keyBy(
cleanupEmpty({ filterNodeTo(members as ts.NodeArray<ts.ClassElement | ts.TypeElement>, isProperty).map(property =>
comment: this.includeComments ? extractComment(property) : undefined, cleanupEmpty({
name: resolvePropertyName(property.name) ?? property.getText(), comment: this.includeComments ? extractComment(property) : undefined,
type: this.lightweightTypeAtNode(property), name: resolvePropertyName(property.name) ?? property.getText(),
properties: this.collectProperties((property.type as ts.TypeLiteralNode)?.members), type: this.lightweightTypeAtNode(property),
optional: ts.isPropertyDeclaration(property) properties: this.collectProperties((property.type as ts.TypeLiteralNode)?.members),
? property.questionToken === undefined optional: ts.isPropertyDeclaration(property)
? undefined ? property.questionToken === undefined
: true ? undefined
: undefined, : true
}), : undefined,
), }),
it => it.name, ),
); it => it.name,
)
: undefined;
} }
private lightweightTypeAtNode(node: ts.Node): LightweightType { private lightweightTypeAtNode(node: ts.Node): LightweightType {

View File

@@ -12,14 +12,14 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
export * from './easy-ast.js' export * from './easy-ast.js';
export * from './ast-util.js' export * from './ast-util.js';
export * from './types/lightweight-alias-definition.js' export * from './types/lightweight-alias-definition.js';
export * from './types/lightweight-class-definition.js' export * from './types/lightweight-class-definition.js';
export * from './types/lightweight-comment.js' export * from './types/lightweight-comment.js';
export * from './types/lightweight-definition.js' export * from './types/lightweight-definition.js';
export * from './types/lightweight-definition-kind.js' export * from './types/lightweight-definition-kind.js';
export * from './types/lightweight-project.js' export * from './types/lightweight-project.js';
export * from './types/lightweight-property.js' export * from './types/lightweight-property.js';
export * from './types/lightweight-type.js' export * from './types/lightweight-type.js';

View File

@@ -12,15 +12,36 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import path from "path"; import {readdirSync, statSync} from 'fs';
import {readdirSync, statSync} from "fs"; import path from 'path';
/**
* Expand a path to a list of all files deeply contained in it
*/
export function expandPathToFilesSync(sourcePath: string, accept: (fileName: string) => boolean): string[] {
const fullPath = path.resolve(sourcePath);
const directory = statSync(fullPath);
return directory.isDirectory()
? readdirSync(fullPath).flatMap(fragment =>
expandPathToFilesSync(path.resolve(sourcePath, fragment), accept),
)
: [fullPath].filter(accept);
}
/**
* Take a Windows path and make a Unix path out of it
*/
export function toUnixPath(pathString: string): string {
return pathString.replaceAll(path.sep, path.posix.sep);
}
/** /**
* Filters only defined elements * Filters only defined elements
*/ */
export function rejectNil<T>(array: Array<T | undefined | null>): T[] { export function rejectNil<T>(array: Array<T | undefined | null>): T[] {
// eslint-disable-next-line unicorn/no-null // eslint-disable-next-line unicorn/no-null
return array.filter(it => it == null) as T[]; return array.filter(it => it != null) as T[];
} }
/** /**
@@ -45,17 +66,3 @@ export function cleanupEmpty<T extends object>(object: T): T {
} }
return out; return out;
} }
/**
* Expand a path to a list of all files deeply contained in it
*/
export function expandPathToFilesSync(sourcePath: string, accept: (fileName: string) => boolean): string[] {
const fullPath = path.resolve(sourcePath);
const directory = statSync(fullPath);
return directory.isDirectory()
? readdirSync(fullPath).flatMap(fragment =>
expandPathToFilesSync(path.resolve(sourcePath, fragment), accept),
)
: [fullPath].filter(accept);
}

View File

@@ -12,7 +12,7 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {LightweightFile} from '../../src/easy-ast/types/lightweight-project.js'; import {LightweightFile} from '../src/index.js';
export interface EasyAstSpecType { export interface EasyAstSpecType {
testName: string; testName: string;

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) 2021 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {lightweightProjectFromPath} from '../src/index.js';
import {expect} from 'chai';
import {expandPathToFilesSync, toUnixPath} from '../src/util.js';
import type {EasyAstSpecType} from './easy-ast-spec-type.js';
const projectPath = './test/project';
const tests = await Promise.all(
expandPathToFilesSync(projectPath, file => file.endsWith('ast-test.ts')).map(async it => ({
path: toUnixPath(it),
config: await import(`file://${it}`).then(it => it.testConfig as EasyAstSpecType),
})),
);
describe('Easy AST', async function () {
it('should build the project', function () {
const project = lightweightProjectFromPath(projectPath, true);
expect(Object.keys(project).length).to.equal(tests.length);
});
const project = lightweightProjectFromPath(projectPath, true);
for (const {path, config} of tests) {
it(config.testName, function () {
const projectAtPath = project[path];
expect(projectAtPath).not.to.be.undefined;
for (const key in projectAtPath) {
if (key.startsWith('$')) delete projectAtPath[key];
}
expect(projectAtPath).to.be.deep.equal(config.expected);
});
}
});

Some files were not shown because too many files have changed in this diff Show More