I'm using Behat in Symfony2 / Doctrine2. Now, I have this scenario that boils down to the fact that "if I'm logged in and I go to /login, I shoud go to / instead":
#login
Scenario: Go to the login page while being logged in
Given I am logged in
When I go to "/login"
Then I should be on "/"
For the #login, I created the following:
/**
* #BeforeScenario #login
*/
public function loginUser()
{
$doctrine = $this->getContainer()->get('doctrine');
$userRepository = $doctrine->getRepository('MyTestBundle:User');
$user = $userRepository->find(1); // 1 = id
$token = new UsernamePasswordToken($user, NULL, 'main', $user->getRoles());
$this->getContainer()->get('security.context')->setToken($token);
}
In the "when I go to /login" code (the controller gets called), the token seems gone (not what I intended):
/**
* #Route("/login", name="login")
*/
public function loginAction()
{
$token = $this->get('security.context')->getToken();
$fd = fopen('/tmp/debug.log', 'a');
fwrite($fd, $token);
// prints 'AnonymousToken(user="anon.", authenticated=true, roles="")'
...
But in the FeatureContext, it seems to stick around (the way I hoped it would work). In the "Given I am logged in":
/**
* #Given /^I am logged in$/
*/
public function iAmLoggedIn()
{
$token = $this->getContainer()->get('security.context')->getToken();
$fd = fopen('/tmp/debug.log', 'a');
fwrite($fd, $token);
// prints 'UsernamePasswordToken(user="admin", authenticated=true, roles="ROLE_ADMIN")'
...
I run behat like this:
app/console -e=test behat
I also did this in the controller to be sure it's test:
fwrite($fd, $this->get('kernel')->getEnvironment());
// prints 'test'
Any clue how to authenticate a user? I will have to test a lot of admin pages, so it would be nice if I could hook the login into #BeforeSuite, #BeforeFeature (or #BeforeScenario ...) so that I don't get blocked.
(Suggestions on disabling the authentication mechanism for testing, or a way to stub/mock a user are also welcome.)
Oh my. It doesn't work because the DIC inside your FeatureContext isn't shared with your app - your app has separate kernel and DIC. You can get it through Mink. Or, you can simply do it right way :-)
Right way means, that every part of behavior, that is observable by the enduser, should be described inside *.feature, not inside FeatureContext. It means, that if you want to login a user, you should simply describe it with steps (like: "i am on /login", "and i fill in username ...", "i fill in password" and stuf). If you want to do it in multiple times - you should create a metastep.
Metasteps are simply steps, that describe multiple other steps, for example - "i am logged in as everzet". You could read bout them here: http://docs.behat.org/guides/2.definitions.html#step-execution-chaining
Here is an solution for login with OAuth I've used. After number of times of searching for the answer and landing on this page I thought it would be great to share the solution. Hopefully it will help someone.
Background: Symfony2 App using HWIOAuthBundle, hooked up to some OAuth2 provider.
Problem: How do I implement Given I'm logged in when Behat context in not shared with Symfony context?
Solution:
HWIOAuthBundle uses #buzz service for all API calls to OAuth providers. So all you need to do is replace Buzz client with your implementation which doesn't call external services, but returns the result straight away. This is my implementation:
<?php
namespace Acme\ExampleBundle\Mocks;
use Buzz\Client\ClientInterface;
use Buzz\Message\MessageInterface;
use Buzz\Message\RequestInterface;
class HttpClientMock implements ClientInterface
{
public function setVerifyPeer()
{
return $this;
}
public function setTimeout()
{
return $this;
}
public function setMaxRedirects()
{
return $this;
}
public function setIgnoreErrors()
{
return $this;
}
public function send(RequestInterface $request, MessageInterface $response)
{
if(preg_match('/\/oauth2\/token/', $request->getResource()))
{
$response->setContent(json_encode([
'access_token' => 'valid',
'token_type' => 'bearer',
'expires_in' => 3600
]));
}
elseif(preg_match('/\/oauth2\/me/', $request->getResource()))
{
$response->setContent(json_encode([
'id' => 1,
'username' => 'doctor',
'realname' => 'Doctor Who'
]));
}
else throw new \Exception('This Mock object doesn\'t support this resource');
}
}
Next step is to hijack the class used by HWIOAuthBundle/Buzz and replace it with the implementation above. We need to do it only for test environment.
# app/config/config_test.yml
imports:
- { resource: config_dev.yml }
parameters:
buzz.client.class: Acme\ExampleBundle\Mocks\HttpClientMock
And finally, you need to set require_previous_session to false for test environment - therefore I suggest to pass it as parameter.
# app/config/security.yml
security:
firewalls:
secured_area:
oauth:
require_previous_session: false
Now you can implement your step like this.
Specification:
Feature: Access restricted resource
Scenario: Access restricted resource
Given I'm logged in
When I go to "/secured-area"
Then I should be on "/secured-area"
And the response status code should be 200
Implementation:
<?php
/**
* #Given /^I\'m logged in$/
*/
public function iMLoggedIn()
{
$this->getSession()->visit($this->locatePath('/login/check-yourOauthProvider?code=validCode'));
}
The code you're passing is not relevant, anything you pass will be OK as it's not being checked. You can customise this behaviour in HttpClientMock::send method.
http://robinvdvleuten.nl/blog/handle-authenticated-users-in-behat-mink/ is simple, clean article on how to create a login session and set the Mink session cookie so that the Mink session is logged in. This is much better than using the login form every time to login a user.
It’s ok to call into the layer “inside” the UI layer here (in symfony: talk to the models).
And for all the symfony users out there, behat recommends using a Given step with a tables arguments to set up records instead of fixtures. This way you can read the scenario all in one place and make sense out of it without having to jump between files:
Given there are users:
| username | password | email |
| everzet | 123456 | everzet#knplabs.com |
| fabpot | 22#222 | fabpot#symfony.com |
Related
How to customize the path of the reset email blade template in Laravel 5.3?
The template used is: vendor/laravel/framework/src/Illuminate/Notifications/resources/views/email.blade.php
I'd like to build my own.
Also, how to change the text of this email predefined in: vendor/laravel/framework/src/Illuminate/Auth/Notifications/ResetPassword.php
public function toMail()
{
return (new MailMessage)
->line([
'You are receiving this email because we received a password reset request for your account.',
'Click the button below to reset your password:',
])
->action('Reset Password', url('password/reset', $this->token))
->line('If you did not request a password reset, no further action is required.');
}
To change template you should use artisan command php artisan vendor:publish it will create blade templates in your resources/views/vendor directory. To change text of email you should override the sendPasswordResetNotification method on your User model. This is described here https://laravel.com/docs/5.3/passwords in Reset Email Customization section.
You must add new method to your User model.
public function sendPasswordResetNotification($token)
{
$this->notify(new ResetPasswordNotification($token));
}
and use your own class for notification instead ResetPasswordNotification.
UPDATED: for #lewis4u request
Step by step instruction:
To create a new Notification class, you must use this command line php artisan make:notification MyResetPassword . It will make a new Notification Class 'MyResetPassword' at app/Notifications directory.
add use App\Notifications\MyResetPassword; to your User model
add new method to your User model.
public function sendPasswordResetNotification($token)
{
$this->notify(new MyResetPassword($token));
}
run php artisan command php artisan vendor:publish --tag=laravel-notifications After running this command, the mail notification templates will be located in the resources/views/vendor/notifications directory.
Edit your MyResetPassword class method toMail() if you want to. It's described here https://laravel.com/docs/5.3/notifications
Edit your email blade template if you want to. It's resources/views/vendor/notifications/email.blade.php
Bonus: Laracast video: https://laracasts.com/series/whats-new-in-laravel-5-3/episodes/9
PS: Thanks #Garric15 for suggestion about php artisan make:notification
I wanted to elaborate on a very helpful Eugen’s answer, but didn’t have enough reputation to leave a comment.
In case you like to have your own directory structure, you don’t have to use Blade templates published to views/vendor/notifications/... When you create a new Notification class and start building your MailMessage class, it has a view() method that you can use to override default views:
/**
* Get the mail representation of the notification.
*
* #param mixed $notifiable
* #return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->view('emails.password_reset');
// resources/views/emails/password_reset.blade.php will be used instead.
}
In Addition to the above answer for Laravel 5.6, here it is easier to pass variables in an array to your custom email template.
/**
* Get the mail representation of the notification.
*
* #param mixed $notifiable
* #return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
$url = url('/invoice/'.$this->invoice->id);
return (new MailMessage)
->subject('Invoice Paid')
->markdown('emails.password_reset', ['url' => $url]);
}
https://laravel.com/docs/5.6/notifications
I'm trying to test that a user with the wrong permissions sees the correct response when visting a page that has a csrf token in the path.
I've added a Behat context step to create a csrf token using the 'security.csrf.token_manager', however then visit the page with this token in the path, I get a "Cannot set session ID after the session has started" 500 error.
Can anyone advise what I'm doing wrong, or how I work around this, please?
/**
* #When /^I go to the application admin archive page for "(?P<status>[^"]*)" application (?P<number>\d+) with a valid token$/
*
* #param string $status
* #param int $number
*/
public function iGoToTheApplicationAdminArchivePageForApplicationWithAValidToken($status, $number)
{
$tokenManager = $this->kernel->getContainer()->get('security.csrf.token_manager');
$token = $tokenManager->getToken(ApplicationAdminController::CSRF_ARCHIVE);
var_dump($token);
$this->visitAdminPage('archive', $status, $number, ['token' => $token]);
}
The specific error message you get seems to be a result of a bug in symfony2. Try to apply maryo's suggestion about extending MockFileSessionStorage class and checking if the id is empty before attempting to set it like this:
public function setId($id)
{
if ($this->id !== $id) {
parent::setId($id);
}
}
You can then use this fixed class in your functional test by doing something like this after creating the client:
$client->getContainer()->set('session', new Session(new FixedMockFileSessionStorageHelper()));
I still got this error at this moment. I fixed this issue by checking your file config of env at
app/config/config_test.yml
I found this configuration under framework section
framework:
test: ~
session:
storage_id: session.storage.mock_file
profiler:
collect: false
All what you have to do just remove this line test: ~
to be as below:
framework:
session:
storage_id: session.storage.mock_file
profiler:
collect: false
Then clear the cache by
php bin/console cache:clear --env=test --no-debug
Refresh and Enjoy ;)
I followed the Behat 2.5 docs to test mails. After a few tweaks to match Behat 3 I have ended with the following code (I have removed non-relevant parts):
public function getSymfonyProfile()
{
$driver = $this->mink->getSession()->getDriver();
if (!$driver instanceof KernelDriver) {
// Throw exception
}
$profile = $driver->getClient()->getProfile();
if (false === $profile) {
// Throw exception
}
return $profile;
}
/**
* #Then I should get an email with subject :subject on :email
*/
public function iShouldGetAnEmail($subject, $email)
{
$profile = $this->getSymfonyProfile();
$collector = $profile->getCollector('swiftmailer');
foreach ($collector->getMessages() as $message) {
// Assert email
}
// Throw an error if something went wrong
}
When I run this test, it throws the following error:
exception 'LogicException' with message 'Missing default data in Symfony\Bundle\SwiftmailerBundle\DataCollector\MessageDataCollector' in vendor/symfony/swiftmailer-bundle/Symfony/Bundle/SwiftmailerBundle/DataCollector/MessageDataCollector.php:93
Stack trace:
#0 vendor/symfony/swiftmailer-bundle/Symfony/Bundle/SwiftmailerBundle/DataCollector/MessageDataCollector.php(122): Symfony\Bundle\SwiftmailerBundle\DataCollector\MessageDataCollector->getMailerData('default')
#1 features/bootstrap/FeatureContext.php(107): Symfony\Bundle\SwiftmailerBundle\DataCollector\MessageDataCollector->getMessages()
My profiler is configured as follows:
# app/config/config_test.yml
framework:
test: ~
profiler:
enabled: true
collect: true
It seems that the Profile is correctly loaded and the MessageDataCollector from Swiftmailer does exist, but it is not doing its work as expected. Any clue to solve this?
Maybe the issue you have has been fixed as I do not have this anymore (I'm using Behat v3.0.15, BrowserKit driver 1.3.* and Symfony v2.6.6).
I managed to reproduce your error but only when I forgot to enable profiler data collecting:
profiler:
collect: false
Once this problem solved (the configuration you provided solving the problem for me) I managed to check emails in my Behat tests.
Two solutions for this:
Solution #1: Intercepting redirects globally
If it does not break all your other tests you can do so by configuring your web profiler as follows:
web_profiler:
intercept_redirects: true
Solution #2: Preventing client to follow redirections temporarily
For my part, intercepting redirections globally in the configuration broke most of my other functional tests. I therefore use this method instead.
As preventing redirections allows mainly to check data in the data collectors I decided to use a tag #collect on each scenario requiring redirect interception. I then used #BeforeScenario and #AfterScenario to enable this behaviour only for those scenarios:
/**
* Follow client redirection once
*
* #Then /^(?:|I )follow the redirection$/
*/
public function followRedirect()
{
$this->getDriver()->getClient()->followRedirect();
}
/**
* Restore the automatic following of redirections
*
* #param BeforeScenarioScope $scope
*
* #BeforeScenario #collect
*/
public static function disableFollowRedirects(BeforeScenarioScope $scope)
{
$context = $scope->getEnvironment()->getContext(get_class());
$context->getDriver()->getClient()->followRedirects(false);
}
/**
* Restore the automatic following of redirections
*
* #param AfterScenarioScope $scope
*
* #AfterScenario #collect
*/
public static function restoreFollowRedirects(AfterScenarioScope $scope)
{
$context = $scope->getEnvironment()->getContext(get_class());
$context->getDriver()->getClient()->followRedirects(true);
}
It's not the answer your are looking for, but I'm pretty sure it will suits your needs (perhaps more).
If I can suggest, try using Mailcatcher with this bundle: https://packagist.org/packages/alexandresalome/mailcatcher
You'll be able to easily tests if emails are sent, what's their subject, follow a link in the body, and so on...
Many steps are included with this bundle.
How can I manualy make sure that user is not logged in my FeatureContext class in BDD Behat scenario? For example I have this scenario:
Scenario: passing wrong values will not get me in
Given I am on "/login"
And I am not logged in
When I fill in "login" with "KtokolwiekWidzialKtokolwiekWie"
When I fill in "password" with "root_as_allways"
When I press "Login"
Then I should be on "/"
And I should see "Wrong login or password"
And I've defined this context:
/**
* #Given /^I am not logged in$/
*/
public function iAmNotLoggedIn()
{
# logout currently login user, if any
$request = $this->kernel->getContainer()->get('request');
$request->getSession()->invalidate();
}
But this gives me an error when running this feature test.
You cannot create a service ("request") of an inactive scope ("request").
Note: I'm using Symfony 2.1 RC-1
Here's the custom step you can use:
/**
* #Given /^I am not logged in$/
*/
public function iAmNotLoggedIn()
{
$this->getMink()
->getSession()
->visit($this->locatePath('/logout'))
;
}
But you probably need to update your steps order:
Given I am not logged in
And I am on "/login"
Just visit "/logout" and you will be logged out. Do this before you go to "/login"
i'm using the FOSUserBundle and the FOSFacebookBundle (for version SF 2.0.x) in my project. Additionally, i implemented and custom FacebookProvider as described in the FOSFacebookBundle documentation. I would like to achieve the following workflow:
1.) A user visits my portal the first time
2.) He clicks the Facebook-Login-Button
3.) Now i need to check, if this user, who clicked the Facebook-Login-Button has already Facebook-Friends on my portal.
4.) If he has friends, redirect him to a sign-up page (including information from Facebook like, username, first_name, last_name, etc.) with prefilled input fields.
5.) If he has no Facebook-friends on my portal, redirect him to another page
I've started looking at the Webprofiler, which events are called. I've started creating my own event listener as descried on this page: http://www.dobervich.com/2011/10/13/login-redirection-revisited/ but the profile shows me my listener in the list of "not called listeners": security.interactive_login SecurityListener::onSecurityInteractiveLogin
Does anyone know, how i could customize this pre-login-check and redirect a user to a page?
Would be great to get some help on this.
Thank you,
Ramo
You need to configure a custom authentication success handler. Configure a service that implements AuthenticationSuccessHandlerInterface:
facebook_auth_success_handler:
class: MyHandler
public: false
arguments:
# your dependencies...
Then add this handler to security.yml under your fos_facebook block:
firewalls:
foo:
fos_facebook:
success_handler: facebook_auth_success_handler
The handler itself should look something like this:
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
$user = $token->getUser();
$hasFriendsHereAlready = // your logic here
if ($hasFriendsHereAlready) {
$route = 'foo';
} else {
$route = 'bar';
}
return new RedirectResponse($this->router->generate($route));
}