Showing posts with label Vuejs. Show all posts
Showing posts with label Vuejs. Show all posts

Create a login and register web app using Vuejs, Laravel & JWT. Use Jest for testing Vue components.

  

Step 1: Install and setup Laravel

Follow the below link to “Install Laravel sail & Vuejs 3.0 on MAC (M1)”.

After successfully installing laravel, we just need to change the below file.

../app/Providers/AppServiceProvider.php :

<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Facades\Schema; class AppServiceProvider extends ServiceProvider { /** * Register any application services. * * @return void */ public function register() { // } /** * Bootstrap any application services. * * @return void */ public function boot() { Schema::defaultStringLength(191); } }

 

Step 2: Install tymon/jwt-auth package

Update composer.json file placed in the root directory of the project and add this line in require.

"require": { .... .... "laravel/ui": "^3.2", "tymon/jwt-auth": "dev-develop" },

Update the composer from the terminal.

composer update

This command will install the package and the dependencies. Now we are going to publish the package using the below command.

php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

Step 3: Setup Database

Create the database using the following command.

CREATE DATABASE laravelvuedb;

Set DB details on .env file.

# create database laravelvuedb; DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=6033 DB_DATABASE=laravelvuedb DB_USERNAME=user DB_PASSWORD=password

Run artisan to migrate and seed into the database.

php artisan migrate php artisan db:seed

Step 4: User interface setup in Laravel

User model file

Now open the user.php model file from inside the app folder and add the below code to it.

<?php namespace App\Models; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Tymon\JWTAuth\Contracts\JWTSubject; class User extends Authenticatable implements JWTSubject { use Notifiable; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'name', 'email', 'password', ]; /** * The attributes that should be hidden for arrays. * * @var array */ protected $hidden = [ 'password', 'remember_token', ]; public function getJWTIdentifier() { return $this->getKey(); } public function getJWTCustomClaims() { return []; } /** * The attributes that should be cast to native types. * * @var array */ protected $casts = [ 'email_verified_at' => 'datetime', ]; }

User Controller file

Create the ../app/Http/Controllers/UserController.php file and add the below code to it.

Register, login, and get authenticated user functions are defined in this controller.

<?php namespace App\Http\Controllers; use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; use Tymon\JWTAuth\Facades\JWTAuth; use Tymon\JWTAuth\Facades\JWTFactory; use Tymon\JWTAuth\Exceptions\JWTException; use Tymon\JWTAuth\Contracts\JWTSubject; use Tymon\JWTAuth\JWTManager as JWT; use Tymon\JWTAuth\Exceptions\TokenExpiredException; use Tymon\JWTAuth\Exceptions\TokenInvalidException; class UserController extends Controller { public function register(Request $request){ $validator = Validator::make($request->json()->all(), [ 'name' => 'required|string|max:255', 'email' => 'required|string|email|max:255|unique:users', 'password' => 'required|string|min:6', ]); if($validator->fails()){ return response()->json($validator->errors()->toJson(), 400); } $user = User::create([ 'name' => $request->json()->get('name'), 'email' => $request->json()->get('email'), 'password' => Hash::make($request->json()->get('password')), ]); $token = JWTAuth::fromUser($user); return response()->json(compact('user','token'), 201); } public function login(Request $request){ $credentials = $request->json()->all(); try { if(! $token = JWTAuth::attempt($credentials)){ return response()->json(['error'=>'invalid Credentials'], 400); } }catch (JWTException $e){ return response()->json(['error'=>'could_not_create_token'], 500); } return response()->json(compact('token')); } public function getAuthenticatedUser(){ try{ if(!$user = JWTAuth::parseToken()->authenticate()){ return response()->json(['user_not_found'], 400); } }catch (TokenExpiredException $e){ return response()->json(['token_expired'], $e->getStatusCode()); }catch (TokenInvalidException $e){ return response()->json(['token_invalid'], $e->getStatusCode()); }catch (JWTException $e){ return response()->json(['token_absent'], $e->getStatusCode()); } return response()->json(compact('user')); } }

 

Open ../config/app.php and add below service providers and aliases.

in providers add this in existing code.

'providers' => [ .. \Tymon\JWTAuth\Providers\LaravelServiceProvider::class, ]

In aliases add this in existing code.

'aliases' => [ .. 'JWTAuth' => \Tymon\JWTAuth\Facades\JWTAuth::class, 'JWTFactory' => \Tymon\JWTAuth\Facades\JWTFactory::class, ],

Step 5: Create Routes

To create the routes open routes file located here ../routes/web.php and add below routes in it.

<?php use Illuminate\Support\Facades\Route; /* |-------------------------------------------------------------------------- | Web Routes |-------------------------------------------------------------------------- | | Here is where you can register web routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | contains the "web" middleware group. Now create something great! | */ Route::get('/', function () { return view('welcome'); }); Auth::routes(); Route::get('/home', 'HomeController@index')->name('home');

After creating web routes, now we can open the project in the browser to check if that is working.

Now open the API routes file located here ../routes/api.php and add the below routes in it.

<?php use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use App\Http\Controllers\UserController; /* |-------------------------------------------------------------------------- | API Routes |-------------------------------------------------------------------------- | | Here is where you can register API routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | is assigned the "api" middleware group. Enjoy building your API! | */ Route::middleware('auth:api')->get('/user', function (Request $request) { return $request->user(); }); Route::post('register', [UserController::class, 'register']); Route::post('login', [UserController::class, 'login']); Route::get('profile', [UserController::class, 'getAuthenticatedUser']);

After creating API routes, now we can check the created APIs using postman.

Step 6: JWT key / secret

Run PHP artisan to use JWT for authentication and authorization.

php artisan key:generate php artisan jwt:secret (php artisan serve&) // To run on background

 

Check the register and login API’s on the postman. We already have defined functionality in the UserController.php file for these API’s.

Register API test

Login API test

Step 7: Install Dependencies

If you do not have the laravel UI installed for bootstrap the run below command.

composer require laravel/ui php artisan ui vue

Make sure you have installed node, npm, and vue. If you haven’t installed it then follow the below command.

npm install && npm run dev php artisan ui vue --auth npm install

Step 7: Set up Vue components

In step 1, we already have set up the Vue application.

 

Now open package.json file from the vueapp directory.
../vueapp/package.json and add the dependencies. Add axios and bootstrap dependencies. After adding it will look like this.

"dependencies": { "axios": "^0.21.1", "bootstrap": "^4.4", "component": "^1.1.0", "vue": "^2.5.2", "vue-router": "^3.0.1" },

Now go to the vueapp directory.

cd client

Run the below command in the terminal. This command will install the packages that we have just added above.

npm i

Now, Open ../vueapp/config/index.js file and update the below code in it.

'use strict' // Template version: 1.3.1 // see http://vuejs-templates.github.io/webpack for documentation. const path = require('path') module.exports = { dev: { // Paths assetsSubDirectory: 'static', assetsPublicPath: '/', proxyTable: {}, // Various Dev Server settings host: 'localhost', // can be overwritten by process.env.HOST port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined autoOpenBrowser: false, errorOverlay: true, notifyOnErrors: true, poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- // Use Eslint Loader? // If true, your code will be linted during bundling and // linting errors and warnings will be shown in the console. useEslint: true, // If true, eslint errors and warnings will also be shown in the error overlay // in the browser. showEslintErrorsInOverlay: false, /** * Source Maps */ // https://webpack.js.org/configuration/devtool/#development devtool: 'cheap-module-eval-source-map', // If you have problems debugging vue-files in devtools, // set this to false - it *may* help // https://vue-loader.vuejs.org/en/options.html#cachebusting cacheBusting: true, cssSourceMap: true }, build: { // Template for index.html index: path.resolve(__dirname, '../dist/index.html'), // Paths assetsRoot: path.resolve(__dirname, '../dist'), assetsSubDirectory: 'static', assetsPublicPath: '/', /** * Source Maps */ productionSourceMap: true, // https://webpack.js.org/configuration/devtool/#production devtool: '#source-map', // Gzip off by default as many popular static hosts such as // Surge or Netlify already gzip all static assets for you. // Before setting to `true`, make sure to: // npm install --save-dev compression-webpack-plugin productionGzip: false, productionGzipExtensions: ['js', 'css'], // Run the build command with an extra argument to // View the bundle analyzer report after build finishes: // `npm run build --report` // Set to `true` or `false` to always turn it on or off bundleAnalyzerReport: process.env.npm_config_report } }

Open ../client/src/main.js file and update code same as below.

// The Vue build version to load with the `import` command // (runtime-only or standalone) has been set in webpack.base.conf with an alias. import Vue from 'vue' import App from './App' import router from './router' require('../node_modules/bootstrap/dist/css/bootstrap.css') Vue.config.productionTip = false /* eslint-disable no-new */ new Vue({ el: '#app', router, components: { App }, template: '<App/>' })

Open ../client/src/App.vue file and update code same as below.

<template> <div id="app"> <navbar></navbar> <router-view/> </div> </template> <script> import Navbar from './components/Navbar' export default { name: 'App', components: { 'Navbar': Navbar } } </script> <style> #app { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>

open file ../vueapp/src/router/index.js and add the below code in it.

import Vue from 'vue' import Router from 'vue-router' // import HelloWorld from '@/components/HelloWorld' import Home from '../components/Home' import Login from '../components/Login' import Register from '../components/Register' import Profile from '../components/Profile' Vue.use(Router) export default new Router({ routes: [ { path: '/', name: 'Home', component: Home }, { path: '/login', name: 'Login', component: Login }, { path: '/register', name: 'Register', component: Register }, { path: '/profile', name: 'Profile', component: Profile } ] })

Here we have added the routes for vue.

Now create the component of vue inside this folder ../vueapp/src/components and create a new file for that and will add the code in it.

Home.vue

./vueapp/src/components/Home.vue

This file contains the below code in it.

<template> <div class="container"> <div class="jumbotron mt-5"> <div class="col-sm-8 mx-auto"> <h1 class="text-center"> Welcome</h1> </div> </div> </div> </template> <script> </script>

Login.vue

../vueapp/src/components/Login.vue

<template> <div class="container"> <div class="row"> <div class="col-md-6 mt-5 mx-auto"> <form v-on:submit.prevent="login"> <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1> <div class="form-group"> <label for="email"> Email Address</label> <input type="email" v-model="email" class="form-control" name="email" id="email" placeholder="Email Address"> </div> <div class="form-group"> <label for="password"> Password</label> <input type="password" v-model="password" class="form-control" name="password" id="password" placeholder="Password"> </div> <button class="btn btn-lg btn-primary btn-block">Sign in</button> </form> </div> </div> </div> </template> <script> import axios from 'axios' import router from '../router' export default { data () { return { email: '', password: '' } }, methods: { login: function () { axios.post('http://localhost:8000/api/login', { email: this.email, password: this.password }) .then((res) => { localStorage.setItem('usertoken', res.data.token) this.email = '' this.password = '' router.push({name: 'Profile'}) }) .catch((err) => { console.log(err) }) this.emitMethod() } emitMethod () { } } } </script>

../vueapp/src/components/Navbar.vue

<template> <nav class="navbar navbar-expand-lg navbar-dark bg-dark rounded"> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar1" aria-controls="navbar1" aria-expanded="false" aria-label="Toggle Navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse justify-content-md-center" id="navbar1"> <ul class="navbar-nav"> <li class="nav-item"> <router-link class="nav-link" to="/">home</router-link> </li> <li v-if="auth==''" class="nav-item"> <router-link class="nav-link" to="/login">Login</router-link> </li> <li v-if="auth==''" class="nav-item"> <router-link class="nav-link" to="/register">Register</router-link> </li> <li v-if="auth=='loggedin'" class="nav-item"> <router-link class="nav-link" to="/profile">Profile</router-link> </li> <li v-if="auth=='loggedin'" class="nav-item"> <router-link class="nav-link" href="">Logout</router-link> </li> </ul> </div> </nav> </template> <script> export default { data () { return { auth: '', user: '' } }, methods: { logout () { localStorage.removeItem('usertoken') } }, mounted () { } } </script>

Register.vue

../vueapp/src/components/Register.vue

<template> <div class="container"> <div class="row"> <div class="col-md-6 mt-5 mx-auto"> <form v-on:submit.prevent="register"> <h1 class="h3 mb-3 font-weight-normal">Register</h1> <div class="form-group"> <label for="first_name"> First Name</label> <input type="text" v-model="first_name" class="form-control" name="first_name" placeholder="first name"> </div> <div class="form-group"> <label for="last_name"> Last Name</label> <input type="text" v-model="last_name" class="form-control" name="last_name" placeholder="last name"> </div> <div class="form-group"> <label for="email"> Email Address</label> <input type="email" v-model="email" class="form-control" name="email" placeholder="Email Address"> </div> <div class="form-group"> <label for="password"> Password</label> <input type="password" v-model="password" class="form-control" name="password" placeholder="Password"> </div> <button class="btn btn-lg btn-primary btn-block">Register</button> </form> </div> </div> </div> </template> <script> import axios from 'axios' import router from '../router' export default{ data () { return { first_name: '', last_name: '', email: '', password: '' } }, methods: { register () { axios.post('http://localhost:8000/api/register', { name: this.first_name + ' ' + this.last_name, email: this.email, password: this.password }) .then((res) => { console.log(res) router.push({name: 'Login'}) }) .catch((err) => { console.log(err) }) } } } </script>

Profile.vue

../vueapp/src/components/Profile.vue

This file contains below code in it.

<template> <div class="container"> <div class="jumbotron mt-5"> <div class="col-sm-8 mx-auto"> <h1 class="text-center">Profile</h1> </div> <table class="table col-md-6 mx-auto"> <tbody> <tr> <td>Name</td> <td>{{wholename}}</td> </tr> <tr> <td>Email</td> <td>{{email}}</td> </tr> </tbody> </table> </div> </div> </template> <script> import axios from 'axios' export default{ data () { this.getUser().then(res => { this.wholename = res.user.name this.email = res.user.email return res }) return { wholename: '', email: '' } }, methods: { getUser () { console.log(`Bearer ${localStorage.getItem('usertoken')}`) return axios.get('http://localhost:8000/api/profile', { headers: { Authorization: `Bearer ${localStorage.getItem('usertoken')}` } }) .then(res => { return res.data }) .catch(err => { console.log(err) }) } } } </script>

After creating this file just run the project using below command.

npm run dev

it will be look like.

Home page

Register Page

Login Page

Profile Page

So we are done with the basic functionality for Registering and log in to a application using Laravel & VueJs.

 

Write PHPUnit Test for Laravel API end points.

Create a copy of .env file as .env.testing.

Update the test config data [Testing DB details] on .env.testing file.

 

APP_NAME=Laravel APP_ENV=testing APP_KEY=base64:vIMosF+rzAYjEHMmbI6JIkFPKrNhBeHYAxY2oS8uC0U= APP_DEBUG=true APP_URL=http://localhost LOG_CHANNEL=stack LOG_LEVEL=debug # create database laravelvuedb; DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=6033 DB_DATABASE=test_laravelvuedb DB_USERNAME=user DB_PASSWORD=password BROADCAST_DRIVER=log CACHE_DRIVER=file QUEUE_CONNECTION=sync SESSION_DRIVER=file SESSION_LIFETIME=120 MEMCACHED_HOST=127.0.0.1

 

Now open the tests/TestCase.php file from inside the app folder and update the below code to it.

 

<?php namespace Tests; use Faker\Factory; abstract class TestCase extends \Illuminate\Foundation\Testing\TestCase { use CreatesApplication; protected $faker; /** * To generate test data using faker and seeder * * @return void */ public function setUp(): void { parent::setUp(); $this->artisan('migrate'); $this->artisan('db:seed'); $this->faker = Factory::create(); } }

 

Now create a file tests/app/Http/Controllers/UserCase.php file from inside the app folder and add the below code to it.

 

<?php /** * Unit test for the end points used in UserController * * @category Test Case File * @license Not licensed for external use */ class UserTest extends \Tests\TestCase { /** * Create user */ public function testRegister() { $faker = $this->faker; $parameters = [ 'name' => $faker->name, 'email' => !empty($email) ? $email : preg_replace( '/@example\..*/', '@mailinator.com', $faker->unique()->safeEmail ), 'password' => $faker->password, ]; $result = $this->post("api/register", $parameters); $this->assertJson($result->json(),'{"name":[""],"email":[""],"password":[""]}'); } }

 

On terminal run the below command to execute the PHPUnit test.

./vendor/bin/phpunit tests

Result :

So now we are done with the PHPUnit test for one of the laravel API endpoint.

 

Test Your Vue Components Using the Jest Testing Framework

 

Jest is a popular JavaScript testing framework. Lets setup the tester for Vue components.

Setup and Installing Dependencies

If you don’t have the Vue CLI installed on your machine already, start by running either:

$ npm install -g @vue/cli

 

Run the following command to add our testing dependencies (@vue/cli-plugin-unit-jest and @vue/test-utils):

$ npm install @vue/cli-plugin-unit-jest @vue/test-utils

[OR]

Add test-jest to an existing application by running the below command on the terminal.

vue add unit-jest

 

Modify your project’s package.json file to have an entry in scripts as follows.

"scripts": { ... "unit": "jest --config jest.config.js --coverage", ... },

Then, create a file test/unit/jest.config.js with the following content:

jest.config.js

const path = require('path') module.exports = { rootDir: path.resolve(__dirname, ''), moduleFileExtensions: [ 'js', 'json', 'vue' ], moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1' }, transform: { '^.+\\.js$': '<rootDir>/node_modules/babel-jest', '.*\\.(vue)$': '<rootDir>/node_modules/vue-jest' }, testPathIgnorePatterns: [ '<rootDir>/test/e2e' ], snapshotSerializers: ['<rootDir>/node_modules/jest-serializer-vue'], setupFiles: ['<rootDir>/test/unit/setup'], mapCoverage: true, coverageDirectory: '<rootDir>/test/unit/coverage', collectCoverageFrom: [ 'src/**/*.{js,vue}', '!src/main.js', '!src/router/index.js', '!**/node_modules/**' ], verbose: true, testURL: 'http://localhost/' }

 

Coding Up a Simple Test component App

Add. a file JestTest.vue inside src/components as such:

JestTest.vue

<template> <div class="container"> <div class="row"> <div class="col-md-6 mt-5 mx-auto"> <h3>Let us test your arithmetic.</h3> <p>What is the sum of the two numbers?</p> <div class="inline"> <p>{{ x1 }} + {{ x2 }} =</p> <input v-model="guess"> <button v-on:click="check">Check Answer</button> </div> <button v-on:click="refresh">Refresh</button> <p>{{ message }}</p> </div> </div> </div> </template> <script> export default { data () { return { x1: Math.ceil(Math.random() * 100), x2: Math.ceil(Math.random() * 100), guess: '', message: '' } }, methods: { check: function () { if (this.x1 + this.x2 === parseInt(this.guess)) { this.message = 'SUCCESS!' } else { this.message = 'TRY AGAIN' } return this.message }, refresh: function () { this.x1 = Math.ceil(Math.random() * 100) this.x2 = Math.ceil(Math.random() * 100) return 5 } } } </script>

 Update the Navbar.vue to have a link to the above component.

<li v-if="auth==''" class="nav-item"> <router-link class="nav-link" to="/jestTest">Jest Test</router-link> </li>

Update the src/rouer/index.js with the below code :

import JestTest from '../components/JestTest' Vue.use(Router) export default new Router({ routes: [ ... { path: '/jestTest', name: 'JestTest', component: JestTest } ]

Then go ahead and run $ npm run serve from the root directory of your project.

Now you head over to localhost:8000 in your browser and see the working app.

Testing the App with Jest

Created a file called JestTest.spec.js. By default, jest will catch any test files (searching recursively through folders) in your project that are named *.spec.js or *.test.js.

At the top of JestTest.spec.js we’re going to import the following from @vue/test-utils as well as our JestTest component itself:

Here the test covered the data and methods exported on the Vue component.

import { mount } from '@vue/test-utils' import JestTest from '@/components/JestTest.vue' describe('JestTest', () => { // Inspect the raw component options it('has data', () => { expect(typeof JestTest.data).toBe('function') }) }) describe('Mounted App', () => { const wrapper = mount(JestTest) it('renders correctly with different data', async () => { wrapper.setData({ x1: 5, x2: 10 }) await wrapper.vm.$nextTick() expect(wrapper.text()).toContain('10') }) it('button click with correct sum', () => { wrapper.setData({ guess: '15' }) const button = wrapper.find('button') button.trigger('click') expect(wrapper.vm.message).toBe('SUCCESS!') }) test('is a Vue instance', () => { expect(wrapper).toBeTruthy() }) test('refresh', () => { expect(wrapper.vm.refresh()).toEqual(5) }) })

Run $ npm run test in your Terminal – the test should pass.

Happy coding!