mirror of
https://github.com/ditkrg/rswag.git
synced 2026-01-24 23:06:41 +00:00
assert response body based on spec examples
This commit is contained in:
parent
071239727a
commit
9ad7a49e6d
1
Gemfile
1
Gemfile
@ -13,6 +13,7 @@ gemspec
|
|||||||
# To use a debugger
|
# To use a debugger
|
||||||
# gem 'debugger', group: [:development, :test]
|
# gem 'debugger', group: [:development, :test]
|
||||||
|
|
||||||
|
gem 'sqlite3'
|
||||||
gem 'pry'
|
gem 'pry'
|
||||||
gem 'rspec-rails'
|
gem 'rspec-rails'
|
||||||
gem 'generator_spec'
|
gem 'generator_spec'
|
||||||
|
|||||||
@ -105,6 +105,7 @@ GEM
|
|||||||
multi_json (~> 1.0)
|
multi_json (~> 1.0)
|
||||||
rack (~> 1.0)
|
rack (~> 1.0)
|
||||||
tilt (~> 1.1, != 1.3.0)
|
tilt (~> 1.1, != 1.3.0)
|
||||||
|
sqlite3 (1.3.11)
|
||||||
thor (0.19.1)
|
thor (0.19.1)
|
||||||
tilt (1.4.1)
|
tilt (1.4.1)
|
||||||
treetop (1.4.15)
|
treetop (1.4.15)
|
||||||
@ -119,4 +120,5 @@ DEPENDENCIES
|
|||||||
generator_spec
|
generator_spec
|
||||||
pry
|
pry
|
||||||
rspec-rails
|
rspec-rails
|
||||||
|
sqlite3
|
||||||
swagger_rails!
|
swagger_rails!
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
module SwaggerRails
|
module SwaggerRails
|
||||||
|
|
||||||
class TestDataBuilder
|
class TestCaseBuilder
|
||||||
|
|
||||||
def initialize(path_template, http_method, swagger)
|
def initialize(path_template, http_method, swagger)
|
||||||
@path_template = path_template
|
@path_template = path_template
|
||||||
@http_method = http_method
|
@http_method = http_method
|
||||||
@swagger = swagger
|
@swagger = swagger
|
||||||
@param_values = {}
|
@param_values = {}
|
||||||
@expected_status = nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def set(param_values)
|
def set(param_values)
|
||||||
@ -15,7 +14,7 @@ module SwaggerRails
|
|||||||
end
|
end
|
||||||
|
|
||||||
def expect(status)
|
def expect(status)
|
||||||
@expected_status = status
|
@expected_status = status.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_data
|
def test_data
|
||||||
@ -52,35 +51,59 @@ module SwaggerRails
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_params(parameters)
|
def build_params(parameters)
|
||||||
{}.tap do |params|
|
body_param_values = param_values_for(parameters, 'body')
|
||||||
params.merge!(param_values_for(parameters, 'query'))
|
return body_param_values.values.first.to_json if body_param_values.any?
|
||||||
body_param_values = param_values_for(parameters, 'body')
|
param_values_for(parameters, 'query')
|
||||||
params.merge!(body_param_values.values.first) if body_param_values.any?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_headers(parameters)
|
def build_headers(parameters)
|
||||||
param_values_for(parameters, 'header')
|
param_values_for(parameters, 'header')
|
||||||
|
.merge({
|
||||||
|
'CONTENT_TYPE' => 'application/json',
|
||||||
|
'ACCEPT' => 'application/json'
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_expected_response(responses)
|
def build_expected_response(responses)
|
||||||
|
status = @expected_status || responses.keys.find { |k| k.start_with?('2') }
|
||||||
|
response = responses[status] || (raise MetadataError.new('paths', @path_template, @http_method, 'responses', status))
|
||||||
|
{
|
||||||
|
status: status.to_i,
|
||||||
|
body: response_body_for(response)
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def param_values_for(parameters, location)
|
def param_values_for(parameters, location)
|
||||||
applicable_parameters = parameters.select { |p| p['in'] == location }
|
applicable_parameters = parameters.select { |p| p['in'] == location }
|
||||||
Hash[applicable_parameters.map { |p| [ p['name'], value_for(p) ] }]
|
Hash[applicable_parameters.map { |p| [ p['name'], param_value_for(p) ] }]
|
||||||
end
|
end
|
||||||
|
|
||||||
def value_for(param)
|
def param_value_for(parameter)
|
||||||
return @param_values[param['name']] if @param_values.has_key?(param['name'])
|
return @param_values[parameter['name']] if @param_values.has_key?(parameter['name'])
|
||||||
return param['default'] unless param['in'] == 'body'
|
return parameter['default'] unless parameter['in'] == 'body'
|
||||||
schema_for(param['schema'])['example']
|
schema = schema_for(parameter['schema'])
|
||||||
|
schema_example_for(schema)
|
||||||
|
end
|
||||||
|
|
||||||
|
def response_body_for(response)
|
||||||
|
return nil if response['schema'].nil?
|
||||||
|
schema = schema_for(response['schema'])
|
||||||
|
schema_example_for(schema)
|
||||||
end
|
end
|
||||||
|
|
||||||
def schema_for(schema_or_ref)
|
def schema_for(schema_or_ref)
|
||||||
return schema_or_ref if schema_or_ref['$ref'].nil?
|
return schema_or_ref if schema_or_ref['$ref'].nil?
|
||||||
@swagger['definitions'][schema_or_ref['$ref'].sub('#/definitions/', '')]
|
@swagger['definitions'][schema_or_ref['$ref'].sub('#/definitions/', '')]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def schema_example_for(schema)
|
||||||
|
return schema['example'] if schema['example'].present?
|
||||||
|
# If an array, try construct from the item example
|
||||||
|
if schema['type'] == 'array' && schema['item'].present?
|
||||||
|
item_schema = schema_for(schema['item'])
|
||||||
|
return [ schema_example_for(item_schema) ]
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class MetadataError < StandardError
|
class MetadataError < StandardError
|
||||||
@ -1,4 +1,4 @@
|
|||||||
require 'swagger_rails/testing/test_data_builder'
|
require 'swagger_rails/testing/test_case_builder'
|
||||||
|
|
||||||
module SwaggerRails
|
module SwaggerRails
|
||||||
|
|
||||||
@ -9,7 +9,7 @@ module SwaggerRails
|
|||||||
end
|
end
|
||||||
|
|
||||||
def run_test(path_template, http_method, test, &block)
|
def run_test(path_template, http_method, test, &block)
|
||||||
builder = TestDataBuilder.new(path_template, http_method, @swagger)
|
builder = TestCaseBuilder.new(path_template, http_method, @swagger)
|
||||||
builder.instance_exec(&block) if block_given?
|
builder.instance_exec(&block) if block_given?
|
||||||
test_data = builder.test_data
|
test_data = builder.test_data
|
||||||
|
|
||||||
@ -19,7 +19,8 @@ module SwaggerRails
|
|||||||
test_data[:headers]
|
test_data[:headers]
|
||||||
)
|
)
|
||||||
|
|
||||||
test.assert_response(test_data[:expected_status])
|
test.assert_response(test_data[:expected_response][:status])
|
||||||
|
test.assert_equal(test_data[:expected_response][:body], JSON.parse(test.response.body))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,14 +1,22 @@
|
|||||||
class BlogsController < ApplicationController
|
class BlogsController < ApplicationController
|
||||||
|
wrap_parameters Blog
|
||||||
|
respond_to :json
|
||||||
|
|
||||||
|
# POST /blogs
|
||||||
def create
|
def create
|
||||||
render json: {}
|
@blog = Blog.create(params[:blog])
|
||||||
|
respond_with @blog
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# GET /blogs
|
||||||
def index
|
def index
|
||||||
render json: {}
|
@blogs = Blog.all
|
||||||
|
respond_with @blogs
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# GET /blogs/1
|
||||||
def show
|
def show
|
||||||
render json: {}
|
@blog = Blog.find(params[:id])
|
||||||
|
respond_with @blog
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
10
spec/dummy/app/models/blog.rb
Normal file
10
spec/dummy/app/models/blog.rb
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
class Blog < ActiveRecord::Base
|
||||||
|
attr_accessible :content, :title
|
||||||
|
|
||||||
|
def as_json(options)
|
||||||
|
{
|
||||||
|
title: title,
|
||||||
|
content: content
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -1,14 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Dummy</title>
|
|
||||||
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
|
|
||||||
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
|
|
||||||
<%= csrf_meta_tags %>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<%= yield %>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
require File.expand_path('../boot', __FILE__)
|
require File.expand_path('../boot', __FILE__)
|
||||||
|
|
||||||
# Pick the frameworks you want:
|
# Pick the frameworks you want:
|
||||||
# require "active_record/railtie"
|
require "active_record/railtie"
|
||||||
require "action_controller/railtie"
|
require "action_controller/railtie"
|
||||||
require "action_mailer/railtie"
|
require "action_mailer/railtie"
|
||||||
require "action_view/railtie"
|
require "action_view/railtie"
|
||||||
|
|||||||
25
spec/dummy/config/database.yml
Normal file
25
spec/dummy/config/database.yml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
#SQLite version 3.x
|
||||||
|
# gem install sqlite3
|
||||||
|
#
|
||||||
|
# Ensure the SQLite 3 gem is defined in your Gemfile
|
||||||
|
# gem 'sqlite3'
|
||||||
|
development:
|
||||||
|
adapter: sqlite3
|
||||||
|
database: db/development.sqlite3
|
||||||
|
pool: 5
|
||||||
|
timeout: 5000
|
||||||
|
|
||||||
|
# Warning: The database defined as "test" will be erased and
|
||||||
|
# re-generated from your development database when you run "rake".
|
||||||
|
# Do not set this db to the same as development or production.
|
||||||
|
test:
|
||||||
|
adapter: sqlite3
|
||||||
|
database: db/test.sqlite3
|
||||||
|
pool: 5
|
||||||
|
timeout: 5000
|
||||||
|
|
||||||
|
production:
|
||||||
|
adapter: sqlite3
|
||||||
|
database: db/production.sqlite3
|
||||||
|
pool: 5
|
||||||
|
timeout: 5000
|
||||||
@ -1,5 +1,5 @@
|
|||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
mount SwaggerRails::Engine => '/api-docs'
|
resources :blogs, defaults: { :format => :json }
|
||||||
|
|
||||||
resources :blogs, only: [ :create, :index, :show ]
|
mount SwaggerRails::Engine => '/api-docs'
|
||||||
end
|
end
|
||||||
|
|||||||
@ -24,8 +24,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"201": {
|
||||||
"description": "Ok",
|
"description": "Created",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/Blog"
|
"$ref": "#/definitions/Blog"
|
||||||
}
|
}
|
||||||
@ -58,7 +58,10 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"description": "Ok",
|
"description": "Ok",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/Blog"
|
"type": "array",
|
||||||
|
"item": {
|
||||||
|
"$ref": "#/definitions/Blog"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
spec/dummy/db/migrate/20160218212104_create_blogs.rb
Normal file
10
spec/dummy/db/migrate/20160218212104_create_blogs.rb
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
class CreateBlogs < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :blogs do |t|
|
||||||
|
t.string :title
|
||||||
|
t.text :content
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
23
spec/dummy/db/schema.rb
Normal file
23
spec/dummy/db/schema.rb
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# encoding: UTF-8
|
||||||
|
# This file is auto-generated from the current state of the database. Instead
|
||||||
|
# of editing this file, please use the migrations feature of Active Record to
|
||||||
|
# incrementally modify your database, and then regenerate this schema definition.
|
||||||
|
#
|
||||||
|
# Note that this schema.rb definition is the authoritative source for your
|
||||||
|
# database schema. If you need to create the application database on another
|
||||||
|
# system, you should be using db:schema:load, not running all the migrations
|
||||||
|
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
|
||||||
|
# you'll amass, the slower it'll run and the greater likelihood for issues).
|
||||||
|
#
|
||||||
|
# It's strongly recommended to check this file into your version control system.
|
||||||
|
|
||||||
|
ActiveRecord::Schema.define(:version => 20160218212104) do
|
||||||
|
|
||||||
|
create_table "blogs", :force => true do |t|
|
||||||
|
t.string "title"
|
||||||
|
t.text "content"
|
||||||
|
t.datetime "created_at", :null => false
|
||||||
|
t.datetime "updated_at", :null => false
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@ -5,5 +5,14 @@ class V1ContractTest < ActionDispatch::IntegrationTest
|
|||||||
include SwaggerRails::TestHelpers
|
include SwaggerRails::TestHelpers
|
||||||
|
|
||||||
swagger_doc 'v1/swagger.json'
|
swagger_doc 'v1/swagger.json'
|
||||||
swagger_test_all
|
#
|
||||||
|
# test '/blogs post' do
|
||||||
|
# swagger_test '/blogs', 'post'
|
||||||
|
# end
|
||||||
|
|
||||||
|
test '/blogs get' do
|
||||||
|
blog = Blog.create(title: 'Test Blog', content: 'Hello World')
|
||||||
|
|
||||||
|
swagger_test '/blogs', 'get'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
require 'swagger_rails/testing/test_data_builder'
|
require 'swagger_rails/testing/test_case_builder'
|
||||||
|
|
||||||
module SwaggerRails
|
module SwaggerRails
|
||||||
|
|
||||||
describe TestDataBuilder do
|
describe TestCaseBuilder do
|
||||||
subject { described_class.new(path, method, swagger) }
|
subject { described_class.new(path, method, swagger) }
|
||||||
let(:swagger) do
|
let(:swagger) do
|
||||||
file_path = File.join(Rails.root, 'config/swagger', 'v1/swagger.json')
|
file_path = File.join(Rails.root, 'config/swagger', 'v1/swagger.json')
|
||||||
@ -59,20 +59,20 @@ module SwaggerRails
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'operation has body params' do
|
context 'operation has body param' do
|
||||||
let(:path) { '/blogs' }
|
let(:path) { '/blogs' }
|
||||||
let(:method) { 'post' }
|
let(:method) { 'post' }
|
||||||
|
|
||||||
context 'by default' do
|
context 'by default' do
|
||||||
it "includes params built from 'default' values" do
|
it "includes params string based on schema 'example'" do
|
||||||
expect(test_data[:params]).to eq({ 'title' => 'Test Blog', 'content' => 'Hello World' })
|
expect(test_data[:params]).to eq({ 'title' => 'Test Blog', 'content' => 'Hello World' }.to_json)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'values explicitly set' do
|
context 'values explicitly set' do
|
||||||
before { subject.set blog: { 'title' => 'foobar' } }
|
before { subject.set blog: { 'title' => 'foobar' } }
|
||||||
it 'includes params build from set values' do
|
it 'includes params string based on set value' do
|
||||||
expect(test_data[:params]).to eq({ 'title' => 'foobar' })
|
expect(test_data[:params]).to eq({ 'title' => 'foobar' }.to_json)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -83,14 +83,54 @@ module SwaggerRails
|
|||||||
|
|
||||||
context 'by default' do
|
context 'by default' do
|
||||||
it "includes headers built from 'default' values" do
|
it "includes headers built from 'default' values" do
|
||||||
expect(test_data[:headers]).to eq({ 'X-Forwarded-For' => 'client1' })
|
expect(test_data[:headers]).to eq({
|
||||||
|
'X-Forwarded-For' => 'client1',
|
||||||
|
'CONTENT_TYPE' => 'application/json',
|
||||||
|
'ACCEPT' => 'application/json'
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'values explicitly params' do
|
context 'values explicitly params' do
|
||||||
before { subject.set 'X-Forwarded-For' => '192.168.1.1' }
|
before { subject.set 'X-Forwarded-For' => '192.168.1.1' }
|
||||||
it 'includes headers built from set values' do
|
it 'includes headers built from set values' do
|
||||||
expect(test_data[:headers]).to eq({ 'X-Forwarded-For' => '192.168.1.1' })
|
expect(test_data[:headers]).to eq({
|
||||||
|
'X-Forwarded-For' => '192.168.1.1',
|
||||||
|
'CONTENT_TYPE' => 'application/json',
|
||||||
|
'ACCEPT' => 'application/json'
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'operation returns an object' do
|
||||||
|
let(:path) { '/blogs' }
|
||||||
|
let(:method) { 'post' }
|
||||||
|
|
||||||
|
context 'by default' do
|
||||||
|
it "includes expected_response based on spec'd 2xx status" do
|
||||||
|
expect(test_data[:expected_response][:status]).to eq(201)
|
||||||
|
expect(test_data[:expected_response][:body]).to eq({ 'title' => 'Test Blog', 'content' => 'Hello World' })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'expected status explicitly set' do
|
||||||
|
before { subject.expect 400 }
|
||||||
|
it "includes expected_response based on set status" do
|
||||||
|
expect(test_data[:expected_response][:status]).to eq(400)
|
||||||
|
expect(test_data[:expected_response][:body]).to eq({ 'title' => [ 'is required' ] })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'operation returns an array' do
|
||||||
|
let(:path) { '/blogs' }
|
||||||
|
let(:method) { 'get' }
|
||||||
|
|
||||||
|
context 'by default' do
|
||||||
|
it "includes expected_response based on spec'd 2xx status" do
|
||||||
|
expect(test_data[:expected_response][:status]).to eq(200)
|
||||||
|
expect(test_data[:expected_response][:body]).to eq([ { 'title' => 'Test Blog', 'content' => 'Hello World' } ])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -12,19 +12,30 @@ module SwaggerRails
|
|||||||
let(:test) { spy('test') }
|
let(:test) { spy('test') }
|
||||||
|
|
||||||
describe '#run_test' do
|
describe '#run_test' do
|
||||||
|
before do
|
||||||
|
allow(test).to receive(:response).and_return(OpenStruct.new(body: "{}"))
|
||||||
|
end
|
||||||
|
|
||||||
context 'by default' do
|
context 'by default' do
|
||||||
before { subject.run_test('/blogs', 'post', test) }
|
before { subject.run_test('/blogs', 'post', test) }
|
||||||
|
|
||||||
it "submits request based on 'default' and 'example' param values" do
|
it "submits request based on 'default' and 'example' param values" do
|
||||||
expect(test).to have_received(:post).with(
|
expect(test).to have_received(:post).with(
|
||||||
'/blogs',
|
'/blogs',
|
||||||
{ 'title' => 'Test Blog', 'content' => 'Hello World' },
|
{ 'title' => 'Test Blog', 'content' => 'Hello World' }.to_json,
|
||||||
{ 'X-Forwarded-For' => 'client1' }
|
{ 'X-Forwarded-For' => 'client1', 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "asserts response matches first 2xx status in operation 'responses'" do
|
it "asserts response matches spec'd 2xx status" do
|
||||||
expect(test).to have_received(:assert_response).with(200)
|
expect(test).to have_received(:assert_response).with(201)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "asserts response body matches schema 'example' for 2xx status" do
|
||||||
|
expect(test).to have_received(:assert_equal).with(
|
||||||
|
{ 'title' => 'Test Blog', 'content' => 'Hello World' },
|
||||||
|
{}
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -39,22 +50,29 @@ module SwaggerRails
|
|||||||
it 'submits a request based on provided param values' do
|
it 'submits a request based on provided param values' do
|
||||||
expect(test).to have_received(:post).with(
|
expect(test).to have_received(:post).with(
|
||||||
'/blogs',
|
'/blogs',
|
||||||
{ 'title' => 'foobar' },
|
{ 'title' => 'foobar' }.to_json,
|
||||||
{ 'X-Forwarded-For' => '192.168.1.1' }
|
{ 'X-Forwarded-For' => '192.168.1.1', 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'expected status explicitly params' do
|
context 'expected status explicitly set' do
|
||||||
before do
|
before do
|
||||||
subject.run_test('/blogs', 'post', test) do
|
subject.run_test('/blogs', 'post', test) do
|
||||||
expect 400
|
expect 400
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "asserts response matches params status" do
|
it "asserts response matches set status" do
|
||||||
expect(test).to have_received(:assert_response).with(400)
|
expect(test).to have_received(:assert_response).with(400)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "asserts response body matches schema 'example' for set status" do
|
||||||
|
expect(test).to have_received(:assert_equal).with(
|
||||||
|
{ 'title' => [ 'is required' ] },
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user