mp_weixin 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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