@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
153 lines (132 loc) • 5.33 kB
JavaScript
const cds = require('../../../cds')
const { fs: { promises: { rename } }, path: { dirname, join }, exists } = cds.utils
const { readProject } = require('../../projectReader')
const { merge } = require('../../merge')
const { renderAndCopy } = require('../../template')
const genDataAsJson = require('../data/as-json')
const { mockUsersForService } = require('../http')
const { dim } = require('../../../util/term')
const { filterStringAsRegex } = require('../../add')
module.exports = class Test extends require('../../plugin') {
static help() {
return 'tests for services'
}
options() {
return {
'filter': {
type: 'string',
short: 'f',
help: `Filter for services or entities or actions matching the given pattern. If it contains meta
characters like '^' or '*', it is treated as a regular expression,
otherwise as an include pattern, i.e /.*pattern.*/i`
},
'out': {
type: 'string',
short: 'o',
help: `Custom output directory. For Node.js, the default is 'test'.`
}
}
}
async canRun() {
const { isJava } = readProject()
if (isJava) {
throw `Test generation is not supported in Java projects yet.`
}
return true
}
async run() {
const proj = readProject()
await createNodeTest(proj)
}
}
/**
* @param {ReturnType<readProject} proj
*/
async function createNodeTest(proj) {
const { filter, force, out='test' } = cds.cli.options
const csn = await loadModel()
if (!csn) return console.log(dim(`> skipping, no model found`))
const nameFilter = filterStringAsRegex(filter)
const serviceInfo = cds.compile.to.serviceinfo(csn)
for (const service of csn.services) {
if (!service.$location.file) continue
const sInfo = serviceInfo.find(s => s.name === service.name)
proj.serviceName = service.name
proj.serviceClass = service.name.split('.').pop()
proj.servicePath = proj.serviceName ? (proj.serviceName.replace(/\./g, '/')) : ''
const entities = Object.entries(service.entities)
.filter(([name]) => name.match(nameFilter))
.filter(([name]) => !name.match(/[._]texts$/))
.filter(([, e]) => !e['@cds.autoexposed']) // only consider non-autoexposed entities to reduce clutter
.map(([name, e]) => {e._nameUnqualified = name; return e})
let entity // use one entity only (don't want to make users test all entities)
if (entities.length) entity = entities[0]
const sampleData = {}
if (entity) {
// see if we have csv data for the entity
const refData = await cds.deploy.resources(csn)
const data = genDataAsJson.randomFromReferenceData(entity, csn, refData)
sampleData[entity.name] = data ?? []
}
// mark action data to be generated
for (const action of Object.keys(service.actions).filter(name => name.match(nameFilter))) {
sampleData[`${service.name}.${action}`] = []
}
await genDataAsJson(sampleData, csn, 1, {referenceData: {}})
if (entity) {
const elements = Object.values(entity.elements ?? {}).sort(genDataAsJson.elementsSorter(csn))
const keys = elements.filter(el => el.key).map(el => el.name)
const selectElements = [
keys[0],
// select a non-key, non-association element from the entity itself
elements.filter(el => !keys.includes(el.name)).find(el => !(el instanceof cds.Association))?.name
].filter(Boolean)
proj.entities = [{
name: entity.name,
urlPath: `/${sInfo.endpoints[0].path}${entity._nameUnqualified}`,
selectString: selectElements.join(','),
elementsString: JSON.stringify(
Object.entries(sampleData[entity.name][0])
.filter(([name]) => selectElements.includes(name))
.reduce((acc, [name, value]) => { acc[name] = value; return acc }, {})
)
}]
}
proj.actions = Object.entries(service.actions)
.filter(([name]) => name.match(nameFilter))
.map(([name]) => {
return {
name,
paramsString: JSON.stringify(sampleData[`${service.name}.${name}`][0]),
urlPath: `/${sInfo.endpoints[0].path}${name}`,
}})
const users = await mockUsersForService(service.name, false, csn)
if (Object.keys(users).length) {
proj.user = Object.keys(users)[0]
proj.password = users[proj.user].password
}
const destFileType = proj.isTypescript ? '.test.ts' : '.test.js'
const outPath = join(out, service.name + destFileType)
const destFile = join(cds.root, outPath)
if (!force && exists(destFile)) {
console.log(dim(`> skipping ${outPath}`))
continue
}
console.log(dim(`> writing ${outPath}`))
const destPath = dirname(destFile)
await renderAndCopy(join(__dirname, 'files/test'), destPath, proj)
await rename(join(destPath, 'test.xs'), destFile)
await merge(join(__dirname, 'files/package.json')).into('package.json')
}
}
/**
* @returns { Promise<import('@cap-js/cds-types').linked.LinkedCSN | null> }
*/
async function loadModel() {
try {
return cds.linked(cds.minify(await cds.load(cds.env.roots)))
} catch (err) {
if (err.code === 'MODEL_NOT_FOUND') return null
throw new Error(`Error compiling CDS files. Run 'npm install' and try again.`, {cause:err})
}
}