How do I run a phpunit test on Redis pub/sub? - laravel

I'm building a messenger system with Redis publishing on the Laravel end and subscribing on a node server. I would like to test what is stored in the redis pub method using PHPUnit, but I have no idea where to start.
Controller
class MessageController extends Controller
{
public function store(Conversation $conversation, Request $request)
{
$user = Auth::user();
$message = Message::create([
'body' => $request->input('message'),
'conversation_id' => $conversation->id,
'sender_id' => $user->id,
'type' => 'user_message'
]);
$redis = Redis::connection();
$data = new MessageResource($message);
$redis->publish('message', $data);
}
}
Current Test
/** #test */
public function a_user_can_send_a_message()
{
$this->actingAs($user = User::factory()->create());
$message = Message::factory()->make(['sender_id' => $user->id]);
$conversation Conversation::factory()->create();
$response = $this->json('POST', '/api/message/'. $conversation->id, ['message' => $message->body])
->assertStatus(201);
$response->assertJsonStructure([
'data' => [
'body',
'sender',
]
]);
}
Essentially what I'm trying to see is if message has been published on Redis. I'm unsure how to do this, and I think you would probably need to clear the message from Redis after, would you not?

Your test should be like this:
public function test_a_user_can_send_a_message()
{
$redisSpy = Redis::spy();
$redisSpy->shouldReceive('connection')->andReturnSelf();
$this->actingAs($user = User::factory()->create());
$message = Message::factory()->make(['sender_id' => $user->id]);
$conversation = Conversation::factory()->create();
$this->postJson("/api/message/{$conversation->id}", ['message' => $message->body]);
$this->assertDatabaseCount('messages', 1);
$redisSpy->shouldHaveReceived('publish')
->with('message', new MessageResource(Message::first()));
}
As you can see, I have added Redis::spy(); this is going to allow is to "spy" what is called from Redis. You can still mock methods, and we have to do so, because you use Redis::connect(); and then $redis->publish(...), so we will return the spy when connect is called, that is why we do shouldReceive('connection')->andReturnSelf().
At the end of the code, we check that $redis->publish was called with parameters 'message' and a resource with the desired message. Both must match for this assertion to pass, else you will see a mock error.

Related

Laravel: Feature Test fails because of added middleware

After a user signs up and verifies their email, they must complete their signup with additional information. This happens at /register/complete-signup
The issue makes absolutely no sense to me.
For whatever reason, when I added my Middleware has-not-completed-signup, the test starts failing because a App\User no longer has the associated App\Account which is happening in the controller via attach()
As soon as I remove my middleware from the route, it works fine.
My middleware is there to prevent a user who has completed the signup already from visiting or POSTing to those routes. I tested in the browser and the redirect works. The controller method is being used in the test and i can dd($account->users) and get the correct response. But if I do $user->accounts, the collection is empty.
Once I remove my middleware, $user->accounts is no longer empty. But I did a dd() inside my middleware and it's not even running (which is correct because the user doesn't have an account).
So why would this make it fail? I'm completely lost.
I tried to include all relevant information below. If there is something else you need, please let me know.
Edit:
In my middleware, I've commented out the functionality. Something about checking an eloquent relationship makes me test fail. I have no idea why.
This makes the test fail:
if (!auth()->user()->accounts->isEmpty()) {
//return redirect(RouteServiceProvider::HOME);
}
If for example I change it to something useless like this, it works:
if (auth()->user()) {
//return redirect(RouteServiceProvider::HOME);
}
I can do $account->users , but $user->accounts returns empty collection on the controller when I use my middleware
Original:
Here are my routes:
// auth scaffolding
Auth::routes(['verify' => true]);
// main app routes
Route::middleware('verified', 'auth')->group(function() {
// User verified and has an App\Account
Route::middleware('completed-signup')->group(function() {
Route::get("/", 'HomeController#index' )->name('home');
Route::get('/paywall', 'BillingController#paywall')->name('paywall');
});
// The user hasn't attached an App\Account to their User
Route::middleware('has-not-completed-signup')->group(function() {
Route::get("/register/complete-signup", 'AccountController#showCompleteSignup' )->name('complete-signup');
Route::post('/register/complete-signup', 'AccountController#storeCompleteSignup')->name('complete-signup.store');
});
});
has-not-completed-signup Middleware:
public function handle($request, Closure $next)
{
if (auth()->user()->hasCompletedAccountSetup()) {
return redirect(RouteServiceProvider::HOME);
}
return $next($request);
}
App/User method:
Class User extends Authenticatable implements MustVerifyEmail {
...
public function accounts() {
return $this->belongsToMany('App\Account', 'account_role_user')->withPivot('role_id');
}
public function hasCompletedAccountSetup() {
return !$this->accounts->isEmpty();
}
...
AccountController#storeCompletedSignup:
public function storeCompleteSignup(Request $request) {
$validatedData = $request->validate([
'company' => 'required|max:255',
'contact_state' => 'required|max:255',
'contact_country' => 'required|max:255',
'contact_zip' => 'required|max:255',
'contact_city' => 'required|max:255',
'contact_phone' => 'nullable|max:255',
'contact_address_1' => 'required|max:255',
'contact_address_2' => 'nullable|max:255',
'contact_first_name' => 'required',
'contact_last_name' => 'required',
'contact_email' => 'required'
]);
$user = auth()->user();
$account = new Account($validatedData);
$account->contact_first_name = $user->first_name;
$account->contact_last_name = $user->last_name;
$account->contact_email = $user->email;
$account->save();
$account->users()->attach(
$user->id,
['role_id' => Role::where('name', 'owner')->first()->id ]
);
return $request->wantsJson()
? new Response('Signup Completed Successfully', 201)
: redirect()->route('/');
}
My Test:
/**
* #test
*/
public function a_user_can_complete_signup()
{
$user = Factory('App\User')->create();
$this->actingAs($user);
$accountAttributes = factory('App\Account')->raw([
'contact_first_name' => "TEST",
'contact_last_name' => $user->last_name,
'contact_email' => $user->email,
'contact_country' => "USA"
]);
$res = $this->post('/register/complete-signup', $accountAttributes);
$res->assertSessionDoesntHaveErrors();
$this->assertTrue( !$user->accounts->isEmpty() ); // THIS FAILS
$this->assertTrue( $user->accounts->first()->company == $accountAttributes['company']);
$this->assertTrue( $user->accounts->first()->contact_first_name == $user->first_name );
}
The issue wasn't actually with the middleware, but it was because I had to refresh the model after the POST on the test.
$this->assertTrue( !$user->accounts->isEmpty() );
needed to become
$this->assertTrue( !$user->fresh()->accounts->isEmpty() );
which passed the test.
I knew about the fresh and refresh() methods, but the middleware causing the issue didn't make sense to me.

Find data before validate form request laravel

I want to update the data using the request form validation with a unique email role, everything works normally.
Assume I have 3 data from id 1-3 with url:
127.0.0.1:8000/api/user/update/3
Controller:
use App\Http\Requests\Simak\User\Update;
...
public function update(Update $request, $id)
{
try {
// UPDATE DATA
return resp(200, trans('general.message.200'), true);
} catch (\Exception $e) {
// Ambil error
return $e;
}
}
FormRequest "Update":
public function rules()
{
return [
'user_akses_id' => 'required|numeric',
'nama' => 'required|max:50',
'email' => 'required|email|unique:users,email,' . $this->id,
'password' => 'required',
'foto' => 'nullable|image|max:1024|mimes:jpg,png,jpeg',
'ip' => 'nullable|ip',
'status' => 'required|boolean'
];
}
but if the updated id is not found eg:
127.0.0.1:8000/api/user/update/4
The response gets The email has already been taken.
What is the solution so that the return of the data is not found instead of validation first?
The code looks like it should work fine, sharing a few things below that may help.
Solution 1: Check if $this->id contains the id you are updating for.
Solution 2: Try using the following changes, try to get the id from the URL segment.
public function rules()
{
return [
'user_akses_id' => 'required|numeric',
'nama' => 'required|max:50',
'email' => 'required|email|unique:users,email,' . $this->segment(4),
'password' => 'required',
'foto' => 'nullable|image|max:1024|mimes:jpg,png,jpeg',
'ip' => 'nullable|ip',
'status' => 'required|boolean'
];
}
Sharing one more thing that may help you.
Some person uses Request keyword at the end of the request name. The Update sounds generic and the same as the method name you are using the request for. You can use UpdateRequest for more code readability.
What I understand from your question is, you need a way to check if the record really exists or not in the form request. If that's the case create a custom rule that will check if the record exists or not and use that rule inside your request.
CheckRecordRule
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class CheckRecordRule implements Rule
{
protected $recordId;
public function __construct($id)
{
$this->recordId = $id;
}
public function passes($attribute, $value)
{
// this will check and return true/false
return User::where('id', $this->recordId)->exists();
}
public function message()
{
return 'Record not found.';
}
}
Update form request
public function rules()
{
return [
'email' => 'required|email|unique:users,email,' . $this->id.'|'. new CheckRecordRule($this->id),
];
}
So when checking for duplicate it will also check if the record really exists or not and then redirect back with the proper message.

How to write TDD code for user profile update in Laravel

I want to create a TDD first before using my function in the app.
I have already created the update function, update works but before that i want a test case running. So i have created this test case.
/** #test */
public function updateUser()
{
$this->withExceptionHandling();
//creating a user
$user = factory(User::class)->create();
//passing updating values
$response = $this->put('users/update_profile', [
'name' => 'name123',
'phoneno' => 9842345562,
'address' => 'newwwww'
]);
$this->assertEquals('name123', User::first()->name);
$this->assertEquals(9842345562, User::first()->phoneno);
$this->assertEquals('newwwww', User::first()->address);
}
//update function
public function update(UpdateProfileRequest $request)
{
$user = auth()->user();
$user->update([
'name' => $request->name,
'phoneno' => $request->phoneno,
'address' => $request->address
]);
session()->flash('success', 'User Proifile Updated');
return redirect(route('users.view-profile'));
}
Failed asserting that two strings are equal.
--- Expected
+++ Actual
## ##
-'name123'
+'Tad Predovic'
Only getting this error.
You should not rely on User::first() as your first record may not the one you just created. Instead refresh the model you already have to get the updated values from the DB after your new values are set. You can use $user-refresh() before your assertions
/** #test */
public function updateUser() {
$this->withExceptionHandling();
//creating a user
$user = factory(User::class)->create();
//signing in as the new user
$this->actingAs($user);
//passing updating values
$response = $this->put('users/update_profile', [
'name' => 'name123',
'phoneno' => 9842345562,
'address' => 'newwwww'
]);
//Get new values
$user->refresh();
$this->assertEquals('name123', $user->name);
$this->assertEquals(9842345562, $user->phoneno);
$this->assertEquals('newwwww', $user->address);
}

Passing return data to another function in same controller laravel

Try to connect to external API.
In first function, I already received token with authentication.
To send POST request, I need to put xtoken that I received from first function as second function.
I don't know how to send value to second function (registerUser)
Route::get('/connect', 'Guzzlecontroller#registerUser')->name('registeruser');
this is my route file
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Client;
use App\Http\Controllers\Auth\RegisterController;
class GuzzleController extends Controller
{
public function Gettoken()
{
$client = new \GuzzleHttp\Client();
$request = $client->get(
'http://api01.oriental-game.com:8085/token',
[
'headers' => [
'X-Operator' => 'mog189b',
'X-key' => 'sQxAVNaEMe0TCHhU',
]
]
);
$response = $request->getBody();
$tokenReturn = json_decode($response, true);
$xtoken = array("x-token:" . $tokenReturn['data']['token'],);
$this->registerUser($xtoken);
}
public function registerUser($xtoken)
{
$client = new \GuzzleHttp\Client();
$url = "http://api01.oriental-game.com:8085/register";
$request = $client->post($url, [
'headers' => $xtoken,
'body' => [
'username' => 'test1',
'country' => 'Korea',
'fullname' => 'test user1',
'language' => 'kr',
'email' => 'testuser1#test.com',
]
]);
$response = $request->send();
dd($response);
}
}
Too few arguments to function App\Http\Controllers\GuzzleController::registerUser(), 0 passed and exactly 1 expected
this is error I am getting.
please help me to how to send $xtoken value to registerUser function
The problem is Laravel is calling registerUser directly instead of going through getToken. So the token is never retrieved and passed to the register action.
Instead of calling registerUser() from Gettoken(). Have Gettoken() return the token and call it from registerUser()
public function Gettoken()
{
...
return $xtoken;
}
public function registerUser()
{
$xtoken = $this->Gettoken();
...
}

Better way for testing validation errors

I'm testing a form where user must introduce some text between let's say 100 and 500 characters.
I use to emulate the user input:
$this->actingAs($user)
->visit('myweb/create')
->type($this->faker->text(1000),'description')
->press('Save')
->see('greater than');
Here I'm looking for the greater than piece of text in the response... It depends on the translation specified for that validation error.
How could do the same test without having to depend on the text of the validation error and do it depending only on the error itself?
Controller:
public function store(Request $request)
{
$success = doStuff($request);
if ($success){
Flash::success('Created');
} else {
Flash::error('Fail');
}
return Redirect::back():
}
dd(Session::all()):
`array:3 [
"_token" => "ONoTlU2w7Ii2Npbr27dH5WSXolw6qpQncavQn72e"
"_sf2_meta" => array:3 [
"u" => 1453141086
"c" => 1453141086
"l" => "0"
]
"flash" => array:2 [
"old" => []
"new" => []
]
]
you can do it like so -
$this->assertSessionHas('flash_notification.level', 'danger'); if you are looking for a particular error or success key.
or use
$this->assertSessionHasErrors();
I think there is more clear way to get an exact error message from session.
/** #var ViewErrorBag $errors */
$errors = request()->session()->get('errors');
/** #var array $messages */
$messages = $errors->getBag('default')->getMessages();
$emailErrorMessage = array_shift($messages['email']);
$this->assertEquals('Already in use', $emailErrorMessage);
Pre-requirements: code was tested on Laravel Framework 5.5.14
get the MessageBag object from from session erros and get all the validation error names using $errors->get('name')
$errors = session('errors');
$this->assertSessionHasErrors();
$this->assertEquals($errors->get('name')[0],"The title field is required.");
This works for Laravel 5 +
Your test doesn't have a post call. Here is an example using Jeffery Way's flash package
Controller:
public function store(Request $request, Post $post)
{
$post->fill($request->all());
$post->user_id = $request->user()->id;
$created = false;
try {
$created = $post->save();
} catch (ValidationException $e) {
flash()->error($e->getErrors()->all());
}
if ($created) {
flash()->success('New post has been created.');
}
return back();
}
Test:
public function testStoreSuccess()
{
$data = [
'title' => 'A dog is fit',
'status' => 'active',
'excerpt' => 'Farm dog',
'content' => 'blah blah blah',
];
$this->call('POST', 'post', $data);
$this->assertTrue(Post::where($data)->exists());
$this->assertResponseStatus(302);
$this->assertSessionHas('flash_notification.level', 'success');
$this->assertSessionHas('flash_notification.message', 'New post has been created.');
}
try to split your tests into units, say if you testing a controller function
you may catch valication exception, like so:
} catch (ValidationException $ex) {
if it was generated manually, this is how it should be generated:
throw ValidationException::withMessages([
'abc' => ['my message'],
])->status(400);
you can assert it liks so
$this->assertSame('my message', $ex->errors()['abc'][0]);
if you cannot catch it, but prefer testing routs like so:
$response = $this->json('POST', route('user-post'), [
'name' => $faker->name,
'email' => $faker->email,
]);
then you use $response to assert that the validation has happened, like so
$this->assertSame($response->errors->{'name'}[0], 'The name field is required.');
PS
in the example I used
$faker = \Faker\Factory::create();
ValidationException is used liks this
use Illuminate\Validation\ValidationException;
just remind you that you don't have to generate exceptions manually, use validate method for common cases:
$request->validate(['name' => [
'required',
],
]);
my current laravel version is 5.7

Resources