Files
dc/client/cypress/e2e/licensing.cy.ts
T
dc 0fa6fee85b
Build / Build-and-ng-test (pull_request) Canceled after 0s
Build / Build-and-test-development (pull_request) Canceled after 0s
Lighthouse Checks / lighthouse (pull_request) Canceled after 0s
refactor(licensing): one banner component with a state per licence problem
The banner is the only route into the licensing pages now, so it needs a state
for every way the licence can block the user, and each state needs to point at
the page that can fix it:

- a licence problem with no free tier (the startupservice-without-globvars
  case, which previously fell through to 'not registered') reports the problem
  and opens its encoded page;
- free tier keeps its update-key and problem links;
- over capacity, and an unregistered user who cannot register, point at
  licensing/limit, which had no entry point once the forced navigation went;
- an unregistered user points at licensing/register when they can register;
- the expiry banner keeps its update link.

Moving the banner out of app.component means its states can be tested: ten
TestBed cases assert the message and the rendered link for each, including that
nothing renders before the startup data lands or when the licence is in order.

currentUserCanRegister finally has a reader - it decides between the register
and limit links - and the Cypress limit case follows the banner instead of
expecting a redirect.
2026-11-07 02:26:58 +00:00

1078 lines
34 KiB
TypeScript

import { arrayBufferToBase64 } from './../util/helper-functions'
import * as moment from 'moment'
const username = Cypress.env('username')
const password = Cypress.env('password')
const hostUrl = Cypress.env('hosturl')
const appLocation = Cypress.env('appLocation')
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
const fixturePath = 'excels_general/'
const serverType = Cypress.env('serverType')
const site_id = Cypress.env(`site_id_${serverType}`)
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
const testLicenceUserLimits = Cypress.env('testLicenceUserLimits')
/** IMPORTANT NOTICE
* Before running tests, make sure that table `MPE_USERS` is present
*/
interface EditConfigTableCells {
varName: string
varValue: string
}
context('licensing tests: ', function () {
this.beforeAll(() => {
cy.loginAndUpdateValidKey()
})
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
visitPage('home')
})
it('1 | key valid, not expired', (done) => {
let keyData = {
valid_until: moment().add(1, 'year').format('YYYY-MM-DD'),
users_allowed: 4,
hot_license_key: '',
demo: false,
site_id: site_id
}
let keys: { licenseKey: any; activationKey: any }
generateKeys(keyData, (keysGen: any) => {
keys = keysGen
gotoLicensingUpdate().then(() => {
inputLicenseKeyPage(keys.licenseKey, keys.activationKey)
})
cy.then(() => {
acceptTermsIfPresented((result: boolean) => {
visitPage('home')
waitForAppSettled()
cy.get('.nav-tree clr-tree > clr-tree-node', {
timeout: longerCommandTimeout
}).then((treeNodes: any) => {
done()
})
})
})
})
})
it('2 | Key will expire in less then 14 days, not free tier', (done) => {
// make 2 separate for this one
let keyData = {
valid_until: moment().add(10, 'day').format('YYYY-MM-DD'),
users_allowed: 4,
hot_license_key: '',
demo: false,
site_id: site_id
}
let keys: { licenseKey: any; activationKey: any }
generateKeys(keyData, (keysGen: any) => {
keys = keysGen
console.log('keys', keys)
acceptTermsIfPresented((result: boolean) => {
updateLicenseKeyQuick(keysGen, () => {
acceptTermsIfPresented((result: boolean) => {
verifyLicensingWarning('This license key will expire in ', () => {
done()
})
})
})
})
})
})
it('3 | key expired, free tier works', (done) => {
let keyData = {
valid_until: moment().subtract(1, 'day').format('YYYY-MM-DD'),
users_allowed: 4,
hot_license_key: '',
demo: false,
site_id: site_id
}
let keys: { licenseKey: any; activationKey: any }
generateKeys(keyData, (keysGen: any) => {
keys = keysGen
acceptTermsIfPresented((result: boolean) => {
updateLicenseKeyQuick(keysGen, () => {
acceptTermsIfPresented((result: boolean) => {
verifyLicensingWarning('(FREE Tier) - Problem with licence', () => {
openLicenceProblemDetails()
verifyLicensingPage(
'Licence key is expired - please contact',
(success: boolean) => {
if (success) done()
}
)
})
})
})
})
})
})
it('4 | key invalid, free tier works', (done) => {
let keyData = {
valid_until: moment().subtract(1, 'day').format('YYYY-MM-DD'),
users_allowed: 4,
hot_license_key: '',
demo: false,
site_id: site_id
}
let keys: { licenseKey: any; activationKey: any }
generateKeys(keyData, (keysGen: any) => {
keys = keysGen
keys.activationKey = 'invalid' + keys.activationKey
acceptTermsIfPresented((result: boolean) => {
updateLicenseKeyQuick(keysGen, () => {
acceptTermsIfPresented((result: boolean) => {
verifyLicensingWarning('(FREE Tier) - Problem with licence', () => {
openLicenceProblemDetails()
verifyLicensingPage(
'Licence key is invalid - please contact',
(success: boolean) => {
if (success) done()
}
)
})
})
})
})
})
})
it('5 | key for wrong organisation, free tier works', (done) => {
let keyData = {
valid_until: moment().add(1, 'year').format('YYYY-MM-DD'),
users_allowed: 4,
hot_license_key: '',
demo: false,
site_id: 100
}
let keys: { licenseKey: any; activationKey: any }
generateKeys(keyData, (keysGen: any) => {
keys = keysGen
keys.activationKey = keys.activationKey
acceptTermsIfPresented((result: boolean) => {
updateLicenseKeyQuick(keysGen, () => {
acceptTermsIfPresented((result: boolean) => {
verifyLicensingWarning('(FREE Tier) - Problem with licence', () => {
openLicenceProblemDetails()
verifyLicensingPage(
'SYSSITE (below) is not found',
(success: boolean) => {
if (success) done()
}
)
})
})
})
})
})
})
it('6 | pasting an HTTP-format key (identical licence/activation text) on this secure connection warns before applying', (done) => {
visitPage('licensing/update')
cy.contains('button', 'Paste licence', {
timeout: longerCommandTimeout
}).click()
// Defaults to the combined single-field form - switch to the legacy
// two-field layout, since this test targets that path specifically.
cy.get('button').contains('Paste as two separate keys instead').click()
// HTTP-format keys are generated as the exact same string for both
// fields (see detectLicenceKeyProtocolMismatch's own doc comment) -
// any non-empty matching pair triggers the warning, no real key needed.
// Cypress always runs against localhost, which browsers treat as a
// secure connection even over plain http, so this is reachable without
// stubbing anything - unlike the reverse case (an HTTPS-format key
// where WebCrypto is unavailable), which is covered by
// detectLicenceKeyProtocolMismatch.spec.ts's Karma tests instead.
const httpFormatKey = 'same-text-for-both-fields'
cy.get('.license-key-form textarea', { timeout: longerCommandTimeout })
.invoke('val', httpFormatKey)
.trigger('input')
cy.get('.activation-key-form textarea', { timeout: longerCommandTimeout })
.invoke('val', httpFormatKey)
.trigger('input')
cy.get('p.protocol-mismatch-warning')
.should('contain', 'generated for an insecure (HTTP) connection')
.then(() => {
done()
})
})
it('7 | Combined single-string key (default paste format) activates successfully', (done) => {
let keyData = {
valid_until: moment().add(1, 'year').format('YYYY-MM-DD'),
users_allowed: 4,
hot_license_key: '',
demo: false,
site_id: site_id
}
generateKeys(keyData, (keysGen: any) => {
// Navigate there explicitly rather than only acting when already on
// the licensing page - earlier tests may have left the app on it (a
// bad key) or already activated (a good one), and this test should
// pass either way rather than silently no-op when it's the latter.
cy.then(() => {
const proceed = () =>
generateCombinedKey(keysGen.licenseKey, keysGen.activationKey).then(
(combinedKey) => {
inputCombinedKeyPage(combinedKey)
acceptTermsIfPresented((termsResult: boolean) => {
visitPage('home')
cy.get('.nav-tree clr-tree > clr-tree-node', {
timeout: longerCommandTimeout
}).then(() => {
done()
})
})
}
)
gotoLicensingUpdate()
// proceed() generates the combined key asynchronously, then
// enqueues the actual page commands only once its promise
// resolves. Bridged through cy.then() so the command chain stays
// open across that async gap: a bare proceed() call returns a
// promise that nothing awaits, and once the surrounding queue
// has drained, commands enqueued from that promise's continuation
// are never run.
cy.then(() => proceed())
})
})
})
it('8 | Pasting a combined key into the legacy licence-key field alone still auto-detects and activates', (done) => {
let keyData = {
valid_until: moment().add(1, 'year').format('YYYY-MM-DD'),
users_allowed: 4,
hot_license_key: '',
demo: false,
site_id: site_id
}
generateKeys(keyData, (keysGen: any) => {
// Navigate there explicitly rather than only acting when already on
// the licensing page - see test 7's own comment for why.
cy.then(() => {
const proceed = () =>
generateCombinedKey(keysGen.licenseKey, keysGen.activationKey).then(
(combinedKey) => {
cy.contains('button', 'Paste licence', {
timeout: longerCommandTimeout
}).click()
cy.get('button')
.contains('Paste as two separate keys instead')
.click()
// Only the licence-key field gets the combined string - the
// activation-key field is left empty, proving the split
// populates both fields from this one paste.
cy.get('.license-key-form textarea', {
timeout: longerCommandTimeout
})
.invoke('val', combinedKey)
.trigger('input')
.trigger('mouseleave')
cy.get('.activation-key-form textarea', {
timeout: longerCommandTimeout
}).should(($textarea) => {
expect($textarea.val()).to.equal(keysGen.activationKey)
})
cy.get('button.apply-keys').click()
acceptTermsIfPresented((termsResult: boolean) => {
visitPage('home')
cy.get('.nav-tree clr-tree > clr-tree-node', {
timeout: longerCommandTimeout
}).then(() => {
done()
})
})
}
)
// See test 7's own comment for why proceed() is bridged through
// cy.then() rather than called as a plain statement.
gotoLicensingUpdate().then(() => proceed())
})
})
})
it('9 | Uploading a single-line combined-format file activates successfully', (done) => {
let keyData = {
valid_until: moment().add(1, 'year').format('YYYY-MM-DD'),
users_allowed: 4,
hot_license_key: '',
demo: false,
site_id: site_id
}
generateKeys(keyData, (keysGen: any) => {
// Navigate there explicitly rather than only acting when already on
// the licensing page - see test 7's own comment for why.
cy.then(() => {
const proceed = () =>
generateCombinedKey(keysGen.licenseKey, keysGen.activationKey).then(
(combinedKey) => {
cy.get('input[type="file"]').attachFile({
fileContent: new Blob([combinedKey], { type: 'text/plain' }),
fileName: 'datacontroller-licence-combined.txt',
mimeType: 'text/plain'
})
cy.get('button.apply-keys', {
timeout: longerCommandTimeout
}).click()
acceptTermsIfPresented((termsResult: boolean) => {
visitPage('home')
cy.get('.nav-tree clr-tree > clr-tree-node', {
timeout: longerCommandTimeout
}).then(() => {
done()
})
})
}
)
// See test 7's own comment for why proceed() is bridged through
// cy.then() rather than called as a plain statement.
gotoLicensingUpdate().then(() => proceed())
})
})
})
it('10 | Key details preview shows for a validly-pasted key and disappears once the key input is cleared', (done) => {
let keyData = {
valid_until: moment().add(1, 'year').format('YYYY-MM-DD'),
users_allowed: 4,
hot_license_key: '',
demo: false,
site_id: site_id,
// Kept to just the two enabled toggles (relying on
// decodeLicenceFeatures' fallback for the rest) to keep the
// encrypted payload well under this suite's RSA-OAEP plaintext
// ceiling - see generateKeys' modulusLength below.
features: { vb: true, fu: true }
}
generateKeys(keyData, (keysGen: any) => {
// Navigate there explicitly rather than only acting when already on
// the licensing page - see test 7's own comment for why.
cy.then(() => {
const proceed = () =>
generateCombinedKey(keysGen.licenseKey, keysGen.activationKey).then(
(combinedKey) => {
cy.contains('button', 'Paste licence', {
timeout: longerCommandTimeout
}).click()
cy.get('.combined-key-form textarea', {
timeout: longerCommandTimeout
})
.invoke('val', combinedKey)
.trigger('input')
.trigger('mouseleave')
cy.get('.key-details')
.should('contain', 'Valid until:')
.and('contain', 'Allowed users:')
.and('contain', '4')
.and('contain', 'Site ID(s) in this key:')
.and('contain', 'Enabled features:')
.and('contain', 'Viewbox')
.and('contain', 'File Upload')
.invoke('text')
.then((text) => {
expect(text).to.not.contain('Edit Record')
expect(text).to.not.contain('Add Record')
})
cy.get('.combined-key-form textarea')
.invoke('val', '')
.trigger('input')
.trigger('mouseleave')
cy.get('.key-details')
.should('not.exist')
.then(() => {
done()
})
}
)
// See test 7's own comment for why this is chained via .then()
// rather than called as the next plain statement.
gotoLicensingUpdate().then(() => proceed())
})
})
})
// The registration page belongs to an install that is not registered yet. An
// activated one that reaches it by URL has nothing to register, and accepting
// the EULA would post public/registeruser, which aborts for an
// already-registered user, so it is sent home instead of showing the EULA.
it('11 | an activated install is sent home from the registration page', (done) => {
visitPage('licensing/register')
cy.location('hash', { timeout: longerCommandTimeout })
.should('not.include', 'register')
.then(() => {
cy.get('.nav-tree', { timeout: longerCommandTimeout })
.should('exist')
.then(() => {
done()
})
})
})
// Only the actions in LicenseActions have a page. An unknown segment would
// render the licensing component with no action at all, so it goes home.
it('12 | an unknown licensing action is sent home', (done) => {
visitPage('licensing/this-action-does-not-exist')
cy.location('hash', { timeout: longerCommandTimeout })
.should('not.include', 'licensing')
.then(() => {
cy.get('.nav-tree', { timeout: longerCommandTimeout })
.should('exist')
.then(() => {
done()
})
})
})
if (testLicenceUserLimits) {
it('4 | User try to register when limit is reached', (done) => {
let keyData = {
valid_until: moment().add(1, 'month').format('YYYY-MM-DD'),
users_allowed: 10,
hot_license_key: '',
demo: false,
site_id: site_id
}
let keyData2 = {
valid_until: moment().add(1, 'month').format('YYYY-MM-DD'),
users_allowed: 1,
hot_license_key: '',
demo: false,
site_id: site_id
}
generateKeys(keyData, (keysGen: any) => {
generateKeys(keyData2, (keysGen2: any) => {
acceptTermsIfPresented((result: boolean) => {
updateLicenseKeyQuick(keysGen, () => {
acceptTermsIfPresented((result: boolean) => {
updateLicenseKeyQuick(keysGen2, () => {
const random = Cypress._.random(0, 1000)
const newUser = {
username: `randomusername${random}notregistered`,
last_seen_at: moment().add(1, 'month').format('YYYY-MM-DD'),
registered_at: moment().add(1, 'month').format('YYYY-MM-DD')
}
updateUsersTable(
{ deleteAll: true, newUsers: [newUser] },
() => {
logout(() => {
cy.visit(hostUrl + appLocation)
// cy.get('input.username').type(username)
// cy.get('input.password').type(password)
// cy.get('.login-group button').click()
visitPage('home')
waitForAppSettled()
cy.get('#demo-banner', {
timeout: longerCommandTimeout
})
.should('contain', 'reached the limit')
.within(() => {
cy.get('a.update-key').click()
})
verifyLicensingPage(
'The registered number of users reached the limit specified for your licence.',
(success: boolean) => {
if (success) done()
}
)
})
}
)
})
})
})
})
})
})
})
it('5 | Show warning banner when limit is exceeded', (done) => {
let keyData = {
valid_until: moment().add(1, 'month').format('YYYY-MM-DD'),
users_allowed: 10,
hot_license_key: '',
demo: false,
site_id: site_id
}
let keyData2 = {
valid_until: moment().add(1, 'month').format('YYYY-MM-DD'),
users_allowed: 1,
hot_license_key: '',
demo: false,
site_id: site_id
}
generateKeys(keyData, (keysGen: any) => {
generateKeys(keyData2, (keysGen2: any) => {
acceptTermsIfPresented((result: boolean) => {
updateLicenseKeyQuick(keysGen, () => {
acceptTermsIfPresented((result: boolean) => {
const random = Cypress._.random(0, 1000)
const newUser = {
username: `randomusername${random}`,
last_seen_at: moment().add(1, 'month').format('YYYY-MM-DD'),
registered_at: moment().add(1, 'month').format('YYYY-MM-DD')
}
updateUsersTable(
{ deleteAll: true, keep: username, newUsers: [newUser] },
() => {
updateLicenseKeyQuick(keysGen2, () => {
verifyLicensingWarning(
'The registered number of users exceeds the limit specified for your license.',
() => {
done()
}
)
})
}
)
})
})
})
})
})
})
}
})
// A key change re-bootstraps the licence state: .app-loading while the startup
// data loads, then one of these states. Every page these specs act on renders
// one of them, so waiting for the overlay to clear and for one of these
// selectors is a sufficient settled state, and it costs nothing when the app is
// already there. .card-header is the licensing card, which is the only marker
// on the key/update/limit pages when there is no error to show and the install
// has no nav tree or banner yet.
const waitForAppSettled = () => {
cy.get('.app-loading', { timeout: longerCommandTimeout }).should('not.exist')
cy.get('.nav-tree, #demo-banner, .card-header, .key-error, input.username', {
timeout: longerCommandTimeout
}).should('exist')
}
// The paste form exists only on licensing/update. Asking "are we anywhere
// under licensing/" also matches the error page the app shows when the current
// key is bad, which has no paste button at all. Returns the chainable so a
// caller's next commands are queued after the conditional visit - commands
// queued behind a bare call would run before the deferred navigation.
const gotoLicensingUpdate = () => {
waitForAppSettled()
return cy.location('hash').then((hash: string) => {
if (!hash.includes('licensing/update')) visitPage('licensing/update')
})
}
// The banner is the entry point to the licensing page: it names the licence
// problem and its action link opens the page that explains it.
const openLicenceProblemDetails = () => {
// The banner re-renders when the licence state arrives, so wait for the page
// to settle before clicking its link.
waitForAppSettled()
cy.get('#demo-banner a.update-key', { timeout: longerCommandTimeout })
.should('contain', 'More details')
.click()
}
const logout = (callback?: any) => {
cy.get('.header-actions .dropdown-toggle')
.click()
.then(() => {
cy.get('.header-actions .dropdown-menu > .separator')
.next()
.click()
.then(() => {
if (callback) callback()
})
})
}
const acceptTermsIfPresented = (callback?: any) => {
cy.url().then((url: string) => {
if (url.includes('licensing/register')) {
cy.get('.card-block')
.scrollTo('bottom')
.then(() => {
cy.get('#checkbox1')
.click()
.then(() => {
// Accepting the EULA posts public/registeruser, and the app
// reloads once it succeeds. The card is gone after that reload,
// so waiting for it covers the reload; a registration that is
// refused leaves the card in place and fails here.
cy.get('#checkbox1', {
timeout: longerCommandTimeout
}).should('not.exist')
waitForAppSettled()
})
.then(() => {
if (callback) callback(true)
})
})
} else {
if (callback) callback(false)
}
})
}
const isLicensingPage = (callback: any) => {
return cy.url().then((url: string) => {
callback(
url.includes('#/licensing/') && !url.includes('licensing/register')
)
})
}
const verifyLicensingPage = (text: string, callback: any) => {
// A bad key makes the app redirect to the licensing page. Asserting the URL
// and the error text together means a redirect that never comes fails here,
// rather than being read as a page that simply has no error.
cy.location('hash', { timeout: longerCommandTimeout }).should(
'match',
/licensing\/(?!register)/
)
cy.get('.key-error', { timeout: longerCommandTimeout })
.should('contain', text)
.then(() => {
callback(true)
})
}
const verifyLicensingWarning = (text: string, callback: any) => {
visitPage('home')
waitForAppSettled()
cy.get("div[role='alert'] .alert-text", { timeout: longerCommandTimeout })
.invoke('text')
.should('contain', text)
.then(() => {
callback()
})
}
const inputLicenseKeyPage = (licenseKey: string, activationKey: string) => {
cy.contains('button', 'Paste licence', {
timeout: longerCommandTimeout
}).click()
// Defaults to the combined single-field form - this helper exercises the
// two-part path specifically, so switch to it first.
cy.get('button').contains('Paste as two separate keys instead').click()
cy.get('.license-key-form textarea', { timeout: longerCommandTimeout })
.invoke('val', licenseKey)
.trigger('input')
.should('not.be.undefined')
cy.get('.activation-key-form textarea', { timeout: longerCommandTimeout })
.invoke('val', activationKey)
.trigger('input')
.should('not.be.undefined')
cy.get('button.apply-keys').click()
waitForAppSettled()
}
const inputCombinedKeyPage = (combinedKey: string) => {
cy.contains('button', 'Paste licence', {
timeout: longerCommandTimeout
}).click()
// Combined is the default paste format - no toggle click needed here,
// unlike inputLicenseKeyPage's legacy two-field path.
cy.get('.combined-key-form textarea', { timeout: longerCommandTimeout })
.invoke('val', combinedKey)
.trigger('input')
.trigger('mouseleave')
.should('not.be.undefined')
// mouseleave's handler (onCombinedKeyInput) does async work - gzip
// decompress the split, then decrypt for the key-details preview -
// before licenceKeyValue/activationKeyValue are ready to submit.
// Wait for that preview to land rather than racing "Apply licence
// keys" against the still-in-flight promise.
cy.get('.key-details', { timeout: longerCommandTimeout }).should(
'contain',
'Valid until:'
)
cy.get('button.apply-keys').click()
}
const updateUsersTable = (options: any, callback?: any) => {
visitPage('home')
waitForAppSettled()
openTableFromTree(libraryToOpenIncludes, 'mpe_users')
clickOnEdit(() => {
cy.get('.ht_master tbody tr').then((rows: any) => {
if (options.deleteAll) {
for (let row of rows) {
const user_id = row.childNodes[2]
if (!options.keep || user_id.innerText !== options.keep) {
cy.get(row.childNodes[1])
.dblclick()
.then(() => {
cy.focused().type('{selectall}').type('Yes').type('{enter}')
})
}
}
}
if (options.newUsers && options.newUsers.length) {
for (let newUser of options.newUsers) {
clickOnAddRow(() => {
cy.get('#hotInstance tbody tr:last-child').then((rows: any) => {
cy.get(rows[0].childNodes[2])
.dblclick()
.then(() => {
cy.focused()
.type('{selectall}')
.type(newUser.username)
.type('{enter}')
})
// cy.get(rows[0].childNodes[3])
// .dblclick()
// .then(() => {
// cy.focused()
// .type('{selectall}')
// .type(newUser.last_seen_at)
// .type('{enter}')
// })
// cy.get(rows[0].childNodes[4])
// .dblclick()
// .then(() => {
// cy.focused()
// .type('{selectall}')
// .type(newUser.registered_at)
// .type('{enter}')
// })
submitTable(() => {
approveTable(callback)
})
})
})
}
}
})
})
}
const changeLicenseKeyTable = (keys: any, callback?: any) => {
visitPage('home')
openTableFromTree(libraryToOpenIncludes, 'mpe_config')
clickOnEdit(() => {
editTableField(
[
{ varName: 'DC_ACTIVATION_KEY', varValue: keys.activationKey },
{ varName: 'DC_LICENCE_KEY', varValue: keys.licenseKey }
],
() => {
submitTable(() => {
approveTable(() => {
cy.reload()
if (callback) callback()
})
})
}
)
})
}
const updateLicenseKeyQuick = (keys: any, callback: any) => {
isLicensingPage((result: boolean) => {
if (!result) {
visitPage('licensing/update')
}
inputLicenseKeyPage(keys.licenseKey, keys.activationKey)
// Applying keys navigates to '/' and reloads the app once the service
// accepts them, so the caller continues against the rebooted app rather
// than racing the reload.
cy.location('hash', { timeout: longerCommandTimeout }).should(
'not.include',
'licensing'
)
waitForAppSettled()
callback()
})
}
const generateKeys = async (licenseData: any, resultCallback?: any) => {
let keyPair = await window.crypto.subtle.generateKey(
{
name: 'RSA-OAEP',
modulusLength: 2024,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256'
},
true,
['encrypt', 'decrypt']
)
const encoded = new TextEncoder().encode(JSON.stringify(licenseData))
const cipher = await window.crypto.subtle
.encrypt(
{
name: 'RSA-OAEP'
},
keyPair.publicKey,
encoded
)
.then(
(value) => {
return value
},
(err) => {
console.log('Encrpyt error', err)
}
)
if (!cipher) {
alert('Encryptin keys failed')
throw new Error('Encryptin keys failed')
}
const privateKeyBytes = await window.crypto.subtle.exportKey(
'pkcs8',
keyPair.privateKey
)
const activationKey = await arrayBufferToBase64(privateKeyBytes)
const licenseKey = await arrayBufferToBase64(cipher)
if (resultCallback)
resultCallback({
activationKey,
licenseKey
})
}
// Mirrors dckey's own encodeCombinedKey()/gzipCompress() (main.js) and
// DC's own splitCombinedLicenceKey() decode side, so this stays a genuine
// end-to-end check of the real format rather than a fixture the app
// happens to accept.
const COMBINED_KEY_PREFIX = 'DCKEY1:'
// Reads a ReadableStream directly via its own reader, rather than through
// `new Response(readable).arrayBuffer()` - Cypress patches fetch/Response
// globally for its network-interception features, which appears to hang
// a Response built locally around a stream that never touches the network
// (writer.write()/close() resolve fine; only the Response-based read never
// settles).
const streamToArrayBuffer = async (
readable: ReadableStream<Uint8Array>
): Promise<ArrayBuffer> => {
const reader = readable.getReader()
const chunks: Uint8Array[] = []
let totalLength = 0
while (true) {
const { done, value } = await reader.read()
if (done) break
chunks.push(value)
totalLength += value.length
}
const result = new Uint8Array(totalLength)
let offset = 0
for (const chunk of chunks) {
result.set(chunk, offset)
offset += chunk.length
}
return result.buffer
}
const generateCombinedKey = async (
licenseKey: string,
activationKey: string
): Promise<string> => {
const payloadBytes = new TextEncoder().encode(
`${licenseKey} ${activationKey}`
)
const compressionStream = new CompressionStream('gzip')
const writer = compressionStream.writable.getWriter()
const [, compressedBuffer] = await Promise.all([
writer.write(payloadBytes).then(() => writer.close()),
streamToArrayBuffer(compressionStream.readable)
])
const compressedBase64 = await arrayBufferToBase64(compressedBuffer)
return COMBINED_KEY_PREFIX + compressedBase64
}
const editTableField = (edits: EditConfigTableCells[], callback?: any) => {
cy.get('td').then((tdNodes: any) => {
for (let edit of edits) {
let correctRow = false
for (let node of tdNodes) {
if (correctRow) {
cy.get(node)
.dblclick()
.then(() => {
// textarea update on long keys
cy.focused().invoke('val', edit.varValue).type('{enter}')
})
correctRow = false
break
}
if (node.innerText.includes(edit.varName)) {
correctRow = true
}
}
}
if (callback) callback()
})
}
const openTableFromTree = (
libNameIncludes: string,
tablename: string,
callback?: any
) => {
cy.get('.app-loading', { timeout: longerCommandTimeout })
.should('not.exist')
.then(() => {
cy.get('.nav-tree clr-tree > clr-tree-node', {
timeout: longerCommandTimeout
}).then((treeNodes: any) => {
let viyaLib
for (let node of treeNodes) {
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
viyaLib = node
break
}
}
cy.get(viyaLib).within(() => {
cy.get('.clr-tree-node-content-container > button').click()
cy.get('.clr-treenode-link').then((innerNodes: any) => {
for (let innerNode of innerNodes) {
if (innerNode.innerText.toLowerCase().includes(tablename)) {
innerNode.click()
if (callback) callback()
break
}
}
})
})
})
})
}
const clickOnAddRow = (callback?: any) => {
cy.get('.btnCtrl button.btn-success')
.click()
.then(() => {
if (callback) callback()
})
}
const clickOnEdit = (callback?: any) => {
cy.get('.btnCtrl button.btn-primary', { timeout: longerCommandTimeout })
.click()
.then(() => {
if (callback) callback()
})
}
const submitTable = (callback?: any) => {
cy.get('.btnCtrl button.btn-primary', { timout: longerCommandTimeout })
.click()
.then(() => {
cy.get(".modal.ng-star-inserted button[type='submit']")
.click()
.then(() => {
if (callback) callback()
})
})
}
const approveTable = (callback?: any) => {
cy.get('button', { timeout: longerCommandTimeout })
.should('contain', 'Approve')
.then((allButtons: any) => {
for (let approvalButton of allButtons) {
if (approvalButton.innerText.toLowerCase().includes('approve')) {
approvalButton.click()
break
}
}
cy.get('button#acceptBtn', { timeout: longerCommandTimeout })
.should('exist')
.should('not.be.disabled')
.click()
.then(() => {
cy.get('app-history', { timeout: longerCommandTimeout })
.should('exist')
.then(() => {
if (callback) callback()
})
})
})
}
const visitPage = (url: string) => {
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
}