73.邮箱认证(三)

未匹配的标注

本节说明

  • 对应视频教程第 73 小节:Users Must Confirm Their Email Address: #3 - Cleanup

本节内容

上一节我们利用事件机制实现了注册用户时发送认证邮件的功能,但是只为了发送邮件就定义了事件,似乎有点杀鸡用牛刀。所以本节我们通过重写registered()方法很轻松地实现相同的功能:
forum\app\Http\Controllers\Auth\RegisterController.php

<?php

namespace App\Http\Controllers\Auth;

use App\Mail\PleaseConfirmYourEmail;
use App\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Validator;
use Illuminate\Foundation\Auth\RegistersUsers;

class RegisterController extends Controller
{

    use RegistersUsers;

    protected $redirectTo = '/home';

    public function __construct()
    {
        $this->middleware('guest');
    }

    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:users',
            'password' => 'required|string|min:6|confirmed',
        ]);
    }

    protected function create(array $data)
    {
        return User::forceCreate([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => bcrypt($data['password']),
            'confirmation_token' => str_random(25)
        ]);
    }

    protected function registered(Request $request, $user)
    {
        Mail::to($user)->send(new PleaseConfirmYourEmail($user));

        return redirect($this->redirectPath());
    }
}

然后我们可以去掉事件的注册:
forum\app\Providers\EventServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
        'App\Events\ThreadReceivedNewReply' => [
            'App\Listeners\NotifyMentionedUsers',
            'App\Listeners\NotifySubscribers',
        ]
    ];
    .
    .
}

接着修改我们的测试:
tests\Feature\RegisrationTest.php

.
.
/** @test */
public function a_confirmation_email_is_sent_upon_registration()
{
    Mail::fake();

    // 用路由命名代替 url
    $this->post(route('register'),[
        'name' => 'NoNo1',
        'email' => 'NoNo1@example.com',
        'password' => '123456',
        'password_confirmation' => '123456'
    ]);

    Mail::assertSent(PleaseConfirmYourEmail::class);
}
.
.

运行测试:
file
接下来我们来进行些重构,用路由命名代替url
forum\tests\Feature\RegistrationTest.php

<?php

namespace Tests\Feature;

use App\Mail\PleaseConfirmYourEmail;
use App\User;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;

class RegistrationTest extends TestCase
{
    use DatabaseMigrations;

    /** @test */
    public function a_confirmation_email_is_sent_upon_registration()
    {
        Mail::fake();

        // 用路由命名代替 url
        $this->post(route('register'),[
            'name' => 'NoNo1',
            'email' => 'NoNo1@example.com',
            'password' => '123456',
            'password_confirmation' => '123456'
        ]);

        Mail::assertSent(PleaseConfirmYourEmail::class);
    }

    /** @test */
    public function user_can_fully_confirm_their_email_addresses()
    {
        // 用路由命名代替 url
        $this->post(route('register'),[
            'name' => 'NoNo1',
            'email' => 'NoNo1@example.com',
            'password' => '123456',
            'password_confirmation' => '123456'
        ]);

        $user = User::whereName('NoNo1')->first();

        $this->assertFalse($user->confirmed);
        $this->assertNotNull($user->confirmation_token);

        // 用路由命名代替 url
        $this->get(route('register.confirm',['token' => $user->confirmation_token]))
            ->assertRedirect(route('threads'));

        $this->assertTrue($user->fresh()->confirmed);
    }
}

我们来为路由加上命名:
forum\routes\web.php

.
.
Route::get('threads','ThreadsController@index')->name('threads');
.
.
Route::get('/register/confirm','Api\RegisterConfirmationController@index')->name('register.confirm');
.
.

我们运行测试:
file
现在我们来运行全部测试:
file
以上的报错原因是因为我们默认一个新用户是未认证的,而我们对未认证用户做了限制发布话题的设置。对于我们的测试而言,我们大多数的功能测试的前提都是在用户已认证的情况下的,所以我们来对模型工厂文件进行一点修改:
forum\database\factories\ModelFactory.php

<?php

$factory->define(App\User::class, function (Faker\Generator $faker) {
    static $password;

    return [
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'password' => $password ?: $password = bcrypt('123456'),
        'remember_token' => str_random(10),
        'confirmed' => true,
    ];
});

$factory->state(App\User::class,'unconfirmed',function () {
   return [
      'confirmed' => false
   ];
});
.
.

我们默认新用户是已认证状态,当我们需要测试未认证的相关逻辑时,我们调用unconfirmed即可。接下来我们对CreateThreadsTest.php进行清理,因改动处较多,说明放在了代码的注释中:

<?php

namespace Tests\Feature;

use App\Activity;
use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;
class CreateThreadsTest extends TestCase
{
    use DatabaseMigrations;

    /** @test */
    public function guests_may_not_create_threads()
    {
        $this->withExceptionHandling();

        $this->get('/threads/create')
            ->assertRedirect(route('login')); // 应用路由命名

        $this->post(route('threads')) // 应用路由命名
            ->assertRedirect(route('login')); // 应用路由命名
    }

    // 修改测试命名,更加辨识度
    /** @test */
    public function new_users_must_first_confirm_their_email_address_before_creating_threads()
    {
        // 调用 unconfirmed,生成未认证用户
        $user = factory('App\User')->states('unconfirmed')->create();

        $this->signIn($user);

        $thread = make('App\Thread');

        $this->post(route('threads'),$thread->toArray())
            ->assertRedirect('/threads')
            ->assertSessionHas('flash','You must first confirm your email address.');
    }

    // 修改测试命名,更加辨识度
    /** @test */
    public function a_user_can_create_new_forum_threads()
    {
        $this->signIn();

        $thread = make('App\Thread');
        $response = $this->post(route('threads'),$thread->toArray());// 应用路由命名

        $this->get($response->headers->get('Location'))
            ->assertSee($thread->title)
            ->assertSee($thread->body);
    }

    /** @test */
    public function a_thread_requires_a_title()
    {
        $this->publishThread(['title' => null])
            ->assertSessionHasErrors('title');
    }

    /** @test */
    public function a_thread_requires_a_body()
    {
        $this->publishThread(['body' => null])
            ->assertSessionHasErrors('body');
    }

    /** @test */
    public function a_thread_requires_a_valid_channel()
    {
        factory('App\Channel',2)->create(); // 新建两个 Channel,id 分别为 1 跟 2

        $this->publishThread(['channel_id' => null])
            ->assertSessionHasErrors('channel_id');

        $this->publishThread(['channel_id' => 999])  // channle_id 为 999,是一个不存在的 Channel
            ->assertSessionHasErrors('channel_id');
    }

    /** @test */
    public function unauthorized_users_may_not_delete_threads()
    {
        $this->withExceptionHandling();

        $thread = create('App\Thread');

        $this->delete($thread->path())->assertRedirect(route('login')); // 应用路由命名

        $this->signIn();
        $this->delete($thread->path())->assertStatus(403);
    }

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

        $thread = create('App\Thread',['user_id' => auth()->id()]);
        $reply = create('App\Reply',['thread_id' => $thread->id]);

        $response =  $this->json('DELETE',$thread->path());

        $response->assertStatus(204);

        $this->assertDatabaseMissing('threads',['id' => $thread->id]);
        $this->assertDatabaseMissing('replies',['id' => $reply->id]);

        $this->assertEquals(0,Activity::count());
    }

    public function publishThread($overrides = [])
    {
        $this->withExceptionHandling()->signIn();

        $thread = make('App\Thread',$overrides);

        return $this->post(route('threads'),$thread->toArray()); // 应用路由命名
    }
}

运行测试:
file
接下来我们新增一个测试:
forum\tests\Feature\RegistrationTest.php

    .
    .
    /** @test */
    public function user_can_fully_confirm_their_email_addresses()
    {
        // 应用 【邮件模拟】
        Mail::fake();

        $this->post(route('register'),[
            'name' => 'NoNo1',
            'email' => 'NoNo1@example.com',
            'password' => '123456',
            'password_confirmation' => '123456'
        ]);

        $user = User::whereName('NoNo1')->first();

        $this->assertFalse($user->confirmed);
        $this->assertNotNull($user->confirmation_token);

        $this->get(route('register.confirm',['token' => $user->confirmation_token]))
            ->assertRedirect(route('threads'));

        $this->assertTrue($user->fresh()->confirmed);
    }

    /** @test */
    public function confirming_an_invalid_token()
    {
        // 测试无效 Token
        $this->get(route('register.confirm'),['token' => 'invalid'])
            ->assertRedirect(route('threads'))
            ->assertSessionHas('flash','Unknown token.');
    }
}

为了让测试通过,我们要修改控制器:
forum\app\Http\Controllers\Api\RegisterConfirmationController.php

    .
    .
    public function index()
    {
        try {
            User::where('confirmation_token',request('token'))
                ->firstOrFail()
                ->confirm();
        }catch (\Exception $e) {
            return redirect(route('threads'))
                ->with('flash','Unknown token.');
        }

        return redirect(route('threads'))
            ->with('flash','Your account is now confirmed! You may post to the forum.');
    }
    .
    .

现在我们运行测试:
file
运行全部测试:
file

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

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


暂无话题~