diff --git a/scripts/pit/its/cc-identity-management.js b/scripts/pit/its/cc-identity-management.js index c573c6eb..6b774ae3 100644 --- a/scripts/pit/its/cc-identity-management.js +++ b/scripts/pit/its/cc-identity-management.js @@ -1,7 +1,5 @@ const { expect} = require('@playwright/test'); -const fs = require('fs'); -const {log, args, createPage, closePage, takeScreenshot, waitForServerReady} = require('./test-utils'); - +const {log, err, args, createPage, closePage, takeScreenshot, waitForServerReady} = require('./test-utils'); (async () => { const arg = args(); @@ -23,26 +21,39 @@ const {log, args, createPage, closePage, takeScreenshot, waitForServerReady} = r await expect(page.getByLabel('Email')).toBeVisible(); await takeScreenshot(page, __filename, 'view-loaded'); - log(`Logging in as ${arg.login} ${arg.pass}...\n`); + log(`Logging in CC as ${arg.login} ${arg.pass}...\n`); await page.getByLabel('Email').fill(arg.login); await page.getByLabel('Password').fill(arg.pass); await page.getByRole('button', {name: 'Sign In'}).click() await takeScreenshot(page, __filename, 'logged-in'); + log(`Changing Settings for ${app}...\n`); await page.getByRole('link', { name: 'Settings', }).click(); await takeScreenshot(page, __filename, 'settings'); const url = await page.locator(anchorSelectorURL).getAttribute('href'); - log(`App: ${app} installed in: ${url}\n`); + + log(`Checking that ${app} installed in ${url} is running ...\n`); await page.locator('vaadin-select vaadin-input-container div').click(); - await page.getByRole('option', { name: 'bakery-cc' }).locator('div').nth(2).click(); + await page.getByRole('option', { name: app }).locator('div').nth(2).click(); await takeScreenshot(page, __filename, 'selected-app'); + + // When app is not running, localization button might not be enabled + let pageApp = await createPage(arg.headless, arg.ignoreHTTPSErrors); + await waitForServerReady(pageApp, url); + await takeScreenshot(pageApp, __filename, `app-${app}-running`); + await closePage(pageApp); + // Button is enabled after app is running, let's see + log(`Enabling identity Management ...\n`); await page.getByRole('link', { name: 'Identity Management' }).click(); + await takeScreenshot(page, __filename, 'identity-link-clicked'); + await page.waitForTimeout(2000); await page.getByRole('button', { name: 'Enable Identity Management' }).click(); - await takeScreenshot(page, __filename, 'app-updated'); - await takeScreenshot(page, __filename, 'enabled'); + await takeScreenshot(page, __filename, 'identity-enabled'); + log(`Adding Role, Group and User ...\n`); await page.getByRole('link', { name: 'Roles' }).click(); + await page.waitForTimeout(2000); await page.getByRole('button', { name: 'Create' }).click(); await takeScreenshot(page, __filename, 'role-form'); await page.getByLabel('Name').fill(role); @@ -52,6 +63,7 @@ const {log, args, createPage, closePage, takeScreenshot, waitForServerReady} = r await takeScreenshot(page, __filename, 'role-created'); await page.getByRole('link', { name: 'Groups' }).click(); + await page.waitForTimeout(2000); await page.getByRole('button', { name: 'Create' }).click(); await takeScreenshot(page, __filename, 'group-form'); await page.getByLabel('Name').fill(group); @@ -61,6 +73,7 @@ const {log, args, createPage, closePage, takeScreenshot, waitForServerReady} = r await takeScreenshot(page, __filename, 'group-created'); await page.getByRole('link', { name: 'Users' }).click(); + await page.waitForTimeout(2000); await page.getByRole('button', { name: 'Create' }).click(); await takeScreenshot(page, __filename, 'user-form'); await page.getByLabel('First Name').fill(role); @@ -72,15 +85,43 @@ const {log, args, createPage, closePage, takeScreenshot, waitForServerReady} = r await page.getByRole('contentinfo').getByRole('button', { name: 'Create' }).click(); await takeScreenshot(page, __filename, 'user-created'); - await waitForServerReady(page, url); - await takeScreenshot(page, __filename, `app-${app}-loaded`); - log(`Logging in ${app} as ${user} ...\n`); - await page.getByLabel('Email').fill(user); - await page.getByLabel('Password').fill(role); - await page.getByRole('button', {name: 'Sign In'}).click() - await takeScreenshot(page, __filename, `logged-in-${app}`); - await expect(page.getByRole('button', { name: 'New order' })).toBeVisible(); + pageApp = await createPage(arg.headless, arg.ignoreHTTPSErrors); + await waitForServerReady(pageApp, url); + await takeScreenshot(pageApp, __filename, `app-${app}-loaded`); + await pageApp.getByLabel('Email').fill(user); + await pageApp.getByLabel('Password').fill(role); + await pageApp.getByRole('button', {name: 'Sign In'}).click() + await takeScreenshot(pageApp, __filename, `logged-in-${app}`); + await expect(pageApp.getByRole('button', { name: 'New order' })).toBeVisible(); + await closePage(pageApp); + log('Cleaning up...\n'); + try { + await page.getByRole('link', { name: 'Roles' }).click(); + await page.waitForTimeout(2000); + await page.getByText(role, { exact: true }).nth(1).click(); + await page.getByRole('button', { name: 'Delete' }).click(); + await page.locator('vaadin-confirm-dialog-overlay').getByRole('button', { name: 'Delete' }).click(); + await page.getByRole('link', { name: 'Groups' }).click(); + await page.waitForTimeout(2000); + await page.getByText(group, { exact: true }).click(); + await page.getByRole('button', { name: 'Delete' }).click(); + await page.locator('vaadin-confirm-dialog-overlay').getByRole('button', { name: 'Delete' }).click(); + await page.getByRole('link', { name: 'Users' }).click(); + await page.waitForTimeout(2000); + await page.getByText(user, { exact: true }).click(); + await page.getByRole('button', { name: 'Delete' }).click(); + await page.locator('vaadin-confirm-dialog-overlay').getByRole('button', { name: 'Delete' }).click(); + await page.getByRole('link', { name: 'Settings' }).click(); + await page.waitForTimeout(2000); + await page.locator('vaadin-grid').getByText('bakery-cc', { exact: true }).click(); + await page.getByLabel('Identity Management').uncheck(); + await page.getByRole('button', { name: 'Disable' }).click(); + await page.getByRole('button', { name: 'Update' }).click(); + } catch (error) { + err(`Error cleaning up: ${error}\n`); + await takeScreenshot(page, __filename, 'error-cleaning'); + } await closePage(page); })(); diff --git a/scripts/pit/its/cc-install-apps.js b/scripts/pit/its/cc-install-apps.js index aec17298..6357e9c1 100644 --- a/scripts/pit/its/cc-install-apps.js +++ b/scripts/pit/its/cc-install-apps.js @@ -1,6 +1,6 @@ const { expect} = require('@playwright/test'); const fs = require('fs'); -const {log, args, createPage, closePage, takeScreenshot, waitForServerReady} = require('./test-utils'); +const {log, args, run, createPage, closePage, takeScreenshot, waitForServerReady} = require('./test-utils'); const arg = args(); let count = 0; @@ -12,8 +12,6 @@ async function installApp(app, page) { const cert = [ domain, uri ].map(a => `${a}.pem`).filter( a => fs.existsSync(a))[0] console.log(`Installing App: ${app} URI: ${uri} Cert: ${cert}`); - await takeScreenshot(page, __filename, `page-loaded-${app}`); - await page.getByRole('listitem').filter({ hasText: 'Settings'}).click() await page.getByRole('button', {name: 'Deploy'}).click() await takeScreenshot(page, __filename, `form-opened-${app}`); @@ -22,22 +20,26 @@ async function installApp(app, page) { await page.getByLabel('Image', {exact: true}).fill(`k8sdemos/${app}:latest`) await page.getByLabel('Application URI', {exact: true}).locator('input[type="text"]').fill(uri) if (cert) { + log(`Uploading certificate ${cert} for ${app}...\n`); await page.getByLabel('Upload').click(); const fileChooserPromise = page.waitForEvent('filechooser'); await page.getByText('Browse').click(); const fileChooser = await fileChooserPromise; await fileChooser.setFiles(cert); - fileChooserPromise.then(await page.locator('.detail-layout').getByRole('button', {name: 'Deploy'}).click()) + await takeScreenshot(page, __filename, `form-filled-${app}`); + await page.locator('.detail-layout').getByRole('button', {name: 'Deploy'}).click(); } else { + log(`No certificate found for ${app}...\n`); + log(`No certificate found for ${app}\n`); + run(`pwd`); + run(`ls -l`); await page.getByLabel('Generate').click(); + await takeScreenshot(page, __filename, `form-filled-${app}`); await page.locator('.detail-layout').getByRole('button', {name: 'Deploy'}).click(); } - await takeScreenshot(page, __filename, `form-filled-${app}`); - await page.getByRole('listitem').filter({ hasText: 'Settings'}).click() - - await takeScreenshot(page, __filename, `application-created-${app}`); + await takeScreenshot(page, __filename, `form-saved-${app}`); await expect(page.locator('vaadin-grid').getByText(app, { exact: true })).toBeVisible(); await expect(await page.getByRole('listitem').filter({ hasText: 'Applications'}) @@ -64,13 +66,19 @@ async function installApp(app, page) { await installApp(app, page); } - log(`Waiting for the applications to be available...\n`); + log(`Waiting for 2 applications to be available...\n`); const selector = 'vaadin-grid-cell-content span[theme="badge success"]'; - await expect(page.locator(selector).nth(0)).toBeVisible({ timeout: 180000 }); + const startTime = Date.now(); + + await expect(page.locator(selector).nth(0)).toBeVisible({ timeout: 280000 }); + const firstAppTime = (Date.now() - startTime) / 1000; await takeScreenshot(page, __filename, 'app-1-available'); - log(`First application is available\n`); - await expect(page.locator(selector).nth(1)).toBeVisible({ timeout: 180000 }); + log(`First application is available after ${firstAppTime.toFixed(2)} seconds\n`); + + await expect(page.locator(selector).nth(1)).toBeVisible({ timeout: 280000 }); + const secondAppTime = (Date.now() - startTime) / 1000; await takeScreenshot(page, __filename, 'app-2-available'); - log(`Second application is available\n`); + log(`Second application is available after ${secondAppTime.toFixed(2)} seconds\n`); + await closePage(page); })(); diff --git a/scripts/pit/its/cc-localization.js b/scripts/pit/its/cc-localization.js index 382f5ce9..c81d2c7f 100644 --- a/scripts/pit/its/cc-localization.js +++ b/scripts/pit/its/cc-localization.js @@ -1,74 +1,119 @@ -const {chromium} = require('playwright'); -const sleep = ms => new Promise(r => setTimeout(r, ms)); -const path = require('path'); -const {expect} = require('@playwright/test'); - -const ADMIN_EMAIL = 'john.doe@admin.com'; -const ADMIN_PASSWORD = 'adminPassword'; - -let headless = false, host = 'localhost', port = '8000', hub = false; -process.argv.forEach(a => { - if (/^--headless/.test(a)) { - headless = true; - } else if (/^--ip=/.test(a)) { - ip = a.split('=')[1]; - } else if (/^--port=/.test(a)) { - port = a.split('=')[1]; - } -}); +const { expect} = require('@playwright/test'); +const fs = require('fs'); +const {log, err, args, run, createPage, closePage, takeScreenshot, waitForServerReady} = require('./test-utils'); +const { assert } = require('console'); (async () => { - const browser = await chromium.launch({ - headless: headless, - chromiumSandbox: false - }); - const context = await browser.newContext({ignoreHTTPSErrors: true}); - - // Open new page - const page = await context.newPage(); - page.on('console', msg => console.log("> CONSOLE:", (msg.text() + ' - ' + msg.location().url).replace(/\s+/g, ' '))); - page.on('pageerror', err => console.log("> PAGEERROR:", ('' + err).replace(/\s+/g, ' '))); - - // Go to http://${host}:${port}/ - await page.goto(`http://${host}:${port}/`); - - await page.getByLabel('Email').fill(ADMIN_EMAIL) - await page.getByLabel('Password', {exact: true}).fill(ADMIN_PASSWORD) - await page.getByRole('button', {name: 'Sign In'}).click() - - await page.goto(`http://${host}:${port}/settings/apps/app1`); - - const locOpt = page.getByLabel('Localization').click(); - if(await !page.getByLabel('Localization').isChecked()){ - await page.getByLabel('Localization').click({timeout:60000}) + const arg = args(); + if (!arg.login) { + log(`Skipping the setup of Control center because of missing --email= parameter\n`) + process.exit(1); } + const app = `bakery-cc`; + const user = 'admin@vaadin.com'; + const password = 'admin'; + const downloadsDir = './downloads'; + const propsFile = 'translations.properties'; - await page.getByRole('button', {name: 'Update'}).click() - - await page.goto(`http://${host}:${port}/app/app1/i18n/translations`); - - await page.getByRole('menuitem').click() - await page.getByText('Upload translations').click() + const page = await createPage(arg.headless, arg.ignoreHTTPSErrors); + await waitForServerReady(page, arg.url); + await expect(page.getByLabel('Email')).toBeVisible(); + await takeScreenshot(page, __filename, 'view-loaded'); + log(`Logging in CC as ${arg.login} ${arg.pass}...\n`); + await page.getByLabel('Email').fill(arg.login); + await page.getByLabel('Password').fill(arg.pass); + await page.getByRole('button', {name: 'Sign In'}).click() + await takeScreenshot(page, __filename, 'logged-in'); + + log(`Changing Settings for ${app}...\n`); + await page.getByRole('link', { name: 'Settings', }).click(); + await takeScreenshot(page, __filename, 'settings'); + const anchorSelectorURL = `//vaadin-grid-cell-content[.//span[normalize-space(text())="${app}"]]//a`; + const url = await page.locator(anchorSelectorURL).getAttribute('href'); + const previewUrl = url.replace(/:\/\//, '://preview.'); + + log(`Checking that ${app} installed in ${url} is running ...\n`); + // When app is not running, localization cannot be enabled + const pageApp = await createPage(arg.headless, arg.ignoreHTTPSErrors); + await waitForServerReady(pageApp, url); + await takeScreenshot(pageApp, __filename, 'app-running'); + await closePage(pageApp); + + log(`Uploading and updating localization keys ...\n`); + await page.locator('vaadin-select vaadin-input-container div').click(); + await page.getByRole('option', { name: app }).locator('div').nth(2).click(); + await takeScreenshot(page, __filename, 'selected-app'); + + await page.getByRole('link', { name: 'Localization' }).click(); + await page.getByRole('button', { name: 'Enable Localization' }).click(); + await takeScreenshot(page, __filename, 'localization-enabled'); + + fs.writeFileSync(propsFile, 'app.title=Bakery\n'); + await page.getByLabel('Manage translations').locator('svg').click(); + await page.getByText('Upload translations').click(); + await page.getByLabel('I understand that this will').check(); const fileChooserPromise = page.waitForEvent('filechooser'); - await page.getByText('Upload Files...').click(); + await page.getByRole('button', { name: 'Upload Files...' }).click(); const fileChooser = await fileChooserPromise; - await fileChooser.setFiles(path.join(__dirname, 'translations.properties')); - await page.getByLabel('I understand that this will replace all corresponding data.').check() - await page.getByRole('button', {name: 'Replace data'}).click() - - await page.getByText('Hello anonymous!').click(); - await page.locator('vaadin-grid-cell-content').getByRole('textbox').fill('Test'); - - await page.getByText('Say hello').click(); - await page.locator('vaadin-grid-cell-content').getByRole('textbox').fill('Say bonjour'); - await page.locator('.confirm-button').click(); - - expect(page.getByText('Hello anonymous!')).toBeVisible() - expect(page.locator('vaadin-grid-cell-content').filter({ hasText: 'Say bonjour' })).toBeVisible() - - // --------------------- - await context.close(); - await browser.close(); + await fileChooser.setFiles(propsFile); + await page.getByRole('button', { name: 'Replace data' }).click(); + fs.unlinkSync(propsFile); + + await takeScreenshot(page, __filename, 'localization-loaded'); + await page.getByText('Bakery', { exact: true }).click(); + await page.locator('vaadin-text-area.inline textarea').fill('Panaderia'); + await page.locator('vaadin-grid').getByRole('button').first().click(); + await takeScreenshot(page, __filename, 'localization-changed'); + + log(`Downloading and checking localization keys ...\n`); + await page.getByLabel('Manage translations').locator('svg').click(); + const downloadPromise = page.waitForEvent('download'); + await page.getByText('Download translations').click(); + const download = await downloadPromise; + const filePath = `${downloadsDir}/${download.suggestedFilename()}`; + await download.saveAs(filePath); + + await run(`unzip -d ${downloadsDir} -o ${filePath}`); + const str = await fs.readFileSync('./downloads/translations.properties', 'utf8'); + assert(str.includes('app.title=Panaderia')); + await fs.rmSync(downloadsDir, { recursive: true }); + + log(`Starting preview server\n`); + await page.getByRole('button', { name: 'Start preview' }).click(); + await page.waitForTimeout(5000); + + log(`Testing that preview page: ${previewUrl} is up and running\n`); + const pagePrev = await createPage(arg.headless, true /* preview pages do not have a valid certificate */); + await waitForServerReady(pagePrev, previewUrl); + await takeScreenshot(pagePrev, __filename, 'preview-ready'); + const text = await pagePrev.getByText(/Password|Dashboard/).textContent(); + if (text.includes('Password')) { + await pagePrev.getByLabel('Email').fill(user); + await pagePrev.getByLabel('Password').fill(password); + await pagePrev.getByRole('button', {name: 'Sign In'}).click() + await takeScreenshot(pagePrev, __filename, 'preview-logged-in'); + await expect(pagePrev.getByRole('button', { name: 'New order' })).toBeVisible(); + await takeScreenshot(pagePrev, __filename, 'preview-loaded'); + } + // TODO: bakery is not internationalized + // await expect(pagePrev.getByText('Panaderia', { exact: true })).toBeVisible(); + await closePage(pagePrev); + + log('Cleaning up...\n'); + try { + await page.getByRole('button', { name: 'Stop preview' }).click(); + await page.getByRole('link', { name: 'Settings' }).click(); + await page.waitForTimeout(2000); + await page.locator('vaadin-grid').getByText('bakery-cc', { exact: true }).click(); + await page.waitForTimeout(2000); + await page.getByLabel('Localization').uncheck(); + await page.getByRole('button', { name: 'Disable' }).click(); + await page.getByRole('button', { name: 'Update' }).click(); + } catch (error) { + err(`Error cleaning up: ${error}\n`); + await takeScreenshot(page, __filename, 'error-cleaning'); + } + await closePage(page); })(); diff --git a/scripts/pit/its/cc-setup.js b/scripts/pit/its/cc-setup.js index ceb04c28..b307c4bd 100644 --- a/scripts/pit/its/cc-setup.js +++ b/scripts/pit/its/cc-setup.js @@ -33,8 +33,8 @@ const {log, run, args, createPage, closePage, takeScreenshot, waitForServerReady await takeScreenshot(page, __filename, 'password-changed'); - await page.getByLabel('First Name').fill(arg.login); - await page.getByLabel('Last Name').fill(arg.login); + await page.getByLabel('First Name').fill(arg.login.split('@')[0]); + await page.getByLabel('Last Name').fill(arg.login.split('@')[1]); await page.getByRole('button', { name: 'Submit' }).click(); await takeScreenshot(page, __filename, 'user-configured'); diff --git a/scripts/pit/its/test-utils.js b/scripts/pit/its/test-utils.js index 7a585be9..9b1fa4d1 100644 --- a/scripts/pit/its/test-utils.js +++ b/scripts/pit/its/test-utils.js @@ -8,9 +8,26 @@ defineConfig({ expect: {timeout: 30 * 1000}, }); -const log = (s) => process.stderr.write(` ${s}`); +function log(...args) { + process.stderr.write(`\x1b[0m> \x1b[0;32m${args}\x1b[0m`); +} +function out(...args) { + process.stderr.write(`\x1b[2m\x1b[196m${args}\x1b[0m`); +} +function ok(...args) { + process.stderr.write(`\x1b[2m\x1b[92m${args}\x1b[0m`); +} +function warn(...args) { + process.stderr.write(`\x1b[2m\x1b[91m${args}\x1b[0m`); +} +function err(...args) { + process.stderr.write(`\x1b[0;31m${args}\x1b[0m`.split('\n')[0] + '\n'); + out(args); +} + const run = async (cmd) => (await promisify(exec)(cmd)).stdout; + const args = () => { const ret = { headless: false, @@ -48,12 +65,16 @@ async function createPage(headless, ignoreHTTPSErrors) { const browser = await chromium.launch({ headless: headless, chromiumSandbox: false, - slowMo: 500 + slowMo: 500, + args: ['--window-position=0,0'] }); - const context = await browser.newContext({ignoreHTTPSErrors: ignoreHTTPSErrors }); + const context = await browser.newContext({ignoreHTTPSErrors: ignoreHTTPSErrors, viewport: { width: 1792, height: 970 } }); const page = await context.newPage(); - page.on('console', msg => console.log("> CONSOLE:", (msg.text() + ' - ' + msg.location().url).replace(/\s+/g, ' '))); - page.on('pageerror', err => console.log("> PAGEERROR:", ('' + err).replace(/\s+/g, ' '))); + page.on('console', msg => { + const text = `${msg.text()} - ${msg.location().url}`.replace(/\s+/g, ' '); + if (!/vaadinPush|favicon.ico|Autofocus/.test(text)) out("> CONSOLE:", text, '\n'); + }); + page.on('pageerror', e => warn("> JSERROR:", ('' + e).replace(/\s+/g, ' '), '\n')); page.browser = browser; return page; } @@ -68,34 +89,39 @@ async function takeScreenshot(page, name, descr) { const scr = path.basename(name); const cnt = String(++sscount).padStart(2, "0"); const file = `${screenshots}/${scr}-${cnt}-${descr}.png`; - await page.waitForTimeout(1000); + await page.waitForTimeout(/^win/.test(process.platform) ? 10000 : process.env.GITHUB_ACTIONS ? 5000 : 1500); await page.screenshot({ path: file }); - log(`Screenshot taken: ${file}\n`); + out(` 📸 Screenshot taken: ${file}\n`); } // Wait for the server to be ready and to get a valid response async function waitForServerReady(page, url, options = {}) { const { - maxRetries = 20, // Max number of retries + maxRetries = 35, // Max number of retries retryInterval = 5000 // Interval between retries in milliseconds } = options; log(`Opening ${url}\n`); + page.waitForTimeout(1000); for (let attempt = 0; attempt < maxRetries; attempt++) { try { - const response = await page.goto(url); + const response = await page.goto(url, {timeout: 120000}); // Check if the response status is not 503 - if (response && response.status() !== 503) { - log(`Attempt ${attempt} Server is ready and returned a valid response. ${response.status()}\n`); + if (response && response.status() < 400) { + await page.waitForTimeout(1500); + ok(` ✓ Attempt ${attempt} Server is ready and returned a valid response. ${response.status()}\n`); return response; } else { - log(`Attempt ${attempt} Server is not ready yet. ${response.status()}\n`); + out(` ⏲ Attempt ${attempt} Server is not ready yet. ${response.status()}\n`); } } catch (error) { if (error.message.includes('net::ERR_CERT_AUTHORITY_INVALID')) { - log(`Attempt ${attempt} Server has not a valid certificate, install it for ${url} or use --notls flag\n`); + err(` ⏲ Attempt ${attempt} Server has not a valid certificate, install it for ${url} or use --notls flag\n`); } else { - log(`Attempt ${attempt} Server failed with error: ${error.message}\n`); + err(` ⏲ Attempt ${attempt} Server failed with error: ${error.message}\n`); + } + if (attempt >= 10) { + throw new Error(`Server Error ${error}.\n`); } } await page.waitForTimeout(retryInterval); @@ -104,7 +130,7 @@ async function waitForServerReady(page, url, options = {}) { } module.exports = { - log, + log, out, err, warn, run, args, createPage, diff --git a/scripts/pit/lib/lib-args.sh b/scripts/pit/lib/lib-args.sh index 4cd35d4c..c2e4bab8 100644 --- a/scripts/pit/lib/lib-args.sh +++ b/scripts/pit/lib/lib-args.sh @@ -21,7 +21,9 @@ Use: $0 with the next options: --skip-prod Skip production validations --skip-dev Skip dev-mode validations --skip-clean Do not clean maven cache + --skip-helm Do not re-install control-center with helm and continue running tests --skip-pw Do not run playwright tests + --cluster=name Run tests in an existing k8s cluster --keep-cc Keep control-center running after tests --pnpm Use pnpm instead of npm to speed up frontend compilation (default npm) --vite Use vite inetad of webpack to speed up frontend compilation (default webpack) @@ -36,7 +38,7 @@ Use: $0 with the next options: everything after this argument is the function name and arguments passed to the function. you should take care with arguments that contain spaces, they should be quoted twice. --help Show this message - --starters=list List of demos or presets separated by comma to run (default: all) valid options:`echo ,$DEFAULT_STARTERS | sed -e 's/,/\n · /g'` + --starters=list List of demos or presets separated by comma to run (default: all) valid options:`echo ,$DEFAULT_STARTERS | tr ' ' , | sed -e 's/,/\n · /g'` EOF exit 1 } @@ -73,6 +75,8 @@ checkArgs() { --skip-dev) NODEV=true;; --skip-prod) NOPROD=true;; --skip-pw) SKIPPW=true;; + --cluster=*) CLUSTER="$arg";; + --skip-helm) SKIPHELM=true;; --keep-cc) KEEPCC=true;; --pnpm) PNPM="-Dpnpm.enable=true";; --vite) VITE=true;; diff --git a/scripts/pit/lib/lib-ccenter.sh b/scripts/pit/lib/lib-ccenter.sh index 47016a7a..70b6a88b 100644 --- a/scripts/pit/lib/lib-ccenter.sh +++ b/scripts/pit/lib/lib-ccenter.sh @@ -14,12 +14,11 @@ CC_TLS_K=cc-control-login-tls ## Ingress names CC_ING_A=control-center CC_ING_K=control-center-keycloak-ingress -## K8s cluster and namespace -CC_CLUSTER=cc-cluster +# Namespace used for CC CC_NS=control-center ## UI tests to run after the control-center is installed -CC_TESTS="cc-setup.js cc-install-apps.js cc-identity-management.js" +CC_TESTS=${CC_TESTS:-cc-setup.js cc-install-apps.js cc-identity-management.js cc-localization.js} checkDockerRunning() { if ! docker ps > /dev/null 2>&1; then @@ -30,6 +29,7 @@ checkDockerRunning() { ## Install Control Center with Helm installCC() { + [ -n "SKIPHELM" ] && H=`kubectl get pods 2>&1` && echo "$H" | egrep -q 'control-center-[0-9abcdef]+-..... ' && return 0 [ -n "$VERBOSE" ] && D=--debug || D="" [ -n "$CC_KEY" -a -n "$CC_CERT" ] && args="--set app.tlsSecret=$CC_TLS_A --set keycloak.tlsSecret=$CC_TLS_K" || args="" runCmd "$TEST" "Installing Vaadin Control Center" \ @@ -72,31 +72,46 @@ uninstallCC() { [ $? = 0 ] && echo "$H" | egrep -q "^$CC_NS " || return 0 [ -n "$VERBOSE" ] && HD=--debug && KD=--v=10 runCmd "$TEST" "Uninstalling Control-Center" helm uninstall control-center --wait -n $CC_NS $HD - runCmd "$TEST" "Removing namespace $CC_NS" kubectl delete ns $CC_NS $KD + runCmd "$TEST" "Removing namespace $CC_NS" kubectl delete ns $CC_NS $KD $1 +} + +getTLs() { + log "TLS config for ingress: $1" + H=`kubectl get ingress $1 -n $CC_NS -o jsonpath='{.spec.rules[0].host}'` + HS=`kubectl get ingress $1 -n $CC_NS -o jsonpath='{.spec.tls[*].hosts[*]}'` + S=`kubectl get ingress $1 -n $CC_NS -o jsonpath='{.spec.tls[*].secretName}'` + C=`kubectl get secret $S -n $CC_NS -o go-template='{{ index .data "tls.crt" | base64decode }}' | openssl x509 -noout -issuer -subject -enddate | tr '\n' ' '` + log "Host: $H is in ingress $1 with TLS config\n hosts: $HS secret: $S cert: $C" } checkTls() { [ -n "$TEST" ] && return 0 + log "Checking TLS certificates for all ingresses hosted in the cluster" + [ -n "$TEST" -o -n "$SKIPHELM" ] && return 0 for i in `kubectl get ingresses -n $CC_NS | grep nginx | awk '{print $1}'`; do - log "$i" - H=`kubectl get ingress $i -n $CC_NS -o jsonpath='{.spec.rules[0].host}'` - HS=`kubectl get ingress $i -n $CC_NS -o jsonpath='{.spec.tls[*].hosts[*]}'` - S=`kubectl get ingress $i -n $CC_NS -o jsonpath='{.spec.tls[*].secretName}'` - C=`kubectl get secret $S -n $CC_NS -o go-template='{{ index .data "tls.crt" | base64decode }}' | openssl x509 -noout -issuer -subject -enddate | tr '\n' ' '` - log "Host: $H is in ingress $i with TLS config\n hosts: $HS secret: $S cert: $C" + getTLs "$i" done } +reloadIngress() { + [ -n "$TEST" ] && return 0 + pod=`kubectl -n $CC_NS get pods | grep control-center-ingress-nginx-controller | awk '{print $1}'` || return 1 + [ -n "$pod" ] && runCmd "$TEST" "Reloading nginx in $pod" "kubectl exec $pod -n "$CC_NS" -- nginx -s reload" || return 1 + [ -z "$TEST" ] && sleep 3 +} + ## Configure secrets for the control-center and the keycloak servers installTls() { + [ -n "$SKIPHELM" ] && return 0 [ -z "$CC_KEY" -o -z "$CC_CERT" ] && log "No CC_KEY and CC_CERT provided, skiping TLS installation" && return 0 - [ -n "$CC_FULL" ] && CC_CERT=$CC_FULL - [ -z "$TEST" ] && log "Installing TLS $CC_TLS for $CC_CONTROL and $CC_AUT" || cmd "## Creating TLS file '$CC_DOMAIN.pem' from envs" + # [ -n "$CC_FULL" ] && CC_CERT="$CC_FULL" + [ -z "$TEST" ] && log "Installing TLS $CC_TLS for $CC_CONTROL and $CC_AUTH" || cmd "## Creating TLS file '$CC_DOMAIN.pem' from envs" f1=cc-tls.crt f2=cc-tls.key + f3=$CC_DOMAIN.pem echo -e "$CC_CERT" > $f1 || return 1 echo -e "$CC_KEY" > $f2 || return 1 - cat $f1 $f2 > $CC_DOMAIN.pem + cat $f1 $f2 > $f3 # remove old secrets if they exist (only needed for testing purposes since secrets are deleted before running the helm chart) kubectl get secret $CC_TLS_A -n $CC_NS >/dev/null 2>&1 && kubectl delete secret $CC_TLS_A -n $CC_NS @@ -118,9 +133,7 @@ installTls() { [ -n "$TEST" ] && return 0 - pod=`kubectl -n $CC_NS get pods | grep control-center-ingress-nginx-controller | awk '{print $1}'` || return 1 - [ -n "$pod" ] && runCmd "$TEST" "Reloading nginx in $pod" "kubectl exec $pod -n "$CC_NS" -- nginx -s reload" || return 1 - runCmd "$TEST" "Waiting for reloading ingress" sleep 5 + reloadIngress || return 1 } ## Show temporary user-email and password in the terminal @@ -135,42 +148,73 @@ showTemporaryPassword() { ## Run Playwright tests for the control-center runPwTests() { computeNpm + [ -d screenshots.out ] && runCmd "$TEST" "Removing old screenshots" "rm -rf screenshots.out" [ -n "$SKIPPW" ] && return 0 [ -z "$CC_CERT" -o -z "$CC_KEY" ] && NO_TLS=--notls || NO_TLS="" for f in $CC_TESTS; do runPlaywrightTests "$PIT_SCR_FOLDER/its/$f" "" "prod" "control-center" --url=https://$CC_CONTROL --login=$CC_EMAIL $NO_TLS || return 1 - sleep 3 + if [ "$f" = cc-install-apps.js ]; then + reloadIngress && checkTls || return 1 + fi done } +setClusterContext() { + [ "$1" = "$KIND_CLUSTER" ] && current=kind-$1 || current=$1 + ns=$2 + H=`kubectl config get-contexts | tr '*' ' ' | awk '{print $1}' | egrep "^$current$"` + [ -z "$H" ] && log "Cluster $current not found in kubectl contexts" && return 1 + runCmd "$TEST" "Setting context to $current" "kubectl config use-context $current" || return 1 + H=`kubectl config current-context` + [ "$H" != "$current" ] && log "Current context is not $current" && return 1 + runCmd "$TEST" "Setting default namespace to $ns" "kubectl config set-context --current --namespace=$ns" || return 1 + kubectl get ns >/dev/null 2>&1 || return 1 +} + ## Main method for running control center runControlCenter() { - checkCommands kind helm docker kubectl || return 1 - checkBusyPort "443" || return 1 + CLUSTER=${CLUSTER:-$KIND_CLUSTER} + + checkCommands docker kubectl helm unzip || return 1 checkDockerRunning || return 1 - ## Clean up from a previous run - # stopCloudProvider - uninstallCC $CC_CLUSTER $CC_NS - # deleteCluster $CC_CLUSTER - ## Start a new cluster - createCluster $CC_CLUSTER $CC_NS || return 1 - # startCloudProvider || return 1 + + ## Start a new kind cluster if needed + [ "$CLUSTER" != "$KIND_CLUSTER" ] || createKindCluster $CLUSTER $CC_NS || return 1 + + ## Set the context to the cluster + setClusterContext "$CLUSTER" "$CC_NS" || return 1 + + ## Clean up CC from a previous run unless SKIPHELM is set + [ -z "$SKIPHELM" ] && uninstallCC + + ## Check if port 443 is busy + checkBusyPort "443" || return 1 + ## Install Control Center installCC || return 1 ## Control center takes a long time to start waitForCC 900 || return 1 + ## Show temporary user-email and password in the terminal showTemporaryPassword + ## Install TLS certificates for the control-center and keycloak installTls && checkTls || return 1 + ## Forward the ingress (it needs root access since it uses port 443) - forwardIngress $CC_NS || return 1 - ## Run Playwright tests + # checkPort "443" + [ "$CLUSTER" != "$KIND_CLUSTER" ] || forwardIngress $CC_NS || return 1 + + ## Run Playwright tests for the control-center runPwTests || return 1 - if [ -z "$TEST" -a -z "$KEEPCC" ]; then - stopForwardIngress || return 1 - deleteCluster $CC_CLUSTER || return 1 - fi + stopForwardIngress || return 1 + + ## Delete the KinD cluster if it was created in this test if --keep-cc is not set + [ -n "$TEST" -o -n "$KEEPCC" -o "$CLUSTER" != "$KIND_CLUSTER" ] || deleteKindCluster "$CLUSTER" || return 1 + ## Otherwise, uninstall the control-center if --keep-cc is not set + [ -n "$TEST" -o -n "$KEEPCC" -o "$CLUSTER" = "$KIND_CLUSTER" ] || uninstallCC --wait=false || return 1 + + return 0 } diff --git a/scripts/pit/lib/lib-k8s-kind.sh b/scripts/pit/lib/lib-k8s-kind.sh index f2783623..be4adcfd 100644 --- a/scripts/pit/lib/lib-k8s-kind.sh +++ b/scripts/pit/lib/lib-k8s-kind.sh @@ -1,32 +1,37 @@ . `dirname $0`/lib/lib-utils.sh -startCloudProvider() { - [ -z "$TEST" ] && docker container inspect kind-cloud-provider >/dev/null 2>&1 && log "Docker Kind Cloud Provider already running" && return - runCmd "$TEST" "Starting Docker KinD Cloud Provider" \ - "docker run --quiet --name kind-cloud-provider --rm -d \ - -v /var/run/docker.sock:/var/run/docker.sock \ - rophy/cloud-provider-kind:0.4.0-20241026-r1" +## Kind cluster to use if not provided +KIND_CLUSTER=cc-cluster - # --network kind -p 443:443 -} - -stopCloudProvider() { - docker ps | grep kind-cloud-provider || return 0 - runCmd "$TEST" "Stoping Docker KinD Cloud Provider" \ - "docker kill kind-cloud-provider" || return 1 - docker ps | grep envoyproxy/envoy | awk '{print $1}' | xargs docker kill 2>/dev/null +## Check that the command has SUID bit set +# $1: command +hasSUID() { + [ ! -x "$1" ] && return 1 + R=`realpath "$1"` || return 1 + O=`ls -l "$R" | awk '{print $3}'` + P=`ls -l "$R" | awk '{print $1}'` + [ "$O" = "root" ] && expr "$P" : "^-..s" >/dev/null && return 0 || return 1 } -## +## Set SUID bit to the command # $1: command setSuid() { - isWindows && return 0 - W=`which $1` || return 1 + isWindows && echo "$1" && return 0 + T=/tmp/$1 + for W in "$T" `which "$1"`; do + hasSUID "$W" && echo "$W" && return 0 + done R=`realpath $W` || return 1 - O=`ls -l "$R" | awk '{print $3}'` - P=`ls -l "$R" | awk '{print $1}'` - [ "$O" = "root" ] || runCmd "$TEST" "Changing owner to root to: $R" "sudo chown root $R" || return 1 - expr "$P" : "^-..s" >/dev/null || runCmd "$TEST" "Setting sUI to $R" "sudo chmod u+s $R" || return 1 + + sudo -n true >/dev/null 2>&1 || log "It's necessary to provide sudo password to run '$1' as root" + sudo -B true || return 1 + + runCmd "$TEST" "Changing owner to root to: $R" "sudo chown root $R" \ + && runCmd "$TEST" "Changing set-uid to: $R" "sudo chmod u+s $R" && echo "kubectl" && return 0 + + runCmd "$TEST" "Coping $R" "sudo cp $R $T" \ + && runCmd "$TEST" "Changing owner to root to: $T" "sudo chown root $T" \ + && runCmd "$TEST" "Changing set-uid to: $R" "sudo chmod u+s $T" && echo "$T" && return 0 } ## @@ -35,11 +40,17 @@ setSuid() { # $3: port in guest # $4: target port in host startPortForward() { + checkPort "$4" && err "Port $4 is already in use" && return 1 H=`getPids "kubectl port-forward $2"` [ -n "$H" ] && log "Already running k8s port-forward $1 $2 $3 -> $4 with pid $H" && return 0 [ -z "$TEST" ] && log "Starting k8s port-forward $1 $2 $3 -> $4" - [ "$4" -le 1024 ] && setSuid kubectl || return 1 - runInBackgroundToFile "kubectl port-forward $2 $4:$3 -n $1" "k8s-port-forward-$3-$4.log" || return 1 + [ "$4" -le 1024 ] && K=`setSuid kubectl` || return 1 + bgf="k8s-port-forward-$3-$4.log" + rm -f "$bgf" + runInBackgroundToFile "$K port-forward $2 $4:$3 -n $1" "$bgf" + sleep 2 + tail "$bgf" + egrep -q 'Forwarding from' "$bgf" } ## @@ -61,25 +72,21 @@ stopForwardIngress() { ## # $1: cluster name # $2: namespace -createCluster() { +createKindCluster() { + checkCommands kind || return 1 kind get clusters | grep -q "^$1$" && return 0 runCmd "$TEST" "Creating KinD cluster: $1" \ "kind create cluster --name $1" || return 1 runCmd "$TEST" "Setting default namespace $2" \ "kubectl config set-context --current --namespace=$2" -} + } ## # $1: cluster name -deleteCluster() { +deleteKindCluster() { kind get clusters | grep -q "^$1$" || return 0 runCmd "$TEST" "Deleting Cluster $1" \ "kind delete cluster --name $1" || return 1 } - - - - - diff --git a/scripts/pit/lib/lib-playwright.sh b/scripts/pit/lib/lib-playwright.sh index 17a4566c..e70b767c 100644 --- a/scripts/pit/lib/lib-playwright.sh +++ b/scripts/pit/lib/lib-playwright.sh @@ -13,8 +13,7 @@ isInstalledPlaywright() { installPlaywright() { _pfile="playwright-"`uname`".out" _dir=`dirname "$1"` - # @playwright/test - (cd "$_dir" && runToFile "'${NPM}' install --no-audit @playwright/test" "$_pfile" "$VERBOSE") || return 1 + (cd "$_dir" && runToFile "$NPM install --no-audit @playwright/test" "$_pfile" "$VERBOSE") || return 1 (cd "$_dir" && runToFile "npx playwright install chromium" "$_pfile" "$VERBOSE") || return 1 isLinux && (cd "$_dir" && runToFile "'${NODE}' ./node_modules/.bin/playwright install-deps chromium" "$_pfile" "$VERBOSE") || true } @@ -44,10 +43,10 @@ runPlaywrightTests() { PATH=$PATH runToFile "'$NODE' '$_test_file' $_args" "$_pfile" "$VERBOSE" true err=$? [ -n "$TEST" ] && return 0 - H=`grep '> CONSOLE:' "$_pfile" | perl -pe 's/(> CONSOLE: Received xhr.*?feat":).*/$1 .../g'` + H=`grep ' > CONSOLE:' "$_pfile" | perl -pe 's/(> CONSOLE: Received xhr.*?feat":).*/$1 .../g'` H=`echo "$H" | egrep -v 'Atmosphere|Vaadin push loaded|Websocket successfully opened|Websocket closed|404.*favicon.ico'` [ -n "$H" ] && [ "$_mode" = "prod" ] && reportError "Console Warnings in $_mode mode $5" "$H" && echo "$H" - H=`grep '> JSERROR:' "$_pfile"` + H=`grep ' > JSERROR:' "$_pfile"` [ -n "$H" ] && reportError "Console Errors in $_msg" "$H" && echo "$H" && return 1 H=`tail -15 $_pfile` [ $err != 0 ] && reportOutErrors "$_ofile" "Error ($err) running Visual-Test ("`basename $_pfile`")" || echo ">>>> PiT: playwright '$_test_file' done" >> $__file diff --git a/scripts/pit/lib/lib-utils.sh b/scripts/pit/lib/lib-utils.sh index c7338d55..a4f1ed59 100644 --- a/scripts/pit/lib/lib-utils.sh +++ b/scripts/pit/lib/lib-utils.sh @@ -164,10 +164,10 @@ computeGradle() { computeNpm() { _VNODE=~/.vaadin/node _NPMJS=$_VNODE/lib/node_modules/npm/bin/npm-cli.js - NPM=`which npm` + NPM="'"`which npm`"'" NPX=`which npx` NODE=`which node` - [ -x "$_VNODE/bin/node" -a -f "$_NPMJS" ] && export PATH="$_VNODE/bin:$PATH" && NODE="$_VNODE/bin/node" && NPM="'$NODE' $_NPMJS" + [ -x "$_VNODE/bin/node" -a -f "$_NPMJS" ] && export PATH="$_VNODE/bin:$PATH" && NODE="$_VNODE/bin/node" && NPM="'$NODE' '$_NPMJS'" } ## Run a command, and shows a message explaining it @@ -717,7 +717,7 @@ NODE=$NODE Java version: `java -version 2>&1` Node version: `"$NODE" --version` NPM=$NPM -Npm version: `"$NPM" --version` +Npm version: `eval $NPM --version` " }