Wordpress - Testing custom API endpoint with class dependency - php

Sorry I feel like really stuck here.
I have a plugin introducing a new Rest API controller (WP_REST_Controller) with basically a single endpoint which uses a separate class as a client to fetch some data. Let's say:
#my_plugin.php
function register_items_routes() {
if ( ! class_exists( 'WP_REST_My_Controller' ) ) {
require_once __DIR__ . '/class-wp-my-controller.php';
}
$controller = new WP_REST_My_Controller();
$controller->register_routes();
}
add_action( 'rest_api_init', 'register_items_routes' );
_
#class-wp-my-controller.php
class WP_REST_My_Controller extends WP_REST_Controller {
/**
* Registers the routes.
*/
public function register_routes() {
$namespace = 'my/namespace';
$path = 'get-items';
register_rest_route( $namespace, '/' . $path, [
array(
'methods' => 'GET',
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' )
),
] );
}
public function get_items_permissions_check( $request ) {
return true;
}
/**
* Get items from My_Class and return them.
*
* #param WP_REST_Request $request The incoming HTTP request.
*
* #return WP_REST_Response|WP_Error The response containing the items in JSON, WP_Error in case of error.
*/
public function get_items( $request ) {
$client = new My_Class();
try {
$items = $client->fetch_some_items();
} catch ( Exception $e ) {
return new WP_Error(
'some-client-error',
$e->getMessage()
);
// Code to be tested. - Do some stuff with items and return.
return new WP_REST_Response( $items );
}
How am I supposed to stub the My_Class dependency from PhpUnit in order to return a predefined set of items which I could test with?
public function test_get_items() {
$request = new WP_REST_Request( 'GET', '/my/namespace/get-items' );
$data = rest_get_server()->dispatch( $request );
$expected_items = [
'some_key1' => 'some_value1',
'some_key2' => 'some_value2',
];
$this->assertTrue( count($data['items']) == count($expected_items) );
}

Related

MercadoLibre API gives error "getAuthUrl() should not be called statically"

I have the following 2 codes from library https://github.com/javiertelioz/mercadolibre to connect with MercadoLibre's API:
Class Meli.php:
<?php
namespace App\Sources;
use App\Sources\MercadoLibre\Utils;
class Meli extends Utils {
/**
* #version 1.0.0
*/
const VERSION = "1.0.0";
/**
* Configuration for urls
*/
protected $urls = array(
'API_ROOT_URL' => 'https://api.mercadolibre.com',
'AUTH_URL' => 'http://auth.mercadolibre.com.ar/authorization',
'OAUTH_URL' => '/oauth/token'
);
/**
* Configuration for CURL
*/
protected $curl_opts = array(
CURLOPT_USERAGENT => "MELI-PHP-SDK-1.0.0",
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_TIMEOUT => 60
);
protected $client_id;
protected $client_secret;
/**
* Constructor method. Set all variables to connect in Meli
*
* #param string $client_id
* #param string $client_secret
* #param string $access_token
*/
public function __construct($client_id, $client_secret, $urls = null, $curl_opts = null) {
$this->client_id = $client_id;
$this->client_secret = $client_secret;
$this->urls = $urls ? $urls : $this->urls;
$this->curl_opts = $curl_opts ? $curl_opts : $this->curl_opts;
}
/**
* Return an string with a complete Meli login url.
*
* #param string $redirect_uri
* #return string
*/
public function getAuthUrl($redirect_uri) {
$params = array("client_id" => $this->client_id, "response_type" => "code", "redirect_uri" => $redirect_uri);
$auth_uri = $this->urls['AUTH_URL'] . "?" . http_build_query($params);
return $auth_uri;
}
}
and the Controller MeliController.php with the following code:
class MeliController extends Controller
{
/**
* Login Page (Mercado Libre)
*/
public function login() {
session()->regenerate();
return view('auth/melilogin')->with('auth', [
'url' => meli::getAuthUrl(env('ML_AUTHENTICATION_URL', '')),
]);
}
public function logout() {
if(session('profile')) {
session()->forget('profile');
session()->flush();
}
return \Redirect::to('/auth/melilogin');
}
}
But Im receiving error:
Non-static method App\Sources\Meli::getAuthUrl() should not be called
statically
Three procedures I made with no success:
1- using facade (meli) as in the first example
meli::getAuthUrl
2- replacing code:
public function getAuthUrl($redirect_uri) {
$params = array("client_id" => $this->client_id, "response_type" => "code", "redirect_uri" => $redirect_uri);
$auth_uri = $this->urls['AUTH_URL'] . "?" . http_build_query($params);
return $auth_uri;
}
}
with public static function and $self instead of $this but with no success.
3- Making the call dynamic using:
'url' => (new \App\Sources\Meli)->getAuthUrl(env('ML_AUTHENTICATION_URL', '')),
But receiving error
Too few arguments to function App\Sources\Meli::__construct(), 0
passed in
/Applications/MAMP/htdocs/price2b/app/Http/Controllers/MeliController.php
any help appreciated.
The error tells you the problem: you are calling the method statically (meli::getAuthUrl(...)), but it's not a static method. You have to call it on an instance of the class. This means that your third approach:
'url' => (new \App\Sources\Meli)->getAuthUrl(env('ML_AUTHENTICATION_URL', '')),
is the right one.
But, as you pointed out, you get a "too few arguments" error. This is because you are passing no arguments when you instantiate the Meli class. That is, new \App\Sources\Meli is equivalent to new \App\Sources\Meli(), passing zero arguments to the constructor.
But the constructor for the Meli class, which you posted above, looks like this:
public function __construct($client_id, $client_secret, $urls = null, $curl_opts = null)
So, you need to pass at least 2 arguments, not zero. In other words, at a minimum, something like this:
'url' => (new \App\Sources\Meli($someClientId, $someClientSecret))->getAuthUrl(env('ML_AUTHENTICATION_URL', '')),

Middleware causing getArguments() to be null

Here is the middleware flow:
# Post edit
$this->get( '/edit/{id}/{slug}', \Rib\Src\Apps\Post\PostControllers\EditController::class . ':index' )
->add( new EnforceEditDelay() )
->add( new RequireOwner( 'posts' ) )
->add( new RejectBanned() )
->add( new RequireAuth() );
The ->add( new RejectBanned() ) cause the next middleware in the chain to break with:
'Call to a member function getArguments() on null'
RequireAuth():
class RequireAuth
{
# Variable used to disable redirect to '/user/set-username' from itelf. That would cause infinite redirection loop.
# This is passed to the middleWare from the list of routes. Of course only true for '/user/set-username' pages.
private $disableUserNameValidationCheck;
function __construct( $disableUserNameValidationCheck = false )
{
$this->disableUserNameValidationCheck = $disableUserNameValidationCheck;
}
public function __invoke( Request $request, Response $response, $next )
{
$session = $_SESSION;
# User is not authenticated: we ensure this by checking his id which is necessarily set when he is logged in.
if ( ! isset( $session[ 'id' ] ) ) {
FlashMessages::flashIt( 'message', "The page you tried to access requires that you are logged in the site." );
return $response->withRedirect( '/user/login' );
}
# In case user has logged in from a social network and has not set a user name and password. Username is 'temporary-.....'
# We really want the user to set his username. So on designated page we force redirect to page to setup username and email.
if ( ! $this->disableUserNameValidationCheck and isset( $session[ 'username' ] ) and strpos( $session[ 'username' ], 'temporary' ) !== false ) {
FlashMessages::flashIt( 'message',
"This part of the site requires that you complete your profile with a definitive username and email. Thank you for your understanding." );
return $response->withRedirect( '/user/set-username' );
}
$request = $request->withAttribute( 'session', $session );
# Process regular flow if not interrupted by the middleWare.
return $next( $request, $response );
}
}
RejectBanned():
class RejectBanned
{
/**
* Reject banned user
* #param Request $request
* #param Response $response
* #param $next
* #return Response
*/
public function __invoke( Request $request, Response $response, $next )
{
$session = $request->getAttribute( 'session' ) ?? null;
# Get usergroup from db
$user = ( new DbSql() )->db()->table( 'users' )->find( $session['id'] );
$userGroup = $user->user_group;
# Store it in session
$session['user_group'] = $userGroup;
# Redirect user if usergroup = banned
if ( $userGroup === 'banned' ) {
FlashMessages::flashIt( 'message', 'You are not allowed anymore to access this resource.' );
return $response->withRedirect( '/message' );
}
# Store info for the next middleware or controller
$request = $request->withAttributes( [ 'session' => $session ] );
# User is not banned, pursue
return $next( $request, $response );
}
}
RequireOwner() (this is where it breaks, I added a comment where it breaks):
class RequireOwner
{
private $table;
function __construct( $tableName )
{
$this->table = $tableName;
}
public function __invoke( Request $request, Response $response, $next )
{
$session = $request->getAttribute( 'session' ) ?? null;
// BREAKS HERE:
$recordId = $request->getAttribute( 'route' )->getArguments()[ 'id' ] ?? null; // BREAKS HERE
$currentUserGroup = $session[ 'user_group' ] ?? null;
$currentUserId = $session[ 'id' ] ?? null;
$recordInstance = ( new DbSql() )->db()->table( $this->table )->find( $recordId );
# If any info is missing, interrupt
if ( ! $recordInstance or ! $session or ! $recordId or ! $currentUserGroup or ! $currentUserId ) {
throw new Exception( 'Missing information to determine the owner of record' );
}
# Store info for the next middleware or controller
$request = $request->withAttributes( [ 'session' => $session, 'recordInstance' => $recordInstance ] );
# User is an Admin, he can edit any post
if ( $currentUserGroup === 'admin' ) {
return $next( $request, $response );
}
# User is not owner of post
if ( $currentUserId != $recordInstance->author_id ) {
FlashMessages::flashIt( 'message', 'You must be the author of this content to be able to edit it.' );
return $response->withRedirect( '/message' );
}
# User is not admin but is owner of content
return $next( $request, $response );
}
}
So why does the ->add( new RejectBanned() )causes the null value in the next middleware ?
In RejectBanned():
Changed
$request = $request->withAttributes( [ 'session' => $session ] );
to
$request = $request->withAttribute( 'session', $session );
And it fixed the issue.

Echo executed twice

I've added an ACL to my website but when I test the result of my role variable in the SecurityPlugin.php file I get the result twice.
Why does Phalcon show the var_dump of $role twice? I'm fairly new to this framework and my initial thought was it might me due to routing in Phalcon?
Phalcon version: 3.0.3
\app\plugins\SecurityPlugin.php
use Phalcon\Acl;
use Phalcon\Acl\Role;
use Phalcon\Acl\Adapter\Memory as AclList;
use Phalcon\Acl\Resource;
use Phalcon\Events\Event;
use Phalcon\Mvc\User\Plugin;
use Phalcon\Mvc\Dispatcher;
class SecurityPlugin extends Plugin
{
/**
* Returns an existing or new access control list
*
* #returns AclList
*/
public function getAcl()
{
if (!isset($this->persistent->acl)) {
$acl = new AclList();
$acl->setDefaultAction(Acl::DENY);
// Register roles
$roles = [
'admins' => new Role(
'admins',
'Website administrators'
),
'users' => new Role(
'users',
'Member privileges, granted after sign in.'
),
'guests' => new Role(
'guests',
'Anyone browsing the site who is not signed in is considered to be a "Guest".'
)
];
foreach ($roles as $role) {
$acl->addRole($role);
}
//Private area resources
$privateResources = array(
'account' => array('*')
);
$privateResourcesAdmin = array(
'admin' => array('*')
);
//Public area resources
$publicResources = array(
'index' => array('*'),
'register' => array('*'),
'errors' => array('show401', 'show404', 'show500'),
'register' => array('*'),
'login' => array('*'),
'logout' => array('*')
);
foreach ($privateResources as $resource => $actions) {
$acl->addResource(new Resource($resource), $actions);
}
foreach ($privateResourcesAdmin as $resource => $actions) {
$acl->addResource(new Resource($resource), $actions);
}
foreach ($publicResources as $resource => $actions) {
$acl->addResource(new Resource($resource), $actions);
}
//Grant access to public areas to users, admins and guests
foreach ($roles as $role) {
foreach ($publicResources as $resource => $actions) {
foreach ($actions as $action){
$acl->allow($role->getName(), $resource, $action);
}
}
}
//Grant access to private area to role Users
foreach ($privateResources as $resource => $actions) {
foreach ($actions as $action){
$acl->allow('users', $resource, $action);
}
}
foreach ($privateResourcesAdmin as $resource => $actions) {
foreach ($actions as $action){
$acl->allow('admins', $resource, $action);
}
}
//The acl is stored in session, APC would be useful here too
$this->persistent->acl = $acl;
}
return $this->persistent->acl;
}
/**
* This action is executed before execute any action in the application
*
* #param Event $event
* #param Dispatcher $dispatcher
* #return bool
*/
public function beforeExecuteRoute(Event $event, Dispatcher $dispatcher){
$auth = $this->session->get('auth');
if (!$auth){
$role = 'guests';
} else {
if ($this->session->has("account_type")) {
$type = $this->session->get("account_type");
if($type == 99){
$role = 'admins';
} else {
$role = 'users';
}
}
}
var_dump($role);
$controller = $dispatcher->getControllerName();
$action = $dispatcher->getActionName();
$acl = $this->getAcl();
if (!$acl->isResource($controller)) {
$dispatcher->forward([
'controller' => 'errors',
'action' => 'show404'
]);
return false;
}
$allowed = $acl->isAllowed($role, $controller, $action);
if (!$allowed) {
$dispatcher->forward(array(
'controller' => 'errors',
'action' => 'show401'
));
$this->session->destroy();
return false;
}
}
}
\public\index.php
<?php
use Phalcon\Di\FactoryDefault;
use Phalcon\Mvc\Dispatcher; //Used for ACL list and authorization routing
use Phalcon\Events\Manager as EventsManager; //Used for ACL List
use Phalcon\Mvc\Router; //Used for routing logout page
error_reporting(E_ALL);
define('BASE_PATH', dirname(__DIR__));
define('APP_PATH', BASE_PATH . '/app');
try {
/**
* The FactoryDefault Dependency Injector automatically registers
* the services that provide a full stack framework.
*/
$di = new FactoryDefault();
/**
* Read services
*/
include APP_PATH . "/config/services.php";
/**
* Get config service for use in inline setup below
*/
$config = $di->getConfig();
/**
* Include Autoloader
*/
include APP_PATH . '/config/loader.php';
//This makes sure the routes are correctly handled for authorized/unauthorized in people
/**
* MVC dispatcher
*/
$di->set("dispatcher", function () use ($di) {
// Create an events manager
$eventsManager = $di->getShared('eventsManager');
/**
*Check if the user is allowed to access certain action using the SecurityPlugin
*Listen for events produced in the dispatcher using the Security plugin
*/
$eventsManager->attach(
"dispatch:beforeExecuteRoute",
new SecurityPlugin()
);
// Handle exceptions and not-found exceptions using NotFoundPlugin
$eventsManager->attach(
"dispatch:beforeException",
new NotFoundPlugin()
);
$dispatcher = new Dispatcher();
// Assign the events manager to the dispatcher
$dispatcher->setEventsManager($eventsManager);
return $dispatcher;
}
);
/**
* Handle and deploy the application
*/
$application = new \Phalcon\Mvc\Application($di);
echo $application->handle()->getContent();
} catch (\Exception $e) {
echo $e->getMessage() . '<br>';
echo '<pre>' . $e->getTraceAsString() . '</pre>';
}
Because you are doing forward - so this means there is other action executed again and beforeExecuteRoute fired again - that's why 2 times var_dump

wp-async-task don't fire run_action method

I have to work with the techcrunch wp-async-task to run a synchronization task in background in my wordpress plugin.
So to test, at the bottom of the main file I have :
//top of the php file
require_once(dirname(__FILE__) . '/lib/WP_Async_Task.php');
require_once(dirname(__FILE__) . '/class/my_api_status.class.php');
define('API_URL', '...');
/* ... */
// At the bottom of the file
function my_api_status($api_url)
{
sleep(5);
$r = wp_safe_remote_get($api_url);
if (!is_wp_error($r)) {
$body = json_decode(wp_remote_retrieve_body($r));
if (isset($body->success)) {
return;
}
}
}
add_action('wp_async_api_status', 'my_api_status');
function my_init_api_status()
{
new ApiStatusTask();
do_action('api_status', constant('API_URL'));
}
add_action('plugins_loaded', 'my_init_api_status');
And api status task class
class ApiStatusTask extends WP_Async_Task {
protected $action = 'api_status';
/**
* Prepare data for the asynchronous request
* #throws Exception If for any reason the request should not happen
* #param array $data An array of data sent to the hook
* #return array
*/
protected function prepare_data( $data ) {
return array(
'api_url' => $data[0]
);
}
/**
* Run the async task action
*/
protected function run_action() {
if(isset($_POST['api_url'])){
do_action("wp_async_$this->action", $_POST['api_url']);
}
}
}
The function prepare_data is correctly called by launchand after that launch_on_shutdown is also correctly called and finally wp_remote_post is called at the end of launch_on_shutdown with admin-post.php.
But the function run_action is never called ... and so the my_api_status in the main file.
What it possibly go wrong ?
I will put a complete example of a plugin here soon. But for now, I found my problem :
// In the `launch_on_shutdown` method of `WP_Async_Task` class
public function launch_on_shutdown() {
GcLogger::getLogger()->debug('WP_Async_Task::launch_on_shutdown');
if ( ! empty( $this->_body_data ) ) {
$cookies = array();
foreach ( $_COOKIE as $name => $value ) {
$cookies[] = "$name=" . urlencode( is_array( $value ) ? serialize( $value ) : $value );
}
$request_args = array(
'timeout' => 0.01,
'blocking' => false,
'sslverify' => false, //apply_filters( 'https_local_ssl_verify', true ),
'body' => $this->_body_data,
'headers' => array(
'cookie' => implode( '; ', $cookies ),
),
);
$url = admin_url( 'admin-post.php' );
GcLogger::getLogger()->debug('WP_Async_Task::launch_on_shutdown wp_remote_post');
wp_remote_post( $url, $request_args );
}
}
The sslverify option failed in my local environment. I just had to put it on false if we are not in production.
With this option set, the run_action is correctly trigger.

zend framework rest controller question

I have a rest controller example im trying to run that is giving me a headache.
My url im trying to access is localhost/books/edit/1
For some weird reason this route seems to call the getAction with the Controller instead of the editAction. And it throws errors saying that the object doesnt exist.
The controller is,
class BooksController extends Zend_Rest_Controller {
private $_booksTable;
private $_form;
public function init() {
$bootstrap = $this->getInvokeArg ( 'bootstrap' );
$db = $bootstrap->getResource ( 'db' );
$options = $bootstrap->getOption ( 'resources' );
$dbFile = $options ['db'] ['params'] ['dbname'];
if (! file_exists ( $dbFile )) {
$createTable = "CREATE TABLE IF NOT EXISTS books (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name VARCHAR(32) NOT NULL,
price DECIMAL(5,2) NOT NULL
)";
$db->query ( $createTable );
$insert1 = "INSERT INTO books (name, price) VALUES ('jQuery in Action', 39.99)";
$insert2 = "INSERT INTO books (name, price) VALUES ('PHP in Action', 45.99)";
$db->query ( $insert1 );
$db->query ( $insert2 );
}
$this->_booksTable = new Zend_Db_Table ( 'books' );
$this->_form = new Default_Form_Book ();
}
/**
* The index action handles index/list requests; it should respond with a
* list of the requested resources.
*/
public function indexAction() {
$this->view->books = $this->_booksTable->fetchAll ();
}
/**
* The list action is the default for the rest controller
* Forward to index
*/
public function listAction() {
$this->_forward ( 'index' );
}
/**
* The get action handles GET requests and receives an 'id' parameter; it
* should respond with the server resource state of the resource identified
* by the 'id' value.
*/
public function getAction() {
$this->view->book = $this->_booksTable->find ( $this->_getParam ( 'id' ) )->current ();
}
/**
* Show the new book form
*/
public function newAction() {
$this->view->form = $this->_form;
}
/**
* The post action handles POST requests; it should accept and digest a
* POSTed resource representation and persist the resource state.
*/
public function postAction() {
if ($this->_form->isValid ( $this->_request->getParams () )) {
$this->_booksTable->createRow ( $this->_form->getValues () )->save ();
$this->_redirect ( 'books' );
} else {
$this->view->form = $this->_form;
$this->render ( 'new' );
}
}
/**
* Show the edit book form. Url format: /books/edit/2
*/
public function editAction() {
var_dump ($this->getRequest()->getParam ( 'edit' ));
$book = $this->_booksTable->find ( $this->getRequest()->getParam ( 'id' ) )->current ();
var_dump ($book->toArray ());
$this->_form->populate ( $book->toArray () );
$this->view->form = $this->_form;
$this->view->book = $book;
}
/**
* The put action handles PUT requests and receives an 'id' parameter; it
* should update the server resource state of the resource identified by
* the 'id' value.
*/
public function putAction() {
$book = $this->_booksTable->find ( $this->_getParam ( 'id' ) )->current ();
if ($this->_form->isValid ( $this->_request->getParams () )) {
$book->setFromArray ( $this->_form->getValues () )->save ();
$this->_redirect ( 'books' );
} else {
$this->view->book = $book;
$this->view->form = $this->_form;
$this->render ( 'edit' );
}
}
/**
* The delete action handles DELETE requests and receives an 'id'
* parameter; it should update the server resource state of the resource
* identified by the 'id' value.
*/
public function deleteAction() {
$book = $this->_booksTable->find ( $this->_getParam ( 'id' ) )->current ();
$book->delete ();
$this->_redirect ( 'books' );
}
}
The bootstrap is,
class Bootstrap extends Zend_Application_Bootstrap_Bootstrap {
protected function _initAutoload() {
$autoloader = new Zend_Application_Module_Autoloader ( array (
'namespace' => 'Default_',
'basePath' => dirname ( __FILE__ )
) );
return $autoloader;
}
protected function _initRestRoute() {
$this->bootstrap ( 'Request' );
$front = $this->getResource ( 'FrontController' );
$restRoute = new Zend_Rest_Route ( $front, array (), array (
'default' => array ('books' )
) );
$front->getRouter ()->addRoute ( 'rest', $restRoute );
}
protected function _initRequest() {
$this->bootstrap ( 'FrontController' );
$front = $this->getResource ( 'FrontController' );
$request = $front->getRequest ();
if (null === $front->getRequest ()) {
$request = new Zend_Controller_Request_Http ();
$front->setRequest ( $request );
}
return $request;
}
}
Can anyone see what might be causing the getAction to be called when browsing to that link ???
edit should follow the identifier, so the correct edit URL is http://localhost/books/1/edit

Categories