diff --git a/.env.testing.example b/.env.testing.example new file mode 100644 index 000000000..43d3f7642 --- /dev/null +++ b/.env.testing.example @@ -0,0 +1,17 @@ +# WP-Browser configuration +# https://wpbrowser.wptestkit.dev/ +# Copy to .env.testing and adjust for your environment. +# These defaults match the reusable codecoverage workflow (MySQL service on 33306, MYSQL_DATABASE=tests-wordpress). + +WP_ROOT_FOLDER="wordpress" + +TEST_DB_HOST="127.0.0.1" +TEST_DB_PORT="33306" +TEST_DB_USER="root" +TEST_DB_PASSWORD="password" + +TEST_DB_NAME="tests-wordpress" +TEST_TABLE_PREFIX="wp_" + +TEST_SITE_WP_DOMAIN="localhost:8888" +TEST_SITE_ADMIN_EMAIL="admin@example.org" diff --git a/.github/workflows/codecoverage-main.yml b/.github/workflows/codecoverage-main.yml index 923069be9..6629fd72d 100644 --- a/.github/workflows/codecoverage-main.yml +++ b/.github/workflows/codecoverage-main.yml @@ -4,194 +4,38 @@ name: Codecoverage-Main # GitHub Pages, generates a README badge with the coverage percentage. on: - push: - branches: - - trunk - pull_request: - types: [ opened, reopened, ready_for_review, synchronize ] - branches: - - trunk - workflow_dispatch: + push: + branches: + - trunk + pull_request: + types: [ opened, reopened, ready_for_review, synchronize ] + branches: + - trunk + workflow_dispatch: + +permissions: + contents: read jobs: - - codecoverage: - runs-on: ubuntu-latest - - services: - mysql: - image: mysql:5.7 # Password auth did not work on 8.0 on PHP 7.3, it did seem to work for PHP 7.4+ - env: # These are the same username and password as the wp-env container defaults - MYSQL_ROOT_PASSWORD: password - MYSQL_DATABASE: tests-wordpress - ports: # This mapping matches the .wp-env.json configuration - - 33306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 - - strategy: - matrix: # Supported PHP versions - php: [ '7.4', '8.0', '8.1', '8.2', '8.3', '8.4' ] - - steps: - - name: Checkout - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - with: - fetch-depth: 0 # attempting to get all branch names. - - # TODO: a DEPLOY_KEY is needed to enable gh-pages for the first time (the branch can be created and pushed but - # it will not be reachable as a website). - # Consider @see https://github.com/peaceiris/actions-gh-pages - # - name: Check does gh-pages branch need to be created - # run: | - # git branch -l; - # if [[ $(git branch -l gh-pages) == "" ]]; then - # gh_pages_branch_needed=true; - # echo "gh-pages branch is needed"; - # else - # gh_pages_branch_needed=false - # echo "gh-pages branch already exists"; - # fi - # echo "GH_PAGES_BRANCH_NEEDED=$gh_pages_branch_needed" >> $GITHUB_ENV; - # mkdir gh-pages - # - # - name: Maybe create gh-pages branch - # if: ${{ env.GH_PAGES_BRANCH_NEEDED }} - # uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0 - # with: - # github_token: ${{ secrets.GITHUB_TOKEN }} - # publish_dir: ./gh-pages - # force_orphan: true - # allow_empty_commit: true - # commit_message: "🤖 Creating gh-pages branch" - - - name: Checkout GitHub Pages branch for code coverage report - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - with: - ref: gh-pages - path: gh-pages - - - name: Install PHP - uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0 - with: - php-version: ${{ matrix.php }} - coverage: xdebug - tools: composer, jaschilz/php-coverage-badger - extensions: zip - - - name: Read .env.testing - uses: c-py/action-dotenv-to-setenv@925b5d99a3f1e4bd7b4e9928be4e2491e29891d9 # v5 - with: - env-file: .env.testing - - - name: Run composer install - continue-on-error: true - run: composer install -v - - - name: Allow writing to wp-content - if: ${{ hashFiles('wp-content') != '' }} # TODO: This may be incorrect since it's a directory. - run: sudo chmod -R a+w wp-content - - - name: Print refs - run: | - echo "ref: ${{ github.ref }}" - echo "head_ref: ${{ github.head_ref }}" - echo "base_ref: ${{ github.base_ref }}" - - # On trunk, there will be a previous coverage report. On PRs we create a new directory using the commit SHA. - - name: Clear previous code coverage - if: ${{ (matrix.php == '7.3') && (github.ref == 'refs/heads/trunk') }} - run: | - rm -rf gh-pages/phpunit || true; - mkdir gh-pages/phpunit || true; - -# - name: Run unit tests -# if: ${{ hashFiles('tests/phpunit/bootstrap.php') != '' }} # Only run unit tests if they are present. -# run: XDEBUG_MODE=coverage vendor/bin/phpunit --bootstrap tests/phpunit/bootstrap.php --coverage-php tests/_output/unit.cov --debug - - - name: Run wpunit tests - run: XDEBUG_MODE=coverage vendor/bin/codecept run wpunit --coverage tests/_output/wpunit.cov --debug - - # For PRs, we'll generate the coverage report on each pushed commit - - name: Merge code coverage for PR - if: ${{ matrix.php == '7.3' && github.event_name == 'pull_request' }} - run: | - vendor/bin/phpcov merge --clover clover.xml tests/_output/; - vendor/bin/phpcov merge --clover gh-pages/${{ github.event.pull_request.head.sha }}/phpunit/clover.xml --php gh-pages/${{ github.event.pull_request.head.sha }}/phpunit/phpunit.cov --html gh-pages/${{ github.event.pull_request.head.sha }}/phpunit/html/ tests/_output/; - - # On main, we want it output to a different path - - name: Merge code coverage for main - if: ${{ (matrix.php == '7.3') && (github.ref == 'refs/heads/trunk') }} - run: | - vendor/bin/phpcov merge --clover clover.xml tests/_output/; - vendor/bin/phpcov merge --clover gh-pages/phpunit/clover.xml --php gh-pages/phpunit/phpunit.cov --html gh-pages/phpunit/html/ tests/_output/; - - # This makes the coverage percentage available in `{{ steps.coverage-percentage.outputs.coverage-rounded }}`. - - name: Check test coverage - if: ${{ matrix.php == '7.3' }} - uses: johanvanhelden/gha-clover-test-coverage-check@2543c79a701f179bd63aa14c16c6938c509b2cec # v1 - id: coverage-percentage - with: - percentage: 25 - exit: false - filename: clover.xml - rounded-precision: "0" - - # See: https://github.blog/2009-12-29-bypassing-jekyll-on-github-pages/ - - name: Add `.nojekyll` file so code coverage report successfully deploys to gh-pages - if: ${{ (matrix.php == '7.3') }} - working-directory: gh-pages - run: | - touch .nojekyll - git add -- .nojekyll * - - - name: Update README coverage badge - if: ${{ (matrix.php == '7.3') && (github.ref == 'refs/heads/trunk') }} # only commit on trunk, on the PHP version we're using in production. - run: php-coverage-badger clover.xml gh-pages/phpunit/coverage.svg - - - name: Generate PR coverage badge - if: ${{ (matrix.php == '7.3') && github.event_name == 'pull_request' }} - run: php-coverage-badger clover.xml gh-pages/${{ github.event.pull_request.head.sha }}/phpunit/coverage.svg - - - name: Commit code coverage to gh-pages - if: ${{ matrix.php == '7.3' }} - uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0 - with: - repository: gh-pages - branch: gh-pages - commit_message: ${{ format('🤖 Save code coverage report to gh-pages {0}%', steps.coverage-percentage.outputs.coverage-rounded) }} - commit_options: "" - env: - GITHUB_TOKEN: "${{ github.token }}" - - - name: Add coverage badge to PR comment - if: ${{ matrix.php == '7.3' && github.event_name == 'pull_request' }} - run: | - echo "[![Code Coverage ](https://newfold-labs.github.io/wp-module-onboarding/${{ github.event.pull_request.head.sha }}/phpunit/coverage.svg)](https://newfold-labs.github.io/wp-module-onboarding/${{ github.event.pull_request.head.sha }}/phpunit/html/)" >> coverage-comment.md - echo "" >> coverage-comment.md - echo "" >> coverage-comment.md - - - name: Add coverage report link to PR comment - if: ${{ matrix.php == '7.3' && github.event_name == 'pull_request' }} - run: | - echo "${{ format('[project coverage report {0}%](https://newfold-labs.github.io/wp-module-onboarding/{1}/phpunit/html/) @ {2}', steps.coverage-percentage.outputs.coverage-rounded, github.event.pull_request.head.sha, github.event.pull_request.head.sha) }}" >> coverage-comment.md - echo "" >> coverage-comment.md - echo "" >> coverage-comment.md - - - name: Add phpcov uncovered lines report to PR comment - if: ${{ matrix.php == '7.3' && github.event_name == 'pull_request' }} - continue-on-error: true # phpcov can fail if there are no uncovered lines - run: | - BRANCHED_COMMIT=$(git rev-list origin..HEAD | tail -n 1); - echo "BRANCHED_COMMIT=$BRANCHED_COMMIT" - git diff $BRANCHED_COMMIT...${{ github.event.pull_request.head.sha }} > branch.diff; - cat branch.diff; - OUTPUT=${vendor/bin/phpcov patch-coverage --path-prefix $(pwd) ./gh-pages/${{ github.event.pull_request.head.sha }}/phpunit/phpunit.cov branch.diff || true} - echo $OUTPUT; - echo "$OUTPUT" >> coverage-comment.md - - - name: Add coverage PR comment - uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2 - if: ${{ matrix.php == '7.3' && github.event_name == 'pull_request' }} - with: - message-id: coverage-report - message-path: coverage-comment.md + get-repo-name: + runs-on: ubuntu-latest + outputs: + repository-name: ${{ steps.repo-name.outputs.name }} + steps: + - name: Extract repository name + id: repo-name + run: echo "name=$(echo ${{ github.repository }} | cut -d'/' -f2)" >> $GITHUB_OUTPUT + + codecoverage: + needs: get-repo-name + permissions: + contents: write + pull-requests: write + uses: newfold-labs/workflows/.github/workflows/reusable-codecoverage.yml@main + with: + php-versions: '["7.4", "8.0", "8.1", "8.2", "8.3", "8.4"]' + coverage-php-version: '7.4' + repository-name: ${{ needs.get-repo-name.outputs.repository-name }} + minimum-coverage: 25 + secrets: + repo_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index cdab30c2c..ea73427bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store .vscode .env +.env.testing /node_modules /tests/_output /tests/_support/_generated diff --git a/codeception.dist.yml b/codeception.dist.yml index 615b541f4..897635783 100644 --- a/codeception.dist.yml +++ b/codeception.dist.yml @@ -15,6 +15,9 @@ params: coverage: enabled: true include: - - /includes/* -# - /upgrades/* -bootstrap: bootstrap.php + - includes/* + exclude: + - tests/* + - vendor/* + - build/* + - src/* diff --git a/phpunit.xml b/phpunit.xml index 935c100a9..d91159405 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -8,7 +8,6 @@ > - diff --git a/tests/_data/.gitkeep b/tests/_data/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/phpunit/ColorTest.php b/tests/phpunit/ColorTest.php new file mode 100644 index 000000000..d01f718ca --- /dev/null +++ b/tests/phpunit/ColorTest.php @@ -0,0 +1,101 @@ +assertSame( 'Accent 1', $color->get_name() ); + $this->assertSame( 'accent_1', $color->get_slug() ); + $this->assertSame( '#F27121', $color->get_color() ); + } + + /** + * To_array returns expected keys. + * + * @return void + */ + public function test_to_array() { + $color = new Color( 'Accent 1', 'accent_1', '#F27121' ); + $arr = $color->to_array(); + $this->assertSame( + array( + 'name' => 'Accent 1', + 'slug' => 'accent_1', + 'color' => '#F27121', + ), + $arr + ); + } + + /** + * From_array creates equivalent Color. + * + * @return void + */ + public function test_from_array() { + $data = array( + 'name' => 'Accent 1', + 'slug' => 'accent_1', + 'color' => '#F27121', + ); + $color = Color::from_array( $data ); + $this->assertInstanceOf( Color::class, $color ); + $this->assertSame( 'Accent 1', $color->get_name() ); + $this->assertSame( 'accent_1', $color->get_slug() ); + $this->assertSame( '#F27121', $color->get_color() ); + } + + /** + * From_array throws when keys are missing. + * + * @return void + */ + public function test_from_array_throws_when_keys_missing() { + $this->expectException( \InvalidArgumentException::class ); + $this->expectExceptionMessage( 'Array must contain name, slug, and color keys' ); + Color::from_array( + array( + 'slug' => 'accent_1', + 'color' => '#F27121', + ) + ); + } + + /** + * Constructor trims whitespace. + * + * @return void + */ + public function test_trims_whitespace() { + $color = new Color( ' Accent 1 ', ' accent_1 ', ' #F27121 ' ); + $this->assertSame( 'Accent 1', $color->get_name() ); + $this->assertSame( 'accent_1', $color->get_slug() ); + $this->assertSame( '#F27121', $color->get_color() ); + } + + /** + * Constructor throws when name is empty. + * + * @return void + */ + public function test_throws_when_name_empty() { + $this->expectException( \InvalidArgumentException::class ); + $this->expectExceptionMessage( 'Name cannot be empty' ); + new Color( ' ', 'accent_1', '#F27121' ); + } +} diff --git a/tests/phpunit/ExampleTest.php b/tests/phpunit/ExampleTest.php deleted file mode 100644 index 8d03fb66e..000000000 --- a/tests/phpunit/ExampleTest.php +++ /dev/null @@ -1,15 +0,0 @@ -assertEquals( true, true ); - } - -} diff --git a/tests/phpunit/ModuleLoadingTest.php b/tests/phpunit/ModuleLoadingTest.php new file mode 100644 index 000000000..7f67eea4d --- /dev/null +++ b/tests/phpunit/ModuleLoadingTest.php @@ -0,0 +1,25 @@ +assertTrue( class_exists( Types\Color::class ) ); + $this->assertTrue( class_exists( Permissions::class ) ); + $this->assertTrue( class_exists( Compatibility\Status::class ) ); + $this->assertTrue( class_exists( RestApi\RestApi::class ) ); + } +} diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php index 963ed78a6..1cc43daa2 100644 --- a/tests/phpunit/bootstrap.php +++ b/tests/phpunit/bootstrap.php @@ -1,9 +1,13 @@ assertSame( 'Accent 1', $color->get_name() ); + $this->assertSame( 'accent_1', $color->get_slug() ); + $this->assertSame( '#F27121', $color->get_color() ); + } + + /** + * To_array returns expected keys. + * + * @return void + */ + public function test_color_to_array() { + $color = new Color( 'Accent 1', 'accent_1', '#F27121' ); + $arr = $color->to_array(); + $this->assertSame( + array( + 'name' => 'Accent 1', + 'slug' => 'accent_1', + 'color' => '#F27121', + ), + $arr + ); + } + + /** + * From_array creates equivalent Color. + * + * @return void + */ + public function test_color_from_array() { + $data = array( + 'name' => 'Accent 1', + 'slug' => 'accent_1', + 'color' => '#F27121', + ); + $color = Color::from_array( $data ); + $this->assertInstanceOf( Color::class, $color ); + $this->assertSame( 'Accent 1', $color->get_name() ); + $this->assertSame( 'accent_1', $color->get_slug() ); + $this->assertSame( '#F27121', $color->get_color() ); + } + + /** + * From_array throws when name is missing. + * + * @return void + */ + public function test_color_from_array_throws_when_keys_missing() { + $this->expectException( \InvalidArgumentException::class ); + $this->expectExceptionMessage( 'Array must contain name, slug, and color keys' ); + Color::from_array( + array( + 'slug' => 'accent_1', + 'color' => '#F27121', + ) + ); + } + + /** + * Constructor trims whitespace. + * + * @return void + */ + public function test_color_trims_whitespace() { + $color = new Color( ' Accent 1 ', ' accent_1 ', ' #F27121 ' ); + $this->assertSame( 'Accent 1', $color->get_name() ); + $this->assertSame( 'accent_1', $color->get_slug() ); + $this->assertSame( '#F27121', $color->get_color() ); + } + + /** + * Constructor throws when name is empty. + * + * @return void + */ + public function test_color_throws_when_name_empty() { + $this->expectException( \InvalidArgumentException::class ); + $this->expectExceptionMessage( 'Name cannot be empty' ); + new Color( ' ', 'accent_1', '#F27121' ); + } +} diff --git a/tests/wpunit/CompatibilityStatusWPUnitTest.php b/tests/wpunit/CompatibilityStatusWPUnitTest.php new file mode 100644 index 000000000..05c028359 --- /dev/null +++ b/tests/wpunit/CompatibilityStatusWPUnitTest.php @@ -0,0 +1,66 @@ +assertSame( 'unscanned', Status::$default ); + } + + /** + * Get() returns default when no option is set. + * + * @return void + */ + public function test_get_returns_default_when_unset() { + Status::reset(); + $this->assertSame( 'unscanned', Status::get() ); + } + + /** + * Get('all') returns stored data (default string when unset). + * + * @return void + */ + public function test_get_all_returns_stored_data() { + Status::reset(); + $data = Status::get( 'all' ); + $this->assertSame( 'unscanned', $data ); + } + + /** + * Reset() removes stored option. + * + * @return void + */ + public function test_reset_clears_option() { + $scan = new \stdClass(); + $scan->result = 'compatible'; + Status::set( $scan ); + Status::reset(); + $this->assertSame( 'unscanned', Status::get() ); + } +} diff --git a/tests/wpunit/ExampleWPUnitTest.php b/tests/wpunit/ExampleWPUnitTest.php deleted file mode 100644 index 08941487d..000000000 --- a/tests/wpunit/ExampleWPUnitTest.php +++ /dev/null @@ -1,12 +0,0 @@ -assertTrue( class_exists( Application::class ) ); + $this->assertTrue( class_exists( ModuleController::class ) ); + $this->assertTrue( class_exists( Permissions::class ) ); + $this->assertTrue( class_exists( Status::class ) ); + $this->assertTrue( class_exists( Scan::class ) ); + $this->assertTrue( class_exists( ImageSideloadTaskManager::class ) ); + $this->assertTrue( class_exists( ImageSideloadTask::class ) ); + $this->assertTrue( class_exists( RestApi\RestApi::class ) ); + } + + /** + * Verify WordPress factory is available. + * + * @return void + */ + public function test_wordpress_factory_available() { + $this->assertTrue( function_exists( 'get_option' ) ); + $this->assertNotEmpty( get_option( 'blogname' ) ); + } +} diff --git a/tests/wpunit/PermissionsWPUnitTest.php b/tests/wpunit/PermissionsWPUnitTest.php new file mode 100644 index 000000000..29ead300e --- /dev/null +++ b/tests/wpunit/PermissionsWPUnitTest.php @@ -0,0 +1,54 @@ +assertSame( 'manage_options', Permissions::ADMIN ); + $this->assertSame( 'install_themes', Permissions::INSTALL_THEMES ); + $this->assertSame( 'edit_themes', Permissions::EDIT_THEMES ); + } + + /** + * Rest_is_authorized_admin returns false when not logged in. + * + * @return void + */ + public function test_rest_is_authorized_admin_when_logged_out() { + wp_set_current_user( 0 ); + $this->assertFalse( Permissions::rest_is_authorized_admin() ); + } + + /** + * Rest_is_authorized_admin returns true for admin user. + * + * @return void + */ + public function test_rest_is_authorized_admin_when_admin() { + $user_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $user_id ); + $this->assertTrue( Permissions::rest_is_authorized_admin() ); + } + + /** + * Custom_post_authorized_admin requires edit_posts and manage_options. + * + * @return void + */ + public function test_custom_post_authorized_admin_for_administrator() { + $user_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $user_id ); + $this->assertTrue( Permissions::custom_post_authorized_admin() ); + } +} diff --git a/tests/wpunit/RestApiWPUnitTest.php b/tests/wpunit/RestApiWPUnitTest.php new file mode 100644 index 000000000..41609f5b3 --- /dev/null +++ b/tests/wpunit/RestApiWPUnitTest.php @@ -0,0 +1,32 @@ +get_routes(); + $found = array_filter( + array_keys( $routes ), + function ( $route ) { + return strpos( $route, 'newfold-onboarding' ) !== false; + } + ); + $this->assertNotEmpty( $found, 'Expected newfold-onboarding routes to be registered' ); + } +} diff --git a/tests/wpunit/_bootstrap.php b/tests/wpunit/_bootstrap.php index b3d9bbc7f..b4f91d94e 100644 --- a/tests/wpunit/_bootstrap.php +++ b/tests/wpunit/_bootstrap.php @@ -1 +1,16 @@