magpie 0.8.6.1
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +18 -0
- data/README +37 -0
- data/Rakefile +6 -0
- data/bin/mag +4 -0
- data/lib/magpie.rb +49 -0
- data/lib/magpie/server.rb +82 -0
- data/lib/magpie/utils.rb +78 -0
- data/lib/middles/alipay.rb +31 -0
- data/lib/middles/chinabank.rb +31 -0
- data/lib/middles/mothlog.rb +33 -0
- data/lib/models/alipay.rb +172 -0
- data/lib/models/chinabank.rb +124 -0
- data/magpie.gemspec +22 -0
- data/test/helper.rb +9 -0
- data/test/partner.yml +5 -0
- data/test/test_alipay.rb +216 -0
- data/test/test_chinabank.rb +95 -0
- metadata +101 -0
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
data/bin/mag
ADDED
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
|
+
|
data/lib/magpie/utils.rb
ADDED
@@ -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
data/test/partner.yml
ADDED
data/test/test_alipay.rb
ADDED
@@ -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
|