bwrigley's avatar

Validation rule failing to load model during specific test scenario

Hi There,

I'm writing some tests around a method called MessageController::store()

The input is validated using a custom rule ConversationPausedCheck to see if the Message I'm creating will be a associated to a Conversation model which currently has a 'paused' status.

Inside my store() method I also use MessagePolicy to validate that the current user is allowed to add messages to this conversation.

My 'positive test' works fine, validation passes and the message is created.

The problem comes when I try to test that my MessagePolicy is working correctly by running exactly the same test with a user who is not authorised to add messages to the current conversation. This test doesn't get as far as the policy challenge but fails in ConversationPausedCheck rule which is making no sense to me.

my store() method:


    public function store(MessageStoreRequest $request ): RedirectResponse
    {
        $currentUser = Auth::User();

        //get the validated data from the form request
        $validated = $request->validated();

        $message = new Message($validated);
        $message->user_id = $currentUser->id;

        $this->authorize('store',$message);

        $message->save();

        ///
    }

MessageStoreRequest:

    public function rules(): array
    {
        return [
            'message' => 'required',
            'conversation_id' => ['required','exists:conversations,id', new ConversationPausedCheck],
        ];
    }

ConversationPausedCheck:

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {

        $conversation = Conversation::find($value);

        if ($conversation->blocked_by !== null){
            $fail('This :attribute is currently paused from messaging');
        }
    }

My test:

   public function test_store_method_with_unauthorised_user()
    {
        // Arrange
        $user = User::factory()->create();
        $conversation = $this->setUpTestConversation($user);
        $unauthUser = User::factory()->create();

        Notification::fake();

        // ASSERT
        $this->assertDatabaseCount('messages',30);

        // Act

        $this->actingAs($unauthUser);
        $response = $this
                ->from(route('feed'))
                ->followingRedirects()
                ->post(route('message.store'),['conversation_id' => $conversation->id, 'message' => 'This is a test message']);


        //ASSERT
        $response
            ->assertStatus(403);

        $this->assertDatabaseCount('messages',30);

        Notification::assertNothingSent();

    }

This test fails in ConversationPausedCheck because $conversation is null even though $value=1 and there is definitely an entry in conversations table with ID 1.

If I change the test to $this->actingAs($user) then validation passes fine.

I have no other middleware, gates, policies and I am completely baffled!

Sorry for the very long-winded question, but can anyone help me understand?

0 likes
7 replies
tykus's avatar
tykus
Best Answer
Level 104

Is there a default global scope on the Conversations model that uses the authenticated user somehow?

It is impossible to know for sure without seeing the query/ies that were attempted?

bwrigley's avatar

@tykus thanks so much for replying.

No, no global scope in place.

Incidentally if I add this debug:

   public function validate(string $attribute, mixed $value, Closure $fail): void
    {

        $conversation = Conversation::find($value);

        dump(DB::table('conversations')
        ->selectRaw('*')
        ->get());

        dump($value);

        dd($conversation);

        if ($conversation->currentParticipant && $conversation->blocked_by !== null){
            $fail('This :attribute is currently paused from messaging');
        }
    }

I get :

Illuminate\Support\Collection\^ {#5232 // app/Rules/ConversationPausedCheck.php:24
  #items: array:1 [
    0 => {#5204
      +"id": 1
      +"created_at": "2024-02-06 13:25:18"
      +"updated_at": "2024-02-06 13:25:18"
    }
  ]
  #escapeWhenCastingToString: false
}
1 // app/Rules/ConversationPausedCheck.php:26
null // app/Rules/ConversationPausedCheck.php:28
bwrigley's avatar

@tykus forgive my ignorance, but how can I check the exact queries that it runs?

tykus's avatar

@bwrigley the quickest way to isolate the queries:

// Act
\DB::listen(fn($query) => dump([$query->sql, $query->bindings]));	
$this->actingAs($unauthUser);
// ...

Did you override the find method on the Conversation model?

bwrigley's avatar

@tykus ah you nailed it earlier! There was a (badly written) global scope for something completely unrelated that I had totally forgotten about. So sorry for wasting your time and thank you so much for your help, I wouldn't have spotted it.

1 like
Mareco's avatar

Hi bwrigley,

is really $conversation null? Because if it is, there could be a problem with soft deleting a record, if your model are using it. In your validation rule, you are validating a required value, existing in conversations table(but this rule dont filter out soft deleted records!) so this rule will not fail too and then you go to your custom validation rule and trying to find $conversation, but if u have soft deleted record, u get null from this statement.

Please or to participate in this conversation.