Loading tests/home-page.feature→tests/pages.feature +19 −0 Original line number Diff line number Diff line Feature: Homepage The homepage should display either a static page or a feed Feature: Pages Pages should be returned when their URL is requested. Scenario: static homepage Background: A page exists Given a page exists containing """ This is some page content """ And the page is configured as the homepage When the homepage is requested Scenario: A page type post When the page is requested Then OK is returned And we will see the page text Scenario: feed homepage Given a post exists containing """ This is some post content """ And the homepage is the default Scenario: Static homepage Given the page is configured as the homepage When the homepage is requested Then OK is returned And we will see the post text And we will see the page text tests/posts.feature 0 → 100644 +25 −0 Original line number Diff line number Diff line Feature: Posts Posts should be returned when their URL is requested. The post index page should also be accessible. Background: A post exists Given a post exists containing """ This is some page content """ Scenario: Individual posts When the post is requested Then OK is returned And we will see the post text Scenario: Homepage post index When the homepage is requested Then OK is returned And we will see the post text Scenario: Non-homepage post index Given a blank page exists And is configured as the post index When the page is requested Then we will see the post text tests/script-access.feature 0 → 100644 +54 −0 Original line number Diff line number Diff line Feature: Script Access and Restrictions The user-facing parts of a WordPress application should all be either static resources or channeled through the root *index.php* entrypoint. However PHP is architectured in such a way that, if left unrestricted, any PHP file could be accessed as a script. In many cases protections have been put in place in WordPress' PHP files to prevent circumvention of access restriction, as well as some plugins. However this should never be relied on as it introduces additional complexity and may not have been thoroughly tested. It could also be considered a UI bug if a non-404 code is returned. To confuse matters the administration interface *is* accessed in a one-script-per-endpoint manner. Scenario Outline: Direct file access When <path> is requested Then <result> is returned Examples: Static files | path | result | | /wp-includes/images/w-logo-blue.png | OK | | /wp-admin/images/w-logo-blue.png | OK | | /readme.html | Not Found | | /composer.json | Not Found | | /composer.lock | Not Found | Examples: Non-entrypoint PHP files | path | result | | /wp-activate.php | Not Found | | /wp-blog-header.php | Not Found | | /wp-comments-post.php | Not Found | | /wp-config.php | Not Found | | /wp-cron.php | Not Found | | /wp-load.php | Not Found | | /wp-mail.php | Not Found | | /wp-settings.php | Not Found | | /wp-signup.php | Not Found | | /wp-trackback.php | Not Found | | /xmlrpc.php | Not Found | | /wp-includes/user.php | Not Found | Examples: Entrypoint PHP files | path | result | | / | OK | | /index.php | 301 | | /wp-login.php | OK | | /wp-admin/ | 302 | | /wp-admin/index.php | 302 | Scenario: Check the JSON API is accessible When /wp-json/wp/v2/ is requested Then OK is returned And the response body is JSON tests/steps/pages.py +43 −12 Original line number Diff line number Diff line Loading @@ -58,12 +58,13 @@ def assert_not_exist(context: Context, path: str) -> None: f"{context.site.url / path} exists" @given("a blank {post_type:PostType} exists") @given("a {post_type:PostType} exists containing") def create_post(context: Context, post_type: PostType, text: str|None = None) -> None: """ Create a WP post of the given type and store it in the context with the type as the name """ post = use_fixture(wp_post, context, post_type, text or context.text) post = use_fixture(wp_post, context, post_type, text or getattr(context, "text", "")) setattr(context, post_type.value, post) Loading @@ -72,21 +73,15 @@ def set_homepage(context: Context) -> None: """ Set the WP page from the context as the configured front page """ wp = context.site.backend pageid = context.page.path("$.ID", int) wp.cli("option", "update", "page_on_front", str(pageid)) wp.cli("option", "update", "show_on_front", "page") page = use_fixture(wp_post, context, PostType.page) wp.cli("option", "update", "page_for_posts", page.path("$.ID", int, str)) use_fixture(set_specials, context, homepage=context.page) @given("the homepage is the default") def reset_homepage(context: Context) -> None: @given("is configured as the post index") def set_post_index(context: Context) -> None: """ Ensure the front page is reverted to it's default Set the WP page from the context as the post index page """ context.site.backend.cli("option", "update", "show_on_front", "post") use_fixture(set_specials, context, posts=context.page) @when("the {post_type:PostType} is requested") Loading Loading @@ -160,3 +155,39 @@ def wp_post( ) yield post wp.cli("post", "delete", postid) @fixture def set_specials( context: Context, /, homepage: JSONObject|None = None, posts: JSONObject|None = None, *a: Any, **k: Any, ) -> Iterator[None]: """ Set the homepage and post index to new pages, creating default pages if needed Pages are reset at the end of a scenario """ wp = context.site.backend options = { opt["option_name"]: opt["option_value"] for opt in wp.cli("option", "list", "--format=json", deserialiser=JSONArray.from_string) } homepage = homepage or use_fixture(wp_post, context, PostType.page) wp.cli("option", "update", "page_on_front", homepage.path("$.ID", int, str)) wp.cli("option", "update", "show_on_front", "page") posts = posts or use_fixture(wp_post, context, PostType.page) wp.cli("option", "update", "page_for_posts", posts.path("$.ID", int, str)) yield for name in ["page_on_front", "show_on_front", "page_for_posts"]: try: wp.cli("option", "update", name, options[name]) except KeyError: wp.cli("option", "delete", name) tests/steps/request_steps.py +12 −0 Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ Step implementations dealing with HTTP requests from __future__ import annotations import json from typing import Any from behave import then Loading Loading @@ -84,3 +85,14 @@ def assert_response(context: Context, response: ResponseCode) -> None: """ assert context.response.status_code == response, \ f"Expected response {response}: got {context.response.status_code}" @then("the response body is JSON") def assert_is_json(context: Context) -> None: """ Assert the response body of a previous step contains a JSON document """ try: context.response.json() except json.JSONDecodeError: raise AssertionError("Response is not a JSON document") Loading
tests/home-page.feature→tests/pages.feature +19 −0 Original line number Diff line number Diff line Feature: Homepage The homepage should display either a static page or a feed Feature: Pages Pages should be returned when their URL is requested. Scenario: static homepage Background: A page exists Given a page exists containing """ This is some page content """ And the page is configured as the homepage When the homepage is requested Scenario: A page type post When the page is requested Then OK is returned And we will see the page text Scenario: feed homepage Given a post exists containing """ This is some post content """ And the homepage is the default Scenario: Static homepage Given the page is configured as the homepage When the homepage is requested Then OK is returned And we will see the post text And we will see the page text
tests/posts.feature 0 → 100644 +25 −0 Original line number Diff line number Diff line Feature: Posts Posts should be returned when their URL is requested. The post index page should also be accessible. Background: A post exists Given a post exists containing """ This is some page content """ Scenario: Individual posts When the post is requested Then OK is returned And we will see the post text Scenario: Homepage post index When the homepage is requested Then OK is returned And we will see the post text Scenario: Non-homepage post index Given a blank page exists And is configured as the post index When the page is requested Then we will see the post text
tests/script-access.feature 0 → 100644 +54 −0 Original line number Diff line number Diff line Feature: Script Access and Restrictions The user-facing parts of a WordPress application should all be either static resources or channeled through the root *index.php* entrypoint. However PHP is architectured in such a way that, if left unrestricted, any PHP file could be accessed as a script. In many cases protections have been put in place in WordPress' PHP files to prevent circumvention of access restriction, as well as some plugins. However this should never be relied on as it introduces additional complexity and may not have been thoroughly tested. It could also be considered a UI bug if a non-404 code is returned. To confuse matters the administration interface *is* accessed in a one-script-per-endpoint manner. Scenario Outline: Direct file access When <path> is requested Then <result> is returned Examples: Static files | path | result | | /wp-includes/images/w-logo-blue.png | OK | | /wp-admin/images/w-logo-blue.png | OK | | /readme.html | Not Found | | /composer.json | Not Found | | /composer.lock | Not Found | Examples: Non-entrypoint PHP files | path | result | | /wp-activate.php | Not Found | | /wp-blog-header.php | Not Found | | /wp-comments-post.php | Not Found | | /wp-config.php | Not Found | | /wp-cron.php | Not Found | | /wp-load.php | Not Found | | /wp-mail.php | Not Found | | /wp-settings.php | Not Found | | /wp-signup.php | Not Found | | /wp-trackback.php | Not Found | | /xmlrpc.php | Not Found | | /wp-includes/user.php | Not Found | Examples: Entrypoint PHP files | path | result | | / | OK | | /index.php | 301 | | /wp-login.php | OK | | /wp-admin/ | 302 | | /wp-admin/index.php | 302 | Scenario: Check the JSON API is accessible When /wp-json/wp/v2/ is requested Then OK is returned And the response body is JSON
tests/steps/pages.py +43 −12 Original line number Diff line number Diff line Loading @@ -58,12 +58,13 @@ def assert_not_exist(context: Context, path: str) -> None: f"{context.site.url / path} exists" @given("a blank {post_type:PostType} exists") @given("a {post_type:PostType} exists containing") def create_post(context: Context, post_type: PostType, text: str|None = None) -> None: """ Create a WP post of the given type and store it in the context with the type as the name """ post = use_fixture(wp_post, context, post_type, text or context.text) post = use_fixture(wp_post, context, post_type, text or getattr(context, "text", "")) setattr(context, post_type.value, post) Loading @@ -72,21 +73,15 @@ def set_homepage(context: Context) -> None: """ Set the WP page from the context as the configured front page """ wp = context.site.backend pageid = context.page.path("$.ID", int) wp.cli("option", "update", "page_on_front", str(pageid)) wp.cli("option", "update", "show_on_front", "page") page = use_fixture(wp_post, context, PostType.page) wp.cli("option", "update", "page_for_posts", page.path("$.ID", int, str)) use_fixture(set_specials, context, homepage=context.page) @given("the homepage is the default") def reset_homepage(context: Context) -> None: @given("is configured as the post index") def set_post_index(context: Context) -> None: """ Ensure the front page is reverted to it's default Set the WP page from the context as the post index page """ context.site.backend.cli("option", "update", "show_on_front", "post") use_fixture(set_specials, context, posts=context.page) @when("the {post_type:PostType} is requested") Loading Loading @@ -160,3 +155,39 @@ def wp_post( ) yield post wp.cli("post", "delete", postid) @fixture def set_specials( context: Context, /, homepage: JSONObject|None = None, posts: JSONObject|None = None, *a: Any, **k: Any, ) -> Iterator[None]: """ Set the homepage and post index to new pages, creating default pages if needed Pages are reset at the end of a scenario """ wp = context.site.backend options = { opt["option_name"]: opt["option_value"] for opt in wp.cli("option", "list", "--format=json", deserialiser=JSONArray.from_string) } homepage = homepage or use_fixture(wp_post, context, PostType.page) wp.cli("option", "update", "page_on_front", homepage.path("$.ID", int, str)) wp.cli("option", "update", "show_on_front", "page") posts = posts or use_fixture(wp_post, context, PostType.page) wp.cli("option", "update", "page_for_posts", posts.path("$.ID", int, str)) yield for name in ["page_on_front", "show_on_front", "page_for_posts"]: try: wp.cli("option", "update", name, options[name]) except KeyError: wp.cli("option", "delete", name)
tests/steps/request_steps.py +12 −0 Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ Step implementations dealing with HTTP requests from __future__ import annotations import json from typing import Any from behave import then Loading Loading @@ -84,3 +85,14 @@ def assert_response(context: Context, response: ResponseCode) -> None: """ assert context.response.status_code == response, \ f"Expected response {response}: got {context.response.status_code}" @then("the response body is JSON") def assert_is_json(context: Context) -> None: """ Assert the response body of a previous step contains a JSON document """ try: context.response.json() except json.JSONDecodeError: raise AssertionError("Response is not a JSON document")