I wonder how to write proper unit test for my email sending method. It's a problem because inside method I get data from Auth object. Should I send id of user in Request?
public function sendGroupInvite(Request $request){
foreach ($request->get('data') as $item){
$invitations = new \App\Models\Invitations();
$invitations->user_id = Auth::id();
$invitations->name = $item["name"];
$invitations->email = $item["email"];
$invitations->status = 0;
$invitations->token = \UUID::getToken(20);
$invitations->username = Auth::user()->name;
$invitations->save();
$settings = UserSettings::where('user_id', Auth::id())->first();
$email = $item["email"];
$url = 'https://example.com/invite/accept/'.$invitations->token;
$urlreject = 'https://example.com/invite/reject/'.$invitations->token;
$mailproperties = ['token' => $invitations->token,
'name' => $invitations->name,
'url' => $url,
'email' => $email,
'urlreject' => $urlreject,
'userid' => Auth::id(),
'username' => Auth::user()->name,
'user_name' => $settings->name,
'user_lastname' => $settings->lastname,
'user_link' => $settings->user_link,
];
$this->dispatch(new SendMail(new Invitations($mailproperties)));
}
return json_encode(array('msg' => 'ok'));
}
I'm using Auth to get username and user id. When I testing it it's not works, because Auth it's null.
I would go with mocking the queue, something similar to this. Mock Documentation
class MailTester extends TestCase{
/**
* #test
*/
public function test_mail(){
Queue::fake();
// call your api or method
Queue::assertPushed(SendMail, function(SendMail $job) {
return $job->something = $yourProperties;
});
}
You could try "acting as" to deal with the Auth::user().
...
class MyControllerTest extends TestCase{
/**
* #test
*/
public function foo(){
$user = App\Users::find(env('TEST_USER_ID')); //from phpunit.xml
$route = route('foo-route');
$post = ['foo' => 'bar'];
$this->actingAs($user)//a second param is opitonal here for api
->post($route, $post)
->assertStatus(200);
}
}
Related
So I just want to add login with google feature on my working authentication web app (with Codeigniter Shield package). I've already create a login_google function on Login controller that extends LoginController from shield package like this :
LoginController
<?php
namespace App\Controllers;
use App\Controllers\BaseController;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\Shield\Controllers\LoginController;
class Login extends LoginController
{
function __construct()
{
require_once __DIR__ . '/../../vendor/autoload.php';
$this->userModel = new \App\Models\UserModel();
$this->google_client = new \Google_Client();
$this->google_client->setClientId(getenv('OAuth2.clientID'));
$this->google_client->setClientSecret(getenv('OAuth2.clientSecret'));
$this->google_client->setRedirectUri('http://localhost:8080/login_google');
$this->google_client->addScope('email');
$this->google_client->addScope('profile');
}
public function loginView()
{
if (auth()->loggedIn()) {
return redirect()->to(config('Auth')->loginRedirect());
}
/** #var Session $authenticator */
$authenticator = auth('session')->getAuthenticator();
// If an action has been defined, start it up.
if ($authenticator->hasAction()) {
return redirect()->route('auth-action-show');
}
$data['google_button'] = "<a href='".$this->google_client->createAuthUrl()."'><img src='https://developers.google.com/identity/images/btn_google_signin_dark_normal_web.png' /></a>";
return view('login', $data);
}
public function loginAction(): RedirectResponse
{
// Validate here first, since some things,
// like the password, can only be validated properly here.
$rules = $this->getValidationRules();
if (! $this->validate($rules)) {
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
}
$credentials = $this->request->getPost(setting('Auth.validFields'));
$credentials = array_filter($credentials);
$credentials['password'] = $this->request->getPost('password');
$remember = (bool) $this->request->getPost('remember');
/** #var Session $authenticator */
$authenticator = auth('session')->getAuthenticator();
// Attempt to login
$result = $authenticator->remember($remember)->attempt($credentials);
if (! $result->isOK()) {
return redirect()->route('login')->withInput()->with('error', $result->reason());
}
/** #var Session $authenticator */
$authenticator = auth('session')->getAuthenticator();
// If an action has been defined for login, start it up.
if ($authenticator->hasAction()) {
return redirect()->route('auth-action-show')->withCookies();
}
return redirect()->to(config('Auth')->loginRedirect())->withCookies();
}
public function login_google() {
$token = $this->google_client->fetchAccessTokenWithAuthCode($this->request->getVar('code'));
if (!isset($token['error'])) {
$this->google_client->setAccessToken($token['access_token']);
$this->session->set('access_token', $token['access_token']);
$google_service = new \Google\Service\Oauth2($this->google_client);
$data = $google_service->userinfo->get();
$userdata = array();
if ($this->userModel->isAlreadyRegister($data['id'])) {
$userdata = [
'first_name' => $data['givenName'],
'last_name' => $data['familyName'],
'email' => $data['email'],
'avatar' => $data['picture'],
];
$this->userModel->updateUserData($userdata, $data['id']);
} else {
$userdata = [
'first_name' => $data['givenName'],
'last_name' => $data['familyName'],
'email' => $data['email'],
'avatar' => $data['picture'],
'oauth_id' => $data['id'],
];
$this->userModel->insertUserData($userdata);
}
$this->session->set('LoggedUserData', $userdata);
} else {
$this->session->set("error", $token['error']);
return redirect('/register');
}
return redirect()->to('/profile');
}
}
UserModel like this :
UserMode
<?php
namespace App\Models;
use CodeIgniter\Model;
use CodeIgniter\Shield\Models\UserModel as ModelsUserModel;
class UserModel extends ModelsUserModel
{
protected $allowedFields = [
'username',
'status',
'status_message',
'active',
'last_active',
'deleted_at',
'gender',
'first_name',
'last_name',
'avatar',
'phone_number',
'full_address',
'oauth_id',
];
function isAlreadyRegister($authid){
return $this->db->table('users')->getWhere(['id'=>$authid])->getRowArray()>0?true:false;
}
function updateUserData($userdata, $authid){
$this->db->table("users")->where(['id'=>$authid])->update($userdata);
}
function insertUserData($userdata){
$this->db->table("users")->insert($userdata);
}
}
But everytime I clicked sign in with google button, it won't work (the interface for choosing google account to authenticate is worked) and always return to login page
am I missing something when combining CodeIgniter Shield with Google Oauth ? Anyone can help ? TIA
A new package has been created for OAuth with Shield package: https://github.com/datamweb/shield-oauth
You can use it instead of your own one.
I need to make request on some CRM api in my controller. For making this I have pretty big method. It's look like ugly. I know that there are some "Services" and to put additional code into Service is a good way. But I don't know what is this. Is it a custom classes into app folder? Or maybe it's Service-providers? I have read service-providers documentation and I'm not sure that service-providers is suitable for this. Here is my code:
<?php
namespace App\Http\Controllers;
use App\User;
use App\UserInfo;
use Validator;
use Illuminate\Http\Request;
class UserController extends Controller
{
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$users = User::with('info')
->paginate(20);
$users->withPath(DIRECTORY_SEPARATOR . $request->path() .DIRECTORY_SEPARATOR);
return response()->json($users)->setEncodingOptions(JSON_UNESCAPED_UNICODE);
}
/**
* Store a newly created resource in storage.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$data = $request->json()->all();
$rules = [
'name' => 'required',
'phone' => 'required|unique:users'
];
$validator = Validator::make($data, $rules);
if ($validator->fails()) return response()->json(['errors'=>$validator->errors()]);
$user = new User();
$user->name = request('name');
$user->phone = request('phone');
$user_info_obj = $this->storeUserInfo();
if($user_info_obj === null){
return response('Impassible to define user geo data', 400);
}
$user->info_id = $user_info_obj->id;
$user->save();
$this->makeAMOLead($user->name,
$user->phone,
$user_info_obj->user_agent,
$user_info_obj->city,
$user_info_obj->country);
return response()->json(['success' => 'User created successfully']);
}
public function storeUserInfo()
{
$ip = request()->ip();
$reader = new \GeoIp2\Database\Reader('../resources/geo-lite2-city_20180807/GeoLite2-City.mmdb');
try {
$record = $reader->city($ip);
}
catch (\Throwable $e){
// Code bellow is for testing on localhost, Because of maybe exception instead of geo obj on localhost.
$info = new UserInfo();
$info->ip = '127.0.0.1';
$info->city = 'Some city';
$info->country = 'Some country';
$info->country_code = 'Some code';
$info->continent = 'Some continent';
$info->continent_code = 'no';
$info->user_agent = 'User agent';
$info->save();
return $info;
//return null;
}
$city = $record->city->names['ru'];
$continent = $record->continent->names['ru'];
$continent_code = $record->continent->code;
$country = $record->country->names['ru'];
$country_code = $record->country->isoCode;
$user_agent = \request()->userAgent();
$info = new UserInfo();
$info->ip = $ip;
$info->city = $city;
$info->country = $country;
$info->country_code = $country_code;
$info->continent = $continent;
$info->continent_code = $continent_code;
$info->user_agent = $user_agent;
$info->save();
return $info;
}
private function makeAMOLead($name, $phone, $userAgent, $city, $country)
{
$domain = env('AMO_DOMAIN');
$login = env('AMO_LOGIN');
$hash = env('AMO_HASH');
try {
$credentials = new \ddlzz\AmoAPI\CredentialsManager($domain, $login, $hash);
$settings = new \ddlzz\AmoAPI\SettingsStorage();
$settings->setCookiePath(env('AMO_COOKIE_FILE_PATH'));
$request = \ddlzz\AmoAPI\ClientFactory::create($credentials, $settings);
$lead = new \ddlzz\AmoAPI\Model\Amo\Lead();
$lead['name'] = $name;
if(env('AMO_PIPELINE_ID', null)){
$lead['pipeline_id'] = intval(env('AMO_PIPELINE_ID'));
}
$lead['name'] = 'New pickup user ' . $name;
$lead['custom_fields'] = [
[
'id' => env('AMO_NAME_FIELD_ID'),
'values' => [
['value' => $name],
]
],
[
'id' => env('AMO_USER_AGENT_FIELD_ID'),
'values' => [
['value' => $userAgent]
]
],
[
'id' => env('AMO_CITY_FIELD_ID'),
'values' => [
['value' => $city]
]
],
[
'id' => env('AMO_COUNTRY_FIELD_ID'),
'values' => [
['value' => $country]
]
],
];
$lead['created_at'] = time();
$result = $request->add($lead);
$pipelineId = json_decode($result)->_embedded->items{0}->id;
// create contact
$contact = new \ddlzz\AmoAPI\Model\Amo\Contact();
$contact['name'] = $name;
$contact['created_at'] = time();
$contact['leads_id'] = "$pipelineId";
// dd($request->accountInfo(), true); // Call this, if you need to know ids of default fields (like phone, or position)
$contact['custom_fields'] = [
[
'id' => env('AMO_CONTACT_PHONE_ID'),
'values' => [
[
'value' => $phone,
'enum' => 'MOB',
],
]
],
];
$result = $request->add($contact);
} catch (Exception $e) {
echo response()->json(['error' => $e->getFile() . ': ' . $e->getMessage()]);
}
}
}
Look on the makeAMOLead. This is big method in my controller and this is not ok for controller conception.
Please use repository pattern to split all the communication between the application and your data source. and call the repository functions inside your controller. It is good practice. Here is an article you can understand about that
Example:
Your functions can be separate from controller to repository.
storeUserInfo
makeAMOLeadin
Move your functions an repository and call them into your controller.
I have a weird problem with my Laravel application, whereby this code gets called twice once the validation rules kick in. I have abstracted validation logic into a separate class, but no matter how I consume the API (tried using Postman, and with jQuery) it still appears to run twice with the output looking like this:
called{"email":["The email has already been taken."],"country":["The country must be a number."]}called{"email":["The email has already been taken."],"country":["The country must be a number."]}
I am only expecting one JSON response. I'm tearing my hair out, tried on two different connections and can't seem to work out why the custom request is called twice. This is a new Laravel app, so there isn't much code to conflict with it.
//Create User Request extends standard request. Handles Validation
public function __construct(CreateUserRequest $request){
$this->request = $request;
}
public function register()
{
try{
$array = DB::transaction(function(){
$email = $this->request->input('email');
$password = $this->request->input('password');
$companyName = $this->request->input('companyName');
$userName = $this->request->input('name');
$country = $this->request->input('country');
$company = Company::create([
'name' => $companyName,
'active'=>true,
'country_id'=>$country
]);
$user = User::create([
'company_id' => $company->id,
'name'=>'admin',
'email' => $email,
'password' => $password,
'active' =>true
]);
if( !$company || !$user )
{
throw new \Exception('User not created for account');
}
return compact('company', 'user');
});
$token = JWTAuth::fromUser($array['user']);
return Response::json(compact('token'));
}
catch( Exception $e )
{
return Response::json(['error' => $e->getMessage() ], HttpResponse::HTTP_CONFLICT );
}
}
Then the validation custom Request..
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Response;
class CreateUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
public function response(array $errors)
{
// return Response::json(['errorg' => $errors ], 200 );
echo('called');
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'email' => 'required|unique:users',
'password' => 'required',
'companyName' => 'required',
'name' => 'required',
'country' => 'required|numeric'
];
}
}
Interesting.
Try to remove CreateUserRequest $request parameter from __construct() and add it to your register() method like this: register(CreateUserRequest $request). And use your request by calling $request instead of $this->request.
I'm building an API in Laravel, and am using a custom request to validate the inbound data. My problem is that I'm not sure how I 'catch' the validation errors to shape the response.
Here's what I have so far.
Register method wrapped in a transaction:
//Create User Request extends standard request. Handles Validation
public function __construct(CreateUserRequest $request){
$this->request = $request;
}
public function register()
{
try{
$array = DB::transaction(function(){
$email = $this->request->input('email');
$password = $this->request->input('password');
$companyName = $this->request->input('companyName');
$userName = $this->request->input('name');
$country = $this->request->input('country');
$company = Company::create([
'name' => $companyName,
'active'=>true,
'country_id'=>$country
]);
$user = User::create([
'company_id' => $company->id,
'name'=>'admin',
'email' => $email,
'password' => $password,
'active' =>true
]);
if( !$company || !$user )
{
throw new \Exception('User not created for account');
}
return compact('company', 'user');
});
$token = JWTAuth::fromUser($array['user']);
return Response::json(compact('token'));
}
catch( Exception $e )
{
return Response::json(['error' => $e->getMessage() ], HttpResponse::HTTP_CONFLICT );
}
}
The form request looks like this:
class CreateUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'email' => 'required|unique:users',
'password' => 'required',
'companyName' => 'required',
'name' => 'required',
'country' => 'required|numeric'
];
}
}
My errors are coming back automatically, which appear to be the messagebag object serialised to JSON
{"email":["The email has already been taken."]}{"email":["The email has already been taken."]}
Somewhere in there I need to catch the Exception inside the main Controller, but I've used the Custom Request Class to clean up my controller a bit, how would I do that? Note the Exception already caught in this controller, which doesn't seem to pickup whatever is thrown behind the scenes in the custom request.
any pointers? do I need to move validation back to the controller? or is there a cleaner way to do this?
You can override the response method in CreateUserRequest to customize the response:
public function response(array $errors)
{
return parent::response($errors);
}
I followed a serie on creating a shopping cart on codecourse - and it was just what I was looking for.
In the series the payment happens on creating the order - but now my client says that they don't want the payment on creating order - first when they have processed the order - they want to send an email with payment or an link to a payment site.
I thought no big deal - just move the BraintreePayment par out of the create class - create a new class called payment and thats that - but no - so now I am stuck.
I am using swiftmailer to send the mail with the link and that works fine - but the payment part fails.
I am new to this MVC / Slim thing - so please can someone help me in the right direction.
The error it throws say:
Type: TypeError Message: Argument 1 passed to
Cart\Events\OrderWasCreated::__construct() must be an instance of
Cart\Models\Order, integer given, called in
/Applications/AMPPS/www/testshop.dev/cart/app/Controllers/OrderController.php
on line 160 File:
/Applications/AMPPS/www/testshop.dev/cart/app/Events/OrderWasCreated.php
Line: 17
My ordercontroller with the payment class - looks like this:
<?php
namespace Cart\Controllers;
use Slim\Router;
use Slim\Views\Twig;
use Cart\Basket\Basket;
use Cart\Models\Order;
use Cart\Models\Product;
use Cart\Models\Address;
use Cart\Models\Delivery;
use Cart\Models\Customer;
use Cart\Controllers\MailController;
use Cart\Validation\Contracts\ValidatorInterface;
use Cart\Validation\Forms\OrderForm;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Braintree_transaction;
class OrderController
{
protected $basket;
protected $mailcontroller;
protected $router;
protected $validator;
public function __construct(Basket $basket, Mailcontroller $mailcontroller, Router $router, ValidatorInterface $validator)
{
$this->basket = $basket;
$this->mailcontroller = $mailcontroller;
$this->router = $router;
$this->validator = $validator;
}
public function index(Request $request, Response $response, Twig $view)
{
$this->basket->refresh();
if (!$this->basket->subTotal()) {
return $response->withRedirect($this->router->pathFor('cart.index'));
}
return $view->render($response, 'order/index.twig');
}
public function show($hash, Request $request, Response $response, twig $view, Order $order)
{
$order = $order->with('address', 'products')->where('hash', $hash)->first();
$this->mailcontroller->mailLisbeth($hash);
if (!$order) {
return $response->withRedirect($this->router->pathFor('home'));
}
return $view->render($response, 'order/show.twig', [
'order' => $order,
]);
}
public function create(Request $request, Response $response, Customer $customer, Address $address, Delivery $delivery, Order $order)
{
$this->basket->refresh();
$validation = $this->validator->validate($request, OrderForm::rules());
if (!$this->basket->subTotal()) {
return $response->withRedirect($this->router->pathFor('cart.index'));
}
if ($validation->fails()) {
return $response->withRedirect($this->router->pathFor('order.index'));
}
$hash = bin2hex(random_bytes(32));
$customer = $customer->firstOrCreate([
'email' => $request->getParam('email'),
'name' => $request->getParam('name'),
]);
$delivery = $delivery->firstOrCreate([
'delivery' => $request->getParam('delivery'),
'deliverydate' => $request->getParam('deliverydate'),
'deliverytime' => $request->getParam('deliverytime'),
]);
$address = $address->firstOrCreate([
'address1' => $request->getParam('address1'),
'address2' => $request->getParam('address2'),
'city' => $request->getParam('city'),
'postal_code' => $request->getParam('postal_code'),
]);
$order = $customer->orders()->create([
'hash' => $hash,
'accepted' => "2",
'paid' => false,
'total' => $this->basket->subTotal() + 150,
]);
$address->order()->save($order);
$delivery->order()->save($order);
$allItems = $this->basket->all();
$order->products()->saveMany(
$allItems,
$this->getQuantities($allItems)
);
$event = new \Cart\Events\OrderWasCreated($order, $this->basket);
$event->attach([
// new \Cart\Handlers\MarkOrderPaid,
// new \Cart\Handlers\RecordSuccessfulPayment($result->transaction->id),
new \Cart\Handlers\UpdateStock,
new \Cart\Handlers\Emptybasket,
]);
$event->dispatch();
return $view->render($response, 'order/show.twig', [
'order' => $order,
]);
}
//
public function payment($slug, Request $request, Response $response, twig $view, Customer $customer, Address $address, Delivery $delivery, Order $order)
{
$order = $order->with('address', 'products')->where('id', $slug)->first();
// var_dump($order);
// die();
// if (!$request->getParam('payment_method_nonce')) {
// return $response->withRedirect($this->router->pathFor('order.index'));
// }
$order = $customer->orders()->update([
'paid' => true,
]);
$result = Braintree_Transaction::sale([
'amount' => $this->basket->subTotal() + 150,
'paymentMethodNonce' => $request->getParam('payment_method_nonce'),
'options' => [
'submitForSettlement' => true,
]
]);
$event = new \Cart\Events\OrderWasCreated($order, $this->basket);
if (!$result->success) {
$event->attach(new \Cart\Handlers\RecordFailedPayment);
$event->dispatch();
return $response->withRedirect($this->router->pathFor('order.index'));
}
$event->attach([
new \Cart\Handlers\MarkOrderPaid,
new \Cart\Handlers\RecordSuccessfulPayment($result->transaction->id),
// new \Cart\Handlers\UpdateStock,
// new \Cart\Handlers\Emptybasket,
]);
}
protected function getQuantities($items)
{
$quantities = [];
foreach ($items as $item) {
$quantities[] = ['quantity' => $item->quantity];
}
return $quantities;
}
}
The error occures on line 160:
$event = new \Cart\Events\OrderWasCreated($order, $this->basket);
because you invoke Cart\Events\OrderWasCreated constructor, which requires instance of Cart\Models\Order class as first argument. Instead your first argument ($order) is integer.
Now latest assignment of $order is on line 147:
$order = $customer->orders()->update([
'paid' => true,
]);
I'll have a guess and say that this is an operation that returns integer as result (be that a simple 1/0 success/fauilure or the id of the order being updated). Are you sure that you need that? Asking because:
You are assigning $order a value on line 138:
$order = $order->with('address', 'products')->where('id', $slug)->first();
And you're passing $order as an argument to the OrderController::payment.
So, my guess is: you want to remove lines 138 and 147.