vincent15000's avatar

Laravel sanctum testing

Hello,

I have this logout method.

public function logout(Request $request)
{
    $request->user()->currentAccessToken()->delete();

    return response()->json(null, 200);
}

And I test it like this.

public function user_can_logout(): void
{
    $response = $this->postJson('/api/v1/auth/login', [
        'email' => '[email protected]',
        'password' => 'musique',
    ]);

    $response->assertValid()->assertOk();

    $this->assertDatabaseHas('personal_access_tokens', [
        'tokenable_id' => $this->user->id,
    ]);

    Sanctum::actingAs($this->user);

    $response = $this->postJson('/api/v1/auth/logout');

    $response->assertValid()->assertOk();

    $this->assertDatabaseMissing('personal_access_tokens', [
        'tokenable_id' => $this->user->id,
    ]);
}

I get an error saying that the tokenable_id is still in the database, whereas it should have been deleted.

Out of any test, when I login and then logout, it works the token is really deleted from the database, so the logout function works fine. But the test seems to not work properly given that it fails whereas it should success.

The test passes if I replace $request->user()->currentAccessToken()->delete(); by $request->user()->tokens()->delete();.

Can you help me understand why ?

Thanks for your help.

V

0 likes
5 replies
s4muel's avatar
s4muel
Best Answer
Level 50

on a first sight, it seems that the actingAs sanctum method just mocks the token. https://github.com/laravel/sanctum/blob/4.x/src/Sanctum.php#L50

when you use request->user()->tokens()->delete(), all tokens for that user are deleted, so it passes. but i assume the problem lies when trying to get currentAccessToken() in the logout method, the users token from database is imho not really used (during testing only ofc, because of the mock).

i would try to post the token during the logout... but no promises

$response = $this->withToken($token)->postJson('/api/v1/auth/logout');
1 like
vincent15000's avatar

@s4muel What is sure is that a token is really created when executing (Sanctum::actingAs($this->user);``` because the first assertion passes.

$this->assertDatabaseHas('personal_access_tokens', [
    'tokenable_id' => $this->user->id,
]);

So I'm not sure that Sanctum just mocks the token when testing.

I have tested with your suggestion and the test is still failing.

1 like
s4muel's avatar

@vincent15000 but this assert is before the actingAs()

$this->assertDatabaseHas('personal_access_tokens', [
    'tokenable_id' => $this->user->id,
]);

so the token is created by the login request, not the actingAs() so i still think it mocks it. when trying my suggestion, how did you get the $token?, you need to use the plaintext version of it. if you cant get that, i would create it manually first before the logout (omiting the login at all, since the test checks the logout)

1 like
vincent15000's avatar

@s4muel Oh yes you're right.

When trying your suggestion, I got the token like this.

$user->currentAccessToken()

But sure it's not the plain text token.

Then I have tried this (without Sanctum::actingAs()) and the test passes.

public function user_can_logout(): void
{
    $response = $this->postJson('/api/v1/auth/login', [
        'email' => '[email protected]',
        'password' => 'musique',
    ]);

    $response->assertValid()->assertOk();

    $token = $response->json()['token'];

    $this->assertDatabaseHas('personal_access_tokens', [
        'tokenable_id' => $this->user->id,
    ]);

    $response = $this->withToken($token)->postJson('/api/v1/auth/logout');

    $response->assertValid()->assertOk();

    $this->assertDatabaseMissing('personal_access_tokens', [
        'tokenable_id' => $this->user->id,
    ]);
}

Thank you for your help ;).

2 likes
LaryAI's avatar
Level 58

The issue you're encountering is likely due to the fact that the Sanctum::actingAs($this->user) method does not create a real token in the database. Instead, it simulates an authenticated user for the purpose of testing. This means that when you call $request->user()->currentAccessToken()->delete(), there is no actual token to delete in the database.

When you replace $request->user()->currentAccessToken()->delete() with $request->user()->tokens()->delete(), it works because it deletes all tokens associated with the user, not just the current one. This is why your test passes in that case.

To properly test the logout functionality, you need to create a real token for the user in the database. Here is how you can modify your test to achieve that:

  1. Create a real token for the user.
  2. Use that token to authenticate the logout request.

Here is the updated test:

public function user_can_logout(): void
{
    // Create a user
    $user = User::factory()->create([
        'email' => '[email protected]',
        'password' => bcrypt('musique'),
    ]);

    // Create a real token for the user
    $token = $user->createToken('TestToken')->plainTextToken;

    // Assert that the token exists in the database
    $this->assertDatabaseHas('personal_access_tokens', [
        'tokenable_id' => $user->id,
    ]);

    // Use the token to authenticate the logout request
    $response = $this->withHeaders([
        'Authorization' => 'Bearer ' . $token,
    ])->postJson('/api/v1/auth/logout');

    $response->assertOk();

    // Assert that the token is deleted from the database
    $this->assertDatabaseMissing('personal_access_tokens', [
        'tokenable_id' => $user->id,
    ]);
}

In this updated test:

  1. We create a user using a factory.
  2. We create a real token for the user using $user->createToken('TestToken')->plainTextToken.
  3. We assert that the token exists in the database.
  4. We use the token to authenticate the logout request by setting the Authorization header.
  5. We assert that the logout request is successful.
  6. We assert that the token is deleted from the database.

This should resolve the issue and make your test pass.

Please or to participate in this conversation.