tenpay 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ test
2
+ pkg
3
+ .DS_Store
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 [name of plugin creator]
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,57 @@
1
+ Tenpay
2
+ ======
3
+
4
+ A Ruby on Rails plugin for Tenpay(财付通).
5
+
6
+ 使用
7
+ =======
8
+
9
+ 1. 修改Gemfile:
10
+
11
+ gem "tenpay"
12
+
13
+ 2. 生成配置文件:
14
+
15
+ $ script/rails g tenpay
16
+
17
+ 这个命令会在你的应用的config目录创建一个tenpay.yml文件,默认是快钱提供的测试帐号(关于测试帐号的使用方法请参看测试章节)
18
+ 将tenpay.yml中的商户ID和密钥替换为财付通提供给你的真实ID和密钥然后进行下一步。
19
+
20
+ 3. 下面的代码会创建一笔新订单:
21
+
22
+ @request = Tenpay::Request.new(
23
+ 'test product', # 产品名称
24
+ 1, # 订单编号,必须保持唯一
25
+ Time.now, # 订单生成时间
26
+ 4500, # 订单价格,以分为单位
27
+ 'http://return', # 完成支付后的回调地址
28
+ request.remote_ip, # 用于IP验证,开发模式可忽略
29
+ 'attach data') # 自定义数据
30
+ redirect_to @request.url
31
+
32
+ 上面的代码会将用户重定向到财付通的支付页面。
33
+
34
+ 4. 在用户完成支付后,财付通会调用你在支付请求中提供的返回URL:
35
+
36
+ @response = Tenpay::Response.new(params)
37
+ if @response.successful?
38
+ # 支付成功
39
+ else
40
+ # 失败
41
+ end
42
+
43
+ 注意,快钱可能会多次调用你的返回URL,并将结果展现给用户,因此你的代码要考虑多次执行后的输出对用户的有好度。
44
+
45
+ 查询
46
+ ========
47
+
48
+ 如果需要查询订单支付状态,可以使用下面的接口:
49
+
50
+ @query = Tenpay::Query.new(1, # 订单id
51
+ Time.now) # 订单日期
52
+ @query.response.successful? # true => 已支付, false => 未支付
53
+
54
+ ----------------
55
+ 如果在使用中遇到任何的问题或者建议,欢迎和我联系: zhangyuanyi@gmail.com
56
+
57
+ Copyright (c) 2009 Yuanyi Zhang, released under the MIT license
@@ -0,0 +1,29 @@
1
+ require 'rake'
2
+ require 'rake/rdoctask'
3
+
4
+ desc 'Default: run specs.'
5
+ task :default => :spec
6
+
7
+ desc 'Generate documentation for the tenpay plugin.'
8
+ Rake::RDocTask.new(:rdoc) do |rdoc|
9
+ rdoc.rdoc_dir = 'rdoc'
10
+ rdoc.title = 'Tenpay'
11
+ rdoc.options << '--line-numbers' << '--inline-source'
12
+ rdoc.rdoc_files.include('README')
13
+ rdoc.rdoc_files.include('lib/**/*.rb')
14
+ end
15
+
16
+ begin
17
+ require 'jeweler'
18
+ Jeweler::Tasks.new do |s|
19
+ s.name = "tenpay"
20
+ s.summary = "A Ruby wrapper of Tenpay(财付通) Payment API"
21
+ s.email = "zhangyuanyi@gmail.com"
22
+ s.homepage = "http://github.com/yzhang/tenpay"
23
+ s.description = "A Ruby wrapper of Tenpay(财付通) Payment API"
24
+ s.authors = ["Yuanyi Zhang"]
25
+ s.files = FileList["[A-Z]*", "{lib}/**/*", '.gitignore']
26
+ end
27
+ rescue LoadError
28
+ puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
29
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,9 @@
1
+ test:
2
+ spid: '1900000109'
3
+ key: '8934e7d15453e97507ef794cf7b0519d'
4
+ development:
5
+ spid: '1900000109'
6
+ key: '8934e7d15453e97507ef794cf7b0519d'
7
+ production:
8
+ spid: '1900000109'
9
+ key: '8934e7d15453e97507ef794cf7b0519d'
@@ -0,0 +1,11 @@
1
+ require 'rails/generators'
2
+
3
+ class TenpayGenerator < Rails::Generators::Base
4
+ def install_douban
5
+ copy_file("tenpay.yml", 'config/tenpay.yml')
6
+ end
7
+
8
+ def self.source_root
9
+ File.join(File.dirname(__FILE__), 'templates')
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ require 'tenpay/config'
2
+ require 'tenpay/request'
3
+ require 'tenpay/response'
4
+ require 'tenpay/query'
5
+ require 'tenpay/query_response'
@@ -0,0 +1,18 @@
1
+ module Tenpay
2
+ class Config
3
+ class << self
4
+ def spid; config['spid']; end
5
+ def key; config['key']; end
6
+
7
+ def config
8
+ @@config ||= lambda do
9
+ require 'yaml'
10
+ filename = "#{Rails.root}/config/tenpay.yml"
11
+ file = File.open(filename)
12
+ yaml = YAML.load(file)
13
+ return yaml[Rails.env]
14
+ end.call
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,54 @@
1
+ require 'digest/md5'
2
+ require 'cgi'
3
+ require 'iconv'
4
+ require 'net/http'
5
+ require 'uri'
6
+ require 'hpricot'
7
+
8
+ module Tenpay
9
+ class Query
10
+ GATEWAY_URL = "http://mch.tenpay.com/cgi-bin/cfbi_query_order_v3.cgi"
11
+
12
+ def initialize(order_id, date, attach=nil, charset='UTF-8')
13
+ @cmdno = 1
14
+ @date = date.strftime("%Y%m%d")
15
+ @spid = Tenpay::Config.spid
16
+ @key = Tenpay::Config.key
17
+
18
+ @order_id = order_id.to_i
19
+ @attach = attach || 'nil'
20
+ @charset = charset
21
+ end
22
+
23
+ def response
24
+ @response ||= QueryResponse.new(Net::HTTP.get(URI.parse("#{GATEWAY_URL}?#{params}")))
25
+ end
26
+
27
+ def transaction_id
28
+ @transaction_id ||= "%s%s%010d" % [@spid, @date, @order_id]
29
+ end
30
+
31
+ def sign
32
+ @sign ||= Digest::MD5.hexdigest(sign_params).upcase
33
+ end
34
+
35
+ def sign_params
36
+ @sign_params ||= generate_sign_params
37
+ end
38
+
39
+ private
40
+ def params
41
+ @params ||= generate_params
42
+ end
43
+
44
+ def generate_params
45
+ "cmdno=2&date=#{@date}&bargainor_id=#{@spid}&transaction_id=#{transaction_id}&sp_billno=#{@order_id}&attach=#{@attach}&" +
46
+ "output_xml=1&charset=#{@charset}&sign=#{sign}"
47
+ end
48
+
49
+ def generate_sign_params
50
+ "attach=#{@attach}&bargainor_id=#{@spid}&charset=#{@charset}&cmdno=2&date=#{@date}&output_xml=1&" +
51
+ "sp_billno=#{@order_id}&transaction_id=#{transaction_id}&key=#{@key}"
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,65 @@
1
+ module Tenpay
2
+ class QueryResponse
3
+ def initialize(response)
4
+ @body = response
5
+ end
6
+
7
+ def valid?
8
+ sign == Digest::MD5.hexdigest(sign_params).upcase
9
+ end
10
+
11
+ def successful?
12
+ valid? && pay_result == '0'
13
+ end
14
+
15
+ def doc
16
+ @doc ||= Hpricot(@body)
17
+ end
18
+
19
+ def attach
20
+ @attach ||= (doc / 'attach').inner_text
21
+ end
22
+
23
+ def cmdno
24
+ @cmdno ||= (doc / 'cmdno').inner_text
25
+ end
26
+
27
+ def date
28
+ @date ||= (doc / 'date').inner_text
29
+ end
30
+
31
+ def fee_type
32
+ @fee_type ||= (doc / 'fee_type').inner_text
33
+ end
34
+
35
+ def pay_info
36
+ @pay_info ||= (doc / 'pay_info').inner_text
37
+ end
38
+
39
+ def pay_result
40
+ @pay_result ||= (doc / 'pay_result').inner_text
41
+ end
42
+
43
+ def order_id
44
+ @order_id ||= (doc / 'sp_billno').inner_text
45
+ end
46
+
47
+ def total_fee
48
+ @total_fee ||= (doc / 'total_fee').inner_text
49
+ end
50
+
51
+ def transaction_id
52
+ @transaction_id ||= (doc / 'transaction_id').inner_text
53
+ end
54
+
55
+ def sign
56
+ @sign ||= (doc / 'sign').inner_text
57
+ end
58
+ private
59
+ def sign_params
60
+ "attach=#{attach}&bargainor_id=#{Tenpay::Config.spid}&cmdno=#{cmdno}&date=#{date}&fee_type=#{fee_type}" +
61
+ "&pay_info=#{pay_info}&pay_result=#{pay_result}&sp_billno=#{order_id}&total_fee=#{total_fee}&" +
62
+ "transaction_id=#{transaction_id}&key=#{Tenpay::Config.key}"
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,64 @@
1
+ require 'digest/md5'
2
+ require 'cgi'
3
+ require 'iconv'
4
+
5
+ module Tenpay
6
+ class Request
7
+ GATEWAY_URL = "https://www.tenpay.com/cgi-bin/v1.0/pay_gate.cgi"
8
+
9
+ def initialize(description, order_id, created_at, total_fee,
10
+ return_url, purchaser_ip='', attach=nil)
11
+ @cmdno = 1
12
+ @date = created_at.strftime("%Y%m%d")
13
+ @bank_type = 0
14
+ @fee_type = 1
15
+
16
+ @desc = Iconv.iconv('gbk', 'utf-8', description).join
17
+ @spid = Tenpay::Config.spid
18
+ @key = Tenpay::Config.key
19
+
20
+ @order_id = order_id.to_i
21
+ @total_fee = total_fee.to_i
22
+ @return_url = return_url
23
+ @attach = attach || 'nil'
24
+
25
+ @purchaser_ip = (RAILS_ENV == 'production' ? purchaser_ip : '')
26
+ end
27
+
28
+ def transaction_id
29
+ @transaction_id ||= "%s%s%010d" % [@spid, @date, @order_id]
30
+ end
31
+
32
+ def sign
33
+ @sign ||= Digest::MD5.hexdigest(sign_params).upcase
34
+ end
35
+
36
+ def sign_params
37
+ @sign_params ||= generate_sign_params
38
+ end
39
+
40
+ def params
41
+ @params ||= generate_params
42
+ end
43
+
44
+ def url
45
+ "#{GATEWAY_URL}?#{params}&sign=#{sign}"
46
+ end
47
+
48
+ private
49
+ def generate_params
50
+ params = "cmdno=#{@cmdno}&date=#{@date}&bargainor_id=#{@spid}&transaction_id=#{transaction_id}" +
51
+ "&sp_billno=#{@order_id}&total_fee=#{@total_fee}&fee_type=#{@fee_type}" +
52
+ "&return_url=#{@return_url}&attach=#{CGI.escape(@attach)}&desc=#{CGI.escape(@desc)}"
53
+ params << "&spbill_create_ip=#{@purchaser_ip}" unless @purchaser_ip.nil? || @purchaser_ip == ''
54
+ params
55
+ end
56
+ def generate_sign_params
57
+ sign_params = "cmdno=#{@cmdno}&date=#{@date}&bargainor_id=#{@spid}&transaction_id=#{transaction_id}" +
58
+ "&sp_billno=#{@order_id}&total_fee=#{@total_fee}&fee_type=#{@fee_type}" +
59
+ "&return_url=#{@return_url}&attach=#{CGI.escape(@attach)}"
60
+ sign_params << "&spbill_create_ip=#{@purchaser_ip}" unless @purchaser_ip.nil? || @purchaser_ip == ''
61
+ sign_params << "&key=#{@key}"
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,41 @@
1
+ require 'digest/md5'
2
+ require 'cgi'
3
+ require 'iconv'
4
+
5
+ module Tenpay
6
+ class Response
7
+ attr_reader :order_id, :total_fee, :attach, :pay_time
8
+
9
+ def initialize(params)
10
+ @cmdno = params[:cmdno] || ''
11
+ @date = params[:date] || ''
12
+ @fee_type = params[:fee_type] || ''
13
+ @pay_info = params[:pay_info] || ''
14
+ @pay_result = params[:pay_result] || ''
15
+ @pay_time = Time.at((params[:pay_time] || '0').to_i)
16
+ @sign = params[:sign] || ''
17
+ @order_id = (params[:sp_billno] || '').to_i
18
+ @transaction_id = params[:transaction_id] || ''
19
+ @total_fee = params[:total_fee] || ''
20
+ @attach = params[:attach] || ''
21
+
22
+ @spid = Tenpay::Config.spid
23
+ @key = Tenpay::Config.key
24
+ end
25
+
26
+ def successful?
27
+ @pay_info == 'OK' && @pay_result == '0' && valid_sign?
28
+ end
29
+
30
+ def valid_sign?
31
+ @sign == Digest::MD5.hexdigest(sign_params).upcase
32
+ end
33
+
34
+ private
35
+ def sign_params
36
+ @sign_params ||= "cmdno=#{@cmdno}&pay_result=#{@pay_result}&date=#{@date}&transaction_id=#{@transaction_id}" +
37
+ "&sp_billno=#{@order_id}&total_fee=#{@total_fee}&fee_type=#{@fee_type}" +
38
+ "&attach=#{CGI.escape(@attach)}&key=#{@key}"
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,80 @@
1
+ ENV["RAILS_ENV"] = "test"
2
+ require 'spec'
3
+ require File.expand_path(File.dirname(__FILE__) + "/../../../../config/environment")
4
+ require "tenpay"
5
+ require 'net/http'
6
+
7
+ describe Tenpay::Request do
8
+ before do
9
+ @query = Tenpay::Query.new(1, Date.new(2009, 9, 12))
10
+ @sign_params = "attach=nil&bargainor_id=1900000109&charset=UTF-8&cmdno=2&date=20090912&output_xml=1&" +
11
+ "sp_billno=1&transaction_id=1900000109200909120000000001&key=8934e7d15453e97507ef794cf7b0519d"
12
+ @sign = Digest::MD5.hexdigest(@sign_params).upcase
13
+ @params = "cmdno=2&date=20090912&bargainor_id=1900000109&transaction_id=1900000109200909120000000001" +
14
+ "&sp_billno=1&attach=nil&output_xml=1&charset=UTF-8&sign=#{@sign}"
15
+ end
16
+
17
+ it "should return a Query Response object." do
18
+ @uri = mock('uri')
19
+ URI.should_receive(:parse).with("#{Tenpay::Query::GATEWAY_URL}?#{@params}").and_return(@uri)
20
+ Net::HTTP.should_receive(:get).with(@uri)
21
+ Tenpay::QueryResponse.stub!(:new).and_return(mock('response'))
22
+ @query.response
23
+ end
24
+
25
+ it "should construct sign params properly." do
26
+ @query.sign_params.should == @sign_params
27
+ end
28
+ end
29
+
30
+ describe Tenpay::QueryResponse do
31
+ before do
32
+ @valid_response = VALID_XML
33
+ @invalid_response = ''
34
+ end
35
+
36
+ it "should be invalid with invalid response" do
37
+ @response = Tenpay::QueryResponse.new(@invalid_response)
38
+ @response.should_not be_valid
39
+ end
40
+
41
+ it "should be valid with valid response" do
42
+ @response = Tenpay::QueryResponse.new(@valid_response)
43
+ @response.attach.should == 'a'
44
+ @response.cmdno.should == '2'
45
+ @response.date.should == '20090315'
46
+ @response.fee_type.should == '1'
47
+ @response.pay_info.should == 'test'
48
+ @response.pay_result.should == '2006'
49
+ @response.order_id.should == '56212087'
50
+ @response.total_fee.should == '3000'
51
+ @response.transaction_id.should == '1900000108200903150056212087'
52
+ @response.sign.should == 'A8D50F1DC2E2E4CC83191AE39C702C27'
53
+
54
+ @response.should be_valid
55
+ end
56
+
57
+ VALID_XML = %(<?xml version="1.0" encoding="GB2312" ?>
58
+ <root>
59
+ <attach>a</attach>
60
+ <bank_type />
61
+ <bargainor_id>1900000108</bargainor_id>
62
+ <bill_no />
63
+ <charset>GB2312</charset>
64
+ <cmdno>2</cmdno>
65
+ <date>20090315</date>
66
+ <dump_html_file />
67
+ <fee_type>1</fee_type>
68
+ <output_xml>1</output_xml>
69
+ <pay_info>test</pay_info>
70
+ <pay_result>2006</pay_result>
71
+ <retcode>00</retcode>
72
+ <retmsg>交易成功</retmsg>
73
+ <return_url>http://192.168.1.129/bank/cft_return.aspx</return_url>
74
+ <sign>A8D50F1DC2E2E4CC83191AE39C702C27</sign>
75
+ <sp_billno>56212087</sp_billno>
76
+ <total_fee>3000</total_fee>
77
+ <transaction_id>1900000108200903150056212087</transaction_id>
78
+ </root>
79
+ )
80
+ end
@@ -0,0 +1,33 @@
1
+ ENV["RAILS_ENV"] = "test"
2
+ require 'spec'
3
+ require File.expand_path(File.dirname(__FILE__) + "/../../../../config/environment")
4
+ require "tenpay"
5
+
6
+ describe Tenpay::Request do
7
+ before do
8
+ @request = Tenpay::Request.new('test product', 1,
9
+ 4500, 'http://return', '', 'order_id=1')
10
+ today = Date.today
11
+ @date = "%d%02d%02d" % [today.year, today.month, today.day]
12
+ @params = "cmdno=1&date=#{@date}&bargainor_id=1900000109&" +
13
+ "transaction_id=1900000109#{@date}0000000001&sp_billno=1&total_fee=4500&fee_type=1" +
14
+ "&return_url=#{'http://return'}&attach=#{CGI.escape('order_id=1')}"
15
+ @sign = Digest::MD5.hexdigest("#{@params}&key=8934e7d15453e97507ef794cf7b0519d").upcase
16
+ end
17
+
18
+ it "should construct transaction correctly." do
19
+ @request.transaction_id.should == "1900000109#{@date}0000000001"
20
+ end
21
+
22
+ it "should construct params correctly." do
23
+ @request.params.should == "#{@params}&desc=test product"
24
+ end
25
+
26
+ it "should sign the params" do
27
+ @request.sign.should == @sign
28
+ end
29
+
30
+ it "should generate url for user paying" do
31
+ @request.url.should == "https://www.tenpay.com/cgi-bin/v1.0/pay_gate.cgi?#{@params}&desc=test product&sign=#{@sign}"
32
+ end
33
+ end
@@ -0,0 +1,26 @@
1
+ ENV["RAILS_ENV"] = "test"
2
+ require 'spec'
3
+ require File.expand_path(File.dirname(__FILE__) + "/../../../../config/environment")
4
+ require "tenpay"
5
+
6
+ describe Tenpay::Response do
7
+ before do
8
+ @valid_params = {:cmdno => '1', :date => '20090818',
9
+ :fee_type => '1', :pay_info => 'OK', :pay_result => '0',
10
+ :pay_time => '1250554120',
11
+ :sign => 'D2171C4319EE7A7A8008F1A4479C8C93',
12
+ :sp_billno => '2',
13
+ :transaction_id => '1900000109200908180000000002',
14
+ :total_fee => '1',
15
+ :attach => 'nil'}
16
+ end
17
+
18
+ it "should be successful with valid params and key" do
19
+ Tenpay::Response.new(@valid_params).should be_successful
20
+ end
21
+
22
+ it "should not be successful with invalid params and key" do
23
+ @valid_params[:date] = '20100912'
24
+ Tenpay::Response.new(@valid_params).should_not be_successful
25
+ end
26
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tenpay
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Yuanyi Zhang
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-07-06 00:00:00 +08:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: "A Ruby wrapper of Tenpay(\xE8\xB4\xA2\xE4\xBB\x98\xE9\x80\x9A) Payment API"
22
+ email: zhangyuanyi@gmail.com
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files:
28
+ - README
29
+ files:
30
+ - .gitignore
31
+ - MIT-LICENSE
32
+ - README
33
+ - Rakefile
34
+ - VERSION
35
+ - lib/generators/tenpay/templates/tenpay.yml
36
+ - lib/generators/tenpay/tenpay_generator.rb
37
+ - lib/tenpay.rb
38
+ - lib/tenpay/config.rb
39
+ - lib/tenpay/query.rb
40
+ - lib/tenpay/query_response.rb
41
+ - lib/tenpay/request.rb
42
+ - lib/tenpay/response.rb
43
+ has_rdoc: true
44
+ homepage: http://github.com/yzhang/tenpay
45
+ licenses: []
46
+
47
+ post_install_message:
48
+ rdoc_options:
49
+ - --charset=UTF-8
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ segments:
57
+ - 0
58
+ version: "0"
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ segments:
64
+ - 0
65
+ version: "0"
66
+ requirements: []
67
+
68
+ rubyforge_project:
69
+ rubygems_version: 1.3.6
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: "A Ruby wrapper of Tenpay(\xE8\xB4\xA2\xE4\xBB\x98\xE9\x80\x9A) Payment API"
73
+ test_files:
74
+ - spec/query_spec.rb
75
+ - spec/request_spec.rb
76
+ - spec/response_spec.rb