- 编写商品控制器相关的商品行为;
- 添加路允许通过路由控制商品行为;
- 编写商品行为的单元测试,完成相关测试。
上一节我们已经实现了商品模型的验证并实现密码加密。本节我们将继续开发商品相关功能,最终目标是基本实现商品的管理功能。
- 查看商品列表;
- 增加一个商品;
- 从excel导入商品:上传文件到服务器,解析把excel数据生成数组,生成过程中验证数据完整性,客户端最好可以完成该操作
- 查看指定商品详情
- 商铺修改某个商品信息
- 商铺删除某个商品;
- index: 返回商品列表
- create: 新增商品
- show: 返回指定的商品信息
- update:修改指定商品
- destroy: 删除指定商品
该命令会生成控制器文件,以及控制器的测试文件。
$ rails generate controller api::v1::products
Running via Spring preloader in process 2280
create app/controllers/api/v1/products_controller.rb
invoke test_unit
create test/controllers/api/v1/products_controller_test.rb
config/routes.rb
Rails.application.routes.draw do
namespace :api, defaults: { format: :json } do
namespace :v1 do
# 添加路由
resources :products, only: [:index, :show, :create, :update, :destroy]
end
end
end
only 是限制当前只开放对这些方法的路由
$ rails routes -c products
--[ Route 1 ]-------------------------------------------------
Prefix | api_v1_products
Verb | GET
URI | /api/v1/products(.:format)
Controller#Action | api/v1/products#index {:format=>:json}
--[ Route 2 ]------------------------------------------------
Prefix |
Verb | POST
URI | /api/v1/products(.:format)
Controller#Action | api/v1/products#create {:format=>:json}
--[ Route 3 ]--------------------------------------------------
Prefix | api_v1_product
Verb | GET
URI | /api/v1/products/:id(.:format)
Controller#Action | api/v1/products#show {:format=>:json}
--[ Route 4 ]--------------------------------------------------
Prefix |
Verb | PATCH
URI | /api/v1/products/:id(.:format)
Controller#Action | api/v1/products#update {:format=>:json}
--[ Route 5 ]--------------------------------------------------
Prefix |
Verb | PUT
URI | /api/v1/products/:id(.:format)
Controller#Action | api/v1/products#update {:format=>:json}
--[ Route 6 ]-------------------------------------------------
Prefix |
Verb | DELETE
URI | /api/v1/products/:id(.:format)
Controller#Action | api/v1/products#destroy {:format=>:json}
思路解析
获取商品列表,可以直接使用product模型相关方法获取,这里要注意分页和排序的情况。
路由是: api/v1/products?page=1&per_page=10
返回值部分我们采用json数据类型的返回值,成功状态码是200。
至于测试部分,我们应该测试两部分
- 状态码应该是200
- 返回值的商品列表,但是有可能是空的,所以这个可以不测试
- 返回值包含分页数据。
test/controllers/api/v1/products_controller_test.rb
require "test_helper"
class Api::V1::productsControllerTest < ActionDispatch::IntegrationTest
test "index_success: should show products" do
get api_v1_products_path, as: :json
assert_response 200
# 测试分页
json_response = JSON.parse(response.body, symbolize_names:true)
assert_not_nil json_response.dig(:links, :first)
assert_not_nil json_response.dig(:links, :last)
assert_not_nil json_response.dig(:links, :prev)
assert_not_nil json_response.dig(:links, :next)
end
end
运行测试会显示一个失败,原因是找不到 index
方法, 这是当然的,我们还没有定义 index
方法。
$ rails test
.........E
Error:
Api::V1::ProductsControllerTest#test_index_success:_should_show_products:
DRb::DRbRemoteError: The action 'index' could not be found for Api::V1::ProductsController
Did you mean? current_user (AbstractController::ActionNotFound)
test/controllers/api/v1/products_controller_test.rb:5:in `block in <class:ProductsControllerTest>'
......
Finished in 2.637243s, 14.4090 runs/s, 20.0967 assertions/s.
38 runs, 53 assertions, 0 failures, 1 errors, 0 skips
因为要json序列化,所以先要创建 product 的序列化文件
$ rails generate serializer Product title price published shop_id
Running via Spring preloader in process 3784
create app/serializers/product_serializer.rb
修改 app/serializers/product_serializer.rb
文件:
class ProductSerializer
include JSONAPI::Serializer
attributes :title, :price, :published, :shop_id
# 添加模型关系
belongs_to :shop
end
同时也要修改 Shop 的序列化文件,添加模型关系:
class ShopSerializer
include JSONAPI::Serializer
attributes :name, :products_count, :orders_count
belongs_to :user
has_many :products
end
app/controllers/api/v1/products_controller.rb
class Api::V1::ProductsController < ApplicationController
def index
@products = Product.page(current_page).per_page(per_page)
option = get_links_serializer_options 'api_v1_products_path', @products
render json: serializer_product(@products, 0, 'ok', option), status: 200
end
private
def serializer_product(product, error_code=0, message='ok', option = {})
product_hash = ProductSerializer.new(product, option).serializable_hash
product_hash['error_code'] = error_code
product_hash['message'] = message
return product_hash
end
end
再次运行测试,这次测试通过了。
$ rails test
# Running:
........
Finished in 2.228403s, 17.0526 runs/s, 26.0276 assertions/s.
38 runs, 58 assertions, 0 failures, 0 errors, 0 skips
请求 api/v1/products
得到的数据:
{
"data": [
{
"id": "1",
"type": "product",
"attributes": {
"title": "123",
"price": 1,
"published": 1,
"shop_id": 1
},
"relationships": {
"shop": {
"data": {
"id": "1",
"type": "shop"
}
}
}
}
],
"links": {
"first": "/api/v1/products?page=1",
"last": "/api/v1/products?page=1",
"prev": "/api/v1/products",
"next": "/api/v1/products"
},
"error_code": 0,
"message": "ok"
}
$ git add .
$ git commit -m "add products_controller index action"
思路解析
获取指定商品,路由是:get:api/v1/products/:id
。
我们首先要获取参数中的商品id,然后根据商品id找到具体的商品,返回商品信息,状态码仍是200。
至于测试部分,我们应该测试两部分
-
状态码应该是200
-
返回值的商品就是我们指定的商品信息。返回值数据类型:
{ "id": "1", "type": "product", "attributes": { "title": "123", "price": 1, "published": 1, "shop_id": 1 }, "relationships": { "shop": { "data": { "id": "1", "type": "shop" } } } }
test/controllers/api/v1/products_controller_test.rb
class Api::V1::productsControllerTest < ActionDispatch::IntegrationTest
setup do
@product = products(:one)
end
# ... ...
test "show_success: should show product" do
get api_v1_product_path(@product), as: :json
json_response = JSON.parse(self.response.body, symbolize_names:true)
# 验证状态码
assert_response 200
# 验证返回数据
assert_equal @product.title, json_response.dig(:data, :attributes, :title)
end
end
运行测试会显示一个失败,原因是找不到 show
方法, 这是当然的,我们还没有定义 show
方法。
$ rails test
Error:
Api::V1::ProductsControllerTest#test_show_success:_should_show_product:
DRb::DRbRemoteError: The action 'show' could not be found for Api::V1::ProductsController
app/controllers/api/v1/products_controller.rb
class Api::V1::productsController < ApplicationController
before_action :set_product, only: [:show]
def show
render json: serializer_product(@product), status:200
end
private
#......
def set_product
@product = Product.find_by_id params[:id].to_i
@product = @product || {}
end
end
再次运行测试,这次测试通过了。
$ rails test
Finished in 0.207461s, 43.3816 runs/s, 48.2018 assertions/s.
9 runs, 10 assertions, 0 failures, 0 errors, 0 skips
请求api/v1/products/1
获取数据:
{
"data": {
"id": "1",
"type": "product",
"attributes": {
"title": "123",
"price": 1,
"published": 1,
"shop_id": 1
},
"relationships": {
"shop": {
"data": {
"id": "1",
"type": "shop"
}
}
}
},
"error_code": 0,
"message": "ok"
}
$ git add .
$ git commit -m "add products_controller show action"
思路解析
新建商品,我们的路由是:post:api/v1/products
。
我们的参数应该类似: {product:{title:'first test', price:1, published:1}}
这个接口应该是要求用户登录状态下操作的,所以一些信息我们需要通过用户信息自动获取,比如 shop_id
首先要创建一个商品对象来接受参数,然后保存!
如果保存成功,则返回成功的信息;否则,返回失败的信息。
至于测试部分,我们应该测试两部分
- 状态码应该是
201
- 添加成功后商品的总数增加 1。
test/controllers/api/v1/products_controller_test.rb
require "test_helper"
class Api::V1::ProductsControllerTest < ActionDispatch::IntegrationTest
setup do
@product = products(:one)
@user = users(:two)
end
# ... ...
test "create_success: should create product" do
# 验证某个值变化了
assert_difference('Product.count', 1) do
post api_v1_products_path,
headers: { Authorization: JsonWebToken.encode(user_id: @user.id) },
params: {product:{title:'first test', price:1, published:1}},
as: :json
end
assert_response 201
end
end
运行测试会显示一个失败,原因是找不到 create
方法, 这是当然的,我们还没有定义 create
方法。
$ rails test
Error:
Api::V1::ProductsControllerTest#test_create_success:_should_create_product:
DRb::DRbRemoteError: The action 'create' could not be found for Api::V1::ProductsController
app/controllers/api/v1/products_controller.rb
class Api::V1::productsController < ApplicationController
def create
@product = current_user&.shop&.products&.build(product_params)
if @product.save
render json: serializer_product(@product), status: 201
else
render json: {error_code:500, message:@product.errors}, status: 201
end
end
private
def product_params
params.require(:product).permit(:title, :price, :published)
end
end
再次运行测试,这次测试通过了。
$ rails test
Finished in 2.333086s, 17.1447 runs/s, 26.5742 assertions/s.
40 runs, 62 assertions, 0 failures, 0 errors, 0 skips
$ git add .
$ git commit -m "add products_controller create action"
思路解析
修改商品,路由是:put:api/v1/products/:id
。
我们的参数应该类似: {product:{title:'first test', price:1, published:1}}
首先要利用商品id找到对应商品,然后把接受到的商品信息赋值给该商品并保存,完成修改!
如果保存成功,则返回成功的信息;否则,返回失败的信息。
但是要注意,该操作需要用户登录并且要修改的商品必须是当前用户的商铺内的商品才可以。
至于测试部分,我们应该测试状态码即可
- 状态码应该是 202
- 错误的用户返回403
test/controllers/api/v1/products_controller_test.rb
class Api::V1::productsControllerTest < ActionDispatch::IntegrationTest
test "update_success: should update product" do
put api_v1_product_path(@product),
headers: { Authorization: JsonWebToken.encode(user_id: @user.id) },
params: {product:{title:'first test', price:1, published:1}},
as: :json
assert_response 202
end
test "update_forbidden: forbidden update product cause unonwer" do
put api_v1_product_path(@product),
headers: { Authorization: JsonWebToken.encode(user_id: users(:one).id) },
params: {product:{title:'first test', price:1, published:1}},
as: :json
assert_response 403
end
end
运行测试会显示一个失败,原因是找不到 update
方法, 这是当然的,我们还没有定义 update
方法。
$ rails test
Error:
Api::V1::ProductsControllerTest#test_update_success:_should_update_product:
DRb::DRbRemoteError: The action 'update' could not be found for Api::V1::ProductsController
app/controllers/api/v1/products_controller.rb
class Api::V1::ProductsController < ApplicationController
before_action :set_product, only: [:update, :show]
before_action :check_owner, only: [:update]
# ... ...
def update
if @product.update(product_params)
render json: serializer_product(@product), status: 202
else
render json: {error_code:500, message:@product.errors}, status: 202
end
end
private
# ... ...
def check_owner
head 403 unless current_user&.shop&.products&.exists?(id:params[:id].to_i)
end
end
再次运行测试,这次测试通过了。
$ rails test
Finished in 2.222468s, 18.8979 runs/s, 28.7968 assertions/s.
42 runs, 64 assertions, 0 failures, 0 errors, 0 skips
$ git add .
$ git commit -m "add products_controller update action"
思路解析
删除商品,我们的路由是:delete:api/v1/products/:id
。
首先要利用商品id找到对应商品,然后直接删除即可!
如果删除成功,则返回成功的信息;否则,返回失败的信息。
当然本操作也要用户登录并且商品是用户店铺内商品
至于测试部分,我们应该测试两种场景
- 成功
- 状态码应该是 204
- 删除成功后商品的总数减少1。
- 失败
- 错误用户返回403
test/controllers/api/v1/products_controller_test.rb
class Api::V1::productsControllerTest < ActionDispatch::IntegrationTest
test "destroy_success: should destroy product" do
# 验证某个值变化了
assert_difference('Product.count', -1) do
delete api_v1_product_path(@product),
headers: { Authorization: JsonWebToken.encode(user_id: @user.id) },
as: :json
end
assert_response 204
end
test "destroy_forbidden: forbidden destroy product cause unowner" do
delete api_v1_product_path(@product),
headers: { Authorization: JsonWebToken.encode(user_id: users(:one).id) },
as: :json
assert_response 403
end
end
运行测试会显示一个失败,原因是找不到 destroy
方法, 这是当然的,我们还没有定义 destroy
方法。
$ rails test
Error:
Api::V1::ProductsControllerTest#test_destroy_forbidden:_forbidden_destroy_product_cause_unowner:
DRb::DRbRemoteError: The action 'destroy' could not be found for Api::V1::ProductsController
app/controllers/api/v1/products_controller.rb
class Api::V1::productsController < ApplicationController
before_action :check_owner, only: [:update, :destroy]
before_action :set_product, only: [:show, :update, :destroy]
def destroy
@product.destroy
head 204
end
end
再次运行测试,这次测试通过了。
$ rails test
Finished in 2.122942s, 20.7260 runs/s, 31.5600 assertions/s.
44 runs, 67 assertions, 0 failures, 0 errors, 0 skips
$ git add .
$ git commit -m "add products_controller destroy action"
$ git checkout master
$ git merge chapter08
我们本节完成了商品控制器的相关方法,并设置了对应路由提供商品在外部直接访问。 现在有了商铺,有了商品,下一节我们就可以让我们的用户购物了,下一节我们一起开发订单模块!