I am working on a Laravel project that is accessing an API for all backend data storage. This includes the user authentication.
I have an API Service as well as a API Service Provider that substantiates the Service and then feeds that service to all of the pages with the request var.
I figured it was easier just to substantiate the API Service in the Test Class so that's what I am doing but I am running into issues with the sessions.
Here is what I have so far:
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use App\Services\ApiService;
class LoginTest extends TestCase
{
private $apiService;
public function __construct(){
$this->withSession(['user' => '', 'auth_token' => '']);
$url = env('API_URL');
$this->$apiService = new ApiService($url);
}
I am getting this error:
Call to a member function isStarted() on null.
Is there a better way to call an API Service that relies on session variables for testing?
How we ended up doing this:
We changed my constructor to a parent::SetUp to call the API Service
public function setUp(): void
{
parent::setUp();
$url = env('API_URL');
$this->ApiService = new ApiService($url);
}
Then in tests/TestCase we called post login routing passing the hard coded test username and password info.
public function login()
{
$this->post("/login", ['email' => 'admin#big-api.test', 'password' => 'ThisUserWillBeDeleted']);
}
Now I just call the login function on each of my tests cases where I need to be logged in through the API in order to access a page.
This ended up being a lot simpler than I thought but it took a bit to get there. Hope this is helpful for someone else.
Related
I'm trying to test an authenticated API route which only an authenticated user can post to a specific route.
Looking at the Laravel Sanctum docs, I can use the code below to create and authenticate a user:
Sanctum::actingAs(
factory(User::class)->create(),
['*']
);
When I try replicate this, I get an error running the test
BadMethodCallException: Call to undefined method App\User::withAccessToken()
My test code is as follows:
public function an_authenticated_user_can_add_a_client()
{
$user = Sanctum::actingAs(
factory(User::class)->create(),
['*']
);
dd($user);
// $this->post('/api/clients', $this->data());
}
api.php
Route::middleware('auth:sanctum')->group(function () {
//Clients
Route::get('/clients/{client}','ContactsController#show');
Route::post('/clients','ContactsController#store');
Route::patch('/clients/{client}','ContactsController#update');
Route::delete('/clients/{client}','ContactsController#destroy');
});
I don't have the method withAccessToken() in my User class and can't see where this method is coming from or specified anywhere.
Any help would be greatly appreciated.
Laravel Sanctum for SPA uses normal session authentication so the default actingAs method works fine. The actingAs method in Sanctum is meant to use for api tokens. Hope it helps.
Your User model is missing the HasApiTokens trait, that gives the function you are missing to the User model. Also described in the documentation, under the section Issuing API Tokens.
use Laravel\Sanctum\HasApiTokens;
class User {
use HasApiTokens;
}
I'm using Socialite in a Laravel application to allow users to connect via Github.
My login controller contains the following two methods:
/**
* GET /login/github
* Redirect the user to the GitHub authentication page.
*/
public function redirectToProvider()
{
return Socialite::driver('github')->redirect();
}
/**
* GET /login/github/callback
* Obtain the user information from GitHub.
*/
public function handleProviderCallback(Request $request)
{
$githubUser = Socialite::driver('github')->user();
// Actual login procedures go here; redacted for brevity
return redirect('/');
}
When I manually test these methods in the browser, they work as expected. I visit /login/github where I'm redirected to Github to authenticate, then I'm sent back to /login/github/callback?state=somelongrandomkey which then redirects me home (/).
I'm also attempting to test these methods via Laravel Dusk, mocking Socialite.
My Dusk test method looks like this:
public function testReceivesGithubRequestAndCreatesNewUser()
{
$this->browse(function (Browser $browser) {
$user = factory('App\Models\User')->create([
'github_token' => 'foobar',
'github_username' => 'foobar'
]);
# Mock 1 - A Socialite user
$abstractUser = Mockery::mock('Laravel\Socialite\Two\User');
# Mock 2 - Socialite's Github provider
$provider = Mockery::mock('Laravel\Socialite\Contracts\Provider');
$provider->shouldReceive('user')
->andReturn($abstractUser);
# Mock 3 - Socialite
Socialite::shouldReceive('driver')
->with('github')
->andReturn($provider);
$browser->visit('/login/github/callback')->assertPathIs('/');
});
When I run this test, the visit to /login/github/callback fails with an InvalidStateException.
From the log:
dusk.local.ERROR: {"exception":"[object] (Laravel\\Socialite\\Two\\InvalidStateException(code: 0): at /redacted/vendor/laravel/socialite/src/Two/AbstractProvider.php:210)
[stacktrace]
#0 /redacted/app/Http/Controllers/Auth/LoginController.php(84): Laravel\\Socialite\\Two\\AbstractProvider->user()
[...etc...]
When I trace where the error is coming from in AbstractProvider
I see it's attempting to compare state from the session with state from the query string:
protected function hasInvalidState()
{
if ($this->isStateless()) {
return false;
}
$state = $this->request->session()->pull('state');
return ! (strlen($state) > 0 && $this->request->input('state') === $state);
}
In my Dusk test, when /login/github/callback is visited, there is no state on the query string, so it's logical that it's failing.
I feel I'm missing some key component in setting up the mocks that provides that state, but I'm not sure what.
My test is built using these two examples for reference:
How to Test Laravel Socialite
How I write integration tests for Laravel Socialite powered apps
There's a fundamental difference between Dusk tests and the examples you're mentioning: Dusk opens the website in an actual browser and so the test and your application run in separate processes. That's why mocking doesn't work in Dusk tests.
The idea behind such an integration test is that you simulate a real user, without mocking or any other "shortcuts". In your case, that would mean logging in with a real GitHub account.
Currently I'm trying to write feature tests for laravel nova that assert that the page is loaded correctly and data can be seen.
However when I write the tests I can't find a way to assert that the correct text is shown due to way laravel nova's data is produce. Ontop of that I can't seem to test if a page loads correctly with laravel nova's 404 page coming back as a 200 response when a resource page that doesn't exist loads.
Has anyone found a good way to feature test nova?
TL;DR: check out this repo: https://github.com/bradenkeith/testing-nova. It has helped me find my way on how to test Laravel Nova.
Laravel Nova is basically a CRUD framework. So I'm assuming that, when you say
"that the page is loaded correctly and data can be seen"
You actually mean: my resources are loaded correctly. Or, I can create/update/delete a resource. This is because Nova is loading its resource info asynchronous via api calls.
So that's why, a good method to test your logic is to test the /nova-api/ routes.
For example:
<?php
namespace Tests\Feature\Nova;
use App\Note;
use Tests\TestCase;
class NoteTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
}
/** #test */
public function it_gets_a_note()
{
// given
$note = factory(Note::class)->create();
$response = $this->get('/nova-api/notes/' . $note->id)
->assertStatus(200)
->assertJson([
'resource' => [
'id' => [
'value' => $note->id
]
]
]);
}
}
By calling the route https://my-app.test/resources/notes/1, we can assert that we're getting a 200 response (successful) and that we're actually returning our newly created Note. This is a pretty trustworthy test to be sure a resource detail page is working fine.
If, however, you are talking about Browser Testing, you might want to take a look at Laravel Dusk:
Laravel Dusk provides an expressive, easy-to-use browser automation and testing API.
Once installed, you can start real browser testing:
$user = factory(User::class)->create();
$user->assignRole('admin');
$this->browse(function (Browser $browser) use ($user) {
$browser
->loginAs($user)
->visit('/')
->assertSee('Dashboard');
});
I had the same issue, I found out that the gate in App\Providers\NovaServiceProvider.php is not letting users pass, just return true when testing only and everything must work as expected
protected function gate()
{
Gate::define('viewNova', function ($user) {
return true;
});
}
Add on app/config file in your project directory:
App\Providers\NovaServiceProvider::class,
I'm using Instagram API library to connect user to Instagram profile and then do smth with it. So, as Instagram API wiki says:
Once you have initialized the InstagramAPI class, you must login to an account.
$ig = new \InstagramAPI\Instagram();
$ig->login($username, $password); // Will resume if a previous session exists.
I have initialized InstagramAPI class and then I called $ig->login('username', 'password');. But I have to call it in every function where I need to work with Instagram.
So how could I save this object $ig for using it in the future in other controllers without calling login() any more? Can I save $ig object into the session or cookie file?
P.S. I think saving into the session is not safe way to solve the issue.
UPD: I was trying to save $ig object into the session, however the size is large and session become stop working as well.
Regarding the register method you asked in the comments section, all you need to create a new service provider class in your app\providers directory and declare the register method in there for example:
namespace App\Providers;
use InstagramAPI\Instagram;
use Illuminate\Support\ServiceProvider;
class InstagramServiceProvider extends ServiceProvider
{
public function register()
{
// Use singleton because, always you need the same instance
$this->app->singleton(Instagram::class, function ($app) {
return new Instagram();
});
}
}
Then, add your newly created InstagramServiceProvider class in providers array inside the config/app.php file for example:
'providers' => [
// Other ...
App\Providers\InstagramServiceProvider::class,
]
Now on, in any controller class, whenever you need the Instagram instance, all you need to call the App::make('InstagramAPI\Instagram') or simply call the global function app('InstagramAPI\Instagram') or even you can typehint the class in any method/constructor etc. Some examples:
$ig = App::make('InstagramAPI\Instagram');
$ig = App::make(Instagram::class); // if has use statement at the top fo the class
$ig = app('...');
In a class method as a dependency:
public function someMethod(Instagram $ig)
{
// You can use $ig here
}
Hope this helps but read the documentation properly, there will get everything documented.
I am using laravel 5.2.
Recently, I've updated Auth module to have session based authentication for web and api_token based authentication for external api calls.
Now, I am finding error in using Auth::id() and Auth::user() where I've used api_token based authentication. So that I am forced to use Auth::guard('api')->id() and Auth::guard('api')->user() methods instead.
Now, my question is, is there any common method that I can use for both irrespective of api_token based authentication or session based? What about auth()->user() and auth()->id()?
What if am I using the any method for both of the authentication? For example, methodA() is used within api_token based authentication as well as in session based too, how can I handle that case if I required to use Auth variable?
I think that controllers, that handle regular requests (through session-based authentication), should be separate from api controllers (token-based authentication). So, each controller would have responsibility over a single part of the functionality. Also, changes in api controller will not have side effect in session controller. Therefore, you can specify auth guard explicitly in each controller. Laravel requires specifying guard explicitly, otherwise default guard will be used. There is no way to make intelligent guess about what guard to use natively. Of course, you can make something like this:
public function action(Request $request)
{
$guard = $request->has('api_token') ? 'api' : 'session';
$authUser = Auth::guard($guard)->user();
//your code next
}
If you will go with separate controllers you can generalize common functionality into parent abstract controller. Note, in example below ChildControllers differs only by namespace.
Parent:
<?php
namespace App\Http\Controllers\Api
use App\Http\Controllers\Controller;
abstract class ParentController extends Controller
{
public function action(Request $request)
{
$authUser = Auth::guard($this->guard)->user();
//your code...
}
}
API controller:
<?php
namespace App\Http\Controllers\Api
use App\Http\Controllers\ParentController
class ChildController extends ParentController
{
protected $guard = 'api';
//your code...
}
Session Controller:
<?php
namespace App\Http\Controllers\Session
use App\Http\Controllers\ParentController
class ChildController extends ParentController
{
protected $guard = 'session';
//your code...
}