I'm currently developing an application written using Slim 3 framework, and trying to use the TDD concept, but I'm facing several issues, including when using the test for the POST method, the headers that I embed are always considered missing.
Below is my code
<?php
namespace Tests\routes\shopify;
use PHPUnit\Framework\TestCase;
use Slim\App;
use Slim\Http\Environment;
use Slim\Http\Headers;
use Slim\Http\Request;
use Slim\Http\RequestBody;
use Slim\Http\Response;
use Slim\Http\UploadedFile;
use Slim\Http\Uri;
class ShopifyRoutesTest extends TestCase
{
private $app;
protected function setUp(): void
{
// Use the application settings
$settings = require __DIR__ . '/../../../src/settings.php';
// Instantiate the application
$this->app = new App($settings);
// Set up dependencies
$dependencies = require __DIR__ . '/../../../src/dependencies.php';
$dependencies($this->app);
// Register middleware
$middleware = require __DIR__ . '/../../../src/middleware.php';
$middleware($this->app);
// Register routes
$routes = require __DIR__ . '/../../../src/app/routes/api/shopify/routes.php';
$routes($this->app);
}
public function testPostSyncProductBySkuWithEmptyApikeyShouldReturnBadRequest()
{
// Create a mock environment for testing with
$environment = Environment::mock();
$uri = Uri::createFromString("/channel/shopify/v1/product/sync-by-sku");
$headers = new Headers(array(
"Content-Type" => "application/json",
"Authorization" => "client-apikey",
"x-api-key" => ""
));
$cookies = [];
$serverParams = $environment->all();
$body = new RequestBody();
$uploadedFiles = UploadedFile::createFromEnvironment($environment);
// Set up a request object based on the environment
$request = new Request("POST", $uri, $headers, $cookies, $serverParams, $body, $uploadedFiles);
$reqBody = array(
"shop_name" => "STORE ABCE",
"sku" => "SKU-001"
);
$body->write(json_encode($reqBody));
// Add request data, if it exists
$request = $request->withParsedBody($reqBody);
// Set up a response object
$response = new Response();
$response = $this->app->process($request, $response);
self::assertEquals(400, $response->getStatusCode());
self::assertStringContainsString("Header: x-api-key cannot be empty", $response->getBody());
}
protected function tearDown(): void
{
$this->app = null;
}
}
the first assertion succeeds with a value of 400, but in the second assertion its fails, the string doesn't contain the value "Header: x-api-key cannot be empty" but instead this
Failed asserting that '{\n
"error": "AUTH_FAIL",\n
"errorDetail": "Header: Authorization, x-api-key required",\n
"status": 400,\n
"message": "BAD_REQUEST",\n
"data": ""\n
}' contains "Header: x-api-key cannot be empty".
the strange thing is that when I var_dump($request->getHeaders()) the headers value of the request I made it turns out to be there
array(3) {
["Content-Type"]=>
array(1) {
[0]=>
string(16) "application/json"
}
["Authorization"]=>
array(1) {
[0]=>
string(13) "client-apikey"
}
["x-api-key"]=>
array(1) {
[0]=>
string(0) ""
}
}
I've also tried testing my API endpoint using Postman, and the results are as expected
Request
curl --location --request POST 'http://localhost:8080/channel/shopify/v1/product/sync-by-sku' \
--header 'Authorization: client-apikey' \
--header 'x-api-key: 1293129382938' \
--header 'Content-Type: application/json' \
--header 'Cookie: PHPSESSID=tll8s24tp253rda1harv0koapi' \
--data-raw '{
"shop_name" : "STORE ABC",
"sku" : "SKU-991"
}'
Response
{
"error": "AUTH_FAIL",
"errorDetail": "Header: x-api-key cannot be empty",
"status": 400,
"message": "BAD_REQUEST",
"data": ""
}
Also I've read the answer from stakoverflow as described here Mock Slim endpoint POST requests with PHPUnit
But still I can't find the solution, the header is always presumed to be missing. I really appreciate the solution to this problem, thank you in advance
finally after figuring out the structure and behavior of the Header and also Request in Slim 3, the Header class in Slim 3 always makes the key value to lower-case, I don't know what that means, but finally I need to adjust this behavior in my middleware, from which previously used $request->getHeaders() to $request->getHeaderLine() and also $request->hasHeader(), $request->getHeaders() made the header value upper-case and added HTTP_ to the front of the key
in my case this is the cause of the problem, because the request I use in the unit test must pass the lower-case value and don't have HTTP_ at the front of the key, so the middleware assumes that the key that should exist has never existed
Middleware Before
// Common Channel Auth with client-apikey
$app->add(function (Request $request, Response $response, callable $next) use ($container) {
$uri = $request->getUri();
$path = $uri->getPath();
$headers = $request->getHeaders();
$arrayPath = explode("/", $path);
if ($arrayPath[1] == "channel" && $arrayPath[3] == "tools")
{
return $next($request, $response);
}
elseif ($arrayPath[1] == "channel" && $arrayPath[4] != "webhook")
{
/** #var ClientRepository $clientRepository */
$clientRepository = $container->get("ClientRepository");
// Get Header With Name x-api-key & Authorization
if (isset($headers["HTTP_AUTHORIZATION"]) && isset($headers["HTTP_X_API_KEY"]))
{
if ($headers["HTTP_AUTHORIZATION"][0] == "client-apikey")
{
$reqClientApikey = $headers["HTTP_X_API_KEY"][0];
if (v::notBlank()->validate($reqClientApikey))
{
if ($clientRepository->findByClientApiKey($reqClientApikey))
{
return $next($request, $response);
Middleware After
// Common Channel Auth with client-apikey
$app->add(function (Request $request, Response $response, callable $next) use ($container) {
$uri = $request->getUri();
$path = $uri->getPath();
$arrayPath = explode("/", $path);
if ($arrayPath[1] == "channel" && $arrayPath[3] == "tools")
{
return $next($request, $response);
}
elseif ($arrayPath[1] == "channel" && $arrayPath[4] != "webhook")
{
/** #var ClientRepository $clientRepository */
$clientRepository = $container->get("ClientRepository");
// Using $request-hasHeader & $request->getHeaderLine instead of $headers["HTTP_AUTHORIZATION"]
if ($request->hasHeader("authorization") != null && $request->hasHeader("x-api-key") != null)
{
if ($request->getHeaderLine("authorization") == "client-apikey")
{
$reqClientApikey = $request->getHeaderLine("x-api-key");
if (v::notBlank()->validate($reqClientApikey))
{
if ($clientRepository->findByClientApiKey($reqClientApikey))
{
return $next($request, $response);
}
Unit Test
public function testPostSyncProductBySkuWithEmptyApikeyShouldReturnBadRequest()
{
// Create a mock environment for testing with
$environment = Environment::mock();
$uri = Uri::createFromString("/channel/shopify/v1/product/sync-by-sku");
$headers = new Headers([
"Content-Type" => "application/json",
"Authorization" => "client-apikey",
"x-api-key" => ""
]);
$cookies = [];
$serverParams = $environment->all();
$body = new RequestBody();
$uploadedFiles = UploadedFile::createFromEnvironment($environment);
// Set up a request object based on the environment
$request = new Request("POST", $uri, $headers, $cookies, $serverParams, $body, $uploadedFiles);
$reqBody = array(
"shop_name" => "STORE ABCE",
"sku" => "SKU-001"
);
$body->write(json_encode($reqBody));
// Add request data, if it exists
$request = $request->withParsedBody($reqBody);
// Set up a response object
$response = new Response();
$response = $this->app->process($request, $response);
self::assertEquals(400, $response->getStatusCode());
self::assertStringContainsString("Header: x-api-key cannot be empty", $response->getBody());
}
Once again, I hope this mistake I made will be a record for others, thank you
Related
I am trying to link up with Walmart.io API to get some data from their resources. But I am stuck up in the first phase.
According to Walmart.io Quick Start Doc (https://walmart.io/docs/affiliate/quick-start-guide) I am supposed to follow following steps:
Create an account with Walmart.io
Create an application for Web Application
Generate a certificate ( According to their guide there should be some feature to autogenerate the certificate, but I didn't find it)
Upload public key to the application
We will get consumer id and key version using which along with private key, we can make a request. We need to add additional headers that includes Signature and Timestamp too.
So, I did everything, but it still isn't working.
I am using Open SSL to generate private and public key as suggested by them: https://walmart.io/key-tutorial
I tried avoiding -des3 so that it doesn't ask me for passphrase too, but it didn't work either.
Here is the script I tried with
curl --location --request GET 'https://developer.api.walmart.com/api-proxy/service/affil/product/v2/taxonomy' \
--header 'WM_SEC.KEY_VERSION: 2' \
--header 'WM_CONSUMER.ID: <Consumer_ID>' \
--header 'WM_CONSUMER.INTIMESTAMP: 1594389945813' \
--header 'WM_SEC.AUTH_SIGNATURE: W5PEHIew3LsnATk0zxJddeo416YEpMIjvk1b7lW9VMIZFx55erc/5df/FK9UtS5i48q057oASo0AX3SDd2hx+QSeyiX3FtLAgAgiZnGqQ6nJndySWgL5ih/GaUTXIC6dd048GFEZlC6axXdGoTWNzX9P0n/2DwLF9EtvMjdvjB1kum0z2xKz/lQGlvnjVkGK9sZdSUa5rfgxKSPi7ix+LRIJWYwt6mTKUlGz2vP1YjGcZ7gVwAs9o8iFC//0rHUWFwaEGrT0aZJtS7fvSFtKj5NRfemX4fwRO4cgBRxPWy9MRooQwXPmKxRP75PxHKTerv8X6HvRo0GdGut+2Krqxg==' \
And the response I get is
{
"details": {
"Description": "Could not authenticate in-request, auth signature : Signature verification failed: affil-product, version: 2.0.0, env: prod",
"wm_svc.version": "2.0.0",
"wm_svc.name": "affil-product",
"wm_svc.env": "prod"
}
}
Hope someone gives me some insight into this problem.
Thanks in advance
I've had this issue before, it looks like the format of the data you are trying to sign is incorrect.
In node, the content of the template string should look like this: ${consumerId}\n${timeStamp}\n${keyVersion}\n
Turns out it was issue with generated Signature (That explains why it worked after I changed the script.
Thus here is the script that worked fine:
<?php
use GuzzleHttp\Psr7;
use GuzzleHttp\Exception\RequestException;
class Walmart{
private $host;
private $consumer_id;
private $private_key_file;
private $headers;
private $sec_key_version;
private $client;
private $options;
public function __construct($config){
$this->host = $config['host'];
$this->consumer_id = $config['consumer_id'];
$this->private_key_file = $config['private_key_file'];
$this->sec_key_version = $config['sec_key_version'];
$this->options = array();
$this->client = new GuzzleHttp\Client();
}
public function lookup_product($publisher_id='', $ids='', $upc='', $format='json'){
$this->load_options();
$url_params = array(
'format' => $format,
);
if($publisher_id){
$url_params['publisher_id'] = $publisher_id;
}
if($ids){
$url_params['ids'] = $ids;
}
if($upc){
$url_params['upc'] = $upc;
}
$query = http_build_query($url_params);
$url = $this->host . '/product/v2/items?'.$query;
try {
$res = $this->client->request('GET', $url, $this->options);
$body = $res->getBody();
if($res->getStatusCode() == 200){
return $this->response(false, json_decode($body, true));
}else{
return $this->response(array(
'title' => 'Unable to get products',
'stack' => $body,
));
}
} catch (RequestException $e) {
$err = Psr7\str($e->getRequest());
if ($e->hasResponse()) {
$err .= Psr7\str($e->getResponse());
}
return $this->response(array(
'title' => 'Unable to get products',
'stack' => $err,
));
}
}
private function load_options(){
$timestamp = time()*1000;
$this->options = array(
'debug' => (defined("DEBUG") && DEBUG) ? true: false,
'headers' => array(
'WM_SEC.KEY_VERSION' => $this->sec_key_version,
'WM_CONSUMER.ID' => $this->consumer_id,
'WM_CONSUMER.INTIMESTAMP' => $timestamp,
'WM_SEC.AUTH_SIGNATURE' => $this->get_signature($timestamp),
)
);
}
private function get_signature($timestamp){
$message = $this->consumer_id."\n".$timestamp."\n".$this->sec_key_version."\n";
$pkeyid = openssl_pkey_get_private("file://".$this->private_key_file);
openssl_sign($message, $signature, $pkeyid, OPENSSL_ALGO_SHA256);
$signature = base64_encode($signature);
openssl_free_key($pkeyid);
return $signature;
}
private function response($err, $data=false){
return array(
'error' => $err,
'data' => $data,
);
}
}
Note: It uses guzzlehttp/guzzle library for HTTP Request
Here is a full example based on Abiral's post above:
<?php
/**
* Sample script to sign and send a request to the Walmart Affiliate Marketing API.
*
* https://walmart.io/docs/affiliate/introduction
*
* Usage:
* 1. Fill out the required variables at the top of this script.
* 2. Install dependencies via composer install.
* 3. Run via php index.php or by opening this script in a browser.
*
* Acknowledgements:
* Abiral Neupane at https://stackoverflow.com/a/62847241/1120652
* #gorenstein at https://gitter.im/IO-support/community?at=5f2e5d2051bb7d3380d9b58b
*/
include './vendor/autoload.php';
use \GuzzleHttp\Client;
/**
* Create an account at Walmart.io. Then create an application. Then follow the
* steps at https://walmart.io/key-tutorial to create a set of keys. Upload
* the public key (its contents start with BEGIN PUBLIC KEY) into the
* production environment of the application that you created.
*/
$consumer_id = 'Paste here the consumer id that you will see in your application details after pasting the public key';
$key = 'Paste here the private key. Full, including BEGIN and END PRIVATE KEY lines.';
$version = '1';
$timestamp = round(microtime(true) * 1000);
$message = $consumer_id . "\n" . $timestamp . "\n" . $version . "\n";
$pkeyid = openssl_pkey_get_private($key);
openssl_sign($message, $signature, $pkeyid, OPENSSL_ALGO_SHA256);
$signature = base64_encode($signature);
openssl_free_key($pkeyid);
$api = 'https://developer.api.walmart.com';
$product_resource = 'api-proxy/service/affil/product/v2/items/316226539';
$client = new Client(['base_uri' => $api]);
$response = $client->get($product_resource, [
'headers' => [
'WM_SEC.KEY_VERSION' => $version,
'WM_CONSUMER.ID' => $consumer_id,
'WM_CONSUMER.INTIMESTAMP' => $timestamp,
'WM_SEC.AUTH_SIGNATURE' => $signature,
]
]);
print_r(json_decode($response->getBody()->__toString()));
I published the above at https://github.com/juampynr/walmart-api-v2-php
I'm trying to test an endpoint of my Api with phpunit and the symfony WebTestCase object. I have to send a POST request with the KernelBrowser but I can't figure it out how to add parameters to the body of the request. My request work fine on postman.
I've tried this
$client->request('POST', '/url', ['param1' =>'value1', 'param2' => 'value2']);
It's not working.
I've tried this
$client->request('POST', '/url', [], [], [], '{param1: value, param2: value}');
It doesn't work,
I can't use the $client->submitForm() method because the form is send by another app.
Maybe it came from my Api endpoint because I'm using $_POST variable ?:
$res = false;
if(count($_POST) === 2){
$user = $this->userrepo->findByName($_POST['value1']);
if($user){
if($this->passwordEncoder->isPasswordValid($user[0], $_POST['value2'])){
$res = true;
}
}
}
return new Response($this->serializer->serialize(['isChecked' => $res], 'json'));
My test method has never passed the first if statement,
here my test method:
$client = static::createClient();
$client->request('POST', '/url', ['value1' => 'value1', 'value2' => 'value2']);
$this->assertStringContainsString('{"isChecked":true}', $client->getResponse()->getContent());
Here the POST request I'm trying to send:
curl --location --request POST 'http://localhost:8000/url' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--form 'value1=value1' \
--form 'value2=value2'
Symfony's test client dispatches the request internally. The global $_POST variable will always be empty. You should use the Request object in the controller to access the parameters. The attribute request contains the post data.
public function myAction(Request $request): Response
{
$postParameters = $request->request;
$res = false;
if ($postParameters->count() === 2) {
$user = $this->userrepo->findByName($postParameters->get('value1'));
if ($user) {
if ($this->passwordEncoder->isPasswordValid($user[0], $postParameters->get('value2'))) {
$res = true;
}
}
}
return new Response($this->serializer->serialize(['isChecked' => $res], 'json'));
}
Regarding the different variations of your test call, this one should work with the action above.
$client->request('POST', '/url', ['value1' => 'value1', 'value2' => 'value2']);
I am currently building a Financial micro service application using Laravel/Lumen micro framework.Everything have been working perfectly as expected. My problem now is that i am trying to make a network request to my internal services via Api call from ApiGateway using GuzzleHttp client. The problem is that when i make request to the internal service, it always throws an exception of ClientException.
ClientException.
Client error: GET http://127.0.0.1:8081/v1/admin resulted in a 401
Unauthorized response: {"error":"Unauthorized.","code":401}
I have tried to make network request to the same internal services using postman; and it works fine. However, for some reason still fail to work with GuzzleHttp. I don't know what i am doing wrong. Please your assist will be appreciated.
Here is the httpClient.php in ApiGateway.
//Constructor method
public function __construct() {
$this->baseUri = config('services.auth_admin.base_uri');
}
public function httpRequest($method, $requestUrl, $formParams = [], $headers = []) {
//Instantiate the GazzleHttp Client
$client = new Client([
'base_uri' => $this->baseUri,
]);
//Send the request
$response = $client->request($method, $requestUrl, ['form_params' => $formParams, 'headers' => $headers]);
//Return a response
return $response->getBody();
}
//Internal Service Communication in ApiGateway**
public function getAdmin($header) {
return $this->httpRequest('GET', 'admin', $header);
}
InternalServiceController.php
public function getAdmin(Request $request) {
return $this->successResponse($this->authAdminService->getAdmin($request->header()));
}
I am using Lumen version: 5.8 and GuzzleHttp Version: 6.3
You pass your headers as formParams (third index instead of fourth).
Try below:
return $this->httpRequest('GET', 'admin', [], $header);
I am making some assumptions here which I hope should be helpful to you.
PHP does not support skipping optional parameters and thus you should pass an empty array [] when calling httpRequest().
public function httpRequest($method, $requestUrl, $formParams = [], $headers = [], $type='json', $verify = false) {
//Instantiate the GazzleHttp Client
$client = new Client([
'base_uri' => $this->baseUri,
]);
//the request payload to be sent
$payload = [];
if (!$verify) {
$payload['verify'] = $verify; //basically for SSL and TLS
}
//add the body to the specified payload type
$payload[$type] = $formParams;
//check if any headers have been passed and add it as well
if(count($headers) > 0) {
$payload['headers'] = $headers;
}
//Send the request
$response = $client->request($method, $requestUrl, $payload);
//Return a response
return $response->getBody();
}
Now you need to call it in this manner when you are not passing in any form_params or body
//Internal Service Communication in ApiGateway**
public function getAdmin($header) {
return $this->httpRequest('GET', 'admin', [], $header);
}
This code works, but how can I get the permissions to see the /api content with a get request??
<?php
use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;
require 'vendor/autoload.php';
$app = new \Slim\App();
$app->add(new \Slim\Middleware\JwtAuthentication([
"path" => "/api",
"secret" => "1234"
]));
$app->get('/api', function (Request $request, Response $response) {
echo "Hi";
});
$app->get('/teste', function (Request $request, Response $response) {
echo "Hi";
});
$app->run();
1. Generate Token
Using firebase/php-jwt
$payload = [
"sub" => "user#example.com"
];
$token = JWT::encode($payload,'JWT-secret-key');
2. .htaccess Changes
If using Apache add the following to the .htaccess file. Otherwise PHP wont have access to Authorization: Bearer header
RewriteRule .* - [env=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
3. Middleware
$app->add(new \Slim\Middleware\JwtAuthentication([
"path" => "/api",
"passthrough" => ["/teste"],
"secret" => "JWT-secret-key",
"secure" => false,
"callback" => function ($request, $response, $arguments) use ($container) {
$container["jwt"] = $arguments["decoded"];
},
"error" => function ($request, $response, $arguments) {
$data["status"] = "0";
$data["message"] = $arguments["message"];
$data["data"] = "";
return $response
->withHeader("Content-Type", "application/json")
->write(json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
}
]));
4. Correct Request
5. Wrong Token Request
Reference Link
i used Authorization: Bearer Mykey , the key need to be encode in jwt mode
The majority of responses in my application are either views or JSON. I can't figure out how to put them in objects that implement ResponseInterface in PSR-7.
Here is what I currently do:
// Views
header('Content-Type: text/html; charset=utf-8');
header('Content-Language: en-CA');
echo $twig->render('foo.html.twig', array(
'param' => 'value'
/* ... */
));
// JSON
header('Content-Type: application/json; charset=utf-8');
echo json_encode($foo);
Here is what I am attempting to do with PSR-7:
// Views
$response = new Http\Response(200, array(
'Content-Type' => 'text/html; charset=utf-8',
'Content-Language' => 'en-CA'
));
// what to do here to put the Twig output in the response??
foreach ($response->getHeaders() as $k => $values) {
foreach ($values as $v) {
header(sprintf('%s: %s', $k, $v), false);
}
}
echo (string) $response->getBody();
And I suppose it would be similar for the JSON response just with different headers. As I understand the message body is a StreamInterface and it works when I try to output a file resource created with fopen but how do I do it with strings?
Update
Http\Response in my code is actually my own implementation of the ResponseInterface in PSR-7. I have implemented all of the interfaces as I am currently stuck with PHP 5.3 and I couldn't find any implementations that were compatible with PHP < 5.4. Here is the constructor of Http\Response:
public function __construct($code = 200, array $headers = array()) {
if (!in_array($code, static::$validCodes, true)) {
throw new \InvalidArgumentException('Invalid HTTP status code');
}
parent::__construct($headers);
$this->code = $code;
}
I can modify my implementation to accept the output as a constructor argument, alternatively I can use the withBody method of the MessageInterface implementation. Regardless of how I do it, the issue is how to get a string into a stream.
ResponseInterface extends MessageInterface, which provides the getBody() getter you've found. PSR-7 expects the object implementing ResponseInterface to be immutable, which you will not be able to achieve without modifying your constructor.
As you are running PHP < 5.4 (and can't type-hint effectively), modify it as follows:
public function __construct($code = 200, array $headers = array(), $content='') {
if (!in_array($code, static::$validCodes, true)) {
throw new \InvalidArgumentException('Invalid HTTP status code');
}
parent::__construct($headers);
$this->code = $code;
$this->content = (string) $content;
}
Define a private member $content as follows:
private $content = '';
And a getter:
public function getBody() {
return $this->content;
}
And you're good to go!