Testing a CakePHP 3 REST API - api

I am developing an API on CakePHP 3 using the CRUD Plugin and ADmad's JWT plugin. I've created fixtures for all tables by importing the schema and then defining some dummy records. Now I'd like to know the best way to test my API.
Namely:
How do I set an Authorized user in my tests?
How do I call API methods in the test framework?
I don't have any code to show at the moment because I'm really not sure how to go about this in the correct way.

The one way I see to test API endpoints is by using post() method of the IntegrationTestCase suite. A very basic example:
public function testLogin()
{
// You can add this to the setUp() function to make it global
$this->configRequest([
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'x-www-form-urlencoded'
]
]);
$this->post('/auth/token', ['username' => $username, 'password' => $password]);
$this->assertResponseOk();
$this->assertResponseContains('access_token');
}
Store that access token (or pre-generate one) & use that to set authorization header.
You can send your authorization tokens like so (before EVERY request):
$this->configRequest([
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'x-www-form-urlencoded',
'Authorization' => 'Bearer ' . $token
]
]);
TIP: You can Configure::write() the Security.salt values in bootstrap.php of the test - this way the password salting works! I also found saving the salted password value in your fixture helpful.
More details in CakePHP Cookbook.

Related

Creating auth tokens for a custom model that is not the default User model

How do I create access/api/auth tokens for a different model? From most questions, docs, or tutorials, most are using the default User model.
I read the default doc but it doesn't really say where to substitute the default Model class or how to verify against non-Model classes.
Any suggestions?
To use a different model than User for Laravel Sanctum API authentication.
This is for Laravel 8.
Create new model, php artisan make:model ModelName -m
the flag m is used to instantiate a migration file for this model.
Go to the Model class file and extend it with Illuminate\Foundation\Auth\User, ensure it uses HasApiTokens, and list out your fillable fields for record creation.
...
use Illuminate\Foundation\Auth\User as Authenticatable;
class ModelName extends Authenticatable{
use ..., HasApiTokens;
protected $fillable = [...]
}
Go to config/auth.php and add new provider and new guard.
'guards' => [
...
,
'api' => [
'driver' => 'sanctum',
'provider' => 'model-name',
'hash' => false,
]
],
'providers' => [
...
,
'model-name' => [
'driver' => 'eloquent',
'model' => App\Models\ModelName::class,
]
]
Go to your api routes and wrap your routes as below.
Route::middleware(['auth:sanctum'])->group(function(){
Route::get('/whatever-route-name',function(){
return 'Authenticated';
});
});
Download Postman or your favored API testing tool, send a GET request to [http://localhost:8000/api/whatever-route-name](http://localhost:8000/api/whatever-route-name) , in Headers, ensure Accept has a value of applcation/json, send the request, and it should return an {”message”: “Unauthenticated.”}
Go to your public routes, create a dummy route to create a record for ModelName
After creation ensure that you call $model_name→createToken($model_name→whatever_field)→plaintTextToken; to get the plain text api key.
Go to back to your API test tool, under Authorization, choose Bearer Token and supply the api key returned from above.
The route wrapped in auth:sanctum is now accessible.

Client credentials invalid error for external API authentication, when credentials are correct in Processmaker

I am trying to obtain the authentication token from the Processmaker to use the APIs. I have used the same API call which works perfectly fine in the test environment, with production urls and respective client id and client secret. But, I am getting below error, although the username and password of the account is correct.
Request:
{
"grant_type": "password",
"scope": "*",
"client_id": "xxxxxx",
"client_secret":"7777777",
"username": "username",
"password": "password"
}
Response:
{
"error": "invalid_client",
"error_description": "The client credentials are invalid"
}
I have tried below steps. But still the same error.
Create a new account without AD user account as the account used in test environment is not a domain account
Change the role of account to 'System Administrator' which is similar to the account in test
**While registering the client to use the APIs, we didn't use the Callback URL as it is optional (we did not configure it in the test environment as well)
Some help is really appreciated, as I have no clue what else to check between the environment to resolve this issue.
I am not sure if you are trying to call API from ProcessMaker to RPA or RPA to ProcessMaker.
For ProcessMaker to RPA:
Using Script: I have built a ProcessMaker script in PHP and with appropriate script configuration, you will be able to run the RPA bot from ProcessMaker.
<?php
/*
* Yo. This script is developed by Abhishek Kadam.
* This script is sufficient to run all the Microbots.
* The Script Configuration contains "release_key" which is the Process ID,
* "robot_id" which is to identify where to run the Bot, "orch_unit_id" which is the folder name
* and "orch_url" which stands for Orchestrator URL. To Run the bot, All the configurations are required.
*/
//******ASSIGNING VARIABLES*****
$client_id = $config['client_id']; // $config to get data from Script Configuration
$refresh_token = $config['refresh_token'];
$release_key = $config['release_key'];
$robot_id = $config['robot_id'];
$orch_url = $config['orch_url'];
$orch_unit_id = $config["orch_unit_id"];
//****** GET ACCESS TOKENS USING CLIENT ID AND REFRESH TOKENS******
$access_token = getAccessToken($client_id,$refresh_token);
$output_response = runBot($access_token,$release_key,$robot_id,$orch_url,$orch_unit_id);
//pass the Access token to runbot() and run the bot ez-pz!
function getAccessToken($client_id,$refresh_token){
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => "https://account.uipath.com/oauth/token",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS =>"{\r\n \"grant_type\": \"refresh_token\",\r\n \"client_id\": \"".$client_id."\",\r\n \"refresh_token\": \"".$refresh_token."\"\r\n}",
CURLOPT_HTTPHEADER => array(
"Content-Type: application/json"
),
));
$response = curl_exec($curl);
curl_close($curl);
$responseDecode = json_decode($response);
$accessToken= $responseDecode -> access_token; //get the access token
return $accessToken;
}
function runBot($access_token,$release_key,$robot_id,$orch_url,$orch_unit_id){
$curl = curl_init(); //Not sure if it's the right way to initialize or not but meh, it works :P
curl_setopt_array($curl, array(
CURLOPT_URL => $orch_url."/odata/Jobs/UiPath.Server.Configuration.OData.StartJobs",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS =>"{ \"startInfo\":\r\n { \"ReleaseKey\": \"".$release_key."\",\r\n \"Strategy\": \"Specific\",\r\n \"RobotIds\": [ ".$robot_id."],\r\n \"JobsCount\": 0,\r\n \"Source\": \"Manual\" \r\n } \r\n}",
// Release key and Robot ID can be concatenated and passed as an argument(once I figure out how to get arguments in PM 4 scripts)
CURLOPT_HTTPHEADER => array(
"Content-Type: application/json",
"Authorization: Bearer ".$access_token,
"X-UIPATH-OrganizationUnitId: ".$orch_unit_id
//There's another way to use the Access token. For now, I found this more helpful.
//As the document is TL;DR. https://www.php.net/manual/en/function.curl-setopt.php
),
));
$response = curl_exec($curl);
curl_close($curl);
return $response;
//echo $response; //Print Response cuz why not? ;)
}
return [$access_token];
?>
I had used the UiPath RPA tool for this without mentioning any callback URL.
Using Data Connectors: Create Data Connectors in ProcessMaker. I prefer to using the Postman application before creating DC. Refer: Postman to UiPath Bot
For RPA Bot to ProcessMaker
In ProcessMaker documentation you can see the Swagger Link for your particular instance. The Swagger Documentation for ProcessMaker was not really helpful. There are few mistakes in the documentation provided.
For ease, I did import the API collection in Postman and proceeded with creating variables: baseURL & accessToken
baseURL: Your URL (https://something.processmaker.net)
ADD /api/1.0
/api/1.0 (https://something.processmaker.net/api/1.0)
Now the URL is correct. Also while sending the request make sure Params are not empty.
Note: For Access Token, Admin --> Users --> Edit --> API Tokens --> Create new Token --> Copy Token.
In Processmaker 4, API tokens are available for individual Users.
I hope this will help you in a way. Thanks!

Mock API when performing a WebTestCase and do $client->submitform() in Symfony 5

I have a Test which submits a form. This form usually results in performing doing a external API call. I want to mock that because I'm not interested in the API but in the action.
Whenever I call submitForm the client is still making an external call, but I don't want that.
The test also fails because the api expects a api key which the test does not have.
class SubscriptionControllerTest extends WebTestCase
{
public function testSubscribe(): void
{
$client = static::createClient();
$userRepository = static::$container->get(UserRepository::class);
$testUser = $userRepository->findOneBy(['email' => UserFixtures::$testUser]);
$clientMock = $this->createMock(ApiClient::class);
//replace the api with the mock one
self::$container->set(ApiClient::class, $clientMock);
$client->loginUser($testUser);
$client->request('GET', '/subscription/new');
$client->submitForm('btn-submit', [
'subscribe[firstName]' => 'firstname',
'subscribe[lastName]' => 'lastname'
]);
$this->assertResponseStatusCodeSame(201);
}
}
What am I doing wrong here?
So the problem is described in this post:
https://stackoverflow.com/a/19951344/3679577
TL:DR
2 requests are being done in my test. A GET and a submitForm (POST). First one uses the proper Mock, second request rebuilds the kernel and the mocks are gone.
My solution was to just use 1 request by submitting the form with a POST request. Using the CSRF token manager to generate a csrf token:
public function testSubscribe(): void
{
$client = static::createClient();
$userRepository = static::$container->get(UserRepository::class);
$testUser = $userRepository->findOneBy(['email' => UserFixtures::$testUser]);
$csrfTokenGenerator = $client->getContainer()->get('security.csrf.token_manager');
$apiClient = $this->createMock(ApiClient::class);
$client->getContainer()->set(ApiClient::class, $apiClient);
$client->loginUser($testUser);
$client->request('POST', '/subscription/new', [
'subscribe' => [
"firstName" => 'firstname',
"lastName" => "lastname",
"_token" => $csrfTokenGenerator->getToken('subscribe')->getValue()
]
]);
$this->assertResponseRedirects('/subscription/thankyou');
}

How to send a patch api request using a variable

I am trying to update a user(s) type in the Zoom conference application using their API. I use PATCH as per their documentation, and this works when I hard code the userId in the URL, but I need to use an array variable instead because multiple users will need to be updated at once.
This code works with the manually entered userId.
The userId and bearer code are made up for the purpose of this question.
require 'vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client();
$response = $client->PATCH('https://api.zoom.us/v2/users/jkdflg4589jlmfdhw7', [
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer my token goes here',
],
'body' => json_encode([
'type' => '1',
])
]);
$body = $response->getBody() ;
$string = $body->getContents();
$json = json_decode($string);
This way the code works and changes my user's type to 1.
The following code is the one that doesn't work.
In the Zoom API reference there is a test section and the userId can be added in a tab called Settings under the field: Path Parameters.
https://marketplace.zoom.us/docs/api-reference/zoom-api/users/userupdate
Hence I can add the userId there and when I run it, it actually replaces {userId} in the URL with the actual userId into the url patch command.
Hence from this ->
PATCH https://api.zoom.us/v2/users/{userId}
It becomes this after all transformations, scripts,
and variable replacements are run.
PATCH https://api.zoom.us/v2/users/jkdflg4589jlmfdhw7
However, when I try it in my code it doesn't work, I don't know where to add the path params. I am more used to PHP but I'll use whatever I can to make it work. Also I would like userId to be a variable that may contain 1 or more userIds (array).
This is my code that doesn't work:
require 'vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client();
$response = $client->PATCH('https://api.zoom.us/v2/users/{userId}', [
'params' => [
'userId' => 'jkdflg4589jlmfdhw7',
],
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer my token goes here',
],
'body' => json_encode([
'type' => '1',
])
]);
$body = $response->getBody() ;
$string = $body->getContents();
$json = json_decode($string);
My code fails with error:
Fatal error: Uncaught GuzzleHttp\Exception\ClientException: Client error: `PATCH https://api.zoom.us/v2/users/%7BuserId%7D` resulted in a `404 Not Found` response: {"code":1001,"message":"User not exist: {userId}"}
in /home/.../Zoom_API_V2/guzzle_response/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:113 Stack trace:
#0 /home/.../Zoom_API_V2/guzzle_response/vendor/guzzlehttp/guzzle/src/Middleware.php(66): GuzzleHttp\Exception\RequestException::create(Object(GuzzleHttp\Psr7\Request), Object(GuzzleHttp\Psr7\Response))
#1 /home/.../Zoom_API_V2/guzzle_response/vendor/guzzlehttp/promises/src/Promise.php(203): GuzzleHttp\Middleware::GuzzleHttp\{closure}(Object(GuzzleHttp\Psr7\Response))
#2 /home/.../Zoom_API_V2/guzzle_response/vendor/guzzlehttp/promises/src/Promise.php(156): GuzzleHttp\Promise\Promise::callHandler(1, Object(GuzzleHttp\Psr7\Response), Array)
#3 /home/.../publ in /home/.../Zoom_API_V2/guzzle_response/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php on line 113
If I understood you correctly, then this is basic string concatenation in PHP that you are trying to do
$userId = 'jkdflg4589jlmfdhw7';
$response = $client->PATCH('https://api.zoom.us/v2/users/' . $userId, [
// other options
]);
However, when I try it in my code it doesn't work, I don't know where to add the path params.
You add URL path in the first argument, since path is part of the URL. You can however set query parameters (e.g. for GET requests) and form data (e.g. for POST form requests) through Guzzle options, but not the path.
Also I would like userId to be a variable that may contain 1 or more userIds (array).
Using just a simple implode to convert an array to a comma separated list should work, but the API point you linked to does not seem to support multiple user IDs.
$userId = ['jkdflg4589jlmfdhw7', 'asdfa123sdfasdf'];
$response = $client->PATCH('https://api.zoom.us/v2/users/' . implode(',', $userId), [
// other options
]);

Passport - "Unauthenticated." - Laravel 5.3

I hope someone could explain why I'm unauthenticated when already has performed a successfull Oauth 2 authentication process.
I've set up the Passport package like in Laravel's documentation and I successfully get authenticated, receives a token value and so on. But, when I try to do a get request on, let say, /api/user, I get a Unauthenticated error as a response. I use the token value as a header with key name Authorization, just as described in the docs.
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware("auth:api");
This function is suppose to give back my self as the authenticated user, but I'm only getting Unauthenticated. Likewise, if I just return the first user, I'm again getting Unauthenticated.
Route::get('/test', function(Request $request) {
return App\User::whereId(1)->first();
})->middleware("auth:api");
In a tutorial from Laracast, guiding through the setup of Passport, the guider doesn't have the ->middleware("auth:api") in his routes. But if its not there, well then there's no need for authentication at all!
Please, any suggestions or answers are more then welcome!
You have to set an expiration date for the tokens you are generating,
set the boot method in your AuthServiceProvider to something like the code below and try generating a new token. Passports default expiration returns a negative number
public function boot()
{
$this->registerPolicies();
Passport::routes();
Passport::tokensExpireIn(Carbon::now()->addDays(15));
Passport::refreshTokensExpireIn(Carbon::now()->addDays(30));
}
Check your user model and the database table, if you have modified the primary id field name to say something other than "id" or even "user_id" you MIGHT run into issues. I debugged an issue regarding modifying the primary id field in my user model and database table to say "acct_id" instead of keeping it as just "id" and the result was "Unauthenticated" When I tried to get the user object via GET /user through the auth:api middleware. Keep in mind I had tried every other fix under the sun until I decided to debug it myself.
ALSO Be sure to UPDATE your passport. As it has had some changes made to it in recent weeks.
I'll link my reference below, it's VERY detailed and well defined as to what I did and how I got to the solution.
Enjoy!
https://github.com/laravel/passport/issues/151
I had this error because of that I deleted passport mysql tables(php artisan migrate:fresh), php artisan passport:install helps me. Remember that after removing tables, you need to re-install passport!
I had exactly the same error because I forgot to put http before the project name.
use Illuminate\Http\Request;
Route::get('/', function () {
$query = http_build_query([
'client_id' => 3,
'redirect_uri' => 'http://consumer.dev/callback',
'response_type' => 'code',
'scope' => '',
]);
// The redirect URL should start with http://
return redirect('passport.dev/oauth/authorize?'.$query);
});
Route::get('/callback', function (Request $request) {
$http = new GuzzleHttp\Client;
$response = $http->post('http://passport.dev/oauth/token', [
'form_params' => [
'grant_type' => 'authorization_code',
'client_id' => 3,
'client_secret' => 'M8y4u77AFmHyYp4clJrYTWdkbua1ftPEUbciW8aq',
'redirect_uri' => 'http://consumer.dev/callback',
'code' => $request->code,
],
]);
return json_decode((string) $response->getBody(), true);
});