8

I have a Laravel 5.3 app that includes a users table. One of the fields on that table is api_token, which in the database definition is set to NOT NULL. If I use Postman and hit my API endpoint for creating a user and don't include the api_token field, I receive the following error: SQLSTATE[HY000]: General error: 1364 Field 'api_token' doesn't have a default value.

I haven't added code to the generate the token yet, so that's exactly what I'd expect. However, I have this PHPUnit test:

/** @test */
public function a_user_can_be_created()
{
    $this->user_data = [
        'first_name' => 'Jane',
        'last_name' => 'Smith',
        'email_address' => '[email protected]',
        'phone' => '1234567890',
        'username' => 'jsmith',
        'password' => 'testing'
    ];

    $this->post('/api/users', $this->user_data, array(
                'Accept' => 'application/json'
            ))
         ->assertResponseStatus(201)
         ->seeJsonEquals(['status' => 'success'])
         ->seeInDatabase('users', $this->user_data);
}

The problem is that the test passes. How can this test be successful when the database should be throwing an exception when the user is saved?

I've 'died and dumped' at various points and confirmed that the data is in fact being saved to the database when the automated test runs. I've also tried using the same DB for both Postman and the automated test (instead of a separate testing DB) - same result.

4
  • Are you sure your testing against the same DB you're serving from? Commented Nov 10, 2016 at 16:27
  • For this specific example, yes Commented Nov 10, 2016 at 17:58
  • What is api_token is saved as when you run this? Does it store null or some value? Commented Nov 13, 2016 at 11:08
  • The API token saves as an empty string Commented Nov 13, 2016 at 16:03

4 Answers 4

8
+25

I tried to build the minimal version of your API and create a similar test on the latest Laravel 5.3.22 version. I also used MariaDB 10.1.13 for the database. And it turns out the test failed as we expected, because of the empty api_token.

1. Users migration file

Here's my migration file for creating the users table:

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration
{
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->string('first_name');
            $table->string('last_name');
            $table->string('email_address')->unique();
            $table->string('phone');
            $table->string('username');
            $table->string('password');
            $table->string('api_token');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('users');
    }
}

2. The User model

Here's my app\User.php file looks like. Pretty minimal, I only update the $fillable property to match our users table structure:

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable;

    protected $fillable = [
        'first_name',
        'last_name',
        'email_address',
        'phone',
        'username',
        'password',
        'api_token',
    ];

    protected $hidden = [
        'password',
    ];
}

3. The API routes file

I create a new API route on routes/api.php file to create a new user. Super simple without any validation involved:

Route::post('users', function (Request $request) {
    $data = $request->only([
        'first_name',
        'last_name',
        'email_address',
        'phone',
        'username',
        'password',
        'api_token',
    ]);

    if (is_string($data['password'])) {
        $data['password'] = bcrypt($data['password']);
    }

    $user = App\User::create($data);

    return response()->json([
        'status' => 'success',
    ], 201);
});

4. Create user API test

And finally here's my test file for testing our create user API.

class ApiTest extends TestCase
{
    /** @test */
    public function a_user_can_be_created()
    {
        $user = [
            'first_name' => 'Jane',
            'last_name' => 'Smith',
            'email_address' => '[email protected]',
            'phone' => '1234567890',
            'username' => 'jsmith',
        ];

        $this->json('POST', 'api/users', $user)
            ->assertResponseStatus(201)
            ->seeJsonEquals(['status' => 'success'])
            ->seeInDatabase('users', array_except($user, ['password']));
    }
}

The only difference is I used array_except() to exclude the password column from database testing since we hashed the value before storing it in the database. But of course this does not relate to your issue.

When I run the test without the api_token being set, the test failed as expected. The API return a status code of 500, while we expected it to be 201.

The create user API failed!

Also as a reference, when I set the api_token column as NOT nullable, then force to create a new user with an empty api_token, the exception message I received is like this:

QueryException in Connection.php line 769:

SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'api_token' cannot be null (SQL: insert into users...

Hope this help you debug your issue!

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for the rundown. I should've specified that I'm using MySQL - I think that might account for the difference.
2

I assume you are using mysql. In mysql there is a problem with NOT NULL. It goes like this. Although the field is NOT NULL you can leave it empty in mysql and the query passes. NOT NULL in mysql means that you can't have NULL value in that field not that you can't leave it empty. I assume you create the user as this

User::create($request->all()) 

If you try to print the generated sql query

dd((new User($request->all())->toSql())

You will see that the generated query contains '' value for api_token field

I switched to posgres mainly because this problem.

If you want to solve this problem you have couple of options. One is to leave the api_token field nullable. The second one is to fill the model manually

3 Comments

Good catch - this might be related to the underlying issue (you're right that it works when I pass "api_token":"" in my json in Postman). What's odd, however, is that when I dump the SQL that is run when I run the automated test, I get this: "insert into users (first_name, last_name, email_address, phone, username, password, user_type_id, updated_at, created_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?)". So it doesn't look like PHPUnit is adding "api_token" as an empty string. Shouldn't that cause a MySQL error?
When api_token field is not added it works. If you api_token field is added as NULL then it doesn't work
Right, but what I'm saying is that for me, that's not true. When I pass the same data from my test (which does not include the api_token field), I get that error I pasted into my question
1

You should define a default value to your api_token column in users table or it should be set to nullable, your migration for users table would be like this:

Schema::create('users', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name');
    $table->string('api_token')->nullable();
    // Or you can set it to default value - 0
    // $table->string('api_token')->default('0');
    $table->timestamps();
});

Hope this helps!

1 Comment

Thanks for the suggestion. I think I'll end up setting it to nullable, but I still want to understand why the tests aren't behaving as I'd expect
1

I think the problem is with the Unit Test. Why don't you try with a Laravel test using Eloquent? Something like this:

public function a_user_can_be_created()
{
    $user = new User;

    $user->first_name = 'Jane';
    $user->last_name = 'Smith';
    $user->email_address = '[email protected]';
    $user->phone = '1234567890';
    $user->username = 'jsmith';
    $user->password = 'testing';

    $user->save();

    return $user;
}

https://laravel.com/docs/5.3/testing

2 Comments

I'll check this when I have a chance and see if I see similar behavior using your example. In my example though, I'm specifically trying to test an API, so I was following this convention: laravel.com/docs/5.3/application-testing#testing-json-apis
Your approach makes sense to me keeping in mind you are testing an API.. but if it's making a version of the user model in json, then that's your answer. The api_token is in there, buy empty

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.