mp_weixin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +22 -0
  3. data/.rspec +7 -0
  4. data/.travis.yml +20 -0
  5. data/CHANGELOG.md +17 -0
  6. data/Gemfile +5 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +308 -0
  9. data/Rakefile +31 -0
  10. data/lib/config/mp_weixin_error.yml +82 -0
  11. data/lib/mp_weixin.rb +59 -0
  12. data/lib/mp_weixin/access_token.rb +172 -0
  13. data/lib/mp_weixin/client.rb +199 -0
  14. data/lib/mp_weixin/config.rb +36 -0
  15. data/lib/mp_weixin/error.rb +27 -0
  16. data/lib/mp_weixin/interface/base.rb +43 -0
  17. data/lib/mp_weixin/interface/group.rb +92 -0
  18. data/lib/mp_weixin/interface/menu.rb +73 -0
  19. data/lib/mp_weixin/interface/message.rb +38 -0
  20. data/lib/mp_weixin/interface/promotion.rb +48 -0
  21. data/lib/mp_weixin/interface/user.rb +39 -0
  22. data/lib/mp_weixin/models/event.rb +123 -0
  23. data/lib/mp_weixin/models/message.rb +227 -0
  24. data/lib/mp_weixin/models/reply_message.rb +180 -0
  25. data/lib/mp_weixin/response.rb +93 -0
  26. data/lib/mp_weixin/response_rule.rb +46 -0
  27. data/lib/mp_weixin/server.rb +39 -0
  28. data/lib/mp_weixin/server_helper.rb +94 -0
  29. data/lib/mp_weixin/version.rb +3 -0
  30. data/lib/support/active_model.rb +3 -0
  31. data/lib/support/active_model/model.rb +99 -0
  32. data/mp_weixin.gemspec +44 -0
  33. data/spec/client_spec.rb +87 -0
  34. data/spec/mp_weixin/access_token_spec.rb +140 -0
  35. data/spec/mp_weixin/client_spec.rb +111 -0
  36. data/spec/mp_weixin/config_spec.rb +24 -0
  37. data/spec/mp_weixin/interface/base_spec.rb +16 -0
  38. data/spec/mp_weixin/interface/group_spec.rb +133 -0
  39. data/spec/mp_weixin/interface/menu_spec.rb +72 -0
  40. data/spec/mp_weixin/interface/message_spec.rb +36 -0
  41. data/spec/mp_weixin/interface/promotion_spec.rb +48 -0
  42. data/spec/mp_weixin/interface/user_spec.rb +76 -0
  43. data/spec/mp_weixin/models/event_spec.rb +94 -0
  44. data/spec/mp_weixin/models/message_spec.rb +300 -0
  45. data/spec/mp_weixin/models/reply_message_spec.rb +365 -0
  46. data/spec/mp_weixin/server_helper_spec.rb +165 -0
  47. data/spec/mp_weixin/server_spec.rb +56 -0
  48. data/spec/spec_helper.rb +51 -0
  49. data/spec/support/mp_weixin.rb +7 -0
  50. data/spec/support/rspec_mixin.rb +8 -0
  51. data/spec/support/weixin.yml +12 -0
  52. metadata +363 -0
@@ -0,0 +1,46 @@
1
+ # encoding: utf-8
2
+ module MpWeixin
3
+ module ResponseRule
4
+ # '接收普通消息', '接收事件推送', '接收语音识别结果'
5
+ #
6
+ def handle_request(request, &block)
7
+ request.body.rewind # in case someone already read it
8
+ data = request.body.read
9
+ message = Message.from_xml(data)
10
+
11
+ logger.info "Hey, one request from '#{request.url}' been detected, and content is #{message.as_json}"
12
+
13
+ if message.present?
14
+ handle_message(request, message)
15
+ response_message(request, message, &block)
16
+ else
17
+ halt 400, 'unknown message'
18
+ end
19
+ end
20
+
21
+ # handle corrent data post from weixin
22
+ #
23
+ # please @rewrite me
24
+ def handle_message(request, message)
25
+ #
26
+ end
27
+
28
+ # 发送被动响应消息'
29
+ #
30
+ # please @rewrite me
31
+ #
32
+ #
33
+ # can rely with instance of those class eg, TextReplyMessage, ImageReplyMessage, VoiceReplyMessage
34
+ # VideoReplyMessage, MusicReplyMessage, NewsReplyMessage
35
+ # quickly generate reply content through call 'reply_#{msg_type}_message(attributes).to_xml' @see 'spec/mp_weixin/server_helper_spec.rb'
36
+ #
37
+ def response_message(request, message, &block)
38
+ if block_given?
39
+ block.call(request, message)
40
+ end
41
+
42
+ # reply with
43
+ # reply_#{msg_type}_message(attributes).to_xml
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,39 @@
1
+ # encoding: utf-8
2
+ module MpWeixin
3
+ class Server < Sinatra::Base
4
+ configure :production, :development do
5
+ enable :logging
6
+
7
+ set :haml, { :ugly=>true }
8
+ set :clean_trace, true
9
+ Dir.mkdir('log') unless File.exist?('log')
10
+
11
+ file = File.new("./log/mp_weixin_#{settings.environment}.log", 'a+')
12
+ file.sync = true
13
+ use Rack::CommonLogger, file
14
+ end
15
+
16
+ helpers MpWeixin::ServerHelper, MpWeixin::ResponseRule
17
+
18
+
19
+ before '/' do
20
+ unless valid_signature?(signature = params[:signature], timestamp = params[:timestamp], nonce= params[:nonce] )
21
+ halt 401,{'Content-Type' => 'text/plain'}, 'go away!'
22
+ end
23
+ end
24
+
25
+ # 验证消息真实性
26
+ #
27
+ # 通过检验signature对请求进行校验(下面有校验方式)。若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败
28
+ # eg curl http://localhost:4567/?nonce=121212121&signature=9dc548e8c7fe32ac53f887e834a8c719a73cafc3&timestamp=1388028695&echostr=22222222222222222
29
+ get '/' do
30
+ params[:echostr]
31
+ end
32
+
33
+ # '接收普通消息', '发送被动响应消息', '接收事件推送', '接收语音识别结果'
34
+ post "/" do
35
+ content_type 'text/xml'
36
+ handle_request(request)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,94 @@
1
+ # encoding: utf-8
2
+ module MpWeixin
3
+ module ServerHelper
4
+
5
+ # generate a signature string through sha1 encrypt token, timestamp, nonce .
6
+ #
7
+ # @param [String] token the token value
8
+ # @param [String] timestamp the timestamp value from weixin
9
+ # @param [String] nonce the random num from weixin
10
+ #
11
+ # 加密/校验流程如下:
12
+ # 1. 将token、timestamp、nonce三个参数进行字典序排序
13
+ # 2. 将三个参数字符串拼接成一个字符串进行sha1加密
14
+ # 3. 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
15
+ #
16
+ # @return [String]
17
+ def generate_signature(token, timestamp, nonce)
18
+ signature_content = [token.to_s, timestamp.to_s, nonce.to_s].sort.join("")
19
+ Digest::SHA1.hexdigest(signature_content)
20
+ end
21
+
22
+ # Whether or not the signature is eql with local_signature
23
+ #
24
+ # @param [String] signature the signature value need validate
25
+ # @param [String] timestamp the timestamp value from weixin
26
+ # @param [String] nonce the nonce value
27
+ #
28
+ # @return [Boolean]
29
+ def valid_signature?(signature, timestamp, nonce)
30
+ token = Config.token
31
+
32
+ local_signature = generate_signature(token,timestamp,nonce)
33
+ local_signature.eql? signature
34
+ end
35
+
36
+ # initialize an TextReplyMessage
37
+ # @param [Hash] attributes
38
+ # @see 'spec/mp_weixin/models/reply_message_spec.rb'
39
+ def reply_text_message(attributes = {})
40
+ MpWeixin::TextReplyMessage.new(attributes)
41
+ end
42
+
43
+ # initialize an ImageReplyMessage
44
+ # @param [Hash] attributes
45
+ # @see 'spec/mp_weixin/models/reply_message_spec.rb'
46
+ def reply_image_message(attributes = {}, &block)
47
+ reply_message = MpWeixin::ImageReplyMessage.new(attributes)
48
+ block.call(reply_message) if block_given?
49
+
50
+ reply_message
51
+ end
52
+
53
+ # initialize an VoiceReplyMessage
54
+ # @param [Hash] attributes
55
+ # @see 'spec/mp_weixin/models/reply_message_spec.rb'
56
+ def reply_voice_message(attributes = {}, &block)
57
+ reply_message = MpWeixin::VoiceReplyMessage.new(attributes)
58
+ block.call(reply_message) if block_given?
59
+
60
+ reply_message
61
+ end
62
+
63
+ # initialize an VideoReplyMessage
64
+ # @param [Hash] attributes
65
+ # @see 'spec/mp_weixin/models/reply_message_spec.rb'
66
+ def reply_video_message(attributes = {}, &block)
67
+ reply_message = MpWeixin::VideoReplyMessage.new(attributes)
68
+ block.call(reply_message) if block_given?
69
+
70
+ reply_message
71
+ end
72
+
73
+ # initialize an MusicReplyMessage
74
+ # @param [Hash] attributes
75
+ # @see 'spec/mp_weixin/models/reply_message_spec.rb'
76
+ def reply_music_message(attributes = {}, &block)
77
+ reply_message = MpWeixin::MusicReplyMessage.new(attributes)
78
+ block.call(reply_message) if block_given?
79
+
80
+ reply_message
81
+ end
82
+
83
+ # initialize an NewsReplyMessage
84
+ # @param [Hash] attributes
85
+ # @see 'spec/mp_weixin/models/reply_message_spec.rb'
86
+ def reply_news_message(attributes = {}, &block)
87
+ reply_message = MpWeixin::NewsReplyMessage.new(attributes)
88
+
89
+ block.call(reply_message) if block_given?
90
+
91
+ reply_message
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,3 @@
1
+ module MpWeixin
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,3 @@
1
+ if ActiveModel::VERSION::MAJOR < 4
2
+ require 'support/active_model/model'
3
+ end
@@ -0,0 +1,99 @@
1
+ module ActiveModel
2
+
3
+ # == Active \Model Basic \Model
4
+ #
5
+ # Includes the required interface for an object to interact with
6
+ # <tt>ActionPack</tt>, using different <tt>ActiveModel</tt> modules.
7
+ # It includes model name introspections, conversions, translations and
8
+ # validations. Besides that, it allows you to initialize the object with a
9
+ # hash of attributes, pretty much like <tt>ActiveRecord</tt> does.
10
+ #
11
+ # A minimal implementation could be:
12
+ #
13
+ # class Person
14
+ # include ActiveModel::Model
15
+ # attr_accessor :name, :age
16
+ # end
17
+ #
18
+ # person = Person.new(name: 'bob', age: '18')
19
+ # person.name # => 'bob'
20
+ # person.age # => 18
21
+ #
22
+ # Note that, by default, <tt>ActiveModel::Model</tt> implements <tt>persisted?</tt>
23
+ # to return +false+, which is the most common case. You may want to override
24
+ # it in your class to simulate a different scenario:
25
+ #
26
+ # class Person
27
+ # include ActiveModel::Model
28
+ # attr_accessor :id, :name
29
+ #
30
+ # def persisted?
31
+ # self.id == 1
32
+ # end
33
+ # end
34
+ #
35
+ # person = Person.new(id: 1, name: 'bob')
36
+ # person.persisted? # => true
37
+ #
38
+ # Also, if for some reason you need to run code on <tt>initialize</tt>, make
39
+ # sure you call +super+ if you want the attributes hash initialization to
40
+ # happen.
41
+ #
42
+ # class Person
43
+ # include ActiveModel::Model
44
+ # attr_accessor :id, :name, :omg
45
+ #
46
+ # def initialize(attributes={})
47
+ # super
48
+ # @omg ||= true
49
+ # end
50
+ # end
51
+ #
52
+ # person = Person.new(id: 1, name: 'bob')
53
+ # person.omg # => true
54
+ #
55
+ # For more detailed information on other functionalities available, please
56
+ # refer to the specific modules included in <tt>ActiveModel::Model</tt>
57
+ # (see below).
58
+ module Model
59
+ def self.included(base) #:nodoc:
60
+ base.class_eval do
61
+ extend ActiveModel::Naming
62
+ extend ActiveModel::Translation
63
+ include ActiveModel::Validations
64
+ include ActiveModel::Conversion
65
+ end
66
+ end
67
+
68
+ # Initializes a new model with the given +params+.
69
+ #
70
+ # class Person
71
+ # include ActiveModel::Model
72
+ # attr_accessor :name, :age
73
+ # end
74
+ #
75
+ # person = Person.new(name: 'bob', age: '18')
76
+ # person.name # => "bob"
77
+ # person.age # => 18
78
+ def initialize(params={})
79
+ params.each do |attr, value|
80
+ self.public_send("#{attr}=", value)
81
+ end if params
82
+
83
+ super()
84
+ end
85
+
86
+ # Indicates if the model is persisted. Default is +false+.
87
+ #
88
+ # class Person
89
+ # include ActiveModel::Model
90
+ # attr_accessor :id, :name
91
+ # end
92
+ #
93
+ # person = Person.new(id: 1, name: 'bob')
94
+ # person.persisted? # => false
95
+ def persisted?
96
+ false
97
+ end
98
+ end
99
+ end
data/mp_weixin.gemspec ADDED
@@ -0,0 +1,44 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mp_weixin/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mp_weixin"
8
+ spec.version = MpWeixin::VERSION
9
+ spec.authors = ["jhjguxin"]
10
+ spec.email = ["864248765@qq.com"]
11
+ spec.description = %q{A wrapper for weiXin MP platform}
12
+ spec.summary = %q{A Ruby wrapper for weixin MP platform}
13
+ spec.homepage = "https://github.com/jhjguxin/mp_weixin"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "oauth2", [">= 0.5","<= 0.9"]
22
+ spec.add_dependency "sinatra", "~> 1.4.4"
23
+ spec.add_dependency "activemodel", [">= 3.0","<= 4"]
24
+ spec.add_dependency 'multi_json', '>= 1.7.9'
25
+ spec.add_dependency 'multi_xml', '>= 0.5.2'
26
+ spec.add_dependency 'roxml'
27
+ spec.add_dependency 'nestful'
28
+ # spec.add_dependency "data_mapper", "~> 1.2.0"
29
+ # spec.add_dependency "dm-sqlite-adapter", "~> 1.2.0"
30
+
31
+ spec.add_development_dependency "thin"
32
+ spec.add_development_dependency "debugger"
33
+ spec.add_development_dependency "bundler"
34
+ spec.add_development_dependency "rake"
35
+ spec.add_development_dependency 'rspec'
36
+ spec.add_development_dependency 'rack-test'
37
+ # Code coverage for Ruby 1.9+ with a powerful configuration library and automatic merging of coverage across test suites
38
+ spec.add_development_dependency 'simplecov'
39
+ # WebMock allows stubbing HTTP requests and setting expectations on HTTP requests.
40
+ spec.add_development_dependency 'webmock'
41
+ # A Ruby implementation of the Coveralls API.
42
+ spec.add_development_dependency "coveralls"
43
+ spec.add_development_dependency "travis-lint"
44
+ end
@@ -0,0 +1,87 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ describe MpWeixin::Client do
5
+ let(:client) { MpWeixin::Client.new }
6
+ let(:access_token) { 'ACCESS_TOKEN' }
7
+ let(:token_hash) { {"expires_in" => "7200", "access_token" => access_token} }
8
+
9
+
10
+ context "#initialize config" do
11
+ it "should have correct site" do
12
+ client.site.should eq("https://api.weixin.qq.com/")
13
+ end
14
+
15
+ # it "should have correct authorize url" do
16
+ # client.options[:authorize_url].should eq('/oauth/authorize')
17
+ # end
18
+
19
+ it "should have correct token url" do
20
+ client.options[:token_url].should eq('/cgi-bin/token')
21
+ end
22
+
23
+ it 'is_authorized? should been false' do
24
+ expect(subject.is_authorized?).to eq(false)
25
+ end
26
+ end
27
+
28
+ context "#taken_code" do
29
+ let(:json_token) {MultiJson.encode(:expires_in => 7200, :access_token => 'salmon')}
30
+
31
+ let(:client) do
32
+ MpWeixin::Client.new do |builder|
33
+ builder.request :url_encoded
34
+ builder.adapter :test do |stub|
35
+ stub.get("/cgi-bin/token") do |env|
36
+ [200, {'Content-Type' => 'application/json'}, json_token]
37
+ end
38
+ stub.post('/cgi-bin/token') do |env|
39
+ [200, {'Content-Type' => 'application/json'}, json_token]
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ subject do
46
+ client.get_token
47
+ client
48
+ end
49
+
50
+
51
+ it "returns AccessToken with #token" do
52
+ expect(subject.token.token).to eq('salmon')
53
+ end
54
+
55
+ it 'is_authorized? should been true' do
56
+ expect(subject.is_authorized?).to eq(true)
57
+ end
58
+ end
59
+
60
+ context "get_token_from_hash" do
61
+ subject { client.get_token_from_hash(token_hash) }
62
+
63
+ it "return token the initalized AccessToken" do
64
+ expect(subject).to be_a(MpWeixin::AccessToken)
65
+ end
66
+
67
+ it "return token with provide access_token" do
68
+ expect(subject.token).to eq(access_token)
69
+ end
70
+ end
71
+
72
+ context "#from_hash" do
73
+ subject { MpWeixin::Client.from_hash(token_hash) }
74
+
75
+ it "return client the initalized Client" do
76
+ expect(subject).to be_a(MpWeixin::Client)
77
+ end
78
+
79
+ it "return token with provide access_token" do
80
+ expect(subject.token.token).to eq(access_token)
81
+ end
82
+
83
+ it 'is_authorized? should been true' do
84
+ expect(subject.is_authorized?).to eq(true)
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,140 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ describe MpWeixin::AccessToken do
5
+ let(:token) {'ACCESS_TOKEN'}
6
+ let(:refresh_token) {'REFRESH_TOKEN'}
7
+ let(:token_body) {MultiJson.encode(:access_token => 'ACCESS_TOKEN', :expires_in => 7200)}
8
+ let(:client) do
9
+ MpWeixin::Client.new do |builder|
10
+ builder.request :url_encoded
11
+ builder.adapter :test do |stub|
12
+ stub.send(:get, "/cgi-bin/token") {|env| [200, {'Content-Type' => 'application/json'}, token_body]}
13
+ stub.send(:post, "/cgi-bin/token") {|env| [200, {'Content-Type' => 'application/json'}, token_body]}
14
+
15
+ # stub.post('/sns/oauth2/access_token') {|env| [200, {'Content-Type' => 'application/json'}, refresh_body]}
16
+ end
17
+ end
18
+ end
19
+
20
+ subject {MpWeixin::AccessToken.new(client, token)}
21
+
22
+ describe "#initialize" do
23
+ it "assigns client and token" do
24
+ expect(subject.client).to eq(client)
25
+ expect(subject.token).to eq(token)
26
+ end
27
+
28
+ it "assigns extra params" do
29
+ target = MpWeixin::AccessToken.new(client, token, 'foo' => 'bar')
30
+ expect(target.params).to include('foo')
31
+ expect(target.params['foo']).to eq('bar')
32
+ end
33
+
34
+ def assert_initialized_token(target)
35
+ expect(target.token).to eq(token)
36
+ expect(target).to be_expires
37
+ expect(target.params.keys).to include('foo')
38
+ expect(target.params['foo']).to eq('bar')
39
+ end
40
+
41
+ it "initializes with a Hash" do
42
+ hash = {:access_token => token, :expires_in => Time.now.to_i + 200, 'foo' => 'bar'}
43
+ target = MpWeixin::AccessToken.from_hash(client, hash)
44
+ assert_initialized_token(target)
45
+ end
46
+
47
+ it "initializes with a string expires_in" do
48
+ hash = {:access_token => token, :expires_in => '1361396829', 'foo' => 'bar'}
49
+ target = MpWeixin::AccessToken.from_hash(client, hash)
50
+ assert_initialized_token(target)
51
+ expect(target.expires_in).to be_a(Integer)
52
+ end
53
+ end
54
+
55
+ describe "#request" do
56
+
57
+ context ":mode => :body" do
58
+ before do
59
+ subject.options[:mode] = :body
60
+ end
61
+
62
+ it "sends the token in the Authorization header for a GET request" do
63
+ expect(subject.post("/cgi-bin/token").body).to include(token)
64
+ end
65
+
66
+ it "sends the token in the Authorization header for a POST request" do
67
+ expect(subject.post("/cgi-bin/token").body).to include(token)
68
+ end
69
+ end
70
+ end
71
+
72
+ describe "#expires?" do
73
+ it "is false if there is no expires_at" do
74
+ expect(MpWeixin::AccessToken.new(client, token)).not_to be_expires
75
+ end
76
+
77
+ it "is true if there is an expires_in" do
78
+ expect(MpWeixin::AccessToken.new(client, token, :refresh_token => 'REFRESH_TOKEN', :expires_in => 600)).to be_expires
79
+ end
80
+
81
+ end
82
+
83
+ describe "#expired?" do
84
+ it "is false if there is no expires_in or expires_at" do
85
+ expect(MpWeixin::AccessToken.new(client, token)).not_to be_expired
86
+ end
87
+
88
+ it "is false if expires_in is in the future" do
89
+ expect(MpWeixin::AccessToken.new(client, token, :refresh_token => 'REFRESH_TOKEN', :expires_in => 10800)).not_to be_expired
90
+ end
91
+
92
+ it "is true if expires_at is in the past" do
93
+ access = MpWeixin::AccessToken.new(client, token, :refresh_token => 'REFRESH_TOKEN', :expires_in => 7200)
94
+ @now = Time.now + 10800
95
+
96
+ # You can't double a constance by either allow or double. Instead you need to use stub_const
97
+ # https://www.relishapp.com/rspec/rspec-mocks/v/2-14/docs/mutating-constants
98
+
99
+ # Make a mock of Notifier at first
100
+ stub_const "Time", Time
101
+ # Then stub the methods of Notifier
102
+ Time.stub(:now) { @now }
103
+
104
+ expect(access).to be_expired
105
+ end
106
+
107
+ end
108
+
109
+ describe "#refresh!" do
110
+ let(:access) {
111
+ MpWeixin::AccessToken.new(client, token, :refresh_token => 'REFRESH_TOKEN',
112
+ :expires_in => 7200,
113
+ :param_name => 'o_param')
114
+ }
115
+
116
+ it "returns a refresh token with appropriate values carried over" do
117
+ refreshed = access.refresh!
118
+ expect(access.client).to eq(refreshed.client)
119
+ expect(access.options[:param_name]).to eq(refreshed.options[:param_name])
120
+ end
121
+
122
+ context "with a nil refresh_token in the response" do
123
+ let(:refresh_body) { MultiJson.encode(:access_token => 'refreshed_foo', :expires_in => 600, :refresh_token => nil) }
124
+
125
+ it "copies the refresh_token from the original token" do
126
+ refreshed = access.refresh!
127
+
128
+ expect(refreshed.refresh_token).to eq(access.refresh_token)
129
+ end
130
+ end
131
+ end
132
+
133
+ describe '#to_hash' do
134
+ it 'return a hash equals to the hash used to initialize access token' do
135
+ hash = {:access_token => token, :refresh_token => 'foobar', :expires_at => Time.now.to_i + 200, 'foo' => 'bar'}
136
+ access_token = MpWeixin::AccessToken.from_hash(client, hash.dup)
137
+ expect(access_token.to_hash).to eq(hash)
138
+ end
139
+ end
140
+ end