I am having trouble on testing Symfony services that send notification emails.
In my NotificationService I have a function that persists the notification into the database and sends a notification email using a simple MailHelper class:
public function saveNewNotificationUser($newnotificationuser){
// Get necessary repositories
$doctrine = $this->getDoctrine();
$repo_user = $doctrine->getRepository('AppBundle:User');
$em = $doctrine->getManager();
$notificationuser = $this->getNotificationUserById($newnotificationuser['idnotificationuser']);
$user = $repo_user->findOneByIduser($newnotificationuser['iduser']);
$notification = $this->getNotificationById($newnotificationuser['idnotification']);
if ($notification && $user){
// Persist on database
$notificationuser->setDate(new \DateTime("now"));
$notificationuser->setRead(0);
$notificationuser->setUseruser($user);
$notificationuser->setVariables($newnotificationuser['variables']);
$notificationuser->setNotificationnotification($notification);
$em->persist($notificationuser);
$em->flush();
// Send notification email
// Generate notification structure
$not = $this->createNotificationStructure($newnotificationuser['idnotification'],$newnotificationuser['variables']);
// Define user's dafault language and send notification email
$languagecode = $user->getLanguagelanguage()->getCode();
$mailTo = $user->getEmail();
// Get notification next on user's default language
$text = $not["languages"][$languagecode]["language"];
$this->get('mailHelper')->sendMail("notification",$mailTo,array('body' => $text["description"]), $text["title"]);
return $notificationuser->getIdnotificationUser();
}else{
return false;
}
}
When I test the function, the database insert in done correctly but the email is never sent. Here is my test class:
private $container;
private $em;
public function setUp()
{
self::bootKernel();
$this->container = self::$kernel->getContainer();
$this->em = $this->container->get('doctrine')
->getManager();
}
public function testSaveNewNotificationUser()
{
$notificationService = $this->container->get('notificationService');
$newnotificationuser = array(
'idnotificationuser' => '99',
'iduser' => '69',
'idnotification' => '1',
'variables' => '32;12'
);
$id = $notificationService->saveNewNotificationUser($newnotificationuser);
$item = $this->em
->getRepository('AppBundle:NotificationUser')
->findByIdnotificationUser($id);
$this->assertCount(1, $item);
}
protected function tearDown()
{
parent::tearDown();
$this->em->close();
}
public function testNotificationAction()
{
$client = static::createClient();
$crawler = $client->request('GET', '/api/login/testnotif');
$mailCollector = $client->getProfile()->getCollector('swiftmailer');
// Check that an email was sent
$this->assertEquals(1, $mailCollector->getMessageCount());
$this->assertTrue($client->getResponse()->isSuccessful());
}
However, if I call SaveNewNotificationUser within a Controller Action, using the same "fake" data as used in testSaveNewNotificationUser, the email is sent (when disable_delivery set to false) and I can catch it via the mailCollector.
Am I missing anything? Am I taking a wrong approach to build the tests?
Related
I followed this tutorial to setup Oauth2 login with Auth0 in Symfony.
How can I access the email address of the logged in Auth0 user?
Notes:
The login works successfully (oauth2 via google on the auth0 side, then redirected back)
$this->getUser() from the controller shows the correct username
scopes configured in hwi_oauth.yaml: openid profile email
The Auth0 record (on their admin dashboard) contains email addresses for the users
The bottom of the article references OAuthUserProvider to get user data but I've loaded the service and get only the username again
My code is the same as the article I referenced.
This is the controller I need access to the email address in, followed by the output of the dd($this->getUser().
class UserController extends AbstractController
{
/**
* #Route("/user", name="user")
*/
public function index()
{
dd($this->getUser());
return $this->render('user/index.html.twig', [
'controller_name' => 'UserController',
]);
}
}
^ HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUser {#580 ▼
#username: "coder1" }
You have to extend your User entity with HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUser Class, and extend the HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUserProvider Class with your own.
Then register it as a service and use that in your firewall settings. Here is an article walking through it:
https://inchoo.net/dev-talk/symfony-hwioauthbundle-and-google-sign-in/
In your OAuthUserProvider class you can modify the loadUserByOAuthUserResponse and load your user from database.
Here are the important code pieces:
update your firewall
oauth_user_provider:
service: ib_user.oauth_user_provider
add the services
hwi_oauth.user.provider.entity:
class: HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUserProvider
ib_user.oauth_user_provider:
class: Foggyline\Bundle\TickerBundle\Auth\OAuthProvider
arguments: [#session, #doctrine, #service_container]
Here is the OAuthProvider class I'm using:
<?php
namespace App\Auth;
use App\Repository\UserRepository;
use HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUserProvider;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use App\Entity\User;
class OAuthProvider extends OAuthUserProvider
{
protected $session, $doctrine, $admins, $userRepository;
public function __construct($session, $doctrine, $service_container, UserRepository $userRepository)
{
$this->session = $session;
$this->doctrine = $doctrine;
$this->container = $service_container;
$this->userRepository = $userRepository;
}
public function loadUserByUsername($username)
{
$result = $this->userRepository->findBy(['name' => $username]);
if (count($result)) {
return $result[0];
} else {
return new User($username);
}
}
public function loadUserByEmail($email)
{
$result = $this->userRepository->findBy(['email' => $email]);
if (count($result)) {
return $result[0];
} else {
return new User($email);
}
}
public function loadUserByOAuthUserResponse(UserResponseInterface $response)
{
//Data from response
$email = $response->getEmail();
$nickname = $response->getNickname();
$realname = $response->getRealName();
$avatar = $response->getProfilePicture();
//set data in session
$this->session->set('email', $email);
$this->session->set('nickname', $nickname);
$this->session->set('realname', $realname);
$this->session->set('avatar', $avatar);
$result = $this->userRepository->findBy(['email' => $email]);
if (!count($result)) {
$user = new User($email);
$user->setName($realname);
$user->setEmail($email);
$user->setRoles(['ROLE_USER']);
$factory = $this->container->get('security.encoder_factory');
$encoder = $factory->getEncoder($user);
$password = $encoder->encodePassword(md5(uniqid()), $user->getSalt());
$user->setPassword($password);
} else {
$user = $result[0];
$user->setUsername($realname);
}
$em = $this->doctrine->getManager();
$em->persist($user);
$em->flush();
//set id
$this->session->set('id', $user->getId());
return $this->loadUserByEmail($response->getEmail());
}
}
With $this->getUser() you are getting UserInterface object from security token of currently logged in user. So you have access to all the regular methods of your User entity. You can do like this:
$currentUser = $this->getUser()->getEmail();
In a Symfony 5.0.2 project a test of the new Mailer fails with
Error: Call to a member function getSubject() on null
The email service and test are based on symfonycast tutorials.
Adding var_dump($email); in the service immediately after $email = ...; shows object(Symfony\Bridge\Twig\Mime\TemplatedEmail)#24 (11) {..., which says there is a real object created in the service.
services.yaml:
App\Services\EmailerService:
$mailer: '#mailer'
$senderAddress: '%app.sender_address%'
Service:
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
class EmailerService
{
private $mailer;
private $sender;
public function __construct($mailer, $senderAddress)
{
$this->mailer = $mailer;
$this->sender = $senderAddress;
}
public function appMailer($mailParams)
{
$email = (new TemplatedEmail())
->from($this->sender)
->to($mailParams['recipient'])
->subject($mailParams['subject'])
->htmlTemplate($mailParams['view'])
->context($mailParams['context']);
$this->mailer->send($email);
}
}
Test:
use App\Services\EmailerService;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mailer\MailerInterface;
class MailerTest extends TestCase
{
public function testSimpleMessage()
{
$symfonyMailer = $this->createMock(MailerInterface::class);
$symfonyMailer->expects($this->once())
->method('send');
$mailer = new EmailerService($symfonyMailer, 'admin#bogus.info', 'admin#bogus.info');
$mailParams = [
'view' => 'Email/non_user_forgotten_password.html.twig',
'context' => ['supportEmail' => 'admin#bogus.info'],
'recipient' => 'bborko#bogus.info',
'subject' => 'Test message',
];
$email = $mailer->appMailer($mailParams);
$this->assertSame('Test message', $email->getSubject());
}
}
appMailer() must return a TemplatedEmail object so you can call getSubject() on it. Currently it is returning nothing. Change it to:
public function appMailer($mailParams)
{
$email = (new TemplatedEmail())
->from($this->sender)
->to($mailParams['recipient'])
->subject($mailParams['subject'])
->htmlTemplate($mailParams['view'])
->context($mailParams['context']);
$this->mailer->send($email);
return $email; // I added this line.
}
When I sent emails with symfony 3.4 and swiftmailer, I can set a parameter to send all emails to a defined email instead (makes sense in a development environment):
swiftmailer:
delivery_address: "development.email#myProjectDomain.com"
Now in the email itself I can't see where it would go to if this parameter would not be set. I could have a look in the header at the X-Swift-To variable, but that is extremely bothersome ...
What I would like to have is a simple info as first line in the body of the mail, something like
This email would be sent to customer#yahoo.com in production
How can I achieve that? Is there some configuration of swiftmailer to do exactly that? Because when I set up the swiftmailer and sent the email, there is no way for me to know where the email will actually be sent to ...
I created a service that handles this kind of stuff for me:
<?php
namespace AppBundle\Util;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Templating\EngineInterface;
use Swift_Mailer;
use AppBundle\Entity\User;
class MailService
{
private $em;
private $templating;
private $mailer;
private $env;
public function __construct(EntityManager $entityManager, EngineInterface $templating, Swift_Mailer $mailer,$env)
{
$this->em = $entityManager;
$this->templating = $templating;
$this->mailer = $mailer;
$this->env = $env;
}
private function doSend($subject, $to, $template, $vars = array())
{
$message = \Swift_Message::newInstance();
$body = $this->templating->render($template, $vars);
$message->setSubject($subject)
->setFrom("foo#bar.com")
->setTo($to)
->setBody(
$body,
"text/html"
);
if ($this->env != "prod") // Redirect mails in dev/staging environments
{
$message->setSubject("[".$this->env." ".$to."] ".$subject);
$message->setTo("my_dev_mail#foo.com"); // hardcoded for my case, could be changed to config parameter though...
}
$this->mailer->send($message);
}
public function Send($subject, $to, $template, $vars = array())
{
$type = gettype($to);
switch($type)
{
case "string":
$this->doSend($subject, $to, $template, $vars);
break;
case "array":
foreach($to as $t)
{
$this->Send($subject, $to, $template, $vars);
}
break;
case "object":
if ($to instanceof User)
{
$this->Send($subject, $to->getEmail(), $template, $vars);
} elseif (is_iterable($to))
{
foreach($to as $t)
{
$this->Send($subject, $t, $template, $vars);
}
}
break;
default:
throw \Exception("Unknown Recipient Type.");
}
}
}
I can then use $this->get("my.mailer")->Send($subject, $recipient, $template, $templateVars); (I use twig templates for my mails)
Mails in an environment other than prod are redirected and their subject-line altered to contain the original recipient and the current environment.
Note that I added some additional checks to allow $recipient to be a User entity or an array of mails / entities aswell.
Good morning.
Tldr: I think that, I need for Symfony Test Client something similiar to js xhr settings: withcredentals:true.
Symfony 3.1. I have action in rest api controller:
/**
* #Rest\GET("/get-stuff")
* #Rest\View
* #Security("is_granted('IS_AUTHENTICATED_FULLY')")
*/
public function GetStuffAction($userId)
{
// get userId from session
$userId = $this->getUser()->getId();
$em = $this->getDoctrine()->getManager();
$Stuff = $em->getRepository('MyApiBundle:Stuff')->findBy(['user' => $userId]);
return $this->viewGroup($Stuff, ['stuff']);
}
And it does work correct.
Now I want Test, so I have :
class TestCase extends WebTestCase
{
public function testIndex()
{
$this->client = self::createClient();
$this->storage = new MockFileSessionStorage(__dir__.'/../../../../app/cache/test/sessions');
$this->session = new Session($this->storage);
$this->logIn($this->getUser(), new Response());
$this->client->request('GET', '/get-stuff');
$content = $this->client->getResponse()->getContent();
$this->assertEquals({"message":"Full authentication is required to access this resource.","code":0},$content);
}
public function logIn(User $user, Response $response)
{
$this->session->start();
$this->cookie = new Cookie('MOCKSESSID', $this->storage->getId());
$this->cookieJar = new CookieJar();
$this->cookieJar->set($this->cookie);
$this->token = new UsernamePasswordToken($user, 'user', 'main', $user->getRoles());
$this->session->set('_security_main', serialize($this->token));
$this->getSecurityManager()->loginUser(
$this->container->getParameter('fos_user.firewall_name'),
$user,
$response
);
$this->session->save();
}
}
And test for it gives me message: "Full authentication is required to access this resource". After method logIn() I can read session data which are connected with logged user.
How can I make to be logged during this part to not receive message about authentication?:
$this->client->request('GET', '/get-stuff');
$content = $this->client->getResponse()->getContent();
More details:
1. My test class extends WebTestCase.
2. My user is overwritten for FosUserBundle.
I suppose that is something like in javascript:
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
but I don't know what.
Thank you in advance for helping solve my problem!
Ok, I was good way to do this:
public function logIn(User $user) {
$session = $this->session;
$firewallContext = 'secured_area';
$token = new UsernamePasswordToken($user, 'user', 'main', $user->getRoles());
$session->set('_security_'.$firewallContext, serialize($token));
$session->save();
$cookie = new Cookie($session->getName(), $session->getId());
$this->client->getCookieJar()->set($cookie);
}
I'm trying to do a little API with Symfony2.
I send a session id to my controller with a URL like this:
localhost/symfony2/web/app_dev.php/users/getuser/c5auv7mrp45rnd046cfv0vgl96
Then, in Symfony,
/**
* #Route("/getuser/{sessionId}")
*/
public function getSessionAction(Request $request, $sessionId)
{
// Here is what i'm trying to do
$packJson = array(
'user_id' => $userid
);
$response = new JsonResponse();
$response->setData($packJson);
return $response;
}
So, i would like to retrieve my user Id only with the sessionId argument.
Of course, it will be load from Db
I don't understand the logic between Session object and User Objet
Thanks
I think you want to use a token to identify a user. That means you have one token for each user in your database. If that is correct then it has nothing to do with sessions or a session-object.
you could simple retrieve your user with:
/**
* #Route("/getuser/{token}")
*/
public function getSessionAction($token)
{
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('AdminBundle:User')->findOneBy(array('token' => $token);
$response = new JsonResponse();
if (!$entity) {
$response->setData('error' => 'bad token');
return $response;
}
$packJson = array(
'user_id' => $entity->getId()
);
$response->setData($packJson);
return $response;
}