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