- 编写商铺控制器相关的行为;
- 添加路允许通过路由控制商铺相关行为;
- 编写商铺行为的单元测试,完成相关测试。
上一节我们已经实现了商铺模型的开发和测试。本节我们将继续开发商铺的相关功能,最终目标是基本实现用户对商铺的管理功能。
- 查看商铺列表;
- 注册一个商铺;
- 查看指定商铺详情
- 用户修改自己商铺信息
- 用户自己删除自己的商铺;
- index: 返回商铺列表
- create: 注册商铺
- show: 查看指定商铺详情
- update:用户修改自己商铺信息
- destroy: 用户自己删除自己的商铺
-
使用命令创建控制器
$ rails generate controller 复数控制器名
该命令会生成控制器文件,以及控制器的测试文件。
$ rails generate controller api::v1::shops
Running via Spring preloader in process 2320
create app/controllers/api/v1/shops_controller.rb
invoke test_unit
create test/controllers/api/v1/shops_controller_test.rb
config/routes.rb
Rails.application.routes.draw do
namespace :api, defaults: { format: :json } do
namespace :v1 do
# 其它路由
resources :shops, only: [:index, :show, :create, :update, :destroy]
end
end
end
only 是限制当前只开放对这些方法的路由
$ rails routes -c shops
--[ Route 1 ]-------------------------------------------------
Prefix | api_v1_shops
Verb | GET
URI | /api/v1/shops(.:format)
Controller#Action | api/v1/shops#index {:format=>:json}
--[ Route 2 ]-------------------------------------------------
Prefix |
Verb | POST
URI | /api/v1/shops(.:format)
Controller#Action | api/v1/shops#create {:format=>:json}
--[ Route 3 ]--------------------------------------------------
Prefix | api_v1_shop
Verb | GET
URI | /api/v1/shops/:id(.:format)
Controller#Action | api/v1/shops#show {:format=>:json}
--[ Route 4 ]-------------------------------------------------
Prefix |
Verb | PATCH
URI | /api/v1/shops/:id(.:format)
Controller#Action | api/v1/shops#update {:format=>:json}
--[ Route 5 ]--------------------------------------------------
Prefix |
Verb | PUT
URI | /api/v1/shops/:id(.:format)
Controller#Action | api/v1/shops#update {:format=>:json}
--[ Route 6 ]-------------------------------------------------
Prefix |
Verb | DELETE
URI | /api/v1/shops/:id(.:format)
Controller#Action | api/v1/shops#destroy {:format=>:json}
思路解析
获取商铺列表,可以直接使用Shop模型相关方法获取,既然是列表,还要注意分页和排序的情况。
路由是: api/v1/shops?page=1&per_page=10
返回值部分我们采用json数据类型的返回值,成功的http状态码:200。
至于测试部分,我们应该测试两个场景
- 状态码应该是200
- 返回值的商铺列表,但是有可能是空的,所以这个可以不测试
test/controllers/api/v1/shops_controller_test.rb
require "test_helper"
class Api::V1::ShopsControllerTest < ActionDispatch::IntegrationTest
test "index_success: should show shops" do
get api_v1_shops_path, as: :json
assert_response 200
end
end
运行测试会显示一个失败,原因是找不到 index
方法, 这是当然的,我们还没有定义 index
方法。
$ rails test
......
Error:
Api::V1::ShopsControllerTest#test_index_success:_should_show_shops:
DRb::DRbRemoteError: The action 'index' could not be found for Api::V1::ShopsController
app/controllers/api/v1/shops_controller.rb
class Api::V1::ShopsController < ApplicationController
before_action :set_per_page, only: [:index]
before_action :set_page, only: [:index]
def index
@shops = Shop.offset(@page).limit(@per_page)
render json: {error_code:0, data:@shops, message:'ok'}, status: 200
end
private
def _to_i(param, default_no = 1)
param && param&.to_i > 0 ? param&.to_i : default_no.to_i
end
def set_page
@page = _to_i(params[:page], 1)
@page = set_per_page * (@page - 1)
end
def set_per_page
@per_page = _to_i(params[:per_page], 10)
end
end
再次运行测试,这次测试通过了。
$ rails test
......
Finished in 2.230317s, 11.2092 runs/s, 13.4510 assertions/s.
25 runs, 30 assertions, 0 failures, 0 errors, 0 skips
我们先来提交我们的代码到git.
$ git add .
$ git commit -m "add shops_controller index action"
思路分析
实现了index方法,我们需要先暂停以下开发。因为同为 index 方法,用户的index方法和shop的index方法中关于分页有着完全一致的代码,我们可以预计,后面如果遇到列表分页也应该会用到一样的代码。遇到这样的情况,我们需要把公共代码提取出来维护。
首先由于这是在控制器内使用的代码,所以我们遵循通常的约定:把控制器的通用代码放到 控制器所在目录的 concerns 目录下面,我们可以创建一个和分页相关名称的文件存放代码。
然后在 application_controller.rb 文件中引入该文件供所有的控制器使用。
最后修改并删除掉分散在其它控制器内的重复代码,当然改代码,一定要重新测试。
-
创建公共方法文件:
$ touch app/controllers/concerns/paginable.rb
-
编辑文件,把公共代码移植到文件内。
app/controllers/concerns/paginable.rb
module Paginable protected def _to_i(param, default_no = 1) param && param&.to_i > 0 ? param&.to_i : default_no.to_i end def set_page @page = _to_i(params[:page], 1) @page = set_per_page * (@page - 1) end def set_per_page @per_page = _to_i(params[:per_page], 10) end end
当然这里的代码使用权限使用了
protected
来约束,因为这些代码不仅仅当前类私用,而是要与子类共用。 -
在
application_controller
中引入Paginable
:app/controllers/application_controller.rb
class ApplicationController < ActionController::API # 其它代码 include Paginable # 其它代码 end
-
删除到原来文件中的重复代码
-
app/controllers/api/v1/shops_controller.rb
删除重复代码后的文件内容class Api::V1::ShopsController < ApplicationController before_action :set_per_page, only: [:index] before_action :set_page, only: [:index] def index @shops = Shop.offset(@page).limit(@per_page) render json: {error_code:0, data:@shops, message:'ok'}, status: 200 end end
-
app/controllers/api/v1/users_controller.rb
删除重复代码后的文件内容class Api::V1::UsersController < ApplicationController before_action :set_per_page, only: [:index] before_action :set_page, only: [:index] before_action :set_user, only: [:show, :update, :destroy] before_action :check_admin, only: [:index, :destroy] before_action :check_admin_or_owner, only: [:update] def index @users = User.offset(@page).limit(@per_page) render json: {error_code:0, data:@users, message:'ok'}, status: 200 end def show render json: {error_code:0, data:@user, message:'ok'}, status: 200 end def create @user = User.new(user_params) if @user.save render json: {error_code:0, data:@user, message:'ok'}, status: 201 else render json: {error_code:500, message:@user.errors}, status: 201 end end def update if @user.update(user_params) render json: {error_code:0, data:@user, message:'ok'}, status: 202 else render json: {error_code:500, message:@user.errors}, status: 202 end end def destroy @user.destroy render json: {error_code:0, message:'ok'}, status: 204 end private def set_user @user = User.find_by_id params[:id].to_i @user = @user || {} end def user_params params.require(:user).permit(:name, :email, :password) end def is_admin? current_user&.role == 0 end def check_admin head 403 unless is_admin? end def is_owner? @user.id == current_user&.id end def check_admin_or_owner head 403 unless is_admin? || is_owner? end end
-
-
再次测试代码
$ rails test Finished in 2.283189s, 10.9496 runs/s, 13.1395 assertions/s. 25 runs, 30 assertions, 0 failures, 0 errors, 0 skips
这样就保证了我们没有因为抽离代码引发错误。
-
再次提交代码到版本管理
$ git add . $ git commit -m "add paginable file"
思路解析
获取指定商铺,路由是:get:api/v1/shops/:id
。
我们首先要获取参数中的商铺id,然后根据商铺id找到具体的商铺,返回商铺信息,成功状态码是200。
至于测试部分,我们应该测试两部分
-
状态码应该是200
-
返回值的商铺就是我们指定的商铺信息。返回值数据结构:
{ id:1, name: "shop01", products_count: 10, orders_count: 10, created_at: "2021-05-14 04:08:24.249180000 +0000", owner:{ id:1, email:'[email protected]' } #...... }
test/controllers/api/v1/shops_controller_test.rb
class Api::V1::ShopsControllerTest < ActionDispatch::IntegrationTest
setup do
@shop = shops(:one)
end
# ... ...
test "show_success: should show shop" do
get api_v1_shop_path(@shop), as: :json
json_response = JSON.parse(self.response.body)
# 验证状态码
assert_response 200
# 验证返回数据
assert_equal @shop.name, json_response['data']['name']
end
end
运行测试会显示一个失败,原因是找不到 show
方法, 这是当然的,我们还没有定义 show
方法。
$ rails test
Error:
Api::V1::ShopsControllerTest#test_show_success:_should_show_shop:
DRb::DRbRemoteError: The action 'show' could not be found for Api::V1::ShopsController
app/controllers/api/v1/shops_controller.rb
class Api::V1::ShopsController < ApplicationController
before_action :set_shop, only: [:show]
def show
@data = set_response_data(@shop)
render json: {error_code:0, data:@data, message:'ok'}, status: 200
end
private
#......
def set_shop
@shop = Shop.includes(:user).find_by_id params[:id]
@shop = @shop || {}
end
def set_response_data shop
retrun {} unless shop.present?
{
id: shop.id,
name: shop.name,
products_count: shop.products_count,
orders_count: shop.orders_count,
created_at: shop.created_at,
owner:{
id: shop.user.id,
email: shop.user.email
}
}
end
end
再次运行测试,这次测试通过了。
$ rails test
Finished in 1.923310s, 13.5184 runs/s, 16.6380 assertions/s.
26 runs, 32 assertions, 0 failures, 0 errors, 0 skips
$ git add .
$ git commit -m "add shops_controller show action"
思路解析
新建商铺,我们的路由是:post:api/v1/shops
。
我们的参数应该类似: {shop:{name:'shop_name', products_count:10, orders_count:10}}
要创建商铺,首先要求用户是登录的状态!我们可以获取到当前登录的用户信息。
然后要创建一个商铺对象来接受参数,然后保存!
如果保存成功,则返回成功的信息;否则,返回失败的信息。
至于测试部分,我们应该测试两个场景
- 成功:使用有效的token,合法的参数
- 状态码应该是
201
- 添加成功后商铺的总数增加 1。
- 状态码应该是
- 失败:不登录
- 状态码是 401
test/controllers/api/v1/shops_controller_test.rb
class Api::V1::ShopsControllerTest < ActionDispatch::IntegrationTest
setup do
@shop = shops(:one)
@user_5 = users(:five)
end
test "create_success: should create shop with token" do
# 验证某个值变化了
assert_difference('Shop.count', 1) do
post api_v1_shops_path,
headers: { Authorization: JsonWebToken.encode(user_id: @user_5.id) },
params: {shop:{name:'shop_name', products_count:10, orders_count:10}},
as: :json
end
assert_response 201
end
test "create_forbiden: should not create shop without token" do
post api_v1_shops_path,
params: {shop:{name:'shop_name', products_count:10, orders_count:10}},
as: :json
assert_response 401
end
end
运行测试会显示失败,原因是找不到 create
方法, 这是当然的,我们还没有定义 create
方法。
$ rails test
Error:
Api::V1::ShopsControllerTest#test_create_forbiden:_should_not_create_shop_without_token:
DRb::DRbRemoteError: The action 'create' could not be found for Api::V1::ShopsController
app/controllers/api/v1/shops_controller.rb
class Api::V1::ShopsController < ApplicationController
before_action :check_login, only: [:create]
def create
@user = current_user
@shop = Shop.new(shop_params)
@shop.user = @user
@shop.transaction do
@user.role = 2
if @shop.save! && @user.save!
@data = set_response_data(@shop)
render json: {error_code:0, data:@data, message:'ok'}, status: 201
else
render json: {error_code:500, message:@shop.errors}, status: 201
end
end
end
private
def shop_params
params.require(:shop).permit(:name, :products_count, :orders_count)
end
def check_login
head 401 unless current_user
end
end
再次运行测试,这次测试通过了。
$ rails test
......
Finished in 2.027363s, 13.8110 runs/s, 17.2638 assertions/s.
28 runs, 35 assertions, 0 failures, 0 errors, 0 skips
思路解析
修改用户,路由是:put:api/v1/shops/:id
。
我们的参数应该类似: {shop:{name:'shop_name', products_count:10, orders_count:10}}
首先要利用商铺id找到对应商铺,然后要验证当前用户是否是店铺的主人,验证通过,然后把接受到的商铺信息保存,完成修改!
如果保存成功,则返回成功的信息;否则,返回失败的信息。
至于测试部分,我们应该测试两种场景:
- 成功:当前用户是店铺的拥有者
- 状态码: 202
- 失败: 当用户不是店铺拥有者
- 状态码: 403
test/controllers/api/v1/shops_controller_test.rb
require "test_helper"
class Api::V1::ShopsControllerTest < ActionDispatch::IntegrationTest
test "update_success: should update shop with owner" do
put api_v1_shop_path(@shop),
headers: { Authorization: JsonWebToken.encode(user_id: @shop.user.id) },
params: {shop:{name:'shop_name', products_count:10, orders_count:10}},
as: :json
assert_response 201
end
test "update_forbidden: forbidden update shop without owner" do
put api_v1_shop_path(@shop),
headers: { Authorization: JsonWebToken.encode(user_id: @user_5.id) },
params: {shop:{name:'shop_name', products_count:10, orders_count:10}},
as: :json
assert_response 403
end
end
运行测试会显示一个失败,原因是找不到 create
方法, 这是当然的,我们还没有定义 show
方法。
$ rails test
Error:
Api::V1::ShopsControllerTest#test_update_forbidden:_forbidden_update_shop_without_owner:
DRb::DRbRemoteError: The action 'update' could not be found for Api::V1::ShopsController
app/controllers/api/v1/shops_controller.rb
class Api::V1::ShopsController < ApplicationController
before_action :set_shop, only: [:show, :update]
before_action :check_login, only: [:create, :update]
before_action :check_owner, only: [:update]
def update
@user = current_user
if @user.shop.update(shop_params)
@data = set_response_data(@shop)
render json: {error_code:0, data:@data, message:'ok'}, status: 201
else
render json: {error_code:500, message:@shop.errors}, status: 201
end
end
private
def check_owner
head 403 unless current_user.id == @shop.user.id
end
end
再次运行测试,这次测试通过了。
$ rails test
......
Finished in 2.022039s, 14.8365 runs/s, 18.2984 assertions/s.
30 runs, 37 assertions, 0 failures, 0 errors, 0 skips
思路解析
删除商铺,我们的路由是:delete:api/v1/shops/:id
。
首先要利用商铺id找到对应商铺,判断当前用户是否是店铺拥有者,然后直接删除即可!
如果删除成功,则返回成功的信息;否则,返回失败的信息。
至于测试部分,我们应该测试两种场景
- 成功:拥有者删除
- 状态码应该是 204
- 删除成功后用户的总数减少1。
- 失败:使用非商铺拥有着登录
- 状态码:403
test/controllers/api/v1/shops_controller_test.rb
require "test_helper"
class Api::V1::ShopsControllerTest < ActionDispatch::IntegrationTest
test "delete_success: should delete shop with owner" do
assert_difference('Shop.count', -1) do
delete api_v1_shop_path(@shop),
headers: { Authorization: JsonWebToken.encode(user_id: @shop.user.id) },
as: :json
end
assert_response 204
end
test "delete_forbidden: forbidden delete shop without owner" do
delete api_v1_shop_path(@shop),
headers: { Authorization: JsonWebToken.encode(user_id: @user_5.id) },
as: :json
assert_response 403
end
end
运行测试会显示一个失败,原因是找不到 create
方法, 这是当然的,我们还没有定义 show
方法。
$ rails test
Error:
Api::V1::ShopsControllerTest#test_destroy_success:_should_destroy_shop:
DRb::DRbRemoteError: The action 'destroy' could not be found for Api::V1::ShopsController
app/controllers/api/v1/shops_controller.rb
class Api::V1::ShopsController < ApplicationController
before_action :set_shop, only: [:show, :update, :destroy]
before_action :check_login, only: [:create, :update, :destroy]
before_action :check_owner, only: [:update, :destroy]
def destroy
@shop.destroy
head 204
end
end
再次运行测试,这次测试通过了。
$ rails test
Finished in 2.117606s, 15.1114 runs/s, 18.4170 assertions/s.
32 runs, 39 assertions, 0 failures, 0 errors, 0 skip
$ git add .
$ git commit -m "set shops controller and routes"
$ git checkout master
$ git merge chapter06
我们本节完成了商铺控制器的相关方法,并设置了对应路由提供用户在外部直接访问。但就我们今天实现的功能,实际上还是有很多不方便的地方,比如分页,比如json返回值,下一节我们就这两个问题展开探讨和解决。
同志们继续加油!一定要跟上脚步,认真练习!