From 4c467bed196e6c199808913d11bb0af4266b066b Mon Sep 17 00:00:00 2001 From: AlisherAmonulloev Date: Mon, 26 Oct 2026 17:19:26 +0200 Subject: [PATCH 1/6] License CLI: DX1004 for expired trials, new texts, .NET key lookup --- .../devextreme/license/devextreme-license.js | 60 ++++++++++++------- packages/devextreme/license/dx-get-lcx.js | 46 ++++++++++---- packages/devextreme/license/dx-lcx-2-lcp.js | 45 ++++++++++---- packages/devextreme/license/messages.js | 44 +++++++++----- 4 files changed, 134 insertions(+), 61 deletions(-) diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js index ad7332fed5bc..caa5af532ba2 100644 --- a/packages/devextreme/license/devextreme-license.js +++ b/packages/devextreme/license/devextreme-license.js @@ -173,39 +173,50 @@ function main() { const opts = parseArgs(process.argv); if(opts.help) { printHelp(); - process.exit(0); + process.exitCode = 0; + return; } - const { key: lcx, source, currentVersion } = getDevExpressLCXKey() || {}; + const { key: lcx, source, currentVersion, fullVersion } = getDevExpressLCXKey() || {}; + const converted = lcx ? tryConvertLCXtoLCP(lcx) : null; let lcp = TRIAL_VALUE; let licenseId = null; + let exitCode = 0; + + const withDetails = (message, id) => [ + message, + id ? TEMPLATES.licenseId(id) : null, + source ? TEMPLATES.keyWasFound(source.type, source.path) : null, + ].filter(Boolean).join(' '); if(lcx && lcx.trimStart().startsWith('ewog')) { logStderr( - prefixed(`${TEMPLATES.warningPrefix(1000)} ${TEMPLATES.purchaseLicense}`), + prefixed(`${TEMPLATES.warningPrefix(1000)} ${withDetails(TEMPLATES.purchaseLicense(fullVersion), null)}`), TEMPLATES.oldDevExtremeKey(currentVersion), - TEMPLATES.keyWasFound(source.type, source.path), - prefixed(`${TEMPLATES.warningPrefix(1001)} ${TEMPLATES.installationInstructions}`), + prefixed(`${TEMPLATES.warningPrefix(1001)} ${TEMPLATES.installationInstructions(fullVersion)}`), + ); + } else if(lcx && !converted) { + logStderr( + prefixed(`${TEMPLATES.warningPrefix(1000)} ${withDetails(TEMPLATES.purchaseLicense(fullVersion), null)}`), + prefixed(`${TEMPLATES.warningPrefix(1001)} ${TEMPLATES.installationInstructions(fullVersion)}`), ); - } else if(lcx) { - lcp = tryConvertLCXtoLCP(lcx) || TRIAL_VALUE; + } else if(converted) { + lcp = converted; const { warning, licenseId: id } = getLCPInfo(lcp); licenseId = id; - if(warning) { - const lines = []; + if(warning && warning.type === 'trialExpired') { + exitCode = 1; - lines.push( - prefixed(`${TEMPLATES.warningPrefix(1000)} ${TEMPLATES.purchaseLicense}`), + logStderr( + prefixed(`${TEMPLATES.errorPrefix(1004)} ${withDetails(TEMPLATES.trialExpired(fullVersion), licenseId)}`), ); - - if(licenseId) { - lines.push(TEMPLATES.licenseId(licenseId)); - } + } else if(warning) { + const lines = []; lines.push( - TEMPLATES.keyWasFound(source.type, source.path), + prefixed(`${TEMPLATES.warningPrefix(1000)} ${withDetails(TEMPLATES.purchaseLicense(fullVersion), licenseId)}`), ); if(warning.type !== 'trial') { @@ -213,10 +224,10 @@ function main() { if(warning.type === 'incompatibleVersion') { lines.push( - prefixed(`${TEMPLATES.warningPrefix(code)} ${TEMPLATES.keyVerificationFailed(warning.type, warning.keyVersion, warning.currentVersion)}`), + prefixed(`${TEMPLATES.warningPrefix(code)} ${withDetails(TEMPLATES.keyVerificationFailed(warning.type, warning.keyVersion, warning.currentVersion), licenseId)}`), ); } else { - lines.push(prefixed(`${TEMPLATES.warningPrefix(code)} ${TEMPLATES.installationInstructions}`)); + lines.push(prefixed(`${TEMPLATES.warningPrefix(code)} ${TEMPLATES.installationInstructions(fullVersion)}`)); } } @@ -224,15 +235,16 @@ function main() { } } else { logStderr( - prefixed(`${TEMPLATES.warningPrefix(1000)} ${TEMPLATES.purchaseLicense}`), + prefixed(`${TEMPLATES.warningPrefix(1000)} ${TEMPLATES.purchaseLicense(fullVersion)}`), TEMPLATES.keyNotFound, - prefixed(`${TEMPLATES.warningPrefix(1001)} ${TEMPLATES.installationInstructions}`), + prefixed(`${TEMPLATES.warningPrefix(1001)} ${TEMPLATES.installationInstructions(fullVersion)}`), ); } if(!opts.outPath) { process.stdout.write(lcp + '\n'); - process.exit(0); + process.exitCode = exitCode; + return; } const projectRoot = path.resolve(opts.cwd); @@ -241,7 +253,9 @@ function main() { ensureDirExists(path.dirname(outAbs)); if(!opts.force && fs.existsSync(outAbs)) { - fail(`Output file already exists: ${opts.outPath}\nUse --force to overwrite.`); + process.stderr.write(`Output file already exists: ${opts.outPath}\nUse --force to overwrite.\n`); + process.exitCode = exitCode; + return; } const useNonModular = opts.nonModular && outAbs.endsWith('.js'); @@ -253,7 +267,7 @@ function main() { } catch{} } - process.exit(0); + process.exitCode = exitCode; } main(); diff --git a/packages/devextreme/license/dx-get-lcx.js b/packages/devextreme/license/dx-get-lcx.js index 570895982abd..6c0f15edbc99 100644 --- a/packages/devextreme/license/dx-get-lcx.js +++ b/packages/devextreme/license/dx-get-lcx.js @@ -10,10 +10,6 @@ function isNonEmptyString(v) { return typeof v === 'string' && v.trim().length > 0; } -function hasEnvVar(name) { - return Object.prototype.hasOwnProperty.call(process.env, name); -} - function readTextFileIfExists(filePath) { try { if(!filePath) return null; @@ -60,6 +56,13 @@ function getDefaultLicenseFilePath() { return path.join(home, '.config', 'DevExpress', LICENSE_FILE); } +function getMachineLicenseFilePath() { + const root = process.platform === 'win32' ? process.env.ProgramData : '/usr/share'; + + if(!isNonEmptyString(root) || !path.isAbsolute(root)) return null; + return path.join(root, 'DevExpress', LICENSE_FILE); +} + function resolveFromLicensePathEnv(licensePathValue) { if(!isNonEmptyString(licensePathValue)) return null; @@ -91,6 +94,16 @@ function readDevExtremeVersion() { return null; } +function readFullVersion() { + try { + return require('../core/version').fullVersion; + } catch{} + try { + return String(require('../package.json').version); + } catch{} + return ''; +} + function buildVersionString(devExtremeVersion){ const { major, minor, code: currentCode } = devExtremeVersion; return `${major}.${minor}`; @@ -99,30 +112,39 @@ function buildVersionString(devExtremeVersion){ function getDevExpressLCXKey() { const devExtremeVersion = readDevExtremeVersion(); let currentVersion = ''; + const fullVersion = readFullVersion(); if(devExtremeVersion) { currentVersion = buildVersionString(devExtremeVersion); } - if(hasEnvVar(LICENSE_ENV)) { - return { key: normalizeKey(process.env[LICENSE_ENV]), source: { type: 'envVariable' }, currentVersion }; + const fromEnv = normalizeKey(process.env[LICENSE_ENV]); + if(fromEnv) { + return { key: fromEnv, source: { type: 'envVariable' }, currentVersion, fullVersion }; + } + + const licensePath = resolveFromLicensePathEnv(process.env[LICENSE_PATH_ENV]); + const fromPath = normalizeKey(readTextFileIfExists(licensePath)); + if(fromPath) { + return { key: fromPath, source: { type: 'file', path: licensePath }, currentVersion, fullVersion }; } - if(hasEnvVar(LICENSE_PATH_ENV)) { - const licensePath = resolveFromLicensePathEnv(process.env[LICENSE_PATH_ENV]); - const key = normalizeKey(readTextFileIfExists(licensePath)); - return { key, source: { type: 'envPath' }, currentVersion }; + const machinePath = getMachineLicenseFilePath(); + const fromMachine = normalizeKey(readTextFileIfExists(machinePath)); + if(fromMachine) { + return { key: fromMachine, source: { type: 'file', path: machinePath }, currentVersion, fullVersion }; } const defaultPath = getDefaultLicenseFilePath(); const fromDefault = normalizeKey(readTextFileIfExists(defaultPath)); if(fromDefault) { - return { key: fromDefault, source: { type: 'file', path: defaultPath }, currentVersion }; + return { key: fromDefault, source: { type: 'file', path: defaultPath }, currentVersion, fullVersion }; } - return { key: null, source: null, currentVersion }; + return { key: null, source: null, currentVersion, fullVersion }; } module.exports = { getDevExpressLCXKey, + getMachineLicenseFilePath, readDevExtremeVersion, buildVersionString }; diff --git a/packages/devextreme/license/dx-lcx-2-lcp.js b/packages/devextreme/license/dx-lcx-2-lcp.js index 1c3290910606..7ec3b79fb89c 100644 --- a/packages/devextreme/license/dx-lcx-2-lcp.js +++ b/packages/devextreme/license/dx-lcx-2-lcp.js @@ -120,6 +120,8 @@ function tryConvertLCXtoLCP(licenseString) { } const DEVEXTREME_HTMLJS_BIT = 1n << 54n; // ProductKind.DevExtremeHtmlJs from types.ts +const DEVEXTREME_ASPNET_BIT = 1n << 17n; +const TRIAL_EXPIRATION_TOLERANCE_MS = 3 * 24 * 60 * 60 * 1000; const DOTNET_TICKS_EPOCH_OFFSET = 621355968000000000n; const DOTNET_TICKS_PER_MS = 10000n; @@ -140,6 +142,7 @@ const TokenKind = Object.freeze({ const GENERAL_ERROR = { kind: TokenKind.corrupted, error: 'general' }; const DESERIALIZATION_ERROR = { kind: TokenKind.corrupted, error: 'deserialization' }; const PRODUCT_KIND_ERROR = { kind: TokenKind.corrupted, error: 'product-kind' }; +const TRIAL_EXPIRED_ERROR = { kind: TokenKind.corrupted, error: 'trial-expired' }; function readDevExtremeVersion() { try { @@ -181,12 +184,21 @@ function productsFromString(encodedString) { } } -function getMaxExpiration(products) { - const expirations = products - .map(p => p.expiration) - .filter(e => e > 0 && e !== Infinity); - if(expirations.length === 0) return Infinity; - return Math.max(...expirations); +function isTrialExpired(products, versionCode, now = Date.now()) { + if(!Array.isArray(products) || products.length === 0 || !Number.isFinite(versionCode)) return false; + + const maxVersion = Math.max(...products.map(p => p.version)); + const applicable = maxVersion === versionCode + ? products.filter(p => p.version === versionCode) + : products.filter(p => p.version >= versionCode); + + const licensed = applicable.some(p => (p.products & (DEVEXTREME_HTMLJS_BIT | DEVEXTREME_ASPNET_BIT)) !== 0n); + if(licensed) return false; + + const trialEnds = applicable.map(p => p.expiration).filter(e => e > 0); + if(trialEnds.length === 0) return false; + + return Math.max(...trialEnds) < now - TRIAL_EXPIRATION_TOLERANCE_MS; } function findLatestDevExtremeVersion(products) { @@ -196,7 +208,7 @@ function findLatestDevExtremeVersion(products) { return match?.version; } -function parseLCP(lcpString) { +function parseLCP(lcpString, versionCode) { if(typeof lcpString !== 'string' || !lcpString.startsWith(LCP_SIGNATURE)) { return GENERAL_ERROR; } @@ -216,6 +228,10 @@ function parseLCP(lcpString) { return { ...errorToken, licenseId }; } + if(isTrialExpired(products, versionCode)) { + return { ...TRIAL_EXPIRED_ERROR, licenseId }; + } + const maxVersionAllowed = findLatestDevExtremeVersion(products); if(!maxVersionAllowed) { @@ -224,7 +240,7 @@ function parseLCP(lcpString) { return { kind: TokenKind.verified, - payload: { customerId: '', maxVersionAllowed, licenseId }, + payload: { maxVersionAllowed, licenseId }, }; } catch{ return GENERAL_ERROR; @@ -236,10 +252,14 @@ function formatVersionCode(versionCode) { } function getLCPInfo(lcpString) { - const token = parseLCP(lcpString); + const devExtremeVersion = readDevExtremeVersion(); + const token = parseLCP(lcpString, devExtremeVersion?.code); let warning = null; let licenseId = null; let currentVersion = ''; + if(devExtremeVersion) { + currentVersion = buildVersionString(devExtremeVersion); + } if(token.kind === TokenKind.corrupted) { licenseId = token.licenseId || null; @@ -253,12 +273,13 @@ function getLCPInfo(lcpString) { case 'product-kind': warning = { type: 'trial' }; break; + case 'trial-expired': + warning = { type: 'trialExpired' }; + break; } } else { licenseId = token.payload.licenseId || null; - const devExtremeVersion = readDevExtremeVersion(); if(devExtremeVersion) { - currentVersion = buildVersionString(devExtremeVersion); const { maxVersionAllowed } = token.payload; if(maxVersionAllowed < devExtremeVersion.code) { warning = { @@ -278,9 +299,11 @@ function getLCPWarning(lcpString) { } module.exports = { + encode, convertLCXtoLCP, tryConvertLCXtoLCP, parseLCP, + isTrialExpired, getLCPInfo, getLCPWarning, TokenKind, diff --git a/packages/devextreme/license/messages.js b/packages/devextreme/license/messages.js index 109f8dddc547..d46c46cfa9db 100644 --- a/packages/devextreme/license/messages.js +++ b/packages/devextreme/license/messages.js @@ -1,10 +1,9 @@ 'use strict'; const KEY_SOURCES = Object.freeze({ - envVariable: 'License source: Environment Variable (DevExpress_License).', - envPath: 'License source: Environment Variable (DevExpress_LicensePath).', - file: (filePath) => `License source: File "${filePath}".`, - default: 'License source: default.', + envVariable: 'License Source: Environment Variable (DevExpress_License).', + file: (filePath) => `License Source: File (${filePath}).`, + default: 'License Source: default.', }); const WARNING_CODES = Object.freeze({ @@ -12,9 +11,18 @@ const WARNING_CODES = Object.freeze({ incompatibleVersion: 1002, }); +const SUBSCRIPTIONS = 'Universal, DXperience, ASP.NET and Blazor, DevExtreme Complete'; + +const continueUse = (code) => [ + `Please register a valid license (https://devexpress.com/DX${code}) or purchase a new license`, + '(https://devexpress.com/BUY) to continue use of the following DevExpress product libraries:', + `DevExtreme - Included in Subscriptions: ${SUBSCRIPTIONS}.`, +].join(' '); + const TEMPLATES = Object.freeze({ - warningPrefix: (number) => - `Warning DX${number}: For evaluation purposes only. Redistribution prohibited.`, + warningPrefix: (number) => `Warning DX${number}:`, + + errorPrefix: (number) => `Error DX${number}:`, keyNotFound: 'A valid DevExpress license key was not found on this machine.', @@ -27,7 +35,8 @@ const TEMPLATES = Object.freeze({ if(type === 'incompatibleVersion') { return [ `Incompatible DevExpress license key version (${keyVersion}).`, - `Download and register an updated DevExpress license key (${requiredVersion}+).`, + 'Use and Redistribution prohibited.', + `Download and register an updated DevExpress license key (v${requiredVersion}+).`, 'Clear npm/IDE/NuGet cache and rebuild your project (https://devexpress.com/DX1002).', ].join(' '); } @@ -36,23 +45,28 @@ const TEMPLATES = Object.freeze({ warningCodeByType: (type) => WARNING_CODES[type] || WARNING_CODES.general, - purchaseLicense: [ - 'Please register an existing license (https://devexpress.com/DX1000) or purchase a new license', - '(https://devexpress.com/Buy/) to continue use of the following DevExpress product libraries:', - 'DevExtreme - Included in Subscriptions: Universal, DXperience, ASP.NET and Blazor, DevExtreme Complete.' + purchaseLicense: (version) => [ + `For evaluation purposes only (v${version}). Redistribution prohibited.`, + continueUse(1000), + ].join(' '), + + trialExpired: (version) => [ + `Your 30-day trial has expired (v${version}). Use and Redistribution prohibited.`, + continueUse(1004), ].join(' '), - installationInstructions: [ + installationInstructions: (version) => [ + `License key is missing or uses invalid format (v${version}). Use and Redistribution prohibited.`, 'If you own a licensed/registered version or if you are using a 30-day trial version', 'of DevExpress product libraries on a development machine,', - 'download your personal license key and verify it with the devextreme-license tool', - '(https://devexpress.com/DX1001).', + 'download your personal license key (https://devexpress.com/DX1001)', + 'and verify it with the devextreme-license tool.', ].join(' '), oldDevExtremeKey: (version) => `A DevExtreme key (v25.2 or earlier) has been detected. Use DevExpress license key (v${version}+) instead.`, - licenseId: (id) => `License ID: ${id}`, + licenseId: (id) => `License ID: ${id}.`, }); module.exports = { TEMPLATES }; From 0ea4bb9ec4f5051dcf298daa442c340dc215269a Mon Sep 17 00:00:00 2001 From: AlisherAmonulloev Date: Mon, 26 Oct 2026 17:19:37 +0200 Subject: [PATCH 2/6] License CLI: add tests for devextreme-license --- .../build/devextreme-license.test.js | 527 ++++++++++++++++++ 1 file changed, 527 insertions(+) create mode 100644 packages/devextreme/build/devextreme-license.test.js diff --git a/packages/devextreme/build/devextreme-license.test.js b/packages/devextreme/build/devextreme-license.test.js new file mode 100644 index 000000000000..44441fe0e712 --- /dev/null +++ b/packages/devextreme/build/devextreme-license.test.js @@ -0,0 +1,527 @@ +'use strict'; + +const { spawnSync } = require('child_process'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const { encode, getLCPInfo, isTrialExpired } = require('../license/dx-lcx-2-lcp'); +const { getDevExpressLCXKey, getMachineLicenseFilePath } = require('../license/dx-get-lcx'); +const { TEMPLATES } = require('../license/messages'); + +const CLI = path.join(__dirname, '..', 'license', 'devextreme-license.js'); +const pkg = require('../package.json'); + +const DAY = 24 * 60 * 60 * 1000; +const DEVEXTREME_HTMLJS_BIT = 1n << 54n; +const DEVEXTREME_ASPNET_BIT = 1n << 17n; +const DXPERIENCE_WIN_BIT = 1n; +const TICKS_EPOCH_OFFSET = 621355968000000000n; +const DOTNET_MAX_TICKS = '3155378975999999999'; +const SIGN = 'A'.repeat(136); + +const versionCode = () => { + const [major, minor] = pkg.version.split('.'); + return Number(major) * 10 + Number(minor); +}; + +const ticks = (ms) => (BigInt(ms) * 10000n + TICKS_EPOCH_OFFSET).toString(); + +const entry = (version, products, expiration) => `${version},${products},0,${expiration === undefined ? ticks(Date.now() + 365 * DAY) : ticks(expiration)},${ticks(Date.now() + 365 * DAY)}`; + +const rawEntry = (version, products, trialTicks) => `${version},${products},0,${trialTicks},${ticks(Date.now() + 365 * DAY)}`; + +const createLcp = (...entries) => { + const payload = `TESTLICENSEID;${entries.join(';')};`; + return `LCPv1${encode(Buffer.from(SIGN + encode(payload), 'utf8').toString('base64'))}`; +}; + +const createLcx = (...entries) => { + const payload = encode(`TESTLICENSEID;${entries.join(';')};`); + const lcxData = `${SIGN}1\nX${payload.length}\n${payload}`; + return `LCXv1${Buffer.from(SIGN + encode(lcxData), 'utf8').toString('base64')}`; +}; + +describe('isTrialExpired', () => { + const now = Date.UTC(2026, 10, 15); + const item = (version, products, expiration = Infinity) => ({ version, products, expiration }); + + it.each([ + ['expired 4 days ago', [item(262, 0n, now - 4 * DAY)], true], + ['expired 2 days ago (tolerance)', [item(262, 0n, now - 2 * DAY)], false], + ['ends in the future', [item(262, 0n, now + DAY)], false], + ['no Expiration', [item(262, 0n)], false], + ['DevExtreme licensed', [item(262, DEVEXTREME_HTMLJS_BIT, now - 100 * DAY)], false], + ['only ASP.NET licensed (valid inside the wrappers)', [item(262, DEVEXTREME_ASPNET_BIT, now - 100 * DAY)], false], + ['another product licensed', [item(262, DXPERIENCE_WIN_BIT, now - 100 * DAY)], true], + ['older DevExtreme license, expired trial entry', [item(252, DEVEXTREME_HTMLJS_BIT), item(262, 0n, now - 100 * DAY)], true], + ['a later entry covers the build', [item(262, 0n, now - 100 * DAY), item(271, DEVEXTREME_HTMLJS_BIT)], false], + ['no entry for the build version', [item(261, 0n, now - 100 * DAY)], false], + ['the maximum Expiration wins', [item(262, 0n, now - 100 * DAY), item(262, DXPERIENCE_WIN_BIT, now + DAY)], false], + ['exactly 3 days ago (the boundary is not expired)', [item(262, 0n, now - 3 * DAY)], false], + ['3 days and 1 ms ago', [item(262, 0n, now - 3 * DAY - 1)], true], + ['3 days minus 1 ms ago', [item(262, 0n, now - 3 * DAY + 1)], false], + ['an expired entry and a later entry with an active trial', [item(262, 0n, now - 100 * DAY), item(271, 0n, now + DAY)], false], + ['an old DevExtreme license and a newer entry for another product', [item(252, DEVEXTREME_HTMLJS_BIT), item(271, DXPERIENCE_WIN_BIT, now - 100 * DAY)], true], + ])('%s -> %s', (_, products, expected) => { + expect(isTrialExpired(products, 262, now)).toBe(expected); + }); + + it('is false without the build version', () => { + expect(isTrialExpired([item(262, 0n, 1)], undefined, now)).toBe(false); + }); +}); + +describe('getLCPInfo', () => { + it('reports an expired trial with the license id', () => { + const lcp = createLcp(entry(versionCode(), 0, Date.now() - 10 * DAY)); + const info = getLCPInfo(lcp); + + expect(info.warning).toEqual({ type: 'trialExpired' }); + expect(info.licenseId).toBe('TESTLICENSEID'); + }); + + it('reports a trial that is not expired', () => { + const lcp = createLcp(entry(versionCode(), 0, Date.now() + 5 * DAY)); + + expect(getLCPInfo(lcp).warning).toEqual({ type: 'trial' }); + }); + + it('does not expire a trial that ends with DateTime.MaxValue', () => { + const lcp = createLcp(rawEntry(versionCode(), 0, DOTNET_MAX_TICKS)); + + expect(getLCPInfo(lcp).warning).toEqual({ type: 'trial' }); + }); + + it('does not expire a trial with DateTime.MinValue ticks (no trial information)', () => { + const lcp = createLcp(rawEntry(versionCode(), 0, '0')); + + expect(getLCPInfo(lcp).warning).toEqual({ type: 'trial' }); + }); + + it('does not expire an entry that has no trial end', () => { + const lcp = createLcp(`${versionCode()},0,0`); + + expect(getLCPInfo(lcp).warning).toEqual({ type: 'trial' }); + }); + + it('reports nothing for a licensed key', () => { + const lcp = createLcp(entry(versionCode(), DEVEXTREME_HTMLJS_BIT, Date.now() - 100 * DAY)); + + expect(getLCPInfo(lcp).warning).toBeNull(); + }); +}); + +describe('license source text', () => { + it.each([ + [{ type: 'envVariable' }, 'License Source: Environment Variable (DevExpress_License).'], + [{ type: 'file', path: '/home/me/.config/DevExpress/DevExpress_License.txt' }, 'License Source: File (/home/me/.config/DevExpress/DevExpress_License.txt).'], + [{ type: 'file', path: 'C:\\Users\\me\\AppData\\Roaming\\DevExpress\\DevExpress_License.txt' }, 'License Source: File (C:\\Users\\me\\AppData\\Roaming\\DevExpress\\DevExpress_License.txt).'], + ])('%j', (source, expected) => { + expect(TEMPLATES.keyWasFound(source.type, source.path)).toBe(expected); + }); +}); + +describe('machine-wide key file', () => { + let tmp; + let saved; + + const setPlatform = (platform) => Object.defineProperty(process, 'platform', { value: platform, configurable: true }); + const lcx = () => createLcx(entry(versionCode(), 0, Date.now() + 5 * DAY)); + const writeKey = (filePath, content = lcx()) => { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, content); + }; + + beforeEach(() => { + tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'dx-license-machine-')); + saved = { + platform: Object.getOwnPropertyDescriptor(process, 'platform'), + env: { ...process.env }, + }; + ['DevExpress_License', 'DevExpress_LicensePath', 'ProgramData', 'APPDATA'].forEach((name) => delete process.env[name]); + process.env.HOME = tmp; + process.env.USERPROFILE = tmp; + setPlatform('win32'); + process.env.ProgramData = path.join(tmp, 'ProgramData'); + process.env.APPDATA = path.join(tmp, 'AppData'); + }); + + afterEach(() => { + Object.defineProperty(process, 'platform', saved.platform); + Object.keys(process.env).forEach((name) => { delete process.env[name]; }); + Object.assign(process.env, saved.env); + fs.rmSync(tmp, { recursive: true, force: true }); + }); + + const machineFile = () => path.join(tmp, 'ProgramData', 'DevExpress', 'DevExpress_License.txt'); + const userFile = () => path.join(tmp, 'AppData', 'DevExpress', 'DevExpress_License.txt'); + + it('uses %ProgramData%\\DevExpress on Windows', () => { + expect(getMachineLicenseFilePath()).toBe(machineFile()); + }); + + it.each(['linux', 'darwin'])('uses /usr/share/DevExpress on %s like .NET', (platform) => { + setPlatform(platform); + + expect(getMachineLicenseFilePath()).toBe(path.join('/usr/share', 'DevExpress', 'DevExpress_License.txt')); + }); + + it.each([ + ['is not set', undefined], + ['is blank', ' '], + ['is relative', 'ProgramData'], + ])('has no machine-wide file when ProgramData %s', (_, value) => { + if(value === undefined) delete process.env.ProgramData; + else process.env.ProgramData = value; + + expect(getMachineLicenseFilePath()).toBeNull(); + }); + + it('finds the key in the machine-wide file and names it', () => { + writeKey(machineFile()); + + expect(getDevExpressLCXKey()).toMatchObject({ source: { type: 'file', path: machineFile() } }); + }); + + it('prefers the machine-wide file over the per-user file', () => { + writeKey(machineFile(), `${lcx()}\n`); + writeKey(userFile(), 'LCXv1other'); + + expect(getDevExpressLCXKey().source).toEqual({ type: 'file', path: machineFile() }); + }); + + it('prefers DevExpress_LicensePath over the machine-wide file', () => { + const folder = path.join(tmp, 'custom'); + writeKey(path.join(folder, 'DevExpress_License.txt')); + writeKey(machineFile()); + process.env.DevExpress_LicensePath = folder; + + expect(getDevExpressLCXKey().source).toEqual({ type: 'file', path: path.join(folder, 'DevExpress_License.txt') }); + }); + + it('prefers DevExpress_License over every file', () => { + writeKey(machineFile()); + process.env.DevExpress_License = lcx(); + + expect(getDevExpressLCXKey().source).toEqual({ type: 'envVariable' }); + }); + + it.each([ + ['a missing file', () => undefined], + ['an empty file', () => writeKey(machineFile(), '')], + ['a blank file', () => writeKey(machineFile(), ' \n\n ')], + ])('goes on to the per-user file when the machine-wide file is %s', (_, prepare) => { + prepare(); + writeKey(userFile()); + + expect(getDevExpressLCXKey().source).toEqual({ type: 'file', path: userFile() }); + }); + + it('finds nothing when no location has a key', () => { + expect(getDevExpressLCXKey()).toMatchObject({ key: null, source: null }); + }); +}); + +describe('devextreme-license', () => { + let tmp; + + const run = (env, args = []) => { + const result = spawnSync(process.execPath, [CLI, ...args], { + cwd: tmp, + encoding: 'utf8', + env: { + PATH: process.env.PATH, + HOME: tmp, + USERPROFILE: tmp, + APPDATA: tmp, + ...env, + }, + }); + return { code: result.status, stdout: result.stdout, stderr: result.stderr }; + }; + + const defaultKeyFile = () => { + if(process.platform === 'win32') return path.join(tmp, 'DevExpress', 'DevExpress_License.txt'); + if(process.platform === 'darwin') return path.join(tmp, 'Library', 'Application Support', 'DevExpress', 'DevExpress_License.txt'); + return path.join(tmp, '.config', 'DevExpress', 'DevExpress_License.txt'); + }; + + const writeKeyFile = (filePath, lcx) => { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, lcx); + }; + + const activeTrialLcx = () => createLcx(entry(versionCode(), 0, Date.now() + 5 * DAY)); + + beforeEach(() => { + tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'dx-license-cli-')); + }); + + afterEach(() => { + fs.rmSync(tmp, { recursive: true, force: true }); + }); + + it('fails with DX1004 and writes the key for an expired trial', () => { + const lcx = createLcx(entry(versionCode(), 0, Date.now() - 10 * DAY)); + const { code, stderr } = run({ DevExpress_License: lcx }, ['--out', 'key.ts']); + + expect(code).toBe(1); + expect(stderr).toContain(`Error DX1004: Your 30-day trial has expired (v${pkg.version}). Use and Redistribution prohibited. Please register a valid license (https://devexpress.com/DX1004) or purchase a new license (https://devexpress.com/BUY)`); + expect(stderr).toContain('License ID: TESTLICENSEID. License Source: Environment Variable (DevExpress_License).'); + expect(fs.readFileSync(path.join(tmp, 'key.ts'), 'utf8')).toContain("export const licenseKey = 'LCPv1"); + }); + + it('writes nothing but the key and comments to the generated file', () => { + const lcx = createLcx(entry(versionCode(), DEVEXTREME_HTMLJS_BIT)); + const filePath = path.join(tmp, 'license', 'default.txt'); + fs.mkdirSync(path.dirname(filePath)); + fs.writeFileSync(filePath, lcx); + + const { code } = run({ DevExpress_LicensePath: filePath }, ['--out', 'key.ts']); + const lines = fs.readFileSync(path.join(tmp, 'key.ts'), 'utf8').split('\n').filter(Boolean); + + expect(code).toBe(0); + expect(lines.filter((line) => !line.startsWith('//'))).toEqual([expect.stringMatching(/^export const licenseKey = 'LCPv1[^']+';$/)]); + const content = lines.join('\n'); + expect(content).not.toContain(tmp); + expect(content).not.toContain('TESTLICENSEID'); + expect(content).not.toContain('LCXv1'); + }); + + it('keeps the failure when the output file already exists', () => { + const lcx = createLcx(entry(versionCode(), 0, Date.now() - 10 * DAY)); + fs.writeFileSync(path.join(tmp, 'key.ts'), ''); + + expect(run({ DevExpress_License: lcx }, ['--out', 'key.ts']).code).toBe(1); + }); + + it('prints the key and fails without --out for an expired trial', () => { + const lcx = createLcx(entry(versionCode(), 0, Date.now() - 10 * DAY)); + const { code, stdout } = run({ DevExpress_License: lcx }); + + expect(code).toBe(1); + expect(stdout.startsWith('LCPv1')).toBe(true); + }); + + it('warns with DX1000 and exits with 0 for an active trial', () => { + const lcx = createLcx(entry(versionCode(), 0, Date.now() + 5 * DAY)); + const { code, stderr } = run({ DevExpress_License: lcx }); + + expect(code).toBe(0); + expect(stderr).toContain(`Warning DX1000: For evaluation purposes only (v${pkg.version}). Redistribution prohibited. Please register a valid license`); + expect(stderr).toContain('License ID: TESTLICENSEID. License Source: Environment Variable (DevExpress_License).'); + }); + + it('does not report a licensed key', () => { + const lcx = createLcx(entry(versionCode(), DEVEXTREME_HTMLJS_BIT, Date.now() - 100 * DAY)); + const { code, stderr } = run({ DevExpress_License: lcx }); + + expect(code).toBe(0); + expect(stderr).toBe(''); + }); + + it('does not fail the build for a key with only the ASP.NET bit', () => { + const lcx = createLcx(entry(versionCode(), DEVEXTREME_ASPNET_BIT, Date.now() - 100 * DAY)); + const { code, stderr } = run({ DevExpress_License: lcx }); + + expect(code).toBe(0); + expect(stderr).toContain('Warning DX1000: For evaluation purposes only'); + expect(stderr).not.toContain('DX1004'); + }); + + it('warns with DX1000 and DX1001 when there is no key', () => { + const { code, stdout, stderr } = run({}); + + expect(code).toBe(0); + expect(stdout).toBe('TRIAL\n'); + expect(stderr).toContain('Warning DX1000: For evaluation purposes only'); + expect(stderr).toContain('Warning DX1001: License key is missing or uses invalid format'); + }); + + it('warns with DX1002 for an incompatible key version', () => { + const lcx = createLcx(entry(versionCode() - 10, DEVEXTREME_HTMLJS_BIT)); + const { code, stderr } = run({ DevExpress_License: lcx }); + + expect(code).toBe(0); + const [major, minor] = pkg.version.split('.'); + const keyVersion = `v${Math.floor((versionCode() - 10) / 10)}.${(versionCode() - 10) % 10}`; + + expect(stderr).toContain(`Warning DX1002: Incompatible DevExpress license key version (${keyVersion}). Use and Redistribution prohibited. Download and register an updated DevExpress license key (v${major}.${minor}+). Clear npm/IDE/NuGet cache and rebuild your project (https://devexpress.com/DX1002).`); + }); + describe('where the key comes from', () => { + it('reports the file of DevExpress_LicensePath as a file', () => { + const filePath = path.join(tmp, 'license', 'key.txt'); + writeKeyFile(filePath, activeTrialLcx()); + + const { code, stderr } = run({ DevExpress_LicensePath: filePath }); + + expect(code).toBe(0); + expect(stderr).toContain(`License ID: TESTLICENSEID. License Source: File (${filePath}).`); + expect(stderr).not.toContain('DevExpress_LicensePath'); + }); + + it('reads the key file from a folder of DevExpress_LicensePath', () => { + const folder = path.join(tmp, 'license'); + writeKeyFile(path.join(folder, 'DevExpress_License.txt'), activeTrialLcx()); + + const { stderr } = run({ DevExpress_LicensePath: folder }); + + expect(stderr).toContain(`License Source: File (${path.join(folder, 'DevExpress_License.txt')}).`); + }); + + it('reads the key file from the default location', () => { + writeKeyFile(defaultKeyFile(), activeTrialLcx()); + + const { stderr } = run({}); + + expect(stderr).toContain(`License Source: File (${defaultKeyFile()}).`); + }); + + it.each([ + ['an empty variable', ''], + ['a blank variable', ' \n '], + ])('goes on to the default file when DevExpress_License is %s', (_, value) => { + writeKeyFile(defaultKeyFile(), activeTrialLcx()); + + const { stderr } = run({ DevExpress_License: value }); + + expect(stderr).toContain(`License Source: File (${defaultKeyFile()}).`); + expect(stderr).not.toContain('Environment Variable'); + }); + + it.each([ + ['a missing file', () => path.join(tmp, 'missing.txt')], + ['an empty file', () => { const p = path.join(tmp, 'empty.txt'); fs.writeFileSync(p, ''); return p; }], + ['a blank variable', () => ' '], + ])('goes on to the default file when DevExpress_LicensePath is %s', (_, getValue) => { + writeKeyFile(defaultKeyFile(), activeTrialLcx()); + + const { stderr } = run({ DevExpress_LicensePath: getValue() }); + + expect(stderr).toContain(`License Source: File (${defaultKeyFile()}).`); + }); + + it('prefers the variable with the key over the files', () => { + writeKeyFile(defaultKeyFile(), createLcx(entry(versionCode(), DEVEXTREME_HTMLJS_BIT))); + + const { stderr } = run({ DevExpress_License: activeTrialLcx() }); + + expect(stderr).toContain('License Source: Environment Variable (DevExpress_License).'); + }); + + it('prefers DevExpress_LicensePath over the default file', () => { + const filePath = path.join(tmp, 'license', 'key.txt'); + writeKeyFile(filePath, activeTrialLcx()); + writeKeyFile(defaultKeyFile(), createLcx(entry(versionCode(), DEVEXTREME_HTMLJS_BIT))); + + const { stderr } = run({ DevExpress_LicensePath: filePath }); + + expect(stderr).toContain(`License Source: File (${filePath}).`); + }); + + it('reports an expired trial found in the default file when the variable is empty', () => { + writeKeyFile(defaultKeyFile(), createLcx(entry(versionCode(), 0, Date.now() - 10 * DAY))); + + const { code, stderr } = run({ DevExpress_License: '' }); + + expect(code).toBe(1); + expect(stderr).toContain('Error DX1004'); + }); + }); + + describe('a key that cannot be read', () => { + it('warns with DX1000 and DX1001 and names the source of a broken key', () => { + const { code, stdout, stderr } = run({ DevExpress_License: 'LCXv1garbage' }); + + expect(code).toBe(0); + expect(stdout).toBe('TRIAL\n'); + expect(stderr).toContain('Warning DX1000: For evaluation purposes only'); + expect(stderr).toContain('License Source: Environment Variable (DevExpress_License).'); + expect(stderr).not.toContain('License ID'); + expect(stderr).toContain('Warning DX1001: License key is missing or uses invalid format'); + }); + + it('names the source of an old DevExtreme key', () => { + const { code, stderr } = run({ DevExpress_License: 'ewogICJmb3JtYXQiOiAxfQ==.AAAA' }); + + expect(code).toBe(0); + expect(stderr).toContain('Warning DX1000: For evaluation purposes only'); + expect(stderr).toContain('License Source: Environment Variable (DevExpress_License).'); + expect(stderr).toContain('A DevExtreme key (v25.2 or earlier) has been detected.'); + expect(stderr).toContain('Warning DX1001'); + }); + + it('names the file of a broken key', () => { + const filePath = path.join(tmp, 'broken.txt'); + writeKeyFile(filePath, 'LCXv1garbage'); + + const { stderr } = run({ DevExpress_LicensePath: filePath }); + + expect(stderr).toContain(`License Source: File (${filePath}).`); + expect(stderr).toContain('Warning DX1001'); + }); + }); + + describe('the output file', () => { + const expiredLcx = () => createLcx(entry(versionCode(), 0, Date.now() - 10 * DAY)); + + it('is not touched when it already exists', () => { + fs.writeFileSync(path.join(tmp, 'key.ts'), 'old content'); + + const { code, stderr } = run({ DevExpress_License: expiredLcx() }, ['--out', 'key.ts']); + + expect(code).toBe(1); + expect(stderr).toContain('Output file already exists: key.ts'); + expect(fs.readFileSync(path.join(tmp, 'key.ts'), 'utf8')).toBe('old content'); + }); + + it('is overwritten with --force even for an expired trial', () => { + fs.writeFileSync(path.join(tmp, 'key.ts'), 'old content'); + + const { code } = run({ DevExpress_License: expiredLcx() }, ['--out', 'key.ts', '--force']); + + expect(code).toBe(1); + expect(fs.readFileSync(path.join(tmp, 'key.ts'), 'utf8')).toContain("export const licenseKey = 'LCPv1"); + }); + + it('is created in a folder that does not exist', () => { + const { code } = run({ DevExpress_License: expiredLcx() }, ['--out', path.join('a', 'b', 'key.ts')]); + + expect(code).toBe(1); + expect(fs.existsSync(path.join(tmp, 'a', 'b', 'key.ts'))).toBe(true); + }); + + it('is not touched when it already exists and the trial is active (exit code 0)', () => { + fs.writeFileSync(path.join(tmp, 'key.ts'), 'old content'); + + const { code } = run({ DevExpress_License: activeTrialLcx() }, ['--out', 'key.ts']); + + expect(code).toBe(0); + expect(fs.readFileSync(path.join(tmp, 'key.ts'), 'utf8')).toBe('old content'); + }); + }); + + it.each([['--help'], ['-h']])('prints the whole help for %s and exits with 0', (flag) => { + const { code, stdout, stderr } = run({}, [flag]); + + expect(code).toBe(0); + expect(stderr).toBe(''); + expect(stdout.startsWith('Usage:\n devextreme-license --out [options]')).toBe(true); + expect(stdout.trimEnd().endsWith('"prebuild": "devextreme-license --non-modular --out src/.devextreme/license-key.js"')).toBe(true); + }); + + it('does not look for a key when it prints the help', () => { + const { code, stderr } = run({ DevExpress_License: 'LCXv1garbage' }, ['--help']); + + expect(code).toBe(0); + expect(stderr).toBe(''); + }); + + it('prints the whole key to stdout together with the failure code', () => { + const { code, stdout } = run({ DevExpress_License: createLcx(entry(versionCode(), 0, Date.now() - 10 * DAY)) }); + + expect(code).toBe(1); + expect(stdout).toMatch(/^LCPv1\S+\n$/); + }); +}); From 4fb1dad8ab187a9667d5b37afe83b6858fb69ca7 Mon Sep 17 00:00:00 2001 From: AlisherAmonulloev Date: Tue, 27 Oct 2026 16:24:28 +0200 Subject: [PATCH 3/6] License CLI: print the license ID and source once for DX1002 --- packages/devextreme/build/devextreme-license.test.js | 11 +++++++++++ packages/devextreme/license/devextreme-license.js | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/devextreme/build/devextreme-license.test.js b/packages/devextreme/build/devextreme-license.test.js index 44441fe0e712..67a592944e7e 100644 --- a/packages/devextreme/build/devextreme-license.test.js +++ b/packages/devextreme/build/devextreme-license.test.js @@ -349,6 +349,17 @@ describe('devextreme-license', () => { expect(stderr).toContain(`Warning DX1002: Incompatible DevExpress license key version (${keyVersion}). Use and Redistribution prohibited. Download and register an updated DevExpress license key (v${major}.${minor}+). Clear npm/IDE/NuGet cache and rebuild your project (https://devexpress.com/DX1002).`); }); + it('prints the license ID and the source once for an incompatible key version', () => { + const lcx = createLcx(entry(versionCode() - 10, DEVEXTREME_HTMLJS_BIT)); + const { stderr } = run({ DevExpress_License: lcx }); + + expect(stderr).toContain('Warning DX1000:'); + expect(stderr).toContain('Warning DX1002:'); + expect(stderr.match(/License ID: /g)).toHaveLength(1); + expect(stderr.match(/License Source: /g)).toHaveLength(1); + expect(stderr.indexOf('License ID: ')).toBeGreaterThan(stderr.indexOf('Warning DX1002:')); + }); + describe('where the key comes from', () => { it('reports the file of DevExpress_LicensePath as a file', () => { const filePath = path.join(tmp, 'license', 'key.txt'); diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js index caa5af532ba2..e83df9924aa9 100644 --- a/packages/devextreme/license/devextreme-license.js +++ b/packages/devextreme/license/devextreme-license.js @@ -215,8 +215,10 @@ function main() { } else if(warning) { const lines = []; + const purchaseLicense = TEMPLATES.purchaseLicense(fullVersion); + lines.push( - prefixed(`${TEMPLATES.warningPrefix(1000)} ${withDetails(TEMPLATES.purchaseLicense(fullVersion), licenseId)}`), + prefixed(`${TEMPLATES.warningPrefix(1000)} ${warning.type === 'incompatibleVersion' ? purchaseLicense : withDetails(purchaseLicense, licenseId)}`), ); if(warning.type !== 'trial') { From 60aac642a59144537a245af7c3d9d8ba87167f32 Mon Sep 17 00:00:00 2001 From: AlisherAmonulloev Date: Tue, 27 Oct 2026 16:24:56 +0200 Subject: [PATCH 4/6] License CLI: simplify the entries applicable to the build version --- packages/devextreme/license/dx-lcx-2-lcp.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/devextreme/license/dx-lcx-2-lcp.js b/packages/devextreme/license/dx-lcx-2-lcp.js index 7ec3b79fb89c..122bd3eb3706 100644 --- a/packages/devextreme/license/dx-lcx-2-lcp.js +++ b/packages/devextreme/license/dx-lcx-2-lcp.js @@ -187,10 +187,7 @@ function productsFromString(encodedString) { function isTrialExpired(products, versionCode, now = Date.now()) { if(!Array.isArray(products) || products.length === 0 || !Number.isFinite(versionCode)) return false; - const maxVersion = Math.max(...products.map(p => p.version)); - const applicable = maxVersion === versionCode - ? products.filter(p => p.version === versionCode) - : products.filter(p => p.version >= versionCode); + const applicable = products.filter(p => p.version >= versionCode); const licensed = applicable.some(p => (p.products & (DEVEXTREME_HTMLJS_BIT | DEVEXTREME_ASPNET_BIT)) !== 0n); if(licensed) return false; From 9231635b6d31af22aecd490408c5bb67dc578d64 Mon Sep 17 00:00:00 2001 From: AlisherAmonulloev Date: Tue, 27 Oct 2026 16:42:23 +0200 Subject: [PATCH 5/6] License CLI: remove two duplicate tests --- .../devextreme/build/devextreme-license.test.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/devextreme/build/devextreme-license.test.js b/packages/devextreme/build/devextreme-license.test.js index 67a592944e7e..807152b9d03a 100644 --- a/packages/devextreme/build/devextreme-license.test.js +++ b/packages/devextreme/build/devextreme-license.test.js @@ -289,21 +289,6 @@ describe('devextreme-license', () => { expect(content).not.toContain('LCXv1'); }); - it('keeps the failure when the output file already exists', () => { - const lcx = createLcx(entry(versionCode(), 0, Date.now() - 10 * DAY)); - fs.writeFileSync(path.join(tmp, 'key.ts'), ''); - - expect(run({ DevExpress_License: lcx }, ['--out', 'key.ts']).code).toBe(1); - }); - - it('prints the key and fails without --out for an expired trial', () => { - const lcx = createLcx(entry(versionCode(), 0, Date.now() - 10 * DAY)); - const { code, stdout } = run({ DevExpress_License: lcx }); - - expect(code).toBe(1); - expect(stdout.startsWith('LCPv1')).toBe(true); - }); - it('warns with DX1000 and exits with 0 for an active trial', () => { const lcx = createLcx(entry(versionCode(), 0, Date.now() + 5 * DAY)); const { code, stderr } = run({ DevExpress_License: lcx }); From de9b887583c3504b8f1a1abd6cdebc7029e3c9eb Mon Sep 17 00:00:00 2001 From: AlisherAmonulloev Date: Tue, 27 Oct 2026 16:53:24 +0200 Subject: [PATCH 6/6] License CLI: drop the tests that read the machine-wide key folder --- .../build/devextreme-license.test.js | 49 ------------------- 1 file changed, 49 deletions(-) diff --git a/packages/devextreme/build/devextreme-license.test.js b/packages/devextreme/build/devextreme-license.test.js index 807152b9d03a..15e2ee5bfb10 100644 --- a/packages/devextreme/build/devextreme-license.test.js +++ b/packages/devextreme/build/devextreme-license.test.js @@ -315,15 +315,6 @@ describe('devextreme-license', () => { expect(stderr).not.toContain('DX1004'); }); - it('warns with DX1000 and DX1001 when there is no key', () => { - const { code, stdout, stderr } = run({}); - - expect(code).toBe(0); - expect(stdout).toBe('TRIAL\n'); - expect(stderr).toContain('Warning DX1000: For evaluation purposes only'); - expect(stderr).toContain('Warning DX1001: License key is missing or uses invalid format'); - }); - it('warns with DX1002 for an incompatible key version', () => { const lcx = createLcx(entry(versionCode() - 10, DEVEXTREME_HTMLJS_BIT)); const { code, stderr } = run({ DevExpress_License: lcx }); @@ -366,38 +357,6 @@ describe('devextreme-license', () => { expect(stderr).toContain(`License Source: File (${path.join(folder, 'DevExpress_License.txt')}).`); }); - it('reads the key file from the default location', () => { - writeKeyFile(defaultKeyFile(), activeTrialLcx()); - - const { stderr } = run({}); - - expect(stderr).toContain(`License Source: File (${defaultKeyFile()}).`); - }); - - it.each([ - ['an empty variable', ''], - ['a blank variable', ' \n '], - ])('goes on to the default file when DevExpress_License is %s', (_, value) => { - writeKeyFile(defaultKeyFile(), activeTrialLcx()); - - const { stderr } = run({ DevExpress_License: value }); - - expect(stderr).toContain(`License Source: File (${defaultKeyFile()}).`); - expect(stderr).not.toContain('Environment Variable'); - }); - - it.each([ - ['a missing file', () => path.join(tmp, 'missing.txt')], - ['an empty file', () => { const p = path.join(tmp, 'empty.txt'); fs.writeFileSync(p, ''); return p; }], - ['a blank variable', () => ' '], - ])('goes on to the default file when DevExpress_LicensePath is %s', (_, getValue) => { - writeKeyFile(defaultKeyFile(), activeTrialLcx()); - - const { stderr } = run({ DevExpress_LicensePath: getValue() }); - - expect(stderr).toContain(`License Source: File (${defaultKeyFile()}).`); - }); - it('prefers the variable with the key over the files', () => { writeKeyFile(defaultKeyFile(), createLcx(entry(versionCode(), DEVEXTREME_HTMLJS_BIT))); @@ -416,14 +375,6 @@ describe('devextreme-license', () => { expect(stderr).toContain(`License Source: File (${filePath}).`); }); - it('reports an expired trial found in the default file when the variable is empty', () => { - writeKeyFile(defaultKeyFile(), createLcx(entry(versionCode(), 0, Date.now() - 10 * DAY))); - - const { code, stderr } = run({ DevExpress_License: '' }); - - expect(code).toBe(1); - expect(stderr).toContain('Error DX1004'); - }); }); describe('a key that cannot be read', () => {