magpie 0.8.6.1

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.
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