I'm very new to Laravel and unit testing in general. I'm trying to write some tests for my AccountController and I've run into a road block.
I'm using Sentry to handle users and groups in the site. I'm trying to test that my controller is handling exceptions thrown by Sentry properly. So my controller method that handles the login POST looks like this:
public function postLogin(){
$credentials = array(
'email' => Input::get('email'),
'password' => Input::get('password')
);
try{
$user = $this->authRepo->authenticate($credentials, true);
return Redirect::route('get_posts');
}
catch (Exception $e){
$message = $this->getLoginErrorMessage($e);
return View::make('login', array('errorMsg' => $message));
}
}
authRepository is just a repository that uses Sentry to handle authentication. Now I want to test that when an email address is not specified a LoginRequiredException is thrown and the user sees the error message. Here is my test:
public function testPostLoginNoEmailSpecified(){
$args = array(
'email' => 'test#test.com'
);
$this->authMock
->shouldReceive('authenticate')
->once()
->andThrow(new Cartalyst\Sentry\Users\LoginRequiredException);
$this->action('POST', 'MyApp\Controllers\AccountController#postLogin', $args);
$this->assertViewHas('errorMsg', 'Please enter your email address.');
}
However, the test is not passing. For some reason all it spits out is:
There was 1 error:
1) AccountControllerTest::testPostLoginNoEmailSpecified
Cartalyst\Sentry\Users\LoginRequiredException:
Am I using the andThrow() method incorrectly? If anyone can shed any light on what is going on it would be much appreciated.
Thanks in advance!
So I actually just figured the problem out. Turns out it wasn't a problem with my unit tests at all but was actually just a namespacing issue. I forgot the backslash on the Exception class. So in my controller it should have been:
try{
$user = $this->authRepo->authenticate($credentials, true);
return Redirect::route('get_posts');
}
catch (\Exception $e){
$message = $this->getLoginErrorMessage($e);
return View::make('account.login', array('errorMsg' => $message));
}
Related
I am trying to use Service-Repository pattern for the first time in my Laravel app and I have to handle a situation: a record can´t be added to a table (for example because trying to add same unique email....).
I have CustomAuthCOntroller with customRegistration method:
//Validatting request
$request->validated();
//running logic behind this in service
$is_Registered = $this->loginService->register_user(
$request->get('email'),
$request->get('password'),
$request->get('name'),
$request->get('surname')
);
// If everything goes right shol login page
if ($is_Registered) {
return redirect("/login")->withSuccess('Registrácia prebehla úspešne');
} else {
// Else HERE I HAVE TO SHOW what error happened
return redirect("/register")->withError('Registrácia prebehla úspešne');
}
My LoginService->register_user:
public function register_user($email, $password, $firstname, $lastname, $role = 'student') {
$user_attributes = [
'email' => $email,
'password' => $password,
'name' => $firstname,
'surname' => $lastname,
];
$this->userRepositor->create_new_user($user_attributes);
}
And my userRepository->create_new_user:
public function create_new_user(Array $attributes) : bool
{
DB::beginTransaction();
try {
User::create($attributes);
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
}
}
Is this right way how to achieve my goal? How can I show the error in my view if a exception is thrown?
You should not have any problems as you should be validating your input, so if the email/username or whatever that should be unique does already exist, then the validation should fail and return automatically with the errors (no try/catch).
If, you still want to catch any other possible error, let's say the DB timed out because of a connection problem or whatever (something not related to data), then you could either have a general try/catch (as I will show you) or do not catch it at all and let it go to the user, but this last approach depends a lot in your frontend:
If you have an API, then JS should handle this and show the error correctly.
If this is not an API (no AJAX), then the error could be shown to the user as a 500 error (0% descriptive for the user and would be a blank page with a 500 on it).
So, if you want the try/catch approach, you could do this:
//Validatting request
$request->validated();
try {
//running logic behind this in service
$this->loginService->register_user(
$request->get('email'),
$request->get('password'),
$request->get('name'),
$request->get('surname')
);
// If everything goes right shol login page
return redirect("/login")->withSuccess('Registrácia prebehla úspešne');
} catch (Throwable $exception) {
// HERE I HAVE TO SHOW what error happened
// You can even add the exception error with $exception->getMessage()
return redirect("/register")->withError('Registrácia prebehla úspešne');
}
And then, change your create_new_user to either of these:
public function create_new_user(Array $attributes) : bool
{
DB::beginTransaction();
try {
User::create($attributes);
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
Or use DB::transaction(). It will automatically rollback and throw the exception. See the source code here and here.
public function create_new_user(Array $attributes) : bool
{
DB::transaction(function () use ($attributes) {
User::create($attributes);
});
}
One personal recommendatation, do not declare methods as kebab_case like create_new_user, follow the PSR standard and Laravel standard, it should be camelCase: createNewUser.
I used Laravel Cashier to handle my user's subscription, however when I try to do the basic cancellation $user->subscription('main')->cancel(), an exception is being thrown
BadMethodCallException: Call to undefined method Illuminate\Database\Query\Builder::asStripeCustomer() in
\try\vendor\laravel\framework\src\Illuminate\Database\Query\Builder.php:2483
Stack trace:
\try\vendor\laravel\framework\src\Illuminate\Database\Eloquent\Model.php(1470): Illuminate\Database\Eloquent\Builder->__call('asStripeCustome...', Array)
\try\vendor\laravel\cashier\src\Subscription.php(345): Illuminate\Database\Eloquent\Model->__call('asStripeCustome...', Array)
\try\vendor\laravel\cashier\src\Subscription.php(256): Laravel\Cashier\Subscription->asStripeSubscription()
I setup the Model correctly and I uses the Billable trait so I really have no idea what's really causing this error
App\User.php
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Cashier\Billable;
class User extends Authenticatable{
use Billable;
...
}
App\Http\Controllers\UserController.php
public function cancelSubscription(Request $request)
{
$user = $request->user();
try {
if ($user->subscription('main')->onTrial()) {
$user->subscription('main')->cancelNow();
} else {
$user->subscription('main')->cancel();
}
} catch (\Exception $e) {
\Log::error($e);
return [
'success' => 0,
'message' => "Something went wrong while trying cancel your subscription. Please try again later."
];
}
Any help and hints will be greatly appreciated, thanks in advance!
My bad, I just found out that it was actually with my stripe configuration on /config/services.php as I have two models for my Users (because I'm also using another package other than laravel-cashier to handle payments via Authorize.net on which I ended up creating different Models for them to work)
'stripe' => [
// 'model' => App\AnetUser::class, => this actually caused the error as
// ->asStripeCustomer() doesn't exists on an Authorize.net's Billable trait
'model' => App\User::class,
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_SECRET'),
],
I feel so stupid. XD
Hi I've never worked with Laravel Cashier before but, I think the root of your problem might be that you are accessing user from the request, therefore it's not a user instance thats why it triggers undefined methods errors.
So creating a user instance should probably work out for you:
Note: I don't know if $request->user is primary key or whole user instance so I added different solutions
public function cancelSubscription(Request $request)
{
// if $request->user is the user instance you can do this:
$user = App\User::findOrFail($request->user->id);
// if $request->user was any other field from user you could retrieve
// the user using something like->
// App\User::where('fieldName', 'LIKE', $request->user)->firstOrFail();
try {
if ($user->subscription('main')->onTrial()) {
$user->subscription('main')->cancelNow();
} else {
$user->subscription('main')->cancel();
}
} catch (\Exception $e) {
\Log::error($e);
return [
'success' => 0,
'message' => "Something went wrong while trying cancel your subscription. Please try again later."
];
}
}
I'm working on a fairly large JSON API using Slim3. My controllers/actions are currently littered with the following:
return $response->withJson([
'status' => 'error',
'data' => null,
'message' => 'Username or password was incorrect'
]);
At certain points in the application anything can go wrong and the response needs to be appropriate. But one thing that is common is the error responses are always the same. The status is always error, the data is optional (in the case of form validation errors data will contain those) and message is set to indicate to the user or consumer of the API what went wrong.
I smell code duplication. How can I reduce the code duplication?
From the top of my head all I could think of doing was creating a custom exception, something like App\Exceptions\AppException that takes option data and the message will be obtained form $e->getMessage().
<?php
namespace App\Exceptions;
class AppException extends Exception
{
private $data;
public function __construct($message, $data = null, $code = 0, $previous = null)
{
$this->data = $data;
parent::__construct($message, $code, $previous);
}
public function getData()
{
return $this->data;
}
}
Following that create middleware that calls $next wrapped in a try/catch:
$app->add(function($request, $response, $next) {
try {
return $next($request, $response);
}
catch(\App\Exceptions\AppException $e)
{
$container->Logger->addCritical('Application Error: ' . $e->getMessage());
return $response->withJson([
'status' => 'error',
'data' => $e->getData(),
'message' => $e->getMessage()
]);
}
catch(\Exception $e)
{
$container->Logger->addCritical('Unhandled Exception: ' . $e->getMessage());
$container->SMSService->send(getenv('ADMIN_MOBILE'), "Shit has hit the fan! Run to your computer and check the error logs. Beep. Boop.");
return $response->withJson([
'status' => 'error',
'data' => null,
'message' => 'It is not possible to perform this action right now'
]);
}
});
Now all I have to do at points in the code is to throw new \App\Exceptions\AppException("Username or password incorrect", null).
My only issue with this is it feels like I'm using exceptions for the wrong reasons and it may make debugging a little more difficult.
Any suggestions on reducing the duplicates and cleaning up error responses?
You can achieve similar similar results by creating an error handler which outputs JSON.
namespace Slim\Handlers;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
final class ApiError extends \Slim\Handlers\Error
{
public function __invoke(Request $request, Response $response, \Exception $exception)
{
$status = $exception->getCode() ?: 500;
$data = [
"status" => "error",
"message" => $exception->getMessage(),
];
$body = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
return $response
->withStatus($status)
->withHeader("Content-type", "application/json")
->write($body);
}
}
You must also configure Slim to use your custom error handler.
$container = $app->getContainer();
$container["errorHandler"] = function ($container) {
return new Slim\Handlers\ApiError;
};
Check Slim API Skeleton for example implemention.
I want to use Eloquent to create a DB entry like this:
MFUser::create(array(
'user_reference' => $this->userReference,
'first_name' => $this->firstName,
'last_name' => $this->lastName,
'user_type_id' => $this->userTypeId,
'email' => $this->email,
'password' => $this->password
));
It works well, except for the case, when exactly the same data is put into the fields, which is expected, as there should be no duplicate. I get the QueryExecption then.
But how do I properly perform error handling here to check if this query exception occurs so I can return a response from my server to the client via json then?
Just wrap that code in a try-catch block. Something like this:
try {
MFUser::create(array(
'user_reference' => $this->userReference,
'first_name' => $this->firstName,
'last_name' => $this->lastName,
'user_type_id' => $this->userTypeId,
'email' => $this->email,
'password' => $this->password
));
} catch (\Illuminate\Database\QueryException $exception) {
// You can check get the details of the error using `errorInfo`:
$errorInfo = $exception->errorInfo;
// Return the response to the client..
}
I prefer to reserve try/catch's for unexpected events that can't be handled elsewhere. In this case, you can utilise validation as a first measure, and the exception handler as a backup measure.
If the form request fails, the error messages are flashed and the user is returned to the previous page (where the form was submitted) and you can handle the messages gracefully. Validation requests
// First line of defence - gracefully handle
// Controller
public function store(MFUserRequest $request)
{
// The incoming request is valid...
MFUser::create(array(...));
}
// Form Request
class MFUserRequest extends Request
{
public function rules()
{
return [
'email' => 'required|email|unique:users,email',
];
}
}
Elsewhere, in your App\Exceptions directory you have the exception handler class that can be a catch all for various errors. Use this, when you haven't been able to gracefully handle it further down.
// Second line of defence - something was missed, and a model was
// created without going via the above form request
namespace App\Exceptions;
class Handler extends ExceptionHandler
{
public function render($request, Exception $e)
{
if($e instanceof QueryException) {
// log it or similar. then dump them back on the dashboard or general something bad
// has happened screen
return redirect()->route('/dashboard');
}
}
}
Simply make use of try / catch block.
use Illuminate\Database\QueryException;
// ...
try {
// DB query goes here.
} catch (QueryException $e) {
// Logics in case there are QueryException goes here
}
// ...
Try and Catch something might help u and imagine for create try catch for all of them, but have best practice to handle all of QueryException, my recommend is use Laravel Exceptions Handler because it's make u easy for make execption global!
let's try it!, open App\Exception\Handler.php and at render method u can write like this one
public function render($request, Throwable $exception)
{
if ($request->ajax() || $request->wantsJson()) {
if ($exception instanceof QueryException) {
// example algo for make response
return response()->json(['message' => 'xxx'], 403);
}
}
return parent::render($request, $exception);
}
after that, u can get xxx json for every error request triggered by Query
How do i go about mocking facades with arguments in laravel 4? For instance, i'm trying to test my user controller and in my 'login' method.
my controller method
public function login(){
$this->beforeFilter('guest');
$creds = array(
'email' => Input::get('email'),
'password' => Input::get('password'),
);
if(Auth::attempt($creds, true)){
return "successful";
} else {
return Redirect::to('user/login')->with('error', true);
}
}
the redirect test that doesn't work
public function testPostLogin(){
Redirect::shouldReceive('to')->once()->with('error', true);
$response = $this->action('POST', 'UserController#login');
$this->assertRedirectedTo('user/login');
}
I'm getting the following exception. I don't know how to inject the 'user/login' parameter into the Redirect mock
Mockery\Exception\NoMatchingExpectationException : No matching handler found for Illuminate\Routing\Redirector::to("user/login")
In theory you can just mock your Auth class.
Try this:
Auth::shouldReceive('attempt')->once()->andReturn(true);