From b5e5f33825d74098f349fea8617b0950e4906c0c Mon Sep 17 00:00:00 2001 From: Jon Lane Date: Wed, 6 Sep 2023 16:06:49 -0700 Subject: [PATCH] Update to nightly release after reset --- .github/workflows/smoke-test-daily.yml | 6 - .../tests/api-core-tests/global-setup.js | 75 ++++- .../api-core-tests/utils/plugin-utils.js | 260 ++++++++++++++++++ 3 files changed, 329 insertions(+), 12 deletions(-) create mode 100644 plugins/woocommerce/tests/api-core-tests/utils/plugin-utils.js diff --git a/.github/workflows/smoke-test-daily.yml b/.github/workflows/smoke-test-daily.yml index c2febda6161..2b8739d655b 100644 --- a/.github/workflows/smoke-test-daily.yml +++ b/.github/workflows/smoke-test-daily.yml @@ -45,12 +45,6 @@ jobs: working-directory: plugins/woocommerce run: pnpm exec playwright install chromium - - name: Run 'Update WooCommerce' test. - working-directory: plugins/woocommerce - env: - UPDATE_WC: nightly - run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js update-woocommerce.spec.js - - name: Run API tests. working-directory: plugins/woocommerce env: diff --git a/plugins/woocommerce/tests/api-core-tests/global-setup.js b/plugins/woocommerce/tests/api-core-tests/global-setup.js index 2b8b89dd97e..efd4fbf0d2a 100644 --- a/plugins/woocommerce/tests/api-core-tests/global-setup.js +++ b/plugins/woocommerce/tests/api-core-tests/global-setup.js @@ -1,4 +1,46 @@ const { chromium, expect } = require( '@playwright/test' ); +const { GITHUB_TOKEN, UPDATE_WC } = process.env; +const { downloadZip, deleteZip } = require( './utils/plugin-utils' ); +const axios = require( 'axios' ).default; + +const getWCDownloadURL = async () => { + const requestConfig = { + method: 'get', + url: 'https://api.github.com/repos/woocommerce/woocommerce/releases', + headers: { + Accept: 'application/vnd.github+json', + }, + params: { + per_page: 100, + }, + }; + if ( GITHUB_TOKEN ) { + requestConfig.headers.Authorization = `Bearer ${ GITHUB_TOKEN }`; + } + const response = await axios( requestConfig ).catch( ( error ) => { + if ( error.response ) { + console.log( error.response.data ); + } + throw new Error( error.message ); + } ); + const releaseWithTagName = response.data.find( + ( { tag_name } ) => tag_name === UPDATE_WC + ); + if ( ! releaseWithTagName ) { + throw new Error( + `No release with tag_name="${ UPDATE_WC }" found. If "${ UPDATE_WC }" is a draft release, make sure to specify a GITHUB_TOKEN environment variable.` + ); + } + const wcZipAsset = releaseWithTagName.assets.find( ( { name } ) => + name.match( /^woocommerce(-trunk-nightly)?\.zip$/ ) + ); + if ( wcZipAsset ) { + return GITHUB_TOKEN ? wcZipAsset.url : wcZipAsset.browser_download_url; + } + throw new Error( + `WooCommerce release with tag "${ UPDATE_WC }" found, but does not have a WooCommerce ZIP asset.` + ); +}; module.exports = async ( config ) => { const { baseURL, userAgent } = config.projects[ 0 ].use; @@ -8,7 +50,16 @@ module.exports = async ( config ) => { const setupContext = await browser.newContext( contextOptions ); const setupPage = await setupContext.newPage(); - // If API_BASE_URL is configured and doesn't include localhost, running on daily + const url = await getWCDownloadURL(); + const params = { url }; + + if ( GITHUB_TOKEN ) { + params.authorizationToken = GITHUB_TOKEN; + } + + const woocommerceZipPath = await downloadZip( params ); + + // If API_BASE_URL is configured and doesn't include localhost, running on daily host if ( process.env.API_BASE_URL && ! process.env.API_BASE_URL.includes( 'localhost' ) @@ -73,19 +124,31 @@ module.exports = async ( config ) => { console.log( 'Reinstalling WooCommerce Plugin...' ); await setupPage.goto( 'wp-admin/plugin-install.php' ); - await setupPage.locator( '#search-plugins' ).type( 'woocommerce' ); + await setupPage.locator( 'a.upload-view-toggle' ).click(); + await expect( setupPage.locator( 'p.install-help' ) ).toBeVisible(); + await expect( setupPage.locator( 'p.install-help' ) ).toContainText( + 'If you have a plugin in a .zip format, you may install or update it by uploading it here.' + ); + const [ fileChooser ] = await Promise.all( [ + setupPage.waitForEvent( 'filechooser' ), + setupPage.locator( '#pluginzip' ).click(), + ] ); + await fileChooser.setFiles( woocommerceZipPath ); await setupPage - .getByRole( 'link', { - name: /Install WooCommerce \d+\.\d+\.\d+ now/g, - } ) + .locator( '#install-plugin-submit' ) + .click( { timeout: 60000 } ); + await setupPage.waitForLoadState( 'networkidle' ); + await setupPage + .getByRole( 'link', { name: 'Activate Plugin' } ) .click(); - await setupPage.getByRole( 'link', { name: 'Activate' } ).click(); console.log( 'WooCommerce Re-installed.' ); await expect( setupPage.getByRole( 'heading', { name: 'Welcome to Woo!' } ) ).toBeVisible(); + await deleteZip( woocommerceZipPath ); + // await site.reset( process.env.USER_KEY, process.env.USER_SECRET ); } }; diff --git a/plugins/woocommerce/tests/api-core-tests/utils/plugin-utils.js b/plugins/woocommerce/tests/api-core-tests/utils/plugin-utils.js new file mode 100644 index 00000000000..a3be326a9a5 --- /dev/null +++ b/plugins/woocommerce/tests/api-core-tests/utils/plugin-utils.js @@ -0,0 +1,260 @@ +const { APIRequest } = require( '@playwright/test' ); +const axios = require( 'axios' ).default; +const fs = require( 'fs' ); +const path = require( 'path' ); +const { promisify } = require( 'util' ); +const execAsync = promisify( require( 'child_process' ).exec ); + +/** + * Encode basic auth username and password to be used in HTTP Authorization header. + * + * @param {string} username + * @param {string} password + * @returns Base64-encoded string + */ +const encodeCredentials = ( username, password ) => { + return Buffer.from( `${ username }:${ password }` ).toString( 'base64' ); +}; + +/** + * Deactivate and delete a plugin specified by the given `slug` using the WordPress API. + * + * @param {object} params + * @param {APIRequest} params.request + * @param {string} params.baseURL + * @param {string} params.slug + * @param {string} params.username + * @param {string} params.password + */ +export const deletePlugin = async ( { + request, + baseURL, + slug, + username, + password, +} ) => { + // Check if plugin is installed by getting the list of installed plugins, and then finding the one whose `textdomain` property equals `slug`. + const apiContext = await request.newContext( { + baseURL, + extraHTTPHeaders: { + Authorization: `Basic ${ encodeCredentials( username, password ) }`, + cookie: '', + }, + } ); + const listPluginsResponse = await apiContext.get( + `/wp-json/wp/v2/plugins`, + { + failOnStatusCode: true, + } + ); + const pluginsList = await listPluginsResponse.json(); + const pluginToDelete = pluginsList.find( + ( { textdomain } ) => textdomain === slug + ); + + // If installed, get its `plugin` value and use it to deactivate and delete it. + if ( pluginToDelete ) { + const { plugin } = pluginToDelete; + const requestURL = `/wp-json/wp/v2/plugins/${ plugin }`; + + await apiContext.put( requestURL, { + data: { status: 'inactive' }, + } ); + + await apiContext.delete( requestURL ); + } +}; + +/** + * Download the zip file from a remote location. + * + * @param {object} param + * @param {string} param.url + * @param {string} param.repository + * @param {string} param.authorizationToken + * @param {boolean} param.prerelease + * @param {string} param.downloadDir + * + * @param {string} url The URL where the zip file is located. Takes precedence over `repository`. + * @param {string} repository The repository owner and name. For example: `woocommerce/woocommerce`. Ignored when `url` was given. + * @param {string} authorizationToken Authorization token used to authenticate with the GitHub API if required. + * @param {boolean} prerelease Flag on whether to get a prelease or not. Default `false`. + * @param {string} downloadDir Relative path to the download directory. Non-existing folders will be auto-created. Defaults to `tmp` under current working directory. + * + * @return {string} Absolute path to the downloaded zip. + */ +export const downloadZip = async ( { + url, + repository, + authorizationToken, + prerelease = false, + downloadDir = 'tmp', +} ) => { + let zipFilename = path.basename( url || repository ); + zipFilename = zipFilename.endsWith( '.zip' ) + ? zipFilename + : zipFilename.concat( '.zip' ); + const zipFilePath = path.resolve( downloadDir, zipFilename ); + + let response; + + // Create destination folder. + fs.mkdirSync( downloadDir, { recursive: true } ); + + const downloadURL = + url ?? + ( await getLatestReleaseZipUrl( { + repository, + authorizationToken, + prerelease, + } ) ); + + // Download the zip. + const options = { + method: 'get', + url: downloadURL, + responseType: 'stream', + headers: { + Authorization: authorizationToken + ? `token ${ authorizationToken }` + : '', + Accept: 'application/octet-stream', + }, + }; + + response = await axios( options ).catch( ( error ) => { + if ( error.response ) { + console.error( error.response.data ); + } + throw new Error( error.message ); + } ); + + response.data.pipe( fs.createWriteStream( zipFilePath ) ); + + return zipFilePath; +}; + +/** + * Delete a zip file. Useful when cleaning up downloaded plugin zips. + * + * @param {string} zipFilePath Local file path to the ZIP. + */ +export const deleteZip = async ( zipFilePath ) => { + await fs.unlink( zipFilePath, ( err ) => { + if ( err ) throw err; + } ); +}; + +/** + * Get the download URL of the latest release zip for a plugin using GitHub API. + * + * @param {{repository: string, authorizationToken: string, prerelease: boolean, perPage: number}} param + * @param {string} repository The repository owner and name. For example: `woocommerce/woocommerce`. + * @param {string} authorizationToken Authorization token used to authenticate with the GitHub API if required. + * @param {boolean} prerelease Flag on whether to get a prelease or not. + * @param {number} perPage Limit of entries returned from the latest releases list, defaults to 3. + * @return {string} Download URL for the release zip file. + */ +export const getLatestReleaseZipUrl = async ( { + repository, + authorizationToken, + prerelease = false, + perPage = 3, +} ) => { + let release; + + const requesturl = prerelease + ? `https://api.github.com/repos/${ repository }/releases?per_page=${ perPage }` + : `https://api.github.com/repos/${ repository }/releases/latest`; + + const options = { + method: 'get', + url: requesturl, + headers: { + Authorization: authorizationToken + ? `token ${ authorizationToken }` + : '', + }, + }; + + // Get the first prerelease, or the latest release. + let response; + try { + response = await axios( options ); + } catch ( error ) { + let errorMessage = + 'Something went wrong when downloading the plugin.\n'; + + if ( error.response ) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + errorMessage = errorMessage.concat( + `Response status: ${ error.response.status } ${ error.response.statusText }`, + '\n', + `Response body:`, + '\n', + JSON.stringify( error.response.data, null, 2 ), + '\n' + ); + } else if ( error.request ) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + errorMessage = errorMessage.concat( + JSON.stringify( error.request, null, 2 ), + '\n' + ); + } else { + // Something happened in setting up the request that triggered an Error + errorMessage = errorMessage.concat( error.toJSON(), '\n' ); + } + + throw new Error( errorMessage ); + } + + release = prerelease + ? response.data.find( ( { prerelease } ) => prerelease ) + : response.data; + + // If response contains assets, return URL of first asset. + // Otherwise, return the github.com URL from the tag name. + const { assets } = release; + if ( assets && assets.length ) { + return assets[ 0 ].url; + } else { + const tagName = release.tag_name; + return `https://github.com/${ repository }/archive/${ tagName }.zip`; + } +}; + +/** + * Install a plugin using WP CLI within a WP ENV environment. + * This is a workaround to the "The uploaded file exceeds the upload_max_filesize directive in php.ini" error encountered when uploading a plugin to the local WP Env E2E environment through the UI. + * + * @see https://github.com/WordPress/gutenberg/issues/29430 + * + * @param {string} pluginPath + */ +export const installPluginThruWpCli = async ( pluginPath ) => { + const runWpCliCommand = async ( command ) => { + const { stdout, stderr } = await execAsync( + `pnpm exec wp-env run tests-cli -- ${ command }` + ); + + console.log( stdout ); + console.error( stderr ); + }; + + const wpEnvPluginPath = pluginPath.replace( + /.*\/plugins\/woocommerce/, + 'wp-content/plugins/woocommerce' + ); + + await runWpCliCommand( `ls ${ wpEnvPluginPath }` ); + + await runWpCliCommand( + `wp plugin install --activate --force ${ wpEnvPluginPath }` + ); + + await runWpCliCommand( `wp plugin list` ); +};