APIS ON RAILS - Chapter 3: Presenting the users

Trong 2 chap trước thì chúng ta đã thiết kế được bộ khung của app rồi, thậm chí chúng ta đã thêm được phiên bản thông qua headers. Trong bài viết này thì chúng ta sẽ tạo ra products cho từng user và mỗi user có thể tạo order. Bạn có thể clone project ở 2 chap trước bằng link sau:

git clone https://github.com/kurenn/market_place_api.git -b chapter2

Chắc hẳn bạn cũng biết có rất nhiều cách để xử lý đăng nhập trong Rails như Authlogic, Clearance và Devise - Trong bài viết này thì chúng ta sẽ sử dụng Devise. Nào, giờ chúng ta cùng bắt đầu nào:

git checkout -b chapter3

3.1. User model

Đầu tiên chúng ta cần thêm gem devise vào trong Gemfile:

gem "devise"

Sau đó chạy bundle install để cài đặt gem vừa thêm, sau khi lệnh bundle chạy thành công, ta cần chạy lệnh dưới để generate devise:

rails g devise:install

Tiếp theo chúng ta chạy lệnh sau để tạo ra model User:

rails g devise User

Kể từ bây giờ, mỗi khi chúng ta tạo model thì Rails cũng sẽ tự tạo ra một file factory cho model User này. Điều này sẽ giúp chúng ta dễ dàng tạo test và chạy thử.

# spec/factories/users.rb
FactoryGirl.define do
  factory :user do
  end
end

Giờ thì chạy lệnh migrate database và chuẩn bị cho việc test database nào:

rake db:migrate
rake db:test:prepare

Sau khi chạy test ok rồi thì chúng ta tạo commit cho những bước trên thôi:

git add .
git commit -m "Adds devise user model"

3.1.1. First user tests

Chúng ta sẽ thê một số spec để bảo đảm rằng model User sẽ trả về các thuộc tính email, passwordpassword_confirmation được cung cấp bởi devise. Để tiện hơn cho việc test, chúng ta sẽ thêm những thuộc tính trên vào factory:

FactoryGirl.define do
  factory :user do
    email { FFaker::Internet.email }
    password "12345678"
    password_confirmation "12345678"
  end
end

Sau khi thêm những thuộc tính trên vào model User, giờ thì chúng ta test thử nào:

require 'spec_helper'

describe User do
  before { @user = FactoryGirl.build(:user) }

  subject { @user }

  it { should respond_to(:email) }
  it { should respond_to(:password) }
  it { should respond_to(:password_confirmation) }

  it { should be_valid }
end

Bởi vì chúng ta đã tạo ra database để test từ trước với rake db:test:prepare rồi nên giờ chỉ cần chạy:

bundle exec rspec spec/models/user_spec.rb

Giờ thì chúng ta commit tiếp thôi:

git add .
git commit -am "Adds user firsts specs"

3.1.2. Imroving validation tests

Bây giờ chúng ta thử viết vài test cho validate email của thằng User, đầu tiên nên thêm gem shoulda-matchers vào Gemfile trước:

gem "shoulda-matchers"

Viết thử testcase nào:

describe "when email is not present" do
  before { @user.email = " " }
  it { should_not be_valid }
  it { should validate_presence_of(:email) }
  it { should validate_uniqueness_of(:email) }
  it { should validate_confirmation_of(:password) }
  it { should allow_value('[email protected]').for(:email) }
end

Xong phần viết test cho User email rồi, giờ thì commit lại nào:

git add .
git commit -m "Adds shoulda matchers for spec refactors"

3.2. Building users endpoints

Hiện tại chúng ta chỉ mới thêm action show cho user để có thể hiển thị record User ở trong file json. Đầu tiên, chúng ta cần tạo ra users_controller, sau đó thêm những testcase thích hợp và tiến hành code thật sự.

rails g controller users

Câu lệnh này cũng sẽ tạo ra users_controllers_spec.rb. Trước khi chúng ta đi vào viếdt test thì cần biết thêm về 2 bước cơ bản khi test api.

  • JSON sẽ được trả về từ server.
  • Server sẽ trả về trạng thái của code. VD: 200, 201, 204, ...

Để giữ cho code của chúng ta dễ quản lý thì cần tạo một số thư mục bên trong thư mục spec controller để thuận tiện cho việc setup tiếp theo.

mkdir -p spec/controllers/api/v1
mv spec/controllers/users_controller_spec.rb spec/controllers/api/v1

Sau khi tạo ra thư mục tương ứng bằng câu lệnh trên thì chúng ta chuyển phần tên sau describe, từ UsersController sang Api::V1::UsersController:

require "spec_helper"

describe Api::V1::UsersController do

end

Giờ thì thêm các testcase vào nha:

require 'spec_helper'

describe Api::V1::UsersController do
  before(:each) { request.headers['Accept'] = "application/vnd.marketplace.v1" }

  describe "GET #show" do
    before(:each) do
      @user = FactoryGirl.create :user
      get :show, id: @user.id, format: :json
    end

    it "returns the information about a reporter on a hash" do
      user_response = JSON.parse(response.body, symbolize_names: true)
      expect(user_response[:email]).to eql @user.email
    end

    it { should respond_with 200 }
  end
end

Sau khi thêm test như vậy thì chúng ta cũng cần chỉnh sửa controller lại cho phù hợp:

require 'spec_helper'

describe Api::V1::UsersController do
  before(:each) { request.headers['Accept'] = "application/vnd.marketplace.v1" }

  describe "GET #show" do
    before(:each) do
      @user = FactoryGirl.create :user
      get :show, id: @user.id, format: :json
    end

    it "returns the information about a reporter on a hash" do
      user_response = JSON.parse(response.body, symbolize_names: true)
      expect(user_response[:email]).to eql @user.email
    end

    it { should respond_with 200 }
  end
end

Hiện tại khi chạy test thì sẽ báo lỗi vì thiếu routes, do đó thêm phần routes vào thôi:

require 'api_constraints'

MarketPlaceApi::Application.routes.draw do
  devise_for :users
  # Api definition
  namespace :api, defaults: { format: :json },
                              constraints: { subdomain: 'api' }, path: '/'  do
    scope module: :v1,
              constraints: ApiConstraints.new(version: 1, default: true) do
      # We are going to list our resources here
      resources :users, :only => [:show]
    end
  end
end

Nếu bạn chạy lại bundle exec rspec spec/controllers, bạn sẽ thấy tất cả các testcase đều pass lại rồi. Vậy là xong rồi, giờ chúng ta commit lại nhé:

git add .
git commit -m "Adds show action the users controller"

3.2.1. Testing endpoints with CURL

Hiện tại, base uri của chúng ta là api.market_place_api.dev

curl -H 'Accept: application/vnd.marketplace.v1' \http://api.market_place_api.dev/users/1

Khi chạy dòng trên thì nó sẽ quăng ra lỗi, có lẽ bạn cũng đoán được rồi, chúng ta chưa có tạo dữ liệu cho User nên không có User với id là 1. Vì vậy, tiến hành tạo nó trước cái đã:

rails console
User.create({email: "[email protected]", password: "12345678", password_confirmation: "12345678"})

Sau khi tạo User thành công thì khi chạy lệnh vừa nãy sẽ hiển thị ra thông tin của User với id là 1. Nếu đến đây bạn chạy vẫn có lỗi thì hãy vào application_controllers để update đoạn code sau:

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :null_session
end

Sau khi update application_controllers xong thì chúng ta commit chúng lại nào:

git add -A
git commit -m "Updates application controller to prevent CSRF exception from being raised"

3.2.2. Creating users

Đầu tiên cần viết test trước khi code, do đó chúng ta thêm testcase cho hàm create nhé:

 describe "POST #create" do

    context "when is successfully created" do
      before(:each) do
        @user_attributes = FactoryGirl.attributes_for :user
        post :create, { user: @user_attributes }, format: :json
      end

      it "renders the json representation for the user record just created" do
        user_response = JSON.parse(response.body, symbolize_names: true)
        expect(user_response[:email]).to eql @user_attributes[:email]
      end

      it { should respond_with 201 }
    end

    context "when is not created" do
      before(:each) do
        #notice I'm not including the email
        @invalid_user_attributes = { password: "12345678",
                                     password_confirmation: "12345678" }
        post :create, { user: @invalid_user_attributes }, format: :json
      end

      it "renders an errors json" do
        user_response = JSON.parse(response.body, symbolize_names: true)
        expect(user_response).to have_key(:errors)
      end

      it "renders the json errors on why the user could not be created" do
        user_response = JSON.parse(response.body, symbolize_names: true)
        expect(user_response[:errors][:email]).to include "can't be blank"
      end

      it { should respond_with 422 }
    end
  end

Hiện tại khi chúng ta chạy test thì sẽ báo lỗi nên cần thêm update lại code để test có thể xanh:

[...]

def create
    user = User.new(user_params)
    if user.save
      render json: user, status: 201, location: [:api, user]
    else
      render json: { errors: user.errors }, status: 422
    end
  end

  private

    def user_params
      params.require(:user).permit(:email, :password, :password_confirmation)
    end
[...]

Sau khi chạy test lại thì mọi test đã pass rồi, tiến hành commit nó lại nào:

git add .
git commit -m "Adds the user create endpoint"

3.2.3. Updating users

Code để update user tương tự như tạo mới vậy, do đó rất dễ dàng để hiểu đoạn code dưới:

describe "PUT/PATCH #update" do

    context "when is successfully updated" do
      before(:each) do
        @user = FactoryGirl.create :user
        patch :update, { id: @user.id,
                         user: { email: "[email protected]" } }, format: :json
      end

      it "renders the json representation for the updated user" do
        user_response = JSON.parse(response.body, symbolize_names: true)
        expect(user_response[:email]).to eql "[email protected]"
      end

      it { should respond_with 200 }
    end

    context "when is not created" do
      before(:each) do
        @user = FactoryGirl.create :user
        patch :update, { id: @user.id,
                         user: { email: "bademail.com" } }, format: :json
      end

      it "renders an errors json" do
        user_response = JSON.parse(response.body, symbolize_names: true)
        expect(user_response).to have_key(:errors)
      end

      it "renders the json errors on whye the user could not be created" do
        user_response = JSON.parse(response.body, symbolize_names: true)
        expect(user_response[:errors][:email]).to include "is invalid"
      end

      it { should respond_with 422 }
    end
  end

Tương tự như phần trên thì hiện tại khi chạy test thì kết quả vẫn chưa xanh được. Do đó chúng ta cần thêm một số đoạn code sau:

# routes.rb
scope module: :v1, constraints: ApiConstraints.new(version: 1, default: true) do
  # We are going to list our resources here
  resources :users, :only => [:show, :create, :update]
end

# app/controllers/api/v1/users_controller.rb
def update
  user = User.find(params[:id])

  if user.update(user_params)
    render json: user, status: 200, location: [:api, user]
  else
    render json: { errors: user.errors }, status: 422
  end
end

Sau khi thêm đoạn code trên vào thì code đã chạy xanh được rồi, tiến hành add commit thôi:

git add .
git commit -m "Adds update action the users controller"

3.2.4. Destroying users

Tương tự như create và update thì xoá cũng trong khả năng viết được của chúng ta:

#spec/controllers/api/v1/users_controller_spec.rb
describe "DELETE #destroy" do
  before(:each) do
    @user = FactoryGirl.create :user
    delete :destroy, { id: @user.id }, format: :json
  end

  it { should respond_with 204 }

end

Thêm đoạn code sau vào trong app của chúng ta:

def destroy
  user = User.find(params[:id])
  user.destroy
  head 204
end

# app/routes.rb
scope module: :v1, constraints: ApiConstraints.new(version: 1, default: true) do
  # We are going to list our resources here
  resources :users, :only => [:show, :create, :update, :destroy]
end

Sau khi chạy test báo xanh thì chúng ta add commit thôi:

git add .
git commit -m "Adds destroy action to the users controller"

Đến đây thì bài số 3 của series "API ON RAILS" đã kết thúc, cảm ơn các bạn đã theo dõi! Nguồn: http://apionrails.icalialabs.com/book/chapter_three