- licensing.cy.ts: call proceed() unconditionally via cy.then() instead of conditionally chaining .then() after cy.wait(), so the command queue stays open across the async gap and enqueued commands always run - combinedLicenceKey.ts: replace new Response(stream).arrayBuffer() with an explicit streamToArrayBuffer helper for broader browser support - remove debug-license.cy.ts, an untracked throwaway repro for the promise-handling question
1092 lines
32 KiB
TypeScript
1092 lines
32 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
|
|
|
|
cy.wait(2000)
|
|
|
|
isLicensingPage((result: boolean) => {
|
|
if (result) {
|
|
inputLicenseKeyPage(keys.licenseKey, keys.activationKey)
|
|
|
|
cy.wait(2000)
|
|
}
|
|
|
|
acceptTermsIfPresented((result: boolean) => {
|
|
if (result) {
|
|
cy.wait(10000)
|
|
}
|
|
|
|
visitPage('home')
|
|
|
|
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)
|
|
|
|
cy.wait(2000)
|
|
|
|
acceptTermsIfPresented((result: boolean) => {
|
|
if (result) {
|
|
cy.wait(20000)
|
|
}
|
|
updateLicenseKeyQuick(keysGen, () => {
|
|
cy.wait(2000)
|
|
acceptTermsIfPresented((result: boolean) => {
|
|
if (result) {
|
|
cy.wait(20000)
|
|
}
|
|
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
|
|
|
|
cy.wait(2000)
|
|
|
|
acceptTermsIfPresented((result: boolean) => {
|
|
if (result) {
|
|
cy.wait(20000)
|
|
}
|
|
cy.wait(2000)
|
|
updateLicenseKeyQuick(keysGen, () => {
|
|
cy.wait(2000)
|
|
acceptTermsIfPresented((result: boolean) => {
|
|
if (result) {
|
|
cy.wait(20000)
|
|
}
|
|
verifyLicensingPage(
|
|
'Licence key is expired - please contact',
|
|
(success: boolean) => {
|
|
if (success) {
|
|
verifyLicensingWarning(
|
|
'(FREE Tier) - Problem with licence',
|
|
() => {
|
|
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
|
|
|
|
cy.wait(2000)
|
|
|
|
acceptTermsIfPresented((result: boolean) => {
|
|
if (result) {
|
|
cy.wait(20000)
|
|
}
|
|
cy.wait(2000)
|
|
updateLicenseKeyQuick(keysGen, () => {
|
|
cy.wait(2000)
|
|
acceptTermsIfPresented((result: boolean) => {
|
|
if (result) {
|
|
cy.wait(20000)
|
|
}
|
|
verifyLicensingPage(
|
|
'Licence key is invalid - please contact',
|
|
(success: boolean) => {
|
|
if (success) {
|
|
verifyLicensingWarning(
|
|
'(FREE Tier) - Problem with licence',
|
|
() => {
|
|
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
|
|
|
|
cy.wait(2000)
|
|
|
|
acceptTermsIfPresented((result: boolean) => {
|
|
if (result) {
|
|
cy.wait(20000)
|
|
}
|
|
cy.wait(2000)
|
|
updateLicenseKeyQuick(keysGen, () => {
|
|
cy.wait(2000)
|
|
acceptTermsIfPresented((result: boolean) => {
|
|
if (result) {
|
|
cy.wait(20000)
|
|
}
|
|
verifyLicensingPage(
|
|
'SYSSITE (below) is not found',
|
|
(success: boolean) => {
|
|
if (success) {
|
|
verifyLicensingWarning(
|
|
'(FREE Tier) - Problem with licence',
|
|
() => {
|
|
done()
|
|
}
|
|
)
|
|
}
|
|
}
|
|
)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('6 | pasting an HTTP-format key (identical licence/activation text) on this secure connection warns before applying', (done) => {
|
|
visitPage('licensing/update')
|
|
|
|
cy.get('button').contains('Paste licence').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) => {
|
|
cy.wait(2000)
|
|
|
|
// 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.
|
|
isLicensingPage((result: boolean) => {
|
|
const proceed = () =>
|
|
generateCombinedKey(keysGen.licenseKey, keysGen.activationKey).then(
|
|
(combinedKey) => {
|
|
inputCombinedKeyPage(combinedKey)
|
|
|
|
cy.wait(2000)
|
|
|
|
acceptTermsIfPresented((termsResult: boolean) => {
|
|
if (termsResult) {
|
|
cy.wait(10000)
|
|
}
|
|
|
|
visitPage('home')
|
|
|
|
cy.get('.nav-tree clr-tree > clr-tree-node', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
done()
|
|
})
|
|
})
|
|
}
|
|
)
|
|
|
|
if (!result) {
|
|
visitPage('licensing/update')
|
|
cy.wait(2000)
|
|
}
|
|
|
|
// 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) => {
|
|
cy.wait(2000)
|
|
|
|
// Navigate there explicitly rather than only acting when already on
|
|
// the licensing page - see test 7's own comment for why.
|
|
isLicensingPage((result: boolean) => {
|
|
const proceed = () =>
|
|
generateCombinedKey(keysGen.licenseKey, keysGen.activationKey).then(
|
|
(combinedKey) => {
|
|
cy.get('button').contains('Paste licence').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()
|
|
|
|
cy.wait(2000)
|
|
|
|
acceptTermsIfPresented((termsResult: boolean) => {
|
|
if (termsResult) {
|
|
cy.wait(10000)
|
|
}
|
|
|
|
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.
|
|
if (!result) {
|
|
visitPage('licensing/update')
|
|
cy.wait(2000)
|
|
}
|
|
cy.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) => {
|
|
cy.wait(2000)
|
|
|
|
// Navigate there explicitly rather than only acting when already on
|
|
// the licensing page - see test 7's own comment for why.
|
|
isLicensingPage((result: boolean) => {
|
|
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()
|
|
|
|
cy.wait(2000)
|
|
|
|
acceptTermsIfPresented((termsResult: boolean) => {
|
|
if (termsResult) {
|
|
cy.wait(10000)
|
|
}
|
|
|
|
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.
|
|
if (!result) {
|
|
visitPage('licensing/update')
|
|
cy.wait(2000)
|
|
}
|
|
cy.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) => {
|
|
cy.wait(2000)
|
|
|
|
// Navigate there explicitly rather than only acting when already on
|
|
// the licensing page - see test 7's own comment for why.
|
|
isLicensingPage((result: boolean) => {
|
|
const proceed = () =>
|
|
generateCombinedKey(keysGen.licenseKey, keysGen.activationKey).then(
|
|
(combinedKey) => {
|
|
cy.get('button').contains('Paste licence').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.
|
|
if (!result) {
|
|
visitPage('licensing/update')
|
|
cy.wait(2000).then(() => proceed())
|
|
} else {
|
|
proceed()
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
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) => {
|
|
cy.wait(2000)
|
|
|
|
acceptTermsIfPresented((result: boolean) => {
|
|
if (result) {
|
|
cy.wait(20000)
|
|
}
|
|
updateLicenseKeyQuick(keysGen, () => {
|
|
cy.wait(2000)
|
|
acceptTermsIfPresented((result: boolean) => {
|
|
if (result) {
|
|
cy.wait(20000)
|
|
}
|
|
|
|
updateLicenseKeyQuick(keysGen2, () => {
|
|
cy.wait(2000)
|
|
|
|
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')
|
|
|
|
cy.wait(2000)
|
|
|
|
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) => {
|
|
cy.wait(2000)
|
|
acceptTermsIfPresented((result: boolean) => {
|
|
if (result) {
|
|
cy.wait(20000)
|
|
}
|
|
updateLicenseKeyQuick(keysGen, () => {
|
|
cy.wait(2000)
|
|
acceptTermsIfPresented((result: boolean) => {
|
|
if (result) {
|
|
cy.wait(20000)
|
|
}
|
|
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, () => {
|
|
cy.wait(2000)
|
|
verifyLicensingWarning(
|
|
'The registered number of users exceeds the limit specified for your license.',
|
|
() => {
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
}
|
|
)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
}
|
|
})
|
|
|
|
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(() => {
|
|
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) => {
|
|
// visitPage('home')
|
|
|
|
cy.wait(1000)
|
|
isLicensingPage((result: boolean) => {
|
|
if (result) {
|
|
cy.get('.key-error')
|
|
.should('contain', text)
|
|
.then((treeNodes: any) => {
|
|
callback(true)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
const verifyLicensingWarning = (text: string, callback: any) => {
|
|
visitPage('home')
|
|
|
|
cy.wait(1000)
|
|
cy.get("div[role='alert'] .alert-text")
|
|
.invoke('text')
|
|
.should('contain', text)
|
|
.then(() => {
|
|
callback()
|
|
})
|
|
}
|
|
|
|
const inputLicenseKeyPage = (licenseKey: string, activationKey: string) => {
|
|
cy.get('button').contains('Paste licence').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()
|
|
}
|
|
|
|
const inputCombinedKeyPage = (combinedKey: string) => {
|
|
cy.get('button').contains('Paste licence').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')
|
|
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(() => {
|
|
cy.wait(2000)
|
|
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(() => {
|
|
cy.wait(2000)
|
|
approveTable(() => {
|
|
cy.reload()
|
|
if (callback) callback()
|
|
})
|
|
})
|
|
}
|
|
)
|
|
})
|
|
}
|
|
|
|
const updateLicenseKeyQuick = (keys: any, callback: any) => {
|
|
isLicensingPage((result: boolean) => {
|
|
if (!result) {
|
|
visitPage('licensing/update')
|
|
cy.wait(2000)
|
|
}
|
|
inputLicenseKeyPage(keys.licenseKey, keys.activationKey)
|
|
|
|
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}`)
|
|
}
|