54.限制回复频率

未匹配的标注

本节说明

  • 对应视频教程第 54 小节:A User May Not Reply More Than Once Per Minute

本节内容

本节让我们限制用户的回复频率,防止程序恶意刷评论。首先我们按照惯例新建测试:
forum\tests\Feature\ParticipateInForumTest.php

    .
    .
    /** @test */
    public function users_may_only_reply_a_maximum_of_once_per_minute()
    {
        $this->signIn();

        $thread = create('App\Thread');
        $reply = make('App\Reply',[
            'body' => 'My simple reply.'
        ]);

        $this->post($thread->path() . '/replies',$reply->toArray())
            ->assertStatus(200);

        $this->post($thread->path() . '/replies',$reply->toArray())
            ->assertStatus(422);
    }
}

连续地发布两条回复,我们禁止这种操作,所以我们返回 422 状态。现在运行测试会报错,因为我们还没有建立相关逻辑。我们利用已经建立的授权策略,新建一个create()方法:
app\Policies\ReplyPolicy.php

    .
    .
    public function create(User $user)
    {
        return ! $user->lastReply->wasJustPublished();
    }
}

我们通过lastReply关联取出最新的回复,通过wasJustPublished()方法判断是否刚刚已经发表过回复。这是我们接下来要做的两件事情,但是在此之前,我们要为lastReply关联建立测试。我们新建一个单元测试文件:
forum\tests\Unit\UserTest.php

<?php

namespace Tests\Unit;

use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;

class UserTest extends TestCase
{
    use DatabaseMigrations;

    /** @test */
    public function a_user_can_fetch_their_most_recent_reply()
    {
        $user = create('App\User');

        $reply = create('App\Reply',['user_id' => $user->id]);

        $this->assertEquals($reply->id,$user->lastReply->id);
    }
}

建立lastReply关联:
forum\app\User.php

    .
    .
    public function threads()
    {
        return $this->hasMany(Thread::class)->latest();
    }

    public function lastReply()
    {
        return $this->hasOne(Reply::class)->latest();
    }
    .
    .

运行刚刚建立的测试:
file
我们接着新建wasJustPublished()方法:
forum\app\Reply.php

    use Carbon\Carbon;
    .
    .
    public function thread()
    {
        return $this->belongsTo(Thread::class);
    }

    public function wasJustPublished()
    {
        return $this->created_at->gt(Carbon::now()->subMinute());
    }
    .
    .

我们依然要建立测试:
forum\tests\Unit\ReplyTest.php

<?php

namespace Tests\Unit;

use Carbon\Carbon;
use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;

class ReplyTest extends TestCase
{

    use DatabaseMigrations;

    /** @test */
    function it_has_an_owner()
    {
        $reply = create('App\Reply');

        $this->assertInstanceOf('App\User',$reply->owner);
    }

    /** @test */
    public function it_knows_if_it_was_just_published()
    {
        $reply = create('App\Reply');

        $this->assertTrue($reply->wasJustPublished());

        $reply->created_at = Carbon::now()->subMonth();

        $this->assertFalse($reply->wasJustPublished());
    }
}

运行测试:
file
接下来我们来完善我们的授权策略。在前面的代码中,我们根据$user->lastReply->wasJustPublished()来判断授权是否通过,但是我们没有考虑到如果一个用户没法发表过回复,那么$user->lastReply的值将会是null,这就会产生报错。我们要进行完善:
forum\app\Policies\ReplyPolicy.php

    .
    .
    public function create(User $user)
    {
        if(! $lastReply = $user->lastReply) {
            return true;
        }

        return ! $lastReply->wasJustPublished();
    }
}

接着我们应用授权策略:
forum\app\Http\Controllers\RepliesController.php

    .
    .
     public function store($channelId,Thread $thread)
    {
        try{
            $this->authorize('create',new Reply);
            $this->validate(request(),['body' => 'required|spamfree']);

            $reply = $thread->addReply([
                'body' => request('body'),
                'user_id' => auth()->id(),
            ]);
        }catch (\Exception $e){
            return response(
                'Sorry,your reply could not be saved at this time.',422
            );
        }

        return $reply->load('owner');
    }
    .
    .

现在我们可以运行一开始建立的测试了:
file
发现仍然失败了。我们来梳理一下我们的测试:

/** @test */
public function users_may_only_reply_a_maximum_of_once_per_minute()
{
    $this->signIn();

    $thread = create('App\Thread');
    $reply = make('App\Reply',[
        'body' => 'My simple reply.'
    ]);

    $this->post($thread->path() . '/replies',$reply->toArray())
        ->assertStatus(200);

    $this->post($thread->path() . '/replies',$reply->toArray())
        ->assertStatus(422);
}

在测试中,我们模拟新建了一个话题,并连续添加了两条回复。我们现在已经在添加回复时应用了create授权策略:

public function create(User $user)
{
    if(! $lastReply = $user->lastReply) {
            return true;
        }

    return ! $lastReply->wasJustPublished();
}

看出问题的所在了吗?问题出在$user->lastReply上面。其实这个问题我们在前面已经遇见过,我们知道,当作为属性访问 Eloquent 关联时,关联数据是「懒加载」的。这意味着在你第一次访问该属性时,才会加载关联数据。所以我们需要进行以下修改:

$lastReply = $user->fresh()->lastReply;

现在我们再来运行测试:
file
现在我们去应用进行测试:
file
你会发现仍然可以成功添加回复。这是怎么回事呢?明明我们的测试都已经通过了。不要忘了我们曾经在第 24 节设置用户 NoNo1 为管理员,所以对于 NoNo1 而言,所有的授权都会通过。我们换一个其他的账号进行测试:
file
我们还需要修改一下消息提示:
forum\app\Http\Controllers\RepliesController.php

    .
    .
    public function store($channelId,Thread $thread)
    {
        if(Gate::denies('create',new Reply)) {
            return response(
                'You are posting too frequently.Please take a break.:)',422
            );
        }

        try{
            $this->validate(request(),['body' => 'required|spamfree']);

            $reply = $thread->addReply([
                'body' => request('body'),
                'user_id' => auth()->id(),
            ]);
        }catch (\Exception $e){
            return response(
                'Sorry,your reply could not be saved at this time.',422
            );
        }

        return $reply->load('owner');
    }
    .
    .

file
最后,运行全部测试:
file

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。