Skip to content

Commit

Permalink
Add support for faking specific requests by HTTP method (#568)
Browse files Browse the repository at this point in the history
* Adding support for faking a specific request method

* CHANGELOG
  • Loading branch information
srtfisher authored Jul 3, 2024
1 parent 408a5ca commit 4961ae5
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 16 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Changed

- Added support for faking specific HTTP requests by method.
- Added helper for fluently building HTTP sequence responses.

## v1.1.2 - 2024-06-20

### Fixed
Expand Down
29 changes: 27 additions & 2 deletions src/mantle/testing/class-mock-http-sequence.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,41 @@ public function push( Mock_Http_Response $response ) {
*
* @param int $status Http Status.
* @param array $headers Http Headers.
* @return static
*/
public function push_status( int $status, array $headers = [] ) {
public function push_status( int $status, array $headers = [] ): static {
return $this->push(
Mock_Http_Response::create()
->with_response_code( $status )
->with_headers( $headers )
);
}

/**
* Push a response with a specific body to the sequence.
*
* @param string $body Response body.
* @param array $headers Response headers.
*/
public function push_body( string $body, array $headers = [] ): static {
return $this->push(
Mock_Http_Response::create( $body, $headers )
);
}

/**
* Push a JSON response to the sequence.
*
* @param array|string $payload Data to encode as JSON.
* @param array $headers Headers to include in the response.
*/
public function push_json( array|string $payload, array $headers = [] ): static {
return $this->push(
Mock_Http_Response::create( '', $headers )
->with_json( $payload )
->with_headers( [ 'Content-Type' => 'application/json' ] )
);
}

/**
* Make the sequence return a default response when empty.
*
Expand Down
73 changes: 60 additions & 13 deletions src/mantle/testing/concerns/trait-interacts-with-requests.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
namespace Mantle\Testing\Concerns;

use Closure;
use InvalidArgumentException;
use Mantle\Contracts\Support\Arrayable;
use Mantle\Http_Client\Request;
use Mantle\Http_Client\Response;
use Mantle\Support\Collection;
use Mantle\Support\Str;
use Mantle\Testing\Mock_Http_Response;
Expand Down Expand Up @@ -111,24 +111,32 @@ public function allow_stray_requests(): void {
* $this->fake_request( 'https://testing.com/*' );
* $this->fake_request( 'https://testing.com/*' )->with_response_code( 404 )->with_body( 'test body' );
* $this->fake_request( fn () => Mock_Http_Response::create()->with_body( 'test body' ) );
* $this->fake_request( 'https://testing.com/', fn () => Mock_Http_Response::create()->with_body( 'test body' ) );
* $this->fake_request( [ 'https://example.org' => Mock_Http_Response::create()->with_body( 'test body' ) ] );
*
* @link https://mantle.alley.com/docs/testing/remote-requests#faking-requests Documentation
*
* @throws \InvalidArgumentException Thrown on invalid argument.
* @throws InvalidArgumentException Thrown on invalid argument when response object passed twice.
* @throws InvalidArgumentException Thrown on invalid argument.
* @throws InvalidArgumentException Thrown on invalid response type.
*
* @template TCallableReturn of Mock_Http_Sequence|Mock_Http_Response|Arrayable
* @template TCallableReturn of Mock_Http_Sequence|Mock_Http_Response|Arrayable|null
*
* @param (callable(string, array): TCallableReturn)|Mock_Http_Response|string|array<string, Mock_Http_Response|callable> $url_or_callback URL to fake, array of URL and response pairs, or a closure
* that will return a faked response.
* @param Mock_Http_Response|callable $response Optional response object, defaults to a 200 response with no body.
* @param string $method Optional request method to apply to, defaults to all. Does not apply to array of URL and response pairs OR callbacks.
*/
public function fake_request( Mock_Http_Response|callable|string|array|null $url_or_callback = null, Mock_Http_Response|callable $response = null ): static|Mock_Http_Response {
public function fake_request(
Mock_Http_Response|callable|string|array|null $url_or_callback = null,
Mock_Http_Response|callable $response = null,
?string $method = null
): static|Mock_Http_Response {
if ( is_array( $url_or_callback ) ) {
$this->stub_callbacks = $this->stub_callbacks->merge(
collect( $url_or_callback )
->map(
fn ( $response, $url_or_callback ) => $this->create_stub_request_callback( $url_or_callback, $response ),
)
collect( $url_or_callback )->map(
fn ( $response, $url_or_callback ) => $this->create_stub_request_callback( $url_or_callback, $response, $method ),
)
);

return $this;
Expand All @@ -141,9 +149,21 @@ public function fake_request( Mock_Http_Response|callable|string|array|null $url
return $this;
}

// Prevent duplicate responses from being passed.
if ( $url_or_callback instanceof Mock_Http_Response && $response instanceof Mock_Http_Response ) {
throw new InvalidArgumentException( 'Response object passed twice, only one response object should be passed.' );
}

// Allow for a catch-all response to be passed in the first argument.
if ( $url_or_callback instanceof Mock_Http_Response && ! $response ) {
$this->stub_callbacks->push( $this->create_stub_request_callback( '*', $url_or_callback, $method ) );

return $url_or_callback;
}

// Throw an exception on an unknown argument.
if ( ! is_string( $url_or_callback ) && ! is_null( $url_or_callback ) ) {
throw new \InvalidArgumentException(
throw new InvalidArgumentException(
sprintf(
'Expected a URL string or a callback, got %s.',
gettype( $url_or_callback )
Expand All @@ -159,11 +179,32 @@ public function fake_request( Mock_Http_Response|callable|string|array|null $url
$response = new Mock_Http_Response();
}

$this->stub_callbacks->push( $this->create_stub_request_callback( $url, $response ) );
// Ensure that the response is an instance of Mock_Http_Response.
if ( ! $response instanceof Mock_Http_Response ) {
throw new InvalidArgumentException( 'Response must be an instance of Mock_Http_Response or callable, ' . gettype( $response ) . ' given.' );
}

$this->stub_callbacks->push(
$this->create_stub_request_callback( $url, $response, $method ),
);

return $response;
}

/**
* Fluently build a fake request sequence.
*
* @param string $url URL to fake (supports * for wildcard matching).
* @param string|null $method Request method, optional.
*/
public function fake_request_sequence( string $url, ?string $method = null ): Mock_Http_Sequence {
$sequence = Mock_Http_Sequence::create();

$this->fake_request( [ $url => $sequence ], method: $method );

return $sequence;
}

/**
* Create a mock HTTP response.
*
Expand Down Expand Up @@ -229,7 +270,7 @@ public function pre_http_request( $preempt, $request_args, $url ) {
* @param string $url Request URL.
* @param array $request_args Request arguments.
*/
protected function get_stub_response( $url, $request_args ): array|WP_Error|null {
protected function get_stub_response( string $url, array $request_args ): array|WP_Error|null {
if ( ! $this->stub_callbacks->is_empty() ) {
foreach ( $this->stub_callbacks as $stub_callback ) {
$response = $stub_callback( $url, $request_args );
Expand Down Expand Up @@ -312,13 +353,19 @@ protected function store_streamed_response( string $url, array $response, array
*
* @param string $url URL to stub.
* @param callable|Mock_Http_Response $response Response to send.
* @param string $method Request method, optional.
*/
protected function create_stub_request_callback( string $url, Mock_Http_Response|callable $response ): callable {
return function( string $request_url, array $request_args ) use ( $url, $response ) {
protected function create_stub_request_callback( string $url, Mock_Http_Response|callable $response, ?string $method = null ): callable {
return function( string $request_url, array $request_args ) use ( $url, $response, $method ) {
if ( ! Str::is( Str::start( $url, '*' ), $request_url ) ) {
return;
}

// Validate the request method for the stub callback.
if ( $method && isset( $request_args['method'] ) && strtoupper( $method ) !== strtoupper( (string) $request_args['method'] ) ) {
return;
}

return is_callable( $response )
? $response( $request_url, $request_args )
: $response;
Expand Down
53 changes: 52 additions & 1 deletion tests/Testing/Concerns/InteractsWithExternalRequestsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,23 @@
class InteractsWithExternalRequestsTest extends Framework_Test_Case {
use Prevent_Remote_Requests;

public function test_fake_request_default() {
public function test_fake_request_no_arguments() {
$this->fake_request();

$response = wp_remote_get( 'https://example.com/' );
$this->assertEquals( 200, wp_remote_retrieve_response_code( $response ) );
$this->assertEmpty( wp_remote_retrieve_body( $response ) );
}

public function test_fake_request_catch_all() {
$this->fake_request( $this->mock_response()->with_status( 201 )->with_json( [ 'name' => 'John Doe' ] ) );

$response = wp_remote_get( 'https://example.com/' );

$this->assertEquals( 201, wp_remote_retrieve_response_code( $response ) );
$this->assertEquals( 'John Doe', json_decode( wp_remote_retrieve_body( $response ) )->name );
}

public function test_fake_request() {
$this->fake_request( 'https://testing.com/*' )
->with_response_code( 404 )
Expand All @@ -53,6 +62,41 @@ public function test_fake_request() {
$this->assertEquals( 'example body', wp_remote_retrieve_body( $response ) );
}

public function test_fake_request_with_method() {
$this->fake_request( 'https://example.org/api/v1/users', method: 'POST' )
->with_status( 201 )
->with_json( [ 'name' => 'John Doe' ] );

$this->fake_request_sequence( 'https://example.org/api/v1/users', method: 'GET' )
->push_json( [
'items' => [],
] )
->push_json( [
'items' => [
[
'name' => 'John Doe',
],
],
] );

$users = Http::get( 'https://example.org/api/v1/users' );

$this->assertEquals( 200, $users->status() );
$this->assertEmpty( $users->json( 'items' ) );

// Create the user.
$user = Http::post( 'https://example.org/api/v1/users', [ 'name' => 'John Doe' ] );

$this->assertEquals( 201, $user->status() );
$this->assertEquals( 'John Doe', $user->json( 'name' ) );

// Get the users.
$users = Http::get( 'https://example.org/api/v1/users' );

$this->assertEquals( 200, $users->status() );
$this->assertEquals( 'John Doe', $users->json( 'items.0.name' ) );
}

public function test_fake_all_requests() {
$this->fake_request()
->with_response_code( 206 )
Expand All @@ -63,6 +107,13 @@ public function test_fake_all_requests() {
$this->assertEquals( 206, wp_remote_retrieve_response_code( $response ) );
}

public function test_fake_request_double_response_invalid_argument() {
$this->expectException( InvalidArgumentException::class );
$this->expectExceptionMessage( 'Response object passed twice, only one response object should be passed.' );

$this->fake_request( $this->mock_response(), $this->mock_response() );
}

public function test_fake_callback() {
$this->fake_request(
function() {
Expand Down

0 comments on commit 4961ae5

Please sign in to comment.