Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Functions\stubWpUrlFunctions() helper #129

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions docs/functions-testing-tools/function-stubs.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Functions\stubs(
);
```

### Pre-defined stubs for escaping functions
### Pre-defined stubs for WP escaping functions

To stub WordPress escaping functions is a very common usage for `Functions\stubs`.

Expand All @@ -111,7 +111,7 @@ By calling `Functions\stubEscapeFunctions()`, for _all_ of the functions listed

It will _not_ be the exact same escape mechanism that WordPress would apply, but "similar enough" for unit tests purpose and could still be helpful to discover some bugs.

### Pre-defined stubs for translation functions
### Pre-defined stubs for WP translation functions

Another common usage for `Functions\stubs`, since its introduction, has been to stub translation functions.

Expand Down Expand Up @@ -141,6 +141,40 @@ Only for functions that both translate and escape \(`esc_html__()`, `esc_html_x(

Please note how `Functions\stubTranslationFunctions()` creates stubs for functions that _echo_ translated text, something not easily doable with `Functions\stubs()` alone.

### Pre-defined stubs for WP URL functions

One very common and repetitive task testing WordPress code without WordPress loaded is to stub
WordPress URLs, a task made easy by the function:

**`Functions\stubWpUrlFunctions()`**

When called, it will create a stub for _all_ the following functions:

* `home_url()`
* `get_home_url()`
* `site_url()`
* `get_site_url()`
* `admin_url()`
* `get_admin_url()`
* `content_url()`
* `rest_url()`
* `get_rest_url()`
* `includes_url()`
* `network_home_url()`
* `network_site_url()`
* `network_admin_url()`
* `user_admin_url()`
* `wp_login_url()`

The function accepts as first argument the domain to use for subbing the URLs, default to `example.org`.

E.g., calling `Functions\stubWpUrlFunctions()`, the `home_url()` function returns `https://example.org`,
but calling `Functions\stubWpUrlFunctions('acme.com')`, the `home_url()` function returns `https://acme.org`.

The function also accepts a second parameter to force the usage of the HTTPS protocol. By default, that
second parameter is `null` which makes the stub use HTTPS, _unless_ the `is_ssl()` function is defined (stubbed), and
it returns `false`.

### Gotcha for `Functions\stubs`

#### Functions that returns null
Expand Down
34 changes: 34 additions & 0 deletions inc/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function tearDown()
use Brain\Monkey\Container;
use Brain\Monkey\Expectation\EscapeHelper;
use Brain\Monkey\Expectation\FunctionStubFactory;
use Brain\Monkey\Expectation\UrlsHelper;
use Brain\Monkey\Name\FunctionName;

/**
Expand Down Expand Up @@ -182,6 +183,39 @@ function stubEscapeFunctions()
]
);
}

/**
* Stub URL-related functions with default behavior.
*/
function stubWpUrlFunctions($domain = 'example.org', $use_https = null)
{
$helper = new UrlsHelper($domain, $use_https);

stubs([
'home_url' => $helper->stubUrlCallback(),
'get_home_url' => $helper->stubUrlForSiteCallback(),
'site_url' => $helper->stubUrlCallback(),
'get_site_url' => $helper->stubUrlForSiteCallback(),
'admin_url' => $helper->stubUrlCallback('wp-admin', 'admin'),
'get_admin_url' => $helper->stubUrlForSiteCallback('wp-admin', 'admin'),
'content_url' => $helper->stubUrlCallback('wp-content', null, false),
'rest_url' => $helper->stubUrlCallback('wp-json'),
'get_rest_url' => $helper->stubUrlForSiteCallback('wp-json'),
'includes_url' => $helper->stubUrlCallback('wp-includes'),
'network_home_url' => $helper->stubUrlCallback(),
'network_site_url' => $helper->stubUrlCallback(),
'network_admin_url' => $helper->stubUrlCallback('wp-admin/network', 'admin'),
'user_admin_url' => $helper->stubUrlCallback('wp-admin/user', 'admin'),
'wp_login_url' => static function ($redirect = '', $force_reauth = false) use ($helper) {
Comment on lines +195 to +209
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like most of these don't take passed parameters to the original function into account, while WP does support passing parameters.

Stubbing these in the framework without taking the passes parameters into account will probably be confusing and lead to support overhead or to people just not using these stubs as they don't do what they expect.

Copy link
Collaborator Author

@gmazzap gmazzap Apr 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jrfnl I think you need to look at it again :)

$helper->stubUrlCallback() and $helper->stubUrlForSiteCallback() return callbacks that take respectively 2 ($path, $schema) and 3 parameters ($blog_id, $path, $schema), that are the same core functions take.

There are 2 exceptions: content_url() and wp_login_url().

wp_login_url() takes still 2 arguments, but different from the others, and that is why it is stubbed differently.

content_url() takes 1 parameter: $path. That is why stubUrlCallback() accepts a 3rd parameter $use_schema_arg that defaults to true, but it is passed as false for content_url().

To be noted:

  • the $blog_id parameter for functions that accepts it is just ignored in stubbed functions because the base URL is taken from what is passed to the stubWpUrlFunctions() API function.
  • When $schema param is something like 'admin' or 'login' it is taken into account only if HTTPs is not forced (passing true as stubWpUrlFunctions()'s 2nd parameter) and force_ssl_admin() is available (likely because stubbed separately).
  • rest_url() and get_rest_url() use 'rest' as scheme. That is not relevant for the generation of the URL (in WP just like in the stubbed version), it is used in WP only as the 3rd parameter, $orig_scheme, passed to the 'set_url_scheme' hook, something that I think is pretty safe to ignore for the stubbed version of the function.

TL;DR: all the stubbed functions accept the same parameters and work very similarly to the WP counterparts. For some edge cases, it might be needed to stub is_ssl() and/or force_ssl_admin() separately.

You can even see the parameters used with success in tests: https://github.com/Brain-WP/BrainMonkey/blob/feature/stub-wp-urls/tests/cases/unit/Api/FunctionsTest.php#L439-L455

$callback = $helper->stubUrlCallback();
$url = $callback('/wp-login.php', 'login');
gmazzap marked this conversation as resolved.
Show resolved Hide resolved
$has_redirect = ($redirect !== '') && is_string($redirect);
$url .= $has_redirect ? '?redirect_to=' . urlencode($redirect) : '';
$url .= $force_reauth ? ($has_redirect ? '&reauth=1' : '?reauth=1') : '';
return $url;
},
]);
}
}

namespace Brain\Monkey\Actions {
Expand Down
131 changes: 131 additions & 0 deletions src/Expectation/UrlsHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All @param declarations in this file are missing the type information, which makes them kind of useless.

Copy link
Collaborator Author

@gmazzap gmazzap Apr 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All @param are mixed anyway. The type is checked inside the function via is_string and similar. So you can pass whatever. Mixed is implicit so to me no explicit type would be fine.

If we would add Psalm or similar, as soon I would declare @param string and then do is_string() checks inside the function Psalm would rightfully complain for "redundant condition" and "doc bloc type contradiction" (see an example).

I prefer to keep the check at runtime so all the parameters are mixed, and IMO there's no point in typing "mixed" all over the place.
Still, I like to keep the doc block for "visual help". I personally find it hard to digest a wall of code without some "calming" doc block in between functions.

Anyway, I added mixed pretty much everywhere, unless a private method where the type of the parameters is ensured.

/*
* This file is part of the BrainMonkey package.
*
* (c) Giuseppe Mazzapica <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Brain\Monkey\Expectation;

class UrlsHelper
{
const DEFAULT_DOMAIN = 'example.org';

/**
* @var string
*/
private $domain;

/**
* @var bool|null
*/
private $use_https;

/**
* @param mixed $domain
* @param mixed $use_https
*/
public function __construct($domain = null, $use_https = null)
{
$this->domain = (is_string($domain) && $domain !== '') ? $domain : self::DEFAULT_DOMAIN;
$this->use_https = ($use_https === null)
? null
: (bool)filter_var($use_https, FILTER_VALIDATE_BOOLEAN);
}

/**
* @param mixed $base_path
* @param mixed $def_schema
* @return \Closure
*/
public function stubUrlForSiteCallback($base_path = '', $def_schema = null)
{
return function ($site_id, $path = '', $schema = null) use ($base_path, $def_schema) {
if (is_string($def_schema) && ($def_schema !== '') && ($schema === null)) {
$schema = $def_schema;
}
return $this->build_url(
$this->build_relative_path($base_path, $path),
$this->determineSchema($schema)
);
};
}

/**
* @param mixed $base_path
* @param mixed $def_schema
* @param mixed $use_schema_arg
* @return \Closure
*/
public function stubUrlCallback($base_path = '', $def_schema = null, $use_schema_arg = true)
{
return function ($path = '', $schema = null) use ($base_path, $def_schema, $use_schema_arg) {
($def_schema && $schema === null) and $schema = $def_schema;
return $this->build_url(
$this->build_relative_path($base_path, $path),
$this->determineSchema($use_schema_arg ? $schema : null)
);
};
}

/**
* @param string $relative
* @param string|null $schema
* @return string
*/
private function build_url($relative, $schema)
{
return ($schema === null)
? (($relative === '') ? '/' : $relative)
: $schema . $this->domain . $relative;
}

/**
* @param mixed $base_path
* @param mixed $path
* @return string
*/
private function build_relative_path($base_path, $path)
{
$path = (($path !== '') && is_string($path))
? '/' . ltrim($path, '/')
: '';
$base_path = (($base_path !== '') && is_string($base_path))
? '/' . trim($base_path, '/')
: '';

return $base_path . $path;
}

/**
* @param mixed $schema_argument
* @return string|null
*/
private function determineSchema($schema_argument = null)
{
if ($schema_argument === 'relative') {
return null;
}

$use_https = $this->use_https;
$is_ssl = function_exists('is_ssl') ? \is_ssl() : true;
if ($use_https === null && !in_array($schema_argument, ['http', 'https'], true)) {
$use_https = $is_ssl;
if (
!$use_https
&& in_array($schema_argument, ['admin', 'login', 'login_post', 'rpc'])
&& function_exists('force_ssl_admin')
) {
$use_https = \force_ssl_admin();
}
}
if ($schema_argument === 'http') {
$use_https = false;
}

return $use_https ? 'https://' : 'http://';
}
}
60 changes: 60 additions & 0 deletions tests/cases/unit/Api/FunctionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -431,4 +431,64 @@ public function dataStubsEscapeXml()
],
];
}

public function testStubWpUrlFunctionsWithDefaults()
{
Functions\stubWpUrlFunctions();

static::assertSame('https://example.org', home_url());
static::assertSame('https://example.org/', home_url('/'));
static::assertSame('https://example.org/', get_home_url(1, '/'));
static::assertSame('https://example.org/', site_url('/'));
static::assertSame('https://example.org/', get_site_url(2, '/'));
static::assertSame('https://example.org/wp-admin/post-new.php', admin_url('/post-new.php'));
static::assertSame('https://example.org/wp-admin/post-new.php', admin_url('post-new.php'));
static::assertSame('/wp-admin/post-new.php', admin_url('/post-new.php', 'relative'));
static::assertSame('https://example.org/wp-admin/', get_admin_url(1, '/'));
static::assertSame('https://example.org/wp-content/plugins/foo/img.jpg', content_url('/plugins/foo/img.jpg'));
static::assertSame('https://example.org/wp-json', rest_url());
static::assertSame('https://example.org/wp-json/wp/', get_rest_url(1, '/wp/'));
static::assertSame('https://example.org/wp-includes', includes_url());
static::assertSame('https://example.org', network_home_url());
static::assertSame('https://example.org', network_site_url());
static::assertSame('https://example.org/wp-admin/network', network_admin_url());
static::assertSame('https://example.org/wp-admin/user/', user_admin_url('/'));
}

public function testStubWpUrlFunctionsWithSettings()
{
Functions\stubWpUrlFunctions('wikipedia.org', false);

static::assertSame('http://wikipedia.org', home_url());
static::assertSame('http://wikipedia.org/wp-admin/post-new.php', admin_url('/post-new.php'));
}

public function testStubWpUrlViaIsSslFunctions()
{
Functions\when('is_ssl')->justReturn(false);
Functions\when('force_ssl_admin')->justReturn(true);
Functions\stubWpUrlFunctions();

static::assertSame('http://example.org', home_url());
static::assertSame('https://example.org/wp-admin/', admin_url('/'));
static::assertSame('https://example.org/wp-admin/network', network_admin_url());
static::assertSame('https://example.org/wp-login.php', wp_login_url());
}

public function testStubWpLoginUrl()
{
Functions\stubWpUrlFunctions();
Functions\when('is_ssl')->justReturn(true);

static::assertSame('https://example.org/wp-login.php', wp_login_url());
static::assertSame('https://example.org/wp-login.php?reauth=1', wp_login_url('', true));
static::assertSame(
'https://example.org/wp-login.php?redirect_to=https%3A%2F%2Fexample.org',
wp_login_url('https://example.org')
);
static::assertSame(
'https://example.org/wp-login.php?redirect_to=https%3A%2F%2Fexample.org&reauth=1',
wp_login_url('https://example.org', true)
);
}
}