From edbc10419ddfee3aeff52e0abdcc6575178c5b18 Mon Sep 17 00:00:00 2001 From: Mohan Raj Date: Fri, 24 Mar 2023 16:25:56 +0000 Subject: [PATCH 1/4] adds phpunit tests --- .gitignore | 1 + composer.json | 2 +- helper/Helper.php | 7 -- includes/Api/{FlagOptions.php => Flags.php} | 61 +---------- includes/{FeatureFlags.php => Utils.php} | 6 +- phpunit.xml | 16 +++ plugin.php | 13 +-- tests/Unit/FlagsTest.php | 115 ++++++++++++++++++++ tests/Unit/HelperTest.php | 41 +++++++ tests/Unit/UtilsTest.php | 62 +++++++++++ tests/bootstrap.php | 4 + 11 files changed, 249 insertions(+), 79 deletions(-) rename includes/Api/{FlagOptions.php => Flags.php} (51%) rename includes/{FeatureFlags.php => Utils.php} (88%) create mode 100644 phpunit.xml create mode 100644 tests/Unit/FlagsTest.php create mode 100644 tests/Unit/HelperTest.php create mode 100644 tests/Unit/UtilsTest.php create mode 100644 tests/bootstrap.php diff --git a/.gitignore b/.gitignore index d3d035a..3064e04 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules build vendor yarn-error.log +.phpunit.result.cache diff --git a/composer.json b/composer.json index b6d97f8..03a4c59 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,6 @@ "scripts": { "lint:php": "phpcs .", "lint:php-fix": "phpcbf .", - "test:php": "phpunit --dont-report-useless-tests --configuration ./phpunit.xml.dist" + "test:php": "phpunit --dont-report-useless-tests --configuration ./phpunit.xml --testdox" } } diff --git a/helper/Helper.php b/helper/Helper.php index c1f1a58..7e8415d 100644 --- a/helper/Helper.php +++ b/helper/Helper.php @@ -18,13 +18,6 @@ */ class Helper { - /** - * Name of flag environment. - * - * @var string $env_option_name - */ - public static $env_option_name = 'mr_feature_flags_env'; - /** * Flag search helper. * diff --git a/includes/Api/FlagOptions.php b/includes/Api/Flags.php similarity index 51% rename from includes/Api/FlagOptions.php rename to includes/Api/Flags.php index 86641e1..3b5d094 100644 --- a/includes/Api/FlagOptions.php +++ b/includes/Api/Flags.php @@ -16,7 +16,7 @@ * @package mr-feature-flags * @since 1.0.0 */ -class FlagOptions { +class Flags { /** * Name in options table. @@ -25,13 +25,6 @@ class FlagOptions { */ public static $option_name = 'mr_feature_flags'; - /** - * Name of flag environment. - * - * @var string $env_option_name - */ - public static $env_option_name = 'mr_feature_flags_env'; - /** * Register feature flag endpoints. * @@ -58,19 +51,6 @@ function () { ], ] ); - - register_rest_route( - 'feature-flags/v1', - 'flags/env', - [ - [ - 'methods' => \WP_REST_SERVER::READABLE, - 'callback' => [ $this, 'get_flag_env' ], - 'permission_callback' => '__return_true', - ], - ] - ); - } ); } @@ -114,43 +94,4 @@ public function post_flags( $request ) { } } - /** - * Get Feature Flag environment. - * - * @return mixed List of flags. - */ - public function get_flag_env() { - $env = get_option( self::$env_option_name ); - - if ( empty( $env ) ) { - return rest_ensure_response( [ 'env' => 'prod' ] ); - } - - return rest_ensure_response( $env ); - } - - /** - * Register settings action method. - * - * @return void - * @since 1.0.0 - */ - public function register_settings() { - - add_menu_page( - 'Feature Flags', - 'Feature Flags', - 'manage_options', - 'mr-feature-flags', - [ $this, 'render_page' ], - 'data:image/svg+xml;base64,' . base64_encode( '' ) - ); - } - - /** - * Render page - */ - public function render_page() { - echo '
'; - } } diff --git a/includes/FeatureFlags.php b/includes/Utils.php similarity index 88% rename from includes/FeatureFlags.php rename to includes/Utils.php index b121689..a1bbd91 100644 --- a/includes/FeatureFlags.php +++ b/includes/Utils.php @@ -1,6 +1,6 @@ + + + + ./tests/Unit/ + + + + diff --git a/plugin.php b/plugin.php index c557c75..91b819d 100644 --- a/plugin.php +++ b/plugin.php @@ -19,7 +19,7 @@ declare( strict_types = 1 ); namespace MR\FeatureFlags; -use MR\FeatureFlags\Api\FlagOptions; +use MR\FeatureFlags\Api\Flags; // If this file is called directly, abort. if ( ! defined( 'WPINC' ) ) { @@ -141,14 +141,14 @@ function(string $page): void { $mr_feature_flags_admin_settings = new Settings(); $mr_feature_flags_admin_settings->register_feature_settings(); -$mr_feature_flags_register_api = new FlagOptions(); +$mr_feature_flags_register_api = new Flags(); $mr_feature_flags_register_api->register_flags_endpoints(); add_filter( 'plugin_action_links_mr-feature-flags/plugin.php', function ( $links ) { - // Build and escape the URL. + $url = esc_url( add_query_arg( 'page', @@ -156,9 +156,9 @@ function(string $page): void { get_admin_url() . 'admin.php' ) ); - // Create the link. + $settings_link = "" . __( 'Settings', 'mr-feature-flags' ) . ''; - // Adds the link to the end of the array. + array_push( $links, $settings_link @@ -166,6 +166,3 @@ function(string $page): void { return $links; } ); - - -// update_option( 'mr_feature_flags', [ ["id" => 1, "name" => "login", "enabled" => false],["id" => 2, "name" => "Reg", "enabled" => false]] ); diff --git a/tests/Unit/FlagsTest.php b/tests/Unit/FlagsTest.php new file mode 100644 index 0000000..0baa764 --- /dev/null +++ b/tests/Unit/FlagsTest.php @@ -0,0 +1,115 @@ +1, 'name'=>'Test','enabled'=>true]]; + + \Brain\Monkey\Functions\when('get_option')->justReturn($mock_option_value); + \Brain\Monkey\Functions\when('rest_ensure_response')->returnArg(); + + $flags = new Flags(); + $result = $flags->get_all_flags(); + $this->assertEquals($result, $mock_option_value); + } + + public function test_get_all_flags_method_should_return_empty_array_if_value_is_not_set() { + $mock_option_value = ''; + + \Brain\Monkey\Functions\when('get_option')->justReturn($mock_option_value); + \Brain\Monkey\Functions\when('rest_ensure_response')->returnArg(); + + $flags = new Flags(); + $result = $flags->get_all_flags(); + $this->assertEquals($result, []); + } + + public function test_get_all_flags_method_should_return_multiple_flags_from_options_table() { + $mock_option_value = [['id'=>1, 'name'=>'Test','enabled'=>true],['id'=>2, 'name'=>'Test2','enabled'=>false]]; + + \Brain\Monkey\Functions\when('get_option')->justReturn($mock_option_value); + \Brain\Monkey\Functions\when('rest_ensure_response')->returnArg(); + + $flags = new Flags(); + $result = $flags->get_all_flags(); + $this->assertEquals($result, $mock_option_value); + } + + // public function test_post_flag_works() { + // $request = \Brain\Monkey\Functions\mock('WP_Request', [['id'=>1, 'name'=>'Test','enabled'=>true]]); + + // $mock_option_value = [['id'=>1, 'name'=>'Test','enabled'=>true]]; + // \Brain\Monkey\Functions\when('update_option')->justReturn($mock_option_value); + // \Brain\Monkey\Functions\when('rest_ensure_response')->returnArg(); + + // $flags = new Flags(); + // $result = $flags->post_flags($request); + // var_dump($result); + + // } + + // public function test_post_flags() { + // $request_data = array( 'flag1' => true, 'flag2' => false ); + // $request = \Brain\Monkey\Functions\mock( 'WP_REST_Request' ); + // $request->expects( 'get_json_params' )->once()->andReturn( $request_data ); + + // \Brain\Monkey\Functions\expect( 'update_option' ) + // ->once() + // ->with( self::$option_name, $request_data ) + // ->andReturn( true ); + + // $result = $this->obj->post_flags( $request ); + + // $this->assertInstanceOf( 'WP_REST_Response', $result ); + // $this->assertEquals( + // array( + // 'status' => 200, + // 'success' => true, + // ), + // ); + // } + + public function test_post_flags() { + // Set up mock request object with JSON data + $request_data = array( 'flag1' => true, 'flag2' => false ); + $request = new \stdClass(); + $request->set_body_params( $request_data ); + + // Mock the update_option function + $expected_option_name = 'my_option'; + $expected_option_value = $request_data; + \Brain\Monkey\Functions\expect( 'update_option' ) + ->once() + ->with( $expected_option_name, $expected_option_value ) + ->andReturn( true ); + + // Call the post_flags method with the mock request + $response = $this->obj->post_flags( $request ); + + // Check that the response is a WP_REST_Response object with expected data + $this->assertInstanceOf( 'WP_REST_Response', $response ); + $this->assertEquals( array( 'status' => 200, 'success' => true ), $response->get_data() ); + } +} diff --git a/tests/Unit/HelperTest.php b/tests/Unit/HelperTest.php new file mode 100644 index 0000000..e72f74a --- /dev/null +++ b/tests/Unit/HelperTest.php @@ -0,0 +1,41 @@ +1, 'name'=>'Test','enabled'=>true]],'name','Test'); + $this->assertTrue($result); + } + + public function test_search_flag_method_should_return_false_if_flags_are_empty() { + $result = Helper::search_flag([],'name','Test'); + $this->assertFalse($result); + } + + public function test_search_flag_method_should_return_false_if_flag_name_present_but_disabled() { + $result = Helper::search_flag([],'name','Test'); + $this->assertFalse($result); + } + + public function test_search_flag_method_should_return_false_if_flag_not_present() { + $result = Helper::search_flag([['id'=>1, 'name'=>'Test','enabled'=>true]],'name','Test1'); + $this->assertFalse($result); + } +} diff --git a/tests/Unit/UtilsTest.php b/tests/Unit/UtilsTest.php new file mode 100644 index 0000000..5a193f9 --- /dev/null +++ b/tests/Unit/UtilsTest.php @@ -0,0 +1,62 @@ +1, 'name'=>'Test','enabled'=>true]]; + + \Brain\Monkey\Functions\when('get_option')->justReturn($mock_option_value); + + $result = Utils::is_enabled('Test'); + $this->assertTrue($result); + } + + public function test_is_enabled_method_should_return_false_if_no_flags_exist() { + $mock_option_value = ''; + + \Brain\Monkey\Functions\when('get_option')->justReturn($mock_option_value); + + $result = Utils::is_enabled('Test'); + $this->assertFalse($result); + } + + public function test_is_enabled_method_should_return_false_if_flag_name_present_and_disabled() { + $mock_option_value = [['id'=>1, 'name'=>'Test','enabled'=>false]]; + + \Brain\Monkey\Functions\when('get_option')->justReturn($mock_option_value); + + $result = Utils::is_enabled('Test'); + $this->assertFalse($result); + } + + public function test_is_enabled_method_should_return_false_if_flag_name_nor_present() { + $mock_option_value = [['id'=>1, 'name'=>'Test','enabled'=>false]]; + + \Brain\Monkey\Functions\when('get_option')->justReturn($mock_option_value); + + $result = Utils::is_enabled('Test1'); + $this->assertFalse($result); + } + + +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..f4f4f8b --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,4 @@ + Date: Sat, 25 Mar 2023 14:42:29 +0000 Subject: [PATCH 2/4] adds phpunit tests --- tests/Unit/FlagsTest.php | 100 ++++++++++++++++----------------------- tests/bootstrap.php | 4 +- 2 files changed, 45 insertions(+), 59 deletions(-) diff --git a/tests/Unit/FlagsTest.php b/tests/Unit/FlagsTest.php index 0baa764..1dd0db0 100644 --- a/tests/Unit/FlagsTest.php +++ b/tests/Unit/FlagsTest.php @@ -1,15 +1,12 @@ assertEquals($result, $mock_option_value); } - // public function test_post_flag_works() { - // $request = \Brain\Monkey\Functions\mock('WP_Request', [['id'=>1, 'name'=>'Test','enabled'=>true]]); - - // $mock_option_value = [['id'=>1, 'name'=>'Test','enabled'=>true]]; - // \Brain\Monkey\Functions\when('update_option')->justReturn($mock_option_value); - // \Brain\Monkey\Functions\when('rest_ensure_response')->returnArg(); - - // $flags = new Flags(); - // $result = $flags->post_flags($request); - // var_dump($result); - - // } - - // public function test_post_flags() { - // $request_data = array( 'flag1' => true, 'flag2' => false ); - // $request = \Brain\Monkey\Functions\mock( 'WP_REST_Request' ); - // $request->expects( 'get_json_params' )->once()->andReturn( $request_data ); - - // \Brain\Monkey\Functions\expect( 'update_option' ) - // ->once() - // ->with( self::$option_name, $request_data ) - // ->andReturn( true ); - - // $result = $this->obj->post_flags( $request ); - - // $this->assertInstanceOf( 'WP_REST_Response', $result ); - // $this->assertEquals( - // array( - // 'status' => 200, - // 'success' => true, - // ), - // ); - // } - - public function test_post_flags() { - // Set up mock request object with JSON data - $request_data = array( 'flag1' => true, 'flag2' => false ); - $request = new \stdClass(); - $request->set_body_params( $request_data ); - - // Mock the update_option function - $expected_option_name = 'my_option'; - $expected_option_value = $request_data; - \Brain\Monkey\Functions\expect( 'update_option' ) - ->once() - ->with( $expected_option_name, $expected_option_value ) - ->andReturn( true ); - - // Call the post_flags method with the mock request - $response = $this->obj->post_flags( $request ); - - // Check that the response is a WP_REST_Response object with expected data - $this->assertInstanceOf( 'WP_REST_Response', $response ); - $this->assertEquals( array( 'status' => 200, 'success' => true ), $response->get_data() ); - } + public function test_post_flags_methods_should_return_success_if_input_is_array() { + + $request_mock = \Mockery::mock('WP_Request'); + $request_mock->shouldReceive('get_json_params')->andReturn(['param1' => 'value1']); + + \Brain\Monkey\Functions\when('update_option')->justReturn(true); + \Brain\Monkey\Functions\when('rest_ensure_response')->returnArg(); + + global $wp; + $wp = new \stdClass(); + $wp->request = $request_mock; + + $flags = new Flags(); + $result = $flags->post_flags($request_mock); + + $this->assertEquals(['status'=>200, 'success' => true], $result); + + unset($GLOBALS['wp']); + } + + public function test_post_flags_methods_should_throw_error_if_input_is_not_an_array() { + + $request_mock = \Mockery::mock('WP_Request'); + $request_mock->shouldReceive('get_json_params')->andReturn('test'); + + global $wp; + $wp = new \stdClass(); + $wp->request = $request_mock; + + $error_mock = \Mockery::mock('WP_Error'); + + \Brain\Monkey\Functions\expect('post_flags')->andReturn($error_mock); + + + $flags = new Flags(); + $result = $flags->post_flags($request_mock); + + $this->assertInstanceOf('WP_Error', $result); + + } + + } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index f4f4f8b..a04dd1b 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,4 +1,6 @@ Date: Sat, 25 Mar 2023 15:56:24 +0000 Subject: [PATCH 3/4] adds i18n for php and js components --- .eslintrc | 26 ++++++++++---------------- includes/Api/Flags.php | 8 ++++++-- includes/Settings.php | 4 ++-- plugin.php | 2 +- src/components/DeleteModal.tsx | 20 +++++++++++++++----- src/components/Header.tsx | 12 +++++++----- src/components/Layout.tsx | 3 ++- src/components/LineItem.tsx | 20 +++++++++++++++----- src/components/SdkModal.tsx | 7 ++++--- src/components/SubmitControls.tsx | 9 ++++++--- 10 files changed, 68 insertions(+), 43 deletions(-) diff --git a/.eslintrc b/.eslintrc index 0ea0308..38a9ec2 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,31 +5,25 @@ "plugin:cypress/recommended" ], "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], + "plugins": ["@typescript-eslint"], "rules": { "prettier/prettier": "warn", "import/no-unresolved": 0, "@typescript-eslint/ban-ts-comment": "off", "camelcase": "off" }, - "overrides": - [{ - "files": ["*.jsx"], - "rules": { - "@typescript-eslint/explicit-module-boundary-types": ["off"] - } - }], + "overrides": [ + { + "files": ["*.jsx"], + "rules": { + "@typescript-eslint/explicit-module-boundary-types": ["off"] + } + } + ], "settings": { "import/resolver": { "alias": { - "map": [ - [ - "src", - "./src" - ], - ] + "map": [["src", "./src"]] } } } diff --git a/includes/Api/Flags.php b/includes/Api/Flags.php index 3b5d094..ccfcc42 100644 --- a/includes/Api/Flags.php +++ b/includes/Api/Flags.php @@ -42,12 +42,16 @@ function () { [ 'methods' => \WP_REST_SERVER::READABLE, 'callback' => [ $this, 'get_all_flags' ], - 'permission_callback' => '__return_true', + 'permission_callback' => function () { + return current_user_can( 'manage_options' ); + }, ], [ 'methods' => \WP_REST_SERVER::EDITABLE, 'callback' => [ $this, 'post_flags' ], - 'permission_callback' => '__return_true', + 'permission_callback' => function () { + return current_user_can( 'manage_options' ); + }, ], ] ); diff --git a/includes/Settings.php b/includes/Settings.php index 9e057a5..f39360e 100644 --- a/includes/Settings.php +++ b/includes/Settings.php @@ -37,8 +37,8 @@ public function register_feature_settings() { public function register_settings() { add_menu_page( - 'Feature Flags', - 'Feature Flags', + __( 'Feature Flags', 'mr-feature-flags' ), + __( 'Feature Flags', 'mr-feature-flags' ), 'manage_options', 'mr-feature-flags', [ $this, 'render_page' ], diff --git a/plugin.php b/plugin.php index 91b819d..8f7beb4 100644 --- a/plugin.php +++ b/plugin.php @@ -120,7 +120,7 @@ function(string $page): void { true ); - $feature_flag_meta = get_option( FeatureFlags::$option_name ); + $feature_flag_meta = get_option( Utils::$option_name ); $flags_list = []; if(is_array($feature_flag_meta)) { $flags_list = $feature_flag_meta; diff --git a/src/components/DeleteModal.tsx b/src/components/DeleteModal.tsx index 0d17605..762c33a 100644 --- a/src/components/DeleteModal.tsx +++ b/src/components/DeleteModal.tsx @@ -1,20 +1,30 @@ import { Modal, Button } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; import { Flag } from '../../types'; interface DeleteModalProps { closeModal: () => void; item: Flag; handleDeleteFlag: (id: number) => void; } + const DeleteModal = ({ closeModal, item, handleDeleteFlag, }: DeleteModalProps): JSX.Element => { return ( - +

- Are you sure want to delete flag "{item.name} - "? + { + // eslint-disable-next-line @wordpress/i18n-no-variables + __( + `Are you sure want to delete flag "${item.name}" ?`, + 'mr-feature-flags' + ) + }

); diff --git a/src/components/Header.tsx b/src/components/Header.tsx index bf8208c..94ad280 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,18 +1,20 @@ import { Flex, FlexItem } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + export default function (): JSX.Element { return ( -

Flag Name

+

{__('Flag Name', 'mr-feature-flags')}

-

Status

+

{__('Status', 'mr-feature-flags')}

- -

SDK Settings

+ +

{__('SDK Settings', 'mr-feature-flags')}

-

Delete Flag

+

{__('Delete Flag', 'mr-feature-flags')}

); diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index dea5988..7400645 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -5,6 +5,7 @@ import { Flag } from '../../types'; import SubmitControls from './SubmitControls'; import { getFlags } from '../utils'; import Header from './Header'; +import { __ } from '@wordpress/i18n'; const Layout = (): JSX.Element => { const [flags, setFlags] = useState([]); @@ -30,7 +31,7 @@ const Layout = (): JSX.Element => { return ( <>
-

Feature Flags settings

+

{__('Feature Flags settings', 'mr-feature-flags')}

{lastFlag ?
: ''} {isLoading ? ( diff --git a/src/components/LineItem.tsx b/src/components/LineItem.tsx index f851220..c1ce29b 100644 --- a/src/components/LineItem.tsx +++ b/src/components/LineItem.tsx @@ -10,6 +10,7 @@ import { useState, useRef, useEffect } from '@wordpress/element'; import { Flag } from '../../types'; import DeleteModal from './DeleteModal'; import SdkModal from './SdkModal'; +import { __ } from '@wordpress/i18n'; interface LineItemProps { flags: Flag[]; @@ -119,12 +120,15 @@ const LineItem = ({ @@ -132,7 +136,7 @@ const LineItem = ({ icon={'trash'} isDestructive variant="tertiary" - label="Delete Flag" + label={__('Delete Flag', 'mr-feature-flags')} onClick={() => handleDeleteModal(item)} /> @@ -141,13 +145,19 @@ const LineItem = ({ <> {} {} diff --git a/src/components/SdkModal.tsx b/src/components/SdkModal.tsx index 0b64e82..ce16d96 100644 --- a/src/components/SdkModal.tsx +++ b/src/components/SdkModal.tsx @@ -3,6 +3,7 @@ import Snippet from './Snippet'; import { useMemo, useState, useEffect } from '@wordpress/element'; import { useCopyToClipboard } from '@wordpress/compose'; import { Flag } from '../../types'; +import { __ } from '@wordpress/i18n'; interface SdkModalProps { item: Flag; @@ -48,7 +49,7 @@ domReady(function () { }, [item.name]); const phpSnippet = useMemo(() => { - return `if ( MR\\FeatureFlags\\FeatureFlags::is_enabled( '${item.name}' ) ) { + return `if ( MR\\FeatureFlags\\Utils::is_enabled( '${item.name}' ) ) { // php code goes here... }`; }, [item.name]); @@ -68,7 +69,7 @@ domReady(function () { return (
-

PHP Snippet

+

{__('PHP Snippet', 'mr-feature-flags')}

-

JS Snippet

+

{__('JavaScript Snippet', 'mr-feature-flags')}

@@ -72,7 +73,9 @@ const SubmitControls = ({ onClick={handleSave} disabled={disableSave || isSaving} > - {isSaving ? 'Saving' : 'Save'} + {isSaving + ? __('Saving', 'mr-feature-flags') + : __('Save', 'mr-feature-flags')} @@ -80,7 +83,7 @@ const SubmitControls = ({ variant="tertiary" onClick={() => location.reload()} > - Cancel + {__('Cancel', 'mr-feature-flags')} From 9297e4eb5ab28480d08e84e6eb9469ca70baf5ce Mon Sep 17 00:00:00 2001 From: Mohan Raj Date: Sat, 25 Mar 2023 16:02:03 +0000 Subject: [PATCH 4/4] fix jest tests --- src/components/__tests__/DeleteModal.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/__tests__/DeleteModal.test.js b/src/components/__tests__/DeleteModal.test.js index 5a37d88..6513ddf 100644 --- a/src/components/__tests__/DeleteModal.test.js +++ b/src/components/__tests__/DeleteModal.test.js @@ -17,7 +17,7 @@ describe('DeleteModal component', () => { render(); const modalText = screen.getByText( - `Are you sure want to delete flag "${flag.name}"?` + `Are you sure want to delete flag "${flag.name}" ?` ); expect(modalText).toBeInTheDocument(); });