Medium 清新閱讀版:連結
前兩天,我們探討了「網站文章」的情境題;今明兩天,就讓我們探討另一個情境題「會員註冊」吧!
這邊我們同樣假設網站是採前後端分離的設計,因此我們就專注在測試 API 的部分,不過會多一個「註冊驗證信」的部分要實作與做測試驗證。
使用者可填寫註冊資料後送出資料。
使用者可收到註冊驗證信,且該信件內含有專屬於該使用者的驗證連結。
使用者點選註冊驗證信中的驗證連結後,將驗證成功,其帳號驗證狀態將轉為已驗證。
這邊我們使用驗證時間來取代驗證狀態,並以有無驗證時間來判斷是否已驗證
依據以上的使用案例,我們可規畫出以下 API / 功能:
POST /registers
GET /users/{id}/validation?token={token}
接著就來實作 API 與註冊驗證信的邏輯吧!
database/migrations/2014_10_12_000000_create_users_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->string('verify_email_token', 128)->nullable();
            $table->rememberToken();
            $table->timestamps();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
};
app/Models/User.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
        'email_verified_at',
        'verify_email_token',
    ];
    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];
    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}
routes/web.php
<?php
use App\Http\Controllers\UserController;
use Illuminate\Support\Facades\Route;
Route::post('/register', [UserController::class, 'register'])
    ->name('register');
Route::get('/verify-user-email', [UserController::class, 'verifyUserEmail'])
    ->name('verify-user-email');
app/Http/Controllers/UserController.php
<?php
namespace App\Http\Controllers;
use App\Mail\VerifyUserMail;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
class UserController extends Controller
{
    public function register(Request $request)
    {
        $request->validate([
            'name' => 'required',
            'email' => 'required',
            'password' => 'required',
        ]);
        $user = User::create([
           'name' => $request->input('name'),
           'email' => $request->input('email'),
           'password' => Hash::make($request->input('password')),
           'verify_email_token' => Str::random(128),
        ]);
        Mail::to($user->email)
            ->send(new VerifyUserMail($user));
        return response()->json('');
    }
    public function verifyUserEmail(Request $request)
    {
        $request->validate([
           'token' => 'required',
        ]);
        $token = $request->input('token');
        $user = User::where('verify_email_token', $token)->first();
        if (empty($user)) {
            return response('Failed', 404);
        }
        $user->email_verified_at = now();
        $user->save();
        return response('Success');
    }
}
app/Mail/VerifyUserMail.php
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class VerifyUserMail extends Mailable
{
    use Queueable, SerializesModels;
    private $user;
    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct(User $user)
    {
        $this->user = $user;
    }
    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        $data = [
            'verifyLink' => route('verify-user-email', ['token' => $this->user->verify_email_token]),
        ];
        return $this->with($data)
            ->view('view.mail.verify-email');
    }
}
resources/views/mail/verify-email.blade.php
<!DOCTYPE html>
<html >
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Verify Mail</title>
</head>
<body class="antialiased">
<a href="{{ $verifyLink  }}">Verify Your Email</a>
</body>
</html>
這邊我們要準備的是User 的 Factory 類別:
User Factory
<?php
namespace Database\Factories;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Factories\Factory;
class UserFactory extends Factory
{
    /**
    * Define the model's default state.
    *
    * @return array
    */
    public function definition(): array
    {
        return [
            'name' => $this->faker->name,
            'email' => $this->faker->safeEmail,
            'password' => bcrypt($this->faker->password),
            'remember_token' => Str::random(10),
            'verify_email_token' => Str::random(128),
        ];
    }
}
到這邊為止,我們已經把測試目標準備好了,明天我們就來針對各使用案例來寫測試吧!