trevorpan's avatar

findOrFail($jobId); returns 404 but test creates $job

error: Symfony\Component\HttpKernel\Exception\NotFoundHttpException: POST http://bidbird.test/jobs//bidreserve

As you can see above, the $job->id is not found.

class BidReservesController extends Controller
{
    private $paymentGateway;

    public function __construct(PaymentGateway $paymentGateway)
    {
        $this->middleware('auth');
        $this->paymentGateway = $paymentGateway;
    }

....

  /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store($jobId)
    {
        // dd($job);

        $job = Job::incomplete()->findOrFail($jobId);

        dd($job);

        // dd($this->request);
        $this->validate(request(), [
            'bidreserve' => 'required|integer' 
        ]);

        $bidreserve = BidReserve::create([
            'bidreserve' => 500,
            'job_id' => request($job->id),
            'user_id' => auth()->user()->id
            ]);
        
        // Charging the customer
        $this->paymentGateway->charge(request('bidreserve'), request('payment_token'));

        // dd($job);

        return response()->json([], 201);

        return view('jobs.show', compact('bidreserve', 'job'));
    }
<?php

namespace Tests\Feature;

use App\BidReserve;
use App\Job;
use App\User;
use App\Billing\FakePaymentGateway;
use App\Billing\PaymentGateway;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;


class PayBidReservesTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();
        $this->paymentGateway = new FakePaymentGateway;
        $this->app->instance(PaymentGateway::class, $this->paymentGateway);
        Event::fake();
    }

    private function payBidReserve($job, $params)
    {
        $this->response = $this->json('POST', "/jobs/{$job->id}/bidreserve", $params);
    }


    /**
     * A basic feature test example.
     *
     * @return void
     */

    /** @test */
    public function a_bidder_can_pay_reserve_fee()
    {
        $this->withoutExceptionHandling();
        Event::fake();

        $job = factory(Job::class)->make();
        $user = factory(User::class)->make();
        $bidreserve = factory(BidReserve::class)->create();

        // dd($job);

        // dd($this->paymentGateway);
        // $this->actingAs($user);

        // dd($user);

        $this->actingAs($user)->payBidReserve($job, [
            'bidreserve' => 500,
            'user_id' => $user->id,
            'job_id' => $job->id,
            'payment_token' => $this->paymentGateway->getValidTestToken()
        ]);

     
        $this->response->assertStatus(201);
    }
}
<?php

/* @var $factory \Illuminate\Database\Eloquent\Factory */

use App\BidReserve;
use App\Job;
use App\User;
use Faker\Generator as Faker;

$factory->define(BidReserve::class, function (Faker $faker) {
    return [
        'user_id' => function() {
                return factory(User::class)->create()->id;
            },
        'job_id' => function() {
            return factory(Job::class)->create()->id;
        },
        'bidreserve' => 500,
        // 'job_id' => $this->faker->numberBetween(1, 10),
        // 'user_id' => $this->faker->numberBetween(1, 100),
    
    ];
});

Ok, when I dd($job); at the test it gives:

#attributes: array:11 [
    "jobtitle" => "Sequi veniam quia explicabo omnis quidem."
    "body" => "Qui veritatis quia sunt id distinctio soluta fugit. Temporibus ut aliquid veniam alias. Debitis optio maxime non a quos. Velit voluptatem dicta placeat quia possimus. Ex delectus debitis voluptas aperiam."
    "deadline" => "2019-09-10 01:16:36"
    "user_id" => 1
    "projectaddress" => "354 Rolfson Skyway Suite 563"
    "city" => "New Heavenchester"
    "state" => "Indiana"
    "zipcode" => "78306"
    "biddertype" => "consectetur"
    "job" => "eveniet"
    "subjob" => "cumque"
  ]

as expected.

when dd($bidreserve); it gives:

#attributes: array:6 [
    "user_id" => 2
    "job_id" => 1
    "bidreserve" => 500
    "updated_at" => "2019-08-13 01:17:32"
    "created_at" => "2019-08-13 01:17:32"
    "id" => 1
  ]
  #original: array:6 [
    "user_id" => 2
    "job_id" => 1
    "bidreserve" => 500
    "updated_at" => "2019-08-13 01:17:32"
    "created_at" => "2019-08-13 01:17:32"
    "id" => 1
  ]
//routes.web
Route::post('/jobs/{job}/bidreserve', 'BidReservesController@store')->name('bidreserve');

I have a few questions: if you create job_id and user_id in the BidReserveFactory is it proper to use make() in the test vs. create()?

Is that why there are two users?

But still perplexed why the $job->id is not available when it hits the controller.... thank you ~

0 likes
6 replies
takdw's avatar

$job->id is coming back as null because you used the make() factory method instead of create(). You can clearly see that $job has no id field in the dump you shared. Your BidReserve factory is already creating a User and Job when it builds a BidReserve. Remove the first two factory()->make(). Then pass $bidreseve->job (assuming you have setup the relation) to payBidReserve. Or if you haven't setup relations, just pass on $bireserve->job_id and make sure to edit the function to work with an integer.

trevorpan's avatar

Hi, thank you @takdw , I've commented out factory(User/Job::class)->make() which causes:

Time: 1.04 seconds, Memory: 28.00 MB

There was 1 error:

1) Tests\Feature\PayBidReservesTest::a_bidder_can_pay_reserve_fee
ErrorException: Undefined variable: user

/Users/trevorpan/code/bidbird/tests/Feature/PayBidReservesTest.php:65

and am unclear how you pass the job/user property from $bidreserve to payBidReserve method. (still learning this stuff) not sure why the user is not available from the bidreserve factory; this is why the factory(User/Job::class)->make() lines were in there...

    //belongsTo
    public function user() //$job->user
    {
        return $this->belongsTo(User::class);
    }


    public function job()
    {
        return $this->belongsTo(Job::class);
    }

The model does have the proper relations, from what I know. Although, I'm unclear if you need to use App\User; etc in models.

takdw's avatar
takdw
Best Answer
Level 11

I want to point out a couple of things here. From what I understand, a BidReserve is an object created when a when a User places a bid on a Job. I noticed your tests are performing the test in a wrong way. Here is how you should go about testing the process.

First of all, you want to create a Job and a User. This is easy enough with the following lines:

$user = factory (User::class)->create();
$job = factory (Job::class)->create();

Then you'll need to hit some endpoint to create a BidReserve. In your case that'll look something like this:

$response = $this->actingAs($user)->json('POST', "jobs/{$job->id}/bidreserve", [
   "bid_reserve" => 500,
   "payment_token" => $this->paymentGateway->getValidTestToken()
]);

You don't need to pass in a user_id because it's better to fetch that from the Auth facade or auth() helper. You also don't need to pass the job_id because you can get that from the URL. Then you can have assertions such as:

$response->assertStatus(201);
$this->assertCount(1, $job->bids); // assuming there exists such a relationship

Then in your BidReservesController.php make the following changes:

public function store($jobId)
{
    $job = Job::incomplete()->findOrFail($jobId);

    $this->validate(request(), [
        'bid_reserve' => 'required | integer | min:1'
    ]);

    $bidReserve = BidReserve::create([
        'bid_reserve' => request('bid_reserve'),
        'user_id' => auth()->user()->id,
        'job_id' => $jobId
    ]);

    $this->paymentGateway->charge(request('bid_reserve'), request('payment_token'));

    return response()->json($bidReserve, 201);
}

Things to note here. There is probably a better way of creating BidReseves. Like using a relationship on the User or Job. Like auth()->user()->bids()->create(...). You should also account for situations where the payment might fail. Maybe create the bid only after a successful charge. And you had two return statements in your store method which is kinda wrong because the second return is never reached.

trevorpan's avatar

ok, you have some great points! I have not started the exception portion just yet in the controller. Trying to go TDD all the way.

here's the revised BidReservesController

    public function store($jobId)
    {
        $job = Job::incomplete()->findOrFail($jobId);

        $this->validate(request(), [
            'bidreserve' => 'required|integer|min:1' 
        ]);

    // Previous code
        // $bidreserve = BidReserve::create([
        //     'bidreserve' => 500,
        //     'job_id' => request($job->id),
        //     'user_id' => auth()->user()->id
        //     ]);

        $bidReserve = auth()->user()->bidReserves()->create([
            'bidreserve' => 500,
            'job_id' => $jobId
        ]);
        
        // Charging the customer
        $this->paymentGateway->charge(request('bidreserve'), request('payment_token'));

  
        return response()->json($bidReserve, 201);
    }

I believe that's what you meant about using the auth()->user() (above).

There is an existing bidReserves() relation in the job and user models

 public function bidReserves()
    {
        return $this->hasMany(BidReserve::class);
    }

In a sense the bidreserves table is like an orders table, not sure if that's the best method/approach. Kind of the plan is you place a bidreserve, this gives you the authority to make a bid later in the auction - the goal is transparency so the other bidders see "hey, there's 6 bidders - should bid this one?"

If the bidder wins they get a final charge, if the bidder loses they only pay the bidreserve. I haven't decided on the best place to send an order email.

Gave the test a reboot:

class PayBidReservesTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();
        $this->paymentGateway = new FakePaymentGateway;
        $this->app->instance(PaymentGateway::class, $this->paymentGateway);
        Event::fake();
    }

    private function payBidReserve($job, $params)
    {
        $this->response = $this->json('POST', "/jobs/{$job->id}/bidreserve", $params);
    // I thought keeping the params in the test as opposed to this method made it more reusable, is that a valid assumption?
    }

/** @test */
    public function a_bidder_can_pay_reserve_fee()
    {
        $this->withoutExceptionHandling();

        $user = factory(User::class)->create();
        $job = factory(Job::class)->create();

        $this->actingAs($user); //was not able to get that to work on the method, it said $user was an undefined variable

        $this->payBidReserve($job, [
            'bidreserve' => 500,
            'payment_token' => $this->paymentGateway->getValidTestToken()
        ]);

        $this->response->assertStatus(201);

        // $this->assertCount(1, $job->bidreserve);

       $this->assertEquals(500, $this->paymentGateway->totalCharges());
    }
}

the assertCount() line yielded:

There was 1 error:

1) Tests\Feature\PayBidReservesTest::a_bidder_can_pay_reserve_fee
PHPUnit\Framework\Exception: Argument #2 (No Value) of PHPUnit\Framework\Assert::assertCount() must be a countable or iterable

After commenting out the above line the suite passed - sweet!

Trevors-Mac-mini:bidbird trevorpan$ ./vendor/bin/phpunit
PHPUnit 7.5.13 by Sebastian Bergmann and contributors.

...........                                                       11 / 11 (100%)

Time: 872 ms, Memory: 28.00 MB

OK (11 tests, 19 assertions)
takdw's avatar

For such short parameters, refactoring them to a function is not really beneficial in my opinion. I only extract them into a function when they are more than 5 fields and also I end up using them in a lot of tests.

The assertCount() assertion failed probably because the relationship is defined as bidReserves but you're trying to access it as bidreserve (capital 'R' and 's' at the end) and that is returning null.

One last thing I want to point out is that, you are hard coding the bidreserve value in the store method of your controller.

$bidReserve = auth()->user()->bidReserves()->create([
    'bidreserve' => 500, // this should be 'bidreserve' => request('bidreserve')
    'job_id' => $jobId
]);
trevorpan's avatar

Hi,

You are correct! Missed the spelling. Also, I misstated, you are right on the functions being functions not methods. Still getting the hang of everything.

With the hardcoded $5.00 charge, I think it'll be like that until the site gets popular and needs other options (if it gets popular).

Thank you for the tips`

Now on to the Stripe stuff & all that jazz.

Trevors-Mac-mini:bidbird trevorpan$ ./vendor/bin/phpunit
PHPUnit 7.5.13 by Sebastian Bergmann and contributors.

...........                                                       11 / 11 (100%)

Time: 913 ms, Memory: 28.00 MB

OK (11 tests, 20 assertions)

Please or to participate in this conversation.