magpie 0.8.6.1

Sign up to get free protection for your applications and to get access to all the features.
data/COPYING ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2010 jiangguimin <kayak.jaing@gmail.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to
5
+ deal in the Software without restriction, including without limitation the
6
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
+ sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,37 @@
1
+ == 快搭
2
+ sudo gem install magpie
3
+
4
+ 打开终端
5
+ > mag magpie.yml
6
+
7
+ magpie.yml文件用来配置你的商号信息, 假设你在支付宝有个账号:123456, key是:aaabbb, 网银在线有个账号:789789, key是:cccddd
8
+ 那么你在magpie.yml中这样写:
9
+ alipay:
10
+ -["123456", "aaabbb"]
11
+
12
+ chinabank:
13
+ -["789789", "cccddd"]
14
+
15
+ mag命令默认会在本地9292端口启动http服务, 你可以用-p选项指定端口
16
+ mag -p 2010 magpie.yml
17
+
18
+ mag命令的详细帮助可以通过mag -h查看
19
+
20
+ === 使用示例
21
+ 假设你正在实现支付宝支付的相关代码
22
+ 首先启动magpie服务
23
+ > mag magpie.yml
24
+ 然后在你开发的商户系统中,将支付网关由支付宝的网关https://www.alipay.com/cooperate/gateway.do更改为magpie的网关http://127.0.0.1:9292/alipay
25
+ 如果你请求的参数出现错误,你可以通过magpie的日志看到详细的出错信息
26
+ 如果你的支付请求成功, magpie将会模拟支付宝的主动通知模式, 给你商户系统发送通知, 你需要确保发送给magpie的notify_url是可用的,magpie将通过这个
27
+ notify_url将支付成功的通知发到你的商户系统中, 这样你就可以避免去支付宝的页面进行真实的支付.
28
+
29
+ 对于网银在线, 将支付网关由网银在线的网关https://pay3.chinabank.com.cn/PayGate更改为magpie的网关http://127.0.0.1:9292/chinabank
30
+
31
+
32
+
33
+
34
+
35
+
36
+
37
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << "test"
5
+ t.test_files = FileList['test/test_*.rb']
6
+ end
data/bin/mag ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require "magpie"
3
+
4
+ Magpie::Server.start
data/lib/magpie.rb ADDED
@@ -0,0 +1,49 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'open-uri'
3
+ require 'hpricot'
4
+ require 'iconv'
5
+ require 'rack'
6
+ require 'active_model'
7
+
8
+
9
+ module Magpie
10
+ VERSION = [0, 8, 6]
11
+
12
+ class << self
13
+ attr_accessor :yml_db
14
+
15
+ def version
16
+ VERSION.join(".")
17
+ end
18
+ end
19
+
20
+ autoload :Utils, "magpie/utils"
21
+ autoload :Mothlog, "middles/mothlog"
22
+ autoload :Alipay, "middles/alipay"
23
+ autoload :Chinabank, "middles/chinabank"
24
+ autoload :Server, "magpie/server"
25
+
26
+
27
+ APP = Rack::Builder.new {
28
+ use Mothlog
29
+
30
+ map "/alipay" do
31
+ use Alipay
32
+ run lambda{ |env| [200, {"Content-Type" => "text/xml"}, [""]]}
33
+ end
34
+
35
+ map "/chinabank" do
36
+ use Chinabank
37
+ run lambda { |env| [200, { "Content-Type" => "text/xml"}, [""]]}
38
+ end
39
+
40
+ map "/" do
41
+ run lambda{ |env| [200, {"Content-Type" => "text/html"}, ["magpie"]]}
42
+ end
43
+
44
+ }.to_app
45
+
46
+ end
47
+
48
+
49
+
@@ -0,0 +1,82 @@
1
+
2
+ module Magpie
3
+ class Server < Rack::Server
4
+ class Options
5
+ def parse!(args)
6
+ options = {}
7
+ opt_parser = OptionParser.new("", 24, ' ') do |opts|
8
+ opts.banner = "Usage: mag [rack options] [mag config]"
9
+
10
+ opts.separator ""
11
+ opts.separator "Rack options:"
12
+ opts.on("-s", "--server SERVER", "serve using SERVER (webrick/mongrel)") { |s|
13
+ options[:server] = s
14
+ }
15
+
16
+ opts.on("-o", "--host HOST", "listen on HOST (default: 0.0.0.0)") { |host|
17
+ options[:Host] = host
18
+ }
19
+
20
+ opts.on("-p", "--port PORT", "use PORT (default: 9292)") { |port|
21
+ options[:Port] = port
22
+ }
23
+
24
+ opts.on("-D", "--daemonize", "run daemonized in the background") { |d|
25
+ options[:daemonize] = d ? true : false
26
+ }
27
+
28
+ opts.on("-P", "--pid FILE", "file to store PID (default: rack.pid)") { |f|
29
+ options[:pid] = f
30
+ }
31
+
32
+ opts.separator ""
33
+ opts.separator "Common options:"
34
+
35
+ opts.on_tail("-h", "--help", "Show this message") do
36
+ puts opts
37
+ exit
38
+ end
39
+
40
+ opts.on_tail("--version", "Show version") do
41
+ puts "Magpie #{Magpie.version}"
42
+ exit
43
+ end
44
+ end
45
+ opt_parser.parse! args
46
+ options[:yml] = args.last if args.last
47
+ options
48
+ end
49
+ end
50
+
51
+ def app
52
+ Magpie::APP
53
+ end
54
+
55
+ def default_options
56
+ {
57
+ :environment => "development",
58
+ :pid => nil,
59
+ :Port => 9292,
60
+ :Host => "0.0.0.0",
61
+ :AccessLog => [],
62
+ :yml => "magpie.yml"
63
+ }
64
+ end
65
+
66
+ private
67
+ def opt_parser
68
+ Options.new
69
+ end
70
+
71
+ def parse_options(args)
72
+ options = super
73
+ if !::File.exist? options[:yml]
74
+ abort "configuration file #{options[:yml]} not found"
75
+ end
76
+ Magpie.yml_db = ::YAML.load_file(options[:yml])
77
+ options
78
+ end
79
+
80
+ end
81
+ end
82
+
@@ -0,0 +1,78 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'net/https'
3
+ require 'uri'
4
+
5
+ module Magpie
6
+ module Utils
7
+
8
+ private
9
+ def send_req_to(gw, req)
10
+ text = case req.request_method
11
+ when "GET"; get_query(gw, req.query_string)
12
+ when "POST"; post_query(gw, req.params)
13
+ end
14
+ doc = Hpricot text
15
+ end
16
+
17
+ def build_xml(h = { })
18
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
19
+ "<result>" +
20
+ hash_to_xml(h) +
21
+ "</result>"
22
+ end
23
+
24
+ def hash_to_xml(h = { })
25
+ h.inject(""){ |xml, (k, v)|
26
+ xml << "<#{k}>"
27
+ Hash === v ? xml << hash_to_xml(v) : xml << v.to_s
28
+ xml << "</#{k}>"
29
+ }
30
+ end
31
+
32
+ def get_xml_body(env, am, red_text)
33
+ if red_text =~ /错误|\d+/
34
+ final_error = get_final_error red_text
35
+ am.valid?
36
+ xml_body = build_xml(:is_success => "F", :errors => am.errors.merge(:final => final_error))
37
+ env["magpie.errors.info"] = am.errors.merge(:final => final_error)
38
+ else
39
+ begin_at = Time.now
40
+ notify_res = am.send_notify
41
+ now = Time.now
42
+ env["magpie.notify"] = ["POST", am.notify_url, now.strftime("%d/%b/%Y %H:%M:%S"), now - begin_at, am.notify.inspect, notify_res ]
43
+ xml_body = build_xml(:is_success => "T")
44
+ end
45
+ xml_body
46
+ end
47
+
48
+ # 在具体的中间件中重写
49
+ def get_final_error(red_text)
50
+ ""
51
+ end
52
+
53
+ def start_http(url, req)
54
+ http = Net::HTTP.new(url.host, url.port)
55
+ if url.scheme == "https"
56
+ http.use_ssl = true
57
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
58
+ end
59
+ http.start{ |hp| hp.request req }
60
+ end
61
+
62
+ def get_query(url, q_string)
63
+ url = URI.parse(url + "?" + q_string)
64
+ req = Net::HTTP::Get.new("#{url.path}?#{url.query}")
65
+ res = start_http(url, req)
66
+ res.body
67
+ end
68
+
69
+ def post_query(url, params)
70
+ url = URI.parse url
71
+ req = Net::HTTP::Post.new(url.path)
72
+ req.set_form_data params
73
+ res = start_http(url, req)
74
+ res.body
75
+ end
76
+
77
+ end
78
+ end
@@ -0,0 +1,31 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'models/alipay'
3
+
4
+ module Magpie
5
+
6
+ class Alipay
7
+ include Utils
8
+
9
+ def initialize(app, pay_gateway = "https://www.alipay.com/cooperate/gateway.do")
10
+ @app = app
11
+ @pay_gateway = pay_gateway
12
+ end
13
+
14
+ def call(env)
15
+ status, header, body = @app.call(env)
16
+ req = Rack::Request.new(env)
17
+ doc = send_req_to @pay_gateway, req
18
+ red_text = (doc/"//div[@id='Info']/div[@class='ErrorInfo']/div[@class='Todo']").inner_text
19
+ red_text = Iconv.iconv("UTF-8//IGNORE","GBK//IGNORE", red_text).to_s
20
+ am = AlipayModel.new(req.params)
21
+ [status, header, get_xml_body(env, am, red_text)]
22
+ end
23
+
24
+ private
25
+
26
+ def get_final_error(red_text)
27
+ red_text
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'models/chinabank'
3
+
4
+ module Magpie
5
+
6
+ class Chinabank
7
+ include Utils
8
+
9
+ def initialize(app, pay_gateway = "https://pay3.chinabank.com.cn/PayGate")
10
+ @app = app
11
+ @pay_gateway = pay_gateway
12
+ end
13
+
14
+ def call(env)
15
+ status, header, body = @app.call(env)
16
+ req = Rack::Request.new(env)
17
+ doc = send_req_to @pay_gateway, req
18
+ red_text = Iconv.iconv("UTF-8//IGNORE","GBK//IGNORE", (doc/"//strong[@class='red']").inner_text).to_s
19
+ am = ChinabankModel.new(req.params)
20
+ [status, header, get_xml_body(env, am, red_text)]
21
+ end
22
+
23
+ private
24
+
25
+ def get_final_error(red_text)
26
+ red_text.match(/出错了!(.*)/)[1]
27
+ end
28
+
29
+
30
+ end
31
+ end
@@ -0,0 +1,33 @@
1
+
2
+ module Magpie
3
+ class Mothlog
4
+
5
+ FORMAT = %{%s : "%s" \n}
6
+ FORMAT_NOTIFY = %{\t[%s] %s at[%s] (%0.4fms)\nParameters:%s\nResult:%s\n}
7
+
8
+ def initialize(app, logger=nil)
9
+ @app = app
10
+ @logger = logger
11
+ end
12
+
13
+ def call(env)
14
+ status, header, body = @app.call(env)
15
+ log(env)
16
+ [status, header, body]
17
+ end
18
+
19
+ private
20
+ def log(env)
21
+ logger = @logger || env['rack.errors']
22
+ errors_info = env["magpie.errors.info"] || { }
23
+ logger.write("\n\n")
24
+ unless errors_info.empty?
25
+ logger.write("ErrorInfo:\n")
26
+ errors_info.each { |k, v| logger.write FORMAT % [k, v]}
27
+ end
28
+ if errors_info.empty? and env["magpie.notify"]
29
+ logger.write FORMAT_NOTIFY % env["magpie.notify"]
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,172 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ class AlipayModel
4
+ include ActiveModel::Validations
5
+ attr_accessor :service,
6
+ :partner,
7
+ :notify_url,
8
+ :return_url,
9
+ :sign,
10
+ :sign_type,
11
+ :subject,
12
+ :out_trade_no,
13
+ :payment_type,
14
+ :show_url,
15
+ :body,
16
+ :price,
17
+ :total_fee,
18
+ :quantity,
19
+ :seller_email,
20
+ :seller_id,
21
+ :_input_charset
22
+
23
+ validates_presence_of :service, :partner, :notify_url, :return_url, :sign, :sign_type, :subject, :out_trade_no, :payment_type
24
+ validates_length_of :partner, :maximum => 16
25
+ validates_length_of :notify_url, :return_url, :maximum => 190
26
+ validates_length_of :show_url, :maximum => 400
27
+ validates_length_of :body, :maximum => 1000
28
+ validates_length_of :out_trade_no, :maximum => 64
29
+ validates_length_of :payment_type, :maximum => 4
30
+ validates_format_of :price, :total_fee,
31
+ :with => /^[0-9]{1,9}\.[0-9]{1,2}$/,
32
+ :allow_blank => true,
33
+ :message => "format should be Number(13, 2)"
34
+ validates_numericality_of :price, :total_fee,
35
+ :greater_than_or_equal_to => 0.01,
36
+ :less_than_or_equal_to => 100000000.00,
37
+ :allow_blank => true,
38
+ :message => "should between 0.01~100000000.00"
39
+ validates_numericality_of :quantity,
40
+ :only_integer => true,
41
+ :greater_than => 0,
42
+ :less_than => 1000000,
43
+ :allow_blank => true,
44
+ :message => "should be integer and between 1~999999"
45
+ validates_inclusion_of :_input_charset, :in => %w(utf-8 gb2312), :message => "should be utf-8 or gb2312", :allow_blank => true
46
+
47
+ validate do |am|
48
+ am.errors[:money] << "price和total_fee不能同时出现" if am.repeat_money?
49
+ am.errors[:money] << "price and total_fee can not both be blank" if am.money_blank?
50
+ am.errors[:quantity] << "if price is not blank, must input quantity" if am.price_missing_quantity?
51
+ am.errors[:seller] << "seller_email and seller_id can not both be blank" if am.seller_blank?
52
+ am.errors[:sign] << "invalid sign" if am.invalid_sign?
53
+ am.errors[:partner] << "not exist" if am.missing_partner?
54
+ end
55
+
56
+ def initialize(attributes = {})
57
+ @attributes = attributes
58
+ attributes.each do |name, value|
59
+ send("#{name}=", value) if respond_to? name
60
+ end
61
+ end
62
+
63
+ def repeat_money?
64
+ self.price.to_s.length > 0 and self.total_fee.to_s.length > 0
65
+ end
66
+
67
+ def price_missing_quantity?
68
+ self.price.to_s.length > 0 and self.quantity.to_s.length == 0
69
+ end
70
+
71
+ def missing_partner?
72
+ return if self.partner.to_s.length == 0
73
+ self.account == [] ? true : false
74
+ end
75
+
76
+ def seller_blank?
77
+ self.seller_id.to_s.length == 0 and self.seller_email.to_s.length == 0
78
+ end
79
+
80
+ def money_blank?
81
+ self.price.to_s.length == 0 and self.total_fee.to_s.length == 0
82
+ end
83
+
84
+ def invalid_sign?
85
+ attrs = @attributes.dup
86
+ attrs.delete("sign")
87
+ attrs.delete("sign_type")
88
+ text = attrs.delete_if{ |k, v| v.to_s.length == 0 }.sort.collect{ |s| s[0] + "=" + URI.decode(s[1]) }.join("&") + self.key
89
+ self.sign == Digest::MD5.hexdigest(text) ? false : true
90
+ end
91
+
92
+ def account
93
+ @account ||= self.class.accounts.assoc self.partner
94
+ @account ||= []
95
+ end
96
+
97
+ def key
98
+ self.account[1].to_s
99
+ end
100
+
101
+ def self.accounts
102
+ @accounts ||= YAML.load_file('test/partner.yml')['alipay'] if ENV['magpie'] == 'test'
103
+ @accounts ||= Magpie.yml_db['alipay']
104
+ end
105
+
106
+
107
+ def notify
108
+ @notify ||= notify_attrs.inject({ }){ |notify, attr|
109
+ notify[attr] = self.send(attr)
110
+ notify
111
+ }.merge("sign_type" => sign_type, "sign" => notify_sign)
112
+ end
113
+
114
+ def send_notify
115
+ url = URI.parse notify_url
116
+ res = Net::HTTP.post_form url, self.notify
117
+ res.body
118
+ end
119
+
120
+ private
121
+ def notify_id
122
+ @notify_id ||= Time.now.to_i
123
+ end
124
+
125
+ def notify_time
126
+ @notify_time ||= Time.now.strftime("%Y-%m-%d %H:%M:%S")
127
+ end
128
+
129
+ def notify_sign
130
+ @notify_sign ||= Digest::MD5.hexdigest notify_text
131
+ end
132
+
133
+ def notify_text
134
+ @notify_text ||= notify_attrs.sort.collect{ |attr|
135
+ "#{attr}=#{self.send(attr)}"
136
+ }.join("&") + self.key
137
+ end
138
+
139
+ def trade_no
140
+ @trade_no ||= Time.now.to_i.to_s + rand(1000000).to_s
141
+ end
142
+
143
+ def trade_status
144
+ @trade_status ||= %w(TRADE_FINISHED TRADE_SUCCESS)[rand(2)]
145
+ end
146
+
147
+ def notify_attrs
148
+ @notify_attrs ||= %w{ notify_id
149
+ notify_time
150
+ trade_no
151
+ out_trade_no
152
+ payment_type
153
+ subject
154
+ body
155
+ price
156
+ quantity
157
+ total_fee
158
+ trade_status
159
+ seller_email
160
+ seller_id
161
+ refund_status
162
+ buyer_id
163
+ gmt_create
164
+ is_total_fee_adjust
165
+ gmt_payment
166
+ gmt_refund
167
+ use_coupon
168
+ }.select{ |attr| self.respond_to?(attr, true) && self.send(attr).to_s.length > 0 }
169
+ end
170
+
171
+
172
+ end
@@ -0,0 +1,124 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ class ChinabankModel
4
+ include ActiveModel::Validations
5
+
6
+ # 商户编号
7
+ attr_accessor :v_mid
8
+
9
+ # 订单编号
10
+ attr_accessor :v_oid
11
+
12
+ # 订单总金额
13
+ attr_accessor :v_amount
14
+
15
+ # 币种
16
+ attr_accessor :v_moneytype
17
+
18
+ # 消费者完成购物后返回的商户页面,URL参数是以http://开头的完整URL地址
19
+ attr_accessor :v_url
20
+
21
+ # MD5校验码
22
+ attr_accessor :v_md5info
23
+
24
+ # 备注
25
+ attr_accessor :remark1, :remark2
26
+
27
+
28
+ validates_presence_of :v_mid, :v_oid, :v_amount, :v_moneytype, :v_url, :v_md5info
29
+ validates_length_of :v_oid, :maximum => 64
30
+ validates_length_of :v_url, :maximum => 200
31
+ validates_format_of :v_amount, :with => /^[0-9]{1,6}\.[0-9]{1,2}$/, :message => "format should be Number(6, 2)", :allow_blank => true
32
+
33
+ validate do |am|
34
+ am.errors[:sign] << "invalid v_md5info" if am.invalid_sign?
35
+ end
36
+
37
+ def initialize(attributes = {})
38
+ @attributes = attributes
39
+ attributes.each do |name, value|
40
+ send("#{name}=", value) if respond_to? name
41
+ end
42
+ end
43
+
44
+ def invalid_sign?
45
+ text = @attributes["v_amount"]+@attributes["v_moneytype"]+@attributes["v_oid"]+@attributes["v_mid"]+@attributes["v_url"]+self.key
46
+ self.sign == Digest::MD5.hexdigest(text) ? false : true
47
+ rescue => e
48
+ true
49
+ end
50
+
51
+
52
+ def sign
53
+ self.v_md5info
54
+ end
55
+
56
+ def partner
57
+ self.v_mid
58
+ end
59
+
60
+ # 商家系统用来处理网银支付结果的url
61
+ def notify_url
62
+ self.v_url
63
+ end
64
+
65
+ def account
66
+ @account ||= self.class.accounts.assoc self.partner
67
+ @account ||= []
68
+ end
69
+
70
+
71
+ def key
72
+ self.account[1].to_s
73
+ end
74
+
75
+ def self.accounts
76
+ @accounts ||= YAML.load_file('test/partner.yml')['chinabank'] if ENV['magpie'] == 'test'
77
+ @accounts ||= Magpie.yml_db['chinabank']
78
+ end
79
+
80
+ def notify
81
+ @notify ||= { "v_oid" => v_oid,
82
+ "v_pstatus" => v_pstatus,
83
+ "v_amount" => v_amount,
84
+ "v_pstring" => v_pstring,
85
+ "v_pmode" => v_pmode,
86
+ "v_moneytype" => v_moneytype,
87
+ "v_md5str" => notify_sign,
88
+ "remark1" => remark1,
89
+ "remark2" => remark2
90
+ }.delete_if { |k, v| v.to_s.length == 0}
91
+ end
92
+
93
+ def send_notify
94
+ url = URI.parse notify_url
95
+ res = Net::HTTP.post_form url, self.notify
96
+ res.body
97
+ end
98
+
99
+ private
100
+ def notify_sign
101
+ @notify_sign ||= Digest::MD5.hexdigest(notify_text).upcase
102
+ end
103
+
104
+ def v_pstatus
105
+ "20"
106
+ end
107
+
108
+ def v_pstring
109
+ "支付完成"
110
+ end
111
+
112
+ def v_pmode
113
+ %w{ 工商银行 招商银行 建设银行 光大银行 交通银行}[rand(5)]
114
+ end
115
+
116
+ def notify_text
117
+ @notify_text ||= v_oid + v_pstatus + v_amount + v_moneytype + key
118
+ rescue => e
119
+ "invalid sign"
120
+ end
121
+
122
+
123
+
124
+ end
data/magpie.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "magpie"
3
+ s.version = "0.8.6.1"
4
+ s.platform = Gem::Platform::RUBY
5
+ s.summary = "用ruby语言编写的支付平台测试沙盒"
6
+
7
+ s.description = <<-EOF
8
+ Magpie提供了支付宝(alipay), 网银在线(chinabank)的沙盒功能.使用Magpie, 开发人员可以测试商户系统提交到支付平台的参数是否正确, 并且当参数提交出错时, 可以获知详细的错误信息;
9
+ Magpie模拟了各个支付平台的主动通知交互模式,这个功能可以使开发人员不必去支付平台的页面进行真实的支付,而通过Magpie就可以取得支付成功的效果,这样就可以轻松快速地对自己所开发的商户系统进行测试.
10
+ EOF
11
+
12
+ s.files = Dir["*/**/*"] - %w(lib/magpie.yml lib/mag) +
13
+ %w(COPYING magpie.gemspec README Rakefile)
14
+ s.bindir = 'bin'
15
+ s.executables << 'mag'
16
+ s.require_paths = ["lib"]
17
+ s.has_rdoc = true
18
+ s.extra_rdoc_files = ['README']
19
+ s.test_files = Dir['test/test_*.rb']
20
+ s.author = 'jiangguimin'
21
+ s.email = 'kayak.jiang@gmail.com'
22
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'magpie'
3
+ require 'yaml'
4
+ require 'uri'
5
+ require 'digest/md5'
6
+ require 'test/unit'
7
+ require 'rack/test'
8
+
9
+ ENV['magpie'] = 'test'
data/test/partner.yml ADDED
@@ -0,0 +1,5 @@
1
+ alipay:
2
+ - [test123, secret123]
3
+
4
+ chinabank:
5
+ - ["20000400", secret456]
@@ -0,0 +1,216 @@
1
+ # -*- coding: utf-8 -*-
2
+ $:.unshift(File.dirname(__FILE__))
3
+ $:.unshift(File.dirname(__FILE__) + "/.." + "/lib")
4
+ require 'helper'
5
+
6
+ class AlipayTest < Test::Unit::TestCase
7
+ include Rack::Test::Methods
8
+
9
+ def app
10
+ Magpie::APP
11
+ end
12
+
13
+ def setup
14
+ @params = { "service" => "create_direct_pay_by_user", "sign" => "" }
15
+ @gateway = "/alipay/cooperate/gateway.do"
16
+ @accounts = YAML.load_file('test/partner.yml')['alipay']
17
+ end
18
+
19
+ def test_return_xml
20
+ get @gateway, @params
21
+ assert last_response.ok?
22
+ assert last_response.body.include? "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
23
+ assert last_response.headers["Content-type"], "text/xml"
24
+ end
25
+
26
+ def test_return_final_error
27
+ get @gateway, @params.merge("service" => "")
28
+ assert last_response.body.include? "<final>"
29
+ end
30
+
31
+ def test_validates_prensence
32
+ get @gateway, @params.merge("service" => "",
33
+ "notify_url" => "",
34
+ "partner" => "",
35
+ "return_url" => "",
36
+ "sign" => "",
37
+ "sign_type" => "",
38
+ "subject" => "",
39
+ "out_trade_no" => "",
40
+ "payment_type" => "")
41
+ assert last_response.body.include? "<service>can't be blank</service>"
42
+ assert last_response.body.include? "<notify_url>can't be blank</notify_url>"
43
+ assert last_response.body.include? "<partner>can't be blank</partner>"
44
+ assert last_response.body.include? "<return_url>can't be blank</return_url>"
45
+ assert last_response.body.include? "<sign>can't be blank</sign>"
46
+ assert last_response.body.include? "<sign_type>can't be blank</sign_type>"
47
+ assert last_response.body.include? "<subject>can't be blank</subject>"
48
+ assert last_response.body.include? "<out_trade_no>can't be blank</out_trade_no>"
49
+ assert last_response.body.include? "<payment_type>can't be blank</payment_type>"
50
+ end
51
+
52
+ def test_validates_length
53
+ get @gateway, @params.merge("partner" => "200910082009100820091008", "payment_type" => "123abc")
54
+ assert last_response.body.include? "<partner>is too long (maximum is 16 characters)</partner>"
55
+ assert last_response.body.include? "<payment_type>is too long (maximum is 4 characters)</payment_type>"
56
+ end
57
+
58
+ def test_validates_repeat_money
59
+ get @gateway, @params.merge("price" => 10.00, "total_fee" => 20.00)
60
+ assert last_response.body.include? "<money>price和total_fee不能同时出现</money>"
61
+ end
62
+
63
+ def test_validates_numericality
64
+ get @gateway, @params.merge("price" => -2.00, "total_fee" => "1000000000.00")
65
+ assert last_response.body.include? "<price>should between 0.01~100000000.00</price>"
66
+ assert last_response.body.include? "<total_fee>should between 0.01~100000000.00</total_fee>"
67
+ get @gateway, @params.merge("price" => 0.00, "total_fee" => "100000000.01")
68
+ assert last_response.body.include? "<price>should between 0.01~100000000.00</price>"
69
+ assert last_response.body.include? "<total_fee>should between 0.01~100000000.00</total_fee>"
70
+ get @gateway, @params.merge("quantity" => 0)
71
+ assert last_response.body.include? "<quantity>should be integer and between 1~999999</quantity>"
72
+ get @gateway, @params.merge("quantity" => 1.2)
73
+ assert last_response.body.include? "<quantity>should be integer and between 1~999999</quantity>"
74
+ get @gateway, @params.merge("quantity" => 10000000)
75
+ assert last_response.body.include? "<quantity>should be integer and between 1~999999</quantity>"
76
+ end
77
+
78
+ def test_validates_format
79
+ get @gateway, @params.merge("price" => 10.002, "total_fee" => 100)
80
+ assert last_response.body.include? "<price>format should be Number(13, 2)</price>"
81
+ assert last_response.body.include? "<total_fee>format should be Number(13, 2)</total_fee>"
82
+ end
83
+
84
+ def test_validates_if_missing_quantity
85
+ get @gateway, @params.merge("price" => "10.00", "quantity" => "")
86
+ assert last_response.body.include? "<quantity>if price is not blank, must input quantity</quantity>"
87
+ end
88
+
89
+ def test_validates_if_money_blank
90
+ get @gateway, @params.merge("price" => "", "total_fee" => "")
91
+ assert last_response.body.include? "<money>price and total_fee can not both be blank</money>"
92
+ end
93
+
94
+ def test_validates_seller_blank
95
+ get @gateway, @params.merge("seller_id" => "", "seller_email" => "")
96
+ assert last_response.body.include? "<seller>seller_email and seller_id can not both be blank</seller>"
97
+ end
98
+
99
+ def test_validates_charset
100
+ get @gateway, @params.merge("_input_charset" => "utf-9")
101
+ assert last_response.body.include? "<_input_charset>should be utf-8 or gb2312</_input_charset>"
102
+ get @gateway, @params.merge("_input_charset" => "utf-8")
103
+ assert !last_response.body.include?("<_input_charset>should be utf-8 or gb2312</_input_charset>")
104
+ get @gateway, @params.merge("_input_charset" => "gb2312")
105
+ assert !last_response.body.include?("<_input_charset>should be utf-8 or gb2312</_input_charset>")
106
+ end
107
+
108
+ def test_validates_partner
109
+ get @gateway, @params.merge("partner" => "test12348")
110
+ assert last_response.body.include?("<partner>not exist</partner>")
111
+ get @gateway, @params.merge("partner" => "test123")
112
+ assert !last_response.body.include?("<partner>not exist</partner>")
113
+ end
114
+
115
+ def test_validates_sign
116
+ account = @accounts[0]
117
+ text = @params.sort.collect{ |s| s[0] + "=" + s[1].to_s}.join("&") + account[1]
118
+ sign = Digest::MD5.hexdigest(text)
119
+ get @gateway, @params.merge("sign" => sign, "partner" => account[0])
120
+ assert last_response.body.include? "<sign>invalid sign</sign>"
121
+
122
+ params = @params.dup
123
+ params.delete("sign")
124
+ params.delete("sign_type")
125
+ text = params.merge("partner" => account[0]).delete_if{ |k, v| v.to_s.length == 0 }.sort.collect{ |s| s[0] + "=" + s[1].to_s }.join("&") + account[1]
126
+ sign = Digest::MD5.hexdigest(text)
127
+ get @gateway, @params.merge("sign" => sign, "partner" => account[0])
128
+ assert !last_response.body.include?("<sign>invalid sign</sign>")
129
+ end
130
+
131
+
132
+ def test_gen_notify
133
+ am = get_am
134
+ notify = am.notify
135
+ assert notify["sign_type"] == "MD5"
136
+ assert notify["subject"] == "testPPP"
137
+ assert notify["out_trade_no"] == "123456789"
138
+ assert notify["payment_type"] == "1"
139
+ assert notify["body"] == "koPPP"
140
+ assert notify["total_fee"].to_s == "32.0"
141
+ assert notify["seller_email"] == "test@fantong.com"
142
+ assert !notify.has_key?("quantity")
143
+ assert !notify.has_key?("_input_charset")
144
+ assert !notify.has_key?("partner")
145
+ assert notify.has_key?("sign")
146
+ assert notify["sign"].is_a? String
147
+ assert am.send(:notify_text) =~ /subject=/
148
+ assert am.send(:notify_text) =~ /out_trade_no=/
149
+ assert am.send(:notify_text) =~ /payment_type=/
150
+ assert am.send(:notify_text) =~ /body=/
151
+ assert am.send(:notify_text) =~ /total_fee=/
152
+ assert am.send(:notify_text) =~ /seller_email=/
153
+ assert am.send(:notify_text) != /quantity=/
154
+ end
155
+
156
+ def test_send_notify
157
+ am = get_am
158
+ res = am.send_notify
159
+ assert res.is_a? String
160
+ end
161
+
162
+ def test_notify_sign
163
+ am = get_am
164
+ raw_h = notify_params
165
+ raw_sign = am.send :notify_sign
166
+ raw_h.delete("partner")
167
+ raw_h.delete("sign_type")
168
+ raw_h.delete("return_url")
169
+ raw_h.delete("notify_url")
170
+ raw_h.delete("_input_charset")
171
+ raw_h.delete_if { |k, v| v.to_s.length == 0}
172
+ raw_h.merge!("notify_id" => am.send(:notify_id),
173
+ "notify_time" => am.send(:notify_time),
174
+ "trade_no" => am.send(:trade_no),
175
+ "trade_status" => am.send(:trade_status)
176
+ )
177
+ md5_str = Digest::MD5.hexdigest((raw_h.sort.collect{|s|s[0]+"="+s[1].to_s}).join("&")+am.key)
178
+ assert_equal raw_sign, md5_str
179
+ end
180
+
181
+
182
+ private
183
+
184
+ def get_am
185
+ am = AlipayModel.new(:partner => "test123",
186
+ :notify_url => "http://ticket.fantong.com:3000/alipay/notify",
187
+ :return_url => "http://ticket.fantong.com:3000/alipay/feedback",
188
+ :sign_type => "MD5",
189
+ :subject => "testPPP",
190
+ :out_trade_no => "123456789",
191
+ :payment_type => "1",
192
+ :body => "koPPP",
193
+ :total_fee => 32.0,
194
+ :seller_email => "test@fantong.com",
195
+ :_input_charset => "utf-8",
196
+ :quantity => "")
197
+ end
198
+
199
+ def notify_params
200
+ { "partner" => "test123",
201
+ "notify_url" => "http://localhost:3000/alipay/notify",
202
+ "return_url" => "http://localhost:3000/alipay/feedback",
203
+ "sign_type" => "MD5",
204
+ "subject" => "testPPP",
205
+ "out_trade_no" => "123456789",
206
+ "payment_type" => "1",
207
+ "body" => "koPPP",
208
+ "total_fee" => 32.0,
209
+ "seller_email" => "test@fantong.com",
210
+ "_input_charset" => "utf-8",
211
+ "quantity" => ""}
212
+ end
213
+
214
+
215
+
216
+ end
@@ -0,0 +1,95 @@
1
+ # -*- coding: utf-8 -*-
2
+ $:.unshift(File.dirname(__FILE__))
3
+ $:.unshift(File.dirname(__FILE__) + "/.." + "/lib")
4
+
5
+ require 'helper'
6
+ require 'models/chinabank'
7
+
8
+ class ChinabankTest < Test::Unit::TestCase
9
+ include Rack::Test::Methods
10
+
11
+ def app
12
+ Magpie::APP
13
+ end
14
+
15
+ def setup
16
+ @gateway = "/chinabank/PayGate"
17
+ @params = { "v_mid" => "20000400",
18
+ "v_oid" => "12345678",
19
+ "v_amount" => "1.00",
20
+ "v_moneytype" => "CNY",
21
+ "v_url" => "http://localhost:3000/chinabank/feedback",
22
+ "remark2" => "[url:=http://piao.fantong.com/chinabank/notify]"
23
+ }
24
+ @accounts = YAML.load_file('test/partner.yml')['chinabank']
25
+ end
26
+
27
+ def test_return_xml
28
+ post @gateway, @params
29
+ assert last_response.ok?
30
+ assert last_response.body.include? "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
31
+ assert last_response.headers["Content-type"], "text/xml"
32
+ assert last_response.body =~ /<final>.*<\/final>/
33
+ end
34
+
35
+ def test_validates_prensence
36
+ post @gateway, @params.dup.clear
37
+ assert last_response.body.include? "<v_mid>can't be blank</v_mid>"
38
+ assert last_response.body.include? "<v_oid>can't be blank</v_oid>"
39
+ assert last_response.body.include? "<v_amount>can't be blank</v_amount>"
40
+ assert last_response.body.include? "<v_moneytype>can't be blank</v_moneytype>"
41
+ assert last_response.body.include? "<v_url>can't be blank</v_url>"
42
+ assert last_response.body.include? "<v_md5info>can't be blank</v_md5info>"
43
+ end
44
+
45
+ def test_validates_length
46
+ post @gateway, @params.merge("v_oid" => "a" * 68,
47
+ "v_url" => "http://test.com/" + "a" * 200)
48
+ assert last_response.body.include? "<v_oid>is too long (maximum is 64 characters)</v_oid>"
49
+ assert last_response.body.include? "<v_url>is too long (maximum is 200 characters)</v_url>"
50
+ end
51
+
52
+ def test_validates_numericality
53
+ post @gateway, @params.merge("v_amount" => -1.00)
54
+ assert last_response.body.include? "<v_amount>format should be Number(6, 2)</v_amount>"
55
+ end
56
+
57
+ def test_validates_sign
58
+ account = @accounts[0]
59
+ text = @params["v_amount"]+@params["v_moneytype"]+@params["v_oid"]+@params["v_mid"]+@params["v_url"]
60
+ md5_str = Digest::MD5.hexdigest(text+"errorkey")
61
+ post @gateway, @params.merge("v_md5info" => md5_str)
62
+ assert last_response.body.include?("<sign>invalid v_md5info</sign>")
63
+ md5_str = Digest::MD5.hexdigest(text+account[1])
64
+ post @gateway, @params.merge("v_md5info" => md5_str)
65
+ assert !last_response.body.include?("<sign>invalid v_md5info</sign>")
66
+ end
67
+
68
+ def test_key
69
+ am = ChinabankModel.new(@params)
70
+ assert am.key.length > 0
71
+ end
72
+
73
+ def test_notify_sign
74
+ am = ChinabankModel.new(@params)
75
+ raw_hash = @params.dup
76
+ raw_sign = am.send :notify_sign
77
+ raw_hash.delete("remark2")
78
+ raw_hash.merge!("v_pstatus" => am.send(:v_pstatus))
79
+ md5_str = Digest::MD5.hexdigest(raw_hash["v_oid"] + raw_hash["v_pstatus"] + raw_hash["v_amount"] + raw_hash["v_moneytype"] + am.key)
80
+ assert_equal raw_sign, md5_str.upcase
81
+ end
82
+
83
+ def test_notify
84
+ am = ChinabankModel.new(@params)
85
+ assert am.notify.has_key?("v_oid")
86
+ assert am.notify.has_key?("v_pmode")
87
+ assert am.notify.has_key?("v_pstring")
88
+ assert am.notify.has_key?("v_md5str")
89
+ assert am.notify.has_key?("v_amount")
90
+ assert am.notify.has_key?("v_pstatus")
91
+ assert !am.notify.has_key?("remark1")
92
+ end
93
+
94
+
95
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: magpie
3
+ version: !ruby/object:Gem::Version
4
+ hash: 21
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 8
9
+ - 6
10
+ - 1
11
+ version: 0.8.6.1
12
+ platform: ruby
13
+ authors:
14
+ - jiangguimin
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2010-10-20 00:00:00 +08:00
20
+ default_executable:
21
+ dependencies: []
22
+
23
+ description: !binary |
24
+ TWFncGll5o+Q5L6b5LqG5pSv5LuY5a6dKGFsaXBheSksIOe9kemTtuWcqOe6
25
+ vyhjaGluYWJhbmsp55qE5rKZ55uS5Yqf6IO9LuS9v+eUqE1hZ3BpZSwg5byA
26
+ 5Y+R5Lq65ZGY5Y+v5Lul5rWL6K+V5ZWG5oi357O757uf5o+Q5Lqk5Yiw5pSv
27
+ 5LuY5bmz5Y+w55qE5Y+C5pWw5piv5ZCm5q2j56GuLCDlubbkuJTlvZPlj4Lm
28
+ lbDmj5DkuqTlh7rplJnml7YsIOWPr+S7peiOt+efpeivpue7hueahOmUmeiv
29
+ r+S/oeaBrzsKTWFncGll5qih5ouf5LqG5ZCE5Liq5pSv5LuY5bmz5Y+w55qE
30
+ 5Li75Yqo6YCa55+l5Lqk5LqS5qih5byPLOi/meS4quWKn+iDveWPr+S7peS9
31
+ v+W8gOWPkeS6uuWRmOS4jeW/heWOu+aUr+S7mOW5s+WPsOeahOmhtemdoui/
32
+ m+ihjOecn+WunueahOaUr+S7mCzogIzpgJrov4dNYWdwaWXlsLHlj6/ku6Xl
33
+ j5blvpfmlK/ku5jmiJDlip/nmoTmlYjmnpws6L+Z5qC35bCx5Y+v5Lul6L27
34
+ 5p2+5b+r6YCf5Zyw5a+56Ieq5bex5omA5byA5Y+R55qE5ZWG5oi357O757uf
35
+ 6L+b6KGM5rWL6K+VLgo=
36
+
37
+ email: kayak.jiang@gmail.com
38
+ executables:
39
+ - mag
40
+ extensions: []
41
+
42
+ extra_rdoc_files:
43
+ - README
44
+ files:
45
+ - bin/mag
46
+ - lib/middles/mothlog.rb
47
+ - lib/middles/alipay.rb
48
+ - lib/middles/chinabank.rb
49
+ - lib/models/alipay.rb
50
+ - lib/models/chinabank.rb
51
+ - lib/magpie.rb
52
+ - lib/magpie/server.rb
53
+ - lib/magpie/utils.rb
54
+ - test/partner.yml
55
+ - test/test_alipay.rb
56
+ - test/helper.rb
57
+ - test/test_chinabank.rb
58
+ - COPYING
59
+ - magpie.gemspec
60
+ - README
61
+ - Rakefile
62
+ has_rdoc: true
63
+ homepage:
64
+ licenses: []
65
+
66
+ post_install_message:
67
+ rdoc_options: []
68
+
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ hash: 3
77
+ segments:
78
+ - 0
79
+ version: "0"
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ hash: 3
86
+ segments:
87
+ - 0
88
+ version: "0"
89
+ requirements: []
90
+
91
+ rubyforge_project:
92
+ rubygems_version: 1.3.7
93
+ signing_key:
94
+ specification_version: 3
95
+ summary: !binary |
96
+ 55SocnVieeivreiogOe8luWGmeeahOaUr+S7mOW5s+WPsOa1i+ivleaymeeb
97
+ kg==
98
+
99
+ test_files:
100
+ - test/test_alipay.rb
101
+ - test/test_chinabank.rb