diff --git a/projects/plugins/jetpack/_inc/client/components/settings-nav-tabs/test/component.jsx b/projects/plugins/jetpack/_inc/client/components/settings-nav-tabs/test/component.jsx new file mode 100644 index 000000000000..d666c8e95b7c --- /dev/null +++ b/projects/plugins/jetpack/_inc/client/components/settings-nav-tabs/test/component.jsx @@ -0,0 +1,63 @@ +import { MemoryRouter } from 'react-router'; +import { render, screen } from 'test/test-utils'; +import SettingsNavTabs from '../index'; + +jest.mock( 'components/data/query-site-plugins', () => ( { + __esModule: true, + default: () => null, +} ) ); + +jest.mock( 'components/search', () => ( { + __esModule: true, + default: () => null, +} ) ); + +const buildInitialState = () => ( { + jetpack: { + initialState: { + userData: { + currentUser: { + permissions: { manage_modules: true, publish_posts: true }, + }, + }, + }, + modules: { + items: { + 'account-protection': { activated: true }, + monitor: { activated: true }, + protect: { activated: true }, + sso: { activated: true }, + stats: { activated: true }, + waf: { activated: true }, + }, + }, + pluginsData: { items: {} }, + search: { searchTerm: '' }, + }, +} ); + +describe( 'SettingsNavTabs', () => { + afterEach( () => { + delete window.JetpackScriptData; + } ); + + // Atomic keeps the tab for the first release: the Protect page stays off there. + it.each( [ + [ 'has no Security tab on a self-hosted site', undefined, false ], + [ 'keeps the Security tab on an Atomic site', 'woa', true ], + ] )( '%s with every security module', ( _name, host, hasSecurityTab ) => { + window.JetpackScriptData = { site: { host } }; + + render( + + + , + { initialState: buildInitialState() } + ); + + expect( screen.getByRole( 'tab', { name: 'Traffic' } ) ).toBeInTheDocument(); + expect( screen.queryAllByRole( 'tab', { name: 'Security' } ) ).toHaveLength( + hasSecurityTab ? 1 : 0 + ); + } ); +} ); diff --git a/projects/plugins/jetpack/changelog/stuttgart-v1-jetpack-2876-resolution-plan b/projects/plugins/jetpack/changelog/stuttgart-v1-jetpack-2876-resolution-plan new file mode 100644 index 000000000000..a0fb7c77f952 --- /dev/null +++ b/projects/plugins/jetpack/changelog/stuttgart-v1-jetpack-2876-resolution-plan @@ -0,0 +1,5 @@ +Significance: patch +Type: other +Comment: Tests only. + + diff --git a/projects/plugins/jetpack/tests/php/general/Jetpack_Security_Menu_Matrix_Test.php b/projects/plugins/jetpack/tests/php/general/Jetpack_Security_Menu_Matrix_Test.php new file mode 100644 index 000000000000..2af5a6e4613b --- /dev/null +++ b/projects/plugins/jetpack/tests/php/general/Jetpack_Security_Menu_Matrix_Test.php @@ -0,0 +1,211 @@ +user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $user_id ); + + // Mock a connection. + Jetpack_Options::update_option( 'master_user', $user_id ); + Jetpack_Options::update_option( 'id', 1234 ); + Jetpack_Options::update_option( 'blog_token', 'asdasd.123123' ); + Jetpack_Options::update_option( 'user_tokens', array( $user_id => "honey.badger.$user_id" ) ); + + // The Protect page is being built behind this flag, which is removed when it ships. + add_filter( 'jetpack_feature_flag_enabled_jetpack-protect-dashboard', '__return_true' ); + } + + /** + * Tear down. + */ + public function tear_down() { + Jetpack_Options::delete_option( array( 'master_user', 'id', 'blog_token', 'user_tokens' ) ); + Constants::clear_constants(); + parent::tear_down(); + } + + /** + * The site matrix. + * + * @return array[] Site name to whether it has Scan, the standalone Protect plugin, and is Atomic. + */ + public static function provide_sites() { + return array( + 'self-hosted, no Scan' => array( false, false, false ), + 'self-hosted, with Scan' => array( true, false, false ), + 'self-hosted, with the Protect plugin' => array( true, true, false ), + 'Atomic' => array( true, false, true ), + ); + } + + /** + * The Jetpack menu has one security entry: Protect, or on Atomic the Scan history link it already had. + * + * @dataProvider provide_sites + * + * @param bool $has_scan Whether the site has Scan. + * @param bool $has_protect_plugin Whether the standalone Protect plugin is active. + * @param bool $is_atomic Whether the site is on WordPress.com Atomic. + */ + #[DataProvider( 'provide_sites' )] + public function test_jetpack_menu_has_one_security_entry( $has_scan, $has_protect_plugin, $is_atomic ) { + $this->set_scan_entitlement( $has_scan ); + + if ( $is_atomic ) { + $this->make_site_atomic(); + } + + if ( $has_protect_plugin ) { + $this->activate_standalone_protect_plugin(); + } + + // The first release changes nothing on Atomic, where the Protect page stays off. + $expected = $is_atomic + ? array( array( 'Scan ↗', esc_url( Redirect::get_url( 'cloud-scan-history-wp-menu' ) ) ) ) + : array( array( 'Protect', 'jetpack-protect' ) ); + + $this->assertSame( $expected, $this->security_entries( $this->render_jetpack_menu() ) ); + } + + /** + * Makes the site a WordPress.com Atomic one that uses wp-admin, the way Host::is_woa_site() detects it. + */ + private function make_site_atomic() { + Constants::set_constant( 'ATOMIC_SITE_ID', 1 ); + Constants::set_constant( 'ATOMIC_CLIENT_ID', 1 ); + Constants::set_constant( 'WPCOMSH__PLUGIN_FILE', true ); + update_option( 'wpcom_admin_interface', 'wp-admin' ); + // Host caches is_woa_site(), and the plugin bootstrap already asked it. + Cache::clear(); + } + + /** + * Gives the site Scan, or takes it away, under each check the plugin uses for it. + * + * @param bool $has_scan Whether the site has Scan. + */ + private function set_scan_entitlement( $has_scan ) { + $scan = new stdClass(); + $scan->state = $has_scan ? 'idle' : 'unavailable'; + $rewind = new stdClass(); + $rewind->state = 'unavailable'; + set_transient( 'jetpack_scan_state', $scan, WEEK_IN_SECONDS ); + set_transient( 'jetpack_rewind_state', $rewind, WEEK_IN_SECONDS ); + + update_option( + 'jetpack_active_plan', + array( + 'product_slug' => $has_scan ? 'jetpack_scan' : 'jetpack_free', + 'class' => $has_scan ? 'scan' : 'free', + 'features' => array( 'active' => $has_scan ? array( 'scan' ) : array() ), + ) + ); + } + + /** + * Stands in for the Jetpack Protect plugin, which this suite does not install. + * + * Mirrors what Jetpack_Protect::admin_page_init() registers, and the class Jetpack detects it by. + */ + private function activate_standalone_protect_plugin() { + class_alias( get_class( new class() {} ), 'Jetpack_Protect' ); + + add_action( + '_admin_menu', + static function () { + Admin_Menu::add_menu( + 'Jetpack Protect', + 'Protect', + 'manage_options', + 'jetpack-protect', + '__return_null', + null, + array( + 'product' => 'protect', + 'key' => 'jetpack-protect', + ) + ); + } + ); + } + + /** + * Turns the Protect page on, loads the site's modules, then builds the menu. + * + * @return array[] The Jetpack submenu items, in the order WordPress rendered them. + */ + private function render_jetpack_menu() { + // The Protect page is not a default module: the owner turns it on. + add_filter( + 'jetpack_active_modules', + static function ( $modules ) { + return array_values( array_unique( array_merge( $modules, array( 'protect-dashboard' ) ) ) ); + } + ); + Jetpack::load_modules(); + // Jetpack loads its admin on admin requests only, which a PHPUnit run is not. + require_once JETPACK__PLUGIN_DIR . 'class.jetpack-admin.php'; + + do_action( '_admin_menu' ); + do_action( 'admin_menu' ); + + global $submenu; + + return empty( $submenu['jetpack'] ) ? array() : array_values( $submenu['jetpack'] ); + } + + /** + * Picks the visible security entries out of the Jetpack submenu. + * + * @param array[] $items Jetpack submenu items. + * @return array[] A title and slug pair for each entry named Scan or Protect, or pointing at either. + */ + private function security_entries( array $items ) { + $entries = array(); + + foreach ( $items as $item ) { + $title = trim( wp_strip_all_tags( $item[0] ) ); + + // jetpack-mu-wpcom hides an entry by class and leaves it registered. + if ( isset( $item[4] ) && false !== strpos( $item[4], 'hide-if-js' ) ) { + continue; + } + + if ( preg_match( '/\b(Scan|Protect)\b/', $title ) || preg_match( '/scan|jetpack-protect/', $item[2] ) ) { + $entries[] = array( $title, $item[2] ); + } + } + + return $entries; + } +}