Laravel Passport: How can I properly setup a feature test? - laravel

I am trying to create a Feature test that tests getting an OAuth access token. When using Postman, everything works great (actual code) so I am confident it is in my test code I am not implementing the setup correctly.
In the past, I have run the artisan command, and simply copy/paste the client_secret into my .env file. Because I am using CI/CD, I cannot do that. I need to either:
create an artisan command to append the .env
read the secret from the oauth_clients table (seems the easiest route)
I've followed this SO thread to help get where I'm at, but not able to make it work.
Here is what my test looks like:
MyFeatureTest.php
use RefreshDatabase, WithFaker, DatabaseMigrations;
public function setUp(): void
{
parent::setUp();
$this->artisan('passport:install');
}
public function a_user_can_get_a_token()
{
$credentials = [
'username' => 'username',
'password' => 'password',
];
$http = $this->postJson('api/v1/login', $credentials);
$response = json_decode($http->getContent());
// dd($response);
$http->assertStatus(200)
->assertJsonStructure([
'token_type', 'expires_in', 'access_token', 'refresh_token',
])
->assertJson([
'token_type' => $response->token_type,
'expires_in' => $response->expires_in,
'access_token' => $response->access_token,
'refresh_token' => $response->refresh_token,
]);
In my Controller that handles the login route:
public function __construct()
{
$this->client = DB::table('oauth_clients')
->where('id', 2)
->first();
}
...
public function getOauthAccessToken($credentials)
{
$response = Http::asForm()->post(env('APP_URL') . '/oauth/token', [
'grant_type' => 'password',
'client_id' => $this->client->id,
'client_secret' => $this->client->secret,
'username' => $credentials['username'],
'password' => $credentials['password'],
'scope' => '*',
]);
if ($response->successful()) {
return $response->json();
} else {
dd($response->body());
}
}
When using postman to test my routes/controller, everything works great. I get a Barer token back as expected.
When I run my Feature test, my controller returns null.
Trying to troubleshoot, I've tried dd($response->body()) above. Here is what I am getting:
"{"error":"invalid_client","error_description":"Client authentication failed","message":"Client authentication failed"}"
If I dd($this->client->secret), I am able to see the key. This is super confusing as it looks like everything is working...
How can I properly set up my test so that passport is ready to go/configured when I hit the login test(s)? Thank you for any suggestions!

Thank you everyone for your suggestions!
This is what ended up working for me. Since I am the only consumer of my api, I don't need to return the entire /oauth/token response (although it might be helpful in the future).
MyTest.php
use RefreshDatabase, WithFaker, DatabaseMigrations;
public function setUp(): void
{
parent::setUp();
$this->artisan('passport:install', ['--no-interaction' => true,]);
}
/** #test */
public function a_user_can_authenticate_using_email()
{
$credentials = [
'username' => 'test#email.com',
'password' => 'password',
];
$http = $this->postJson('api/v1/login', $credentials)
->assertStatus(200);
}
MyController.php
// authenticate the $credentials to get a $user object.
...
return $this->getOauthAccessToken($user);
...
private function getOauthAccessToken($user)
{
$token = $user->createToken('My Personal Access Token')->accessToken;
return response()->json($token);
}
Instead of validating I get the exact json back (would be ideal) I'm just verifying that I'm getting a 200 response back. Because my authentication is somewhat cumbersome (checking multiple db's) it was important to make sure that I was getting through everything with a 200.
Hope this helps someone!

Related

How can I get api/register with Passport in laravel working?

Following a tutorial: https://www.twilio.com/blog/build-secure-api-php-laravel-passport
I manAged to get Laravel/Passport installed formy Laravel Api and Vue application.
I managed to create attoken with:
localhost:8000/oauth/token
get the login working in Postman:
localhost:8000/api/login?email=jennie05#example.com&password=password
Now when I try to register a user I get returned to the home-page.
I do get some "undefined method" errors from VS Code, but they show up in the login method,
and not in the failinf register method:
Here is the controller:
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
class AuthController extends Controller
{
public function register(Request $request)
{
$validatedData = $request->validate([
'name' => 'required|max:55',
'email' => 'email|required|unique:users',
'password' => 'required|confirmed'
]);
$validatedData['password'] = bcrypt($request->password);
$user = User::create($validatedData);
$accessToken = $user->createToken('authToken')->accessToken;
return response([ 'user' => $user, 'access_token' => $accessToken]);
}
public function login(Request $request)
{
$loginData = $request->validate([
'email' => 'email|required',
'password' => 'required'
]);
if (!auth()->attempt($loginData)) {
return response(['message' => 'Invalid Credentials']);
}
$accessToken = auth()->user()->createToken('authToken')->accessToken;
return response(['user' => auth()->user(), 'access_token' => $accessToken]);
}
}
In these are the routes in api.php:
Route::post( 'register', 'App\Http\Controllers\API\AuthController#register');
Route::post( 'login', 'App\Http\Controllers\API\AuthController#login');
// Route::prefix('v2')->group(function(){ // prefix voor versie 2
Route::apiResource('/cards', 'App\Http\Controllers\CardController');
Route::apiResource('/games', 'App\Http\Controllers\GameController');
//Route::get('games', 'App\Http\Controllers\GameController#index')->middleware('auth:api');
//Route::post('games', 'App\Http\Controllers\GameController#store')->middleware('auth:api');
//Route::get('games/{id}', 'App\Http\Controllers\GameController#show')->middleware('auth:api');
//Route::put('games/{id}', 'App\Http\Controllers\GameController#update')->middleware('auth:api');
//Route::delete('games/{id}', 'App\Http\Controllers\GameController#destroy')->middleware('auth:api');
Route::get('/gameByPin/{pin}', 'App\Http\Controllers\GameController#searchPin');
Route::apiResource('/speler', 'App\Http\Controllers\SpelerController');
//});
Route::middleware('auth:api')->get('/user', function (Request $request) {
return $request->user();
});
is there anyone who can help troubleshooting this?
Postman does not give me errors, just the redirect to homepage..
Wow,
Thanks Patrick, your the best.
Just checked all the steps in the documentation.
reran the installation command I ran when setting up everything. ( Apperently need to do this twice ?!):
php artisan passport:install
It returned:
Encryption keys already exist. Use the --force option to overwrite them.
Personal access client created successfully.
Client ID: 5
Client secret: ikJEfthxxxxxxxxxxxxxxxxxxxxxxxxxxxBjJ36
Password grant client created successfully.
Client ID: 6
Client secret: H42lpMxxxxxxxxxxxxxxxxxxxxxxxxxxxQGZgH
Now the post request results in a token!
The error VS Code shows d for "use HasApiToken" is:
Undefined type 'Laravel\Passport\HasApiTokens'.intelephense(1009)

Laravel: Why am I getting a 401 response from Passport in my feature test?

I am unsuccessfully trying to test authentication using Passport.
Here is my feature test:
public function a_user_can_authenticate_using_email() {
// Install passport.
$this->artisan('passport:install', ['--no-interaction' => true]);
// Create a user.
$user = factory(User::class)->create([
'username' => 'test#example.com',
'password' => 'password',
]);
// Get the id & secret.
$client = DB::table('oauth_clients')
->where('password_client', true)
->first();
// dd('id: ' . $client->id); // works great
// dd('secret: ' . $client->secret); // works great
// dd('username: ' . $credentials['username']); // works great
// dd('password: ' . $credentials['password']); // works great
$response = Http::asForm()->post(env('APP_URL') . '/oauth/token', [
'grant_type' => 'password',
'client_id' => $client->id,
'client_secret' => $client->secret,
'username' => $credentials['username'],
'password' => $credentials['password'],
'scope' => '*',
]);
dd($response->status()); // 401
}
If I dump the response body, I am seeing an invalid client:
dd($response->body());
"{"error":"invalid_client","error_description":"Client authentication failed","message":"Client authentication failed"}"
I don't understand why I am seeing this error in my test.
If I use Postman to test my application, everything works great so I'm confident this is something specific to my test.
I am using sqlite memory database.
If I hardcode these values in my .env or just right in the test block, everything works great as well. How can I properly get the values from the database in my test? Thank you for any suggestions!
You'll have to run $user->save() so the user is saved to the database before using it to log in.

Laravel: Feature Test fails because of added middleware

After a user signs up and verifies their email, they must complete their signup with additional information. This happens at /register/complete-signup
The issue makes absolutely no sense to me.
For whatever reason, when I added my Middleware has-not-completed-signup, the test starts failing because a App\User no longer has the associated App\Account which is happening in the controller via attach()
As soon as I remove my middleware from the route, it works fine.
My middleware is there to prevent a user who has completed the signup already from visiting or POSTing to those routes. I tested in the browser and the redirect works. The controller method is being used in the test and i can dd($account->users) and get the correct response. But if I do $user->accounts, the collection is empty.
Once I remove my middleware, $user->accounts is no longer empty. But I did a dd() inside my middleware and it's not even running (which is correct because the user doesn't have an account).
So why would this make it fail? I'm completely lost.
I tried to include all relevant information below. If there is something else you need, please let me know.
Edit:
In my middleware, I've commented out the functionality. Something about checking an eloquent relationship makes me test fail. I have no idea why.
This makes the test fail:
if (!auth()->user()->accounts->isEmpty()) {
//return redirect(RouteServiceProvider::HOME);
}
If for example I change it to something useless like this, it works:
if (auth()->user()) {
//return redirect(RouteServiceProvider::HOME);
}
I can do $account->users , but $user->accounts returns empty collection on the controller when I use my middleware
Original:
Here are my routes:
// auth scaffolding
Auth::routes(['verify' => true]);
// main app routes
Route::middleware('verified', 'auth')->group(function() {
// User verified and has an App\Account
Route::middleware('completed-signup')->group(function() {
Route::get("/", 'HomeController#index' )->name('home');
Route::get('/paywall', 'BillingController#paywall')->name('paywall');
});
// The user hasn't attached an App\Account to their User
Route::middleware('has-not-completed-signup')->group(function() {
Route::get("/register/complete-signup", 'AccountController#showCompleteSignup' )->name('complete-signup');
Route::post('/register/complete-signup', 'AccountController#storeCompleteSignup')->name('complete-signup.store');
});
});
has-not-completed-signup Middleware:
public function handle($request, Closure $next)
{
if (auth()->user()->hasCompletedAccountSetup()) {
return redirect(RouteServiceProvider::HOME);
}
return $next($request);
}
App/User method:
Class User extends Authenticatable implements MustVerifyEmail {
...
public function accounts() {
return $this->belongsToMany('App\Account', 'account_role_user')->withPivot('role_id');
}
public function hasCompletedAccountSetup() {
return !$this->accounts->isEmpty();
}
...
AccountController#storeCompletedSignup:
public function storeCompleteSignup(Request $request) {
$validatedData = $request->validate([
'company' => 'required|max:255',
'contact_state' => 'required|max:255',
'contact_country' => 'required|max:255',
'contact_zip' => 'required|max:255',
'contact_city' => 'required|max:255',
'contact_phone' => 'nullable|max:255',
'contact_address_1' => 'required|max:255',
'contact_address_2' => 'nullable|max:255',
'contact_first_name' => 'required',
'contact_last_name' => 'required',
'contact_email' => 'required'
]);
$user = auth()->user();
$account = new Account($validatedData);
$account->contact_first_name = $user->first_name;
$account->contact_last_name = $user->last_name;
$account->contact_email = $user->email;
$account->save();
$account->users()->attach(
$user->id,
['role_id' => Role::where('name', 'owner')->first()->id ]
);
return $request->wantsJson()
? new Response('Signup Completed Successfully', 201)
: redirect()->route('/');
}
My Test:
/**
* #test
*/
public function a_user_can_complete_signup()
{
$user = Factory('App\User')->create();
$this->actingAs($user);
$accountAttributes = factory('App\Account')->raw([
'contact_first_name' => "TEST",
'contact_last_name' => $user->last_name,
'contact_email' => $user->email,
'contact_country' => "USA"
]);
$res = $this->post('/register/complete-signup', $accountAttributes);
$res->assertSessionDoesntHaveErrors();
$this->assertTrue( !$user->accounts->isEmpty() ); // THIS FAILS
$this->assertTrue( $user->accounts->first()->company == $accountAttributes['company']);
$this->assertTrue( $user->accounts->first()->contact_first_name == $user->first_name );
}
The issue wasn't actually with the middleware, but it was because I had to refresh the model after the POST on the test.
$this->assertTrue( !$user->accounts->isEmpty() );
needed to become
$this->assertTrue( !$user->fresh()->accounts->isEmpty() );
which passed the test.
I knew about the fresh and refresh() methods, but the middleware causing the issue didn't make sense to me.

Laravel test fails, but works on postman

I have a test for a user logging out and having their token deleted.
use RefreshDatabase;
public function setUp() :void {
parent::setUp();
\Artisan::call('migrate',['-vvv' => true]);
\Artisan::call('passport:install',['-vvv' => true]);
\Artisan::call('db:seed',['-vvv' => true]);
}
...
/**
* #test
*/
public function a_user_has_tokens_removed_when_logged_out()
{
// login
$this->withoutExceptionHandling();
$user = factory('App\User')->create();
$response = $this->post('/api/login', [
'username' => $user->email,
'password' => 'password'
]);
$token = json_decode($response->getContent())->access_token;
$this->assertTrue(!$user->tokens->isEmpty());
// logout
Passport::actingAs($user, ['*']);
$logout = $this->json('POST', 'api/logout')->withHeaders([
'Accept' => 'application/json',
'Authorization' => 'Bearer ' . $token
]);
$this->assertTrue($user->tokens->isEmpty());
}
First I'm creating a user and logging them in so a token is created and related to their user account.
I'm asserting that the token exists after hitting the login route, which passes.
Then I'm calling the logout route which will delete all the tokens the user has:
public function logout() {
auth()->user()->tokens()->each(function($token, $key) {
$token->delete();
});
return response()->json('Logged out successfully', 200);
}
routes/api.php
Route::middleware('auth:api')->post('logout', 'AuthController#logout');
This assertion on the test above is failing:
$this->assertTrue($user->tokens->isEmpty());
If I do a dd($user->tokens); before the assertion to check what's going on, the token shows up - it still exists.
But If I hit this api/logout route with Postman, which has everything stored in MySQL, all the tokens are being deleted successfully.
I don't understand what's going on and why this test is failing. Or rather, I don't understand why the $token->delete() doesn't work on the test, but does via Postman. What's different?
Before executing the assert, reload the user model relations via $user->fresh(), to ensure the deleted relations are reflected in the instance.
I don't know why, but within the testing context, this is not done automatically.

Laravel5 Unit Testing a Login Form

I ran the following test and I am receiving a failed_asserting that false is true. Can someone further explain why this could be?
/** #test */
public function a_user_logs_in()
{
$user = factory(App\User::class)->create(['email' => 'john#example.com', 'password' => bcrypt('testpass123')]);
$this->visit(route('login'));
$this->type($user->email, 'email');
$this->type($user->password, 'password');
$this->press('Login');
$this->assertTrue(Auth::check());
$this->seePageIs(route('dashboard'));
}
Your PHPUnit test is a client, not the web application itself. Therefore Auth::check() shouldn't return true. Instead, you could check that you are on the right page after pressing the button and that you see some kind of confirmation text:
/** #test */
public function a_user_can_log_in()
{
$user = factory(App\User::class)->create([
'email' => 'john#example.com',
'password' => bcrypt('testpass123')
]);
$this->visit(route('login'))
->type($user->email, 'email')
->type('testpass123', 'password')
->press('Login')
->see('Successfully logged in')
->onPage('/dashboard');
}
I believe this is how most developers would do it. Even if Auth::check() worked – it would only mean a session variable is created, you would still have to test that you are properly redirected to the right page, etc.
In your test you can use your Model to get the user, and you can use ->be($user) so that it will get Authenticate.
So i written in my test case for API test
$user = new User(['name' => 'peak']);
$this->be($user)
->get('/api/v1/getManufacturer')
->seeJson([
'status' => true,
]);
it works for me

Resources