extendi-instagram 2.0.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 (91) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +6 -0
  5. data/.yardopts +9 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE.md +30 -0
  8. data/PATENTS.md +23 -0
  9. data/README.md +260 -0
  10. data/Rakefile +27 -0
  11. data/instagram.gemspec +50 -0
  12. data/lib/faraday/loud_logger.rb +78 -0
  13. data/lib/faraday/oauth2.rb +45 -0
  14. data/lib/faraday/raise_http_exception.rb +73 -0
  15. data/lib/instagram/api.rb +31 -0
  16. data/lib/instagram/client/comments.rb +62 -0
  17. data/lib/instagram/client/embedding.rb +28 -0
  18. data/lib/instagram/client/geographies.rb +29 -0
  19. data/lib/instagram/client/likes.rb +58 -0
  20. data/lib/instagram/client/locations.rb +75 -0
  21. data/lib/instagram/client/media.rb +82 -0
  22. data/lib/instagram/client/subscriptions.rb +211 -0
  23. data/lib/instagram/client/tags.rb +59 -0
  24. data/lib/instagram/client/users.rb +310 -0
  25. data/lib/instagram/client/utils.rb +28 -0
  26. data/lib/instagram/client.rb +21 -0
  27. data/lib/instagram/configuration.rb +125 -0
  28. data/lib/instagram/connection.rb +31 -0
  29. data/lib/instagram/error.rb +34 -0
  30. data/lib/instagram/oauth.rb +36 -0
  31. data/lib/instagram/request.rb +83 -0
  32. data/lib/instagram/response.rb +22 -0
  33. data/lib/instagram/version.rb +3 -0
  34. data/lib/instagram.rb +27 -0
  35. data/spec/faraday/response_spec.rb +101 -0
  36. data/spec/fixtures/access_token.json +9 -0
  37. data/spec/fixtures/approve_user.json +8 -0
  38. data/spec/fixtures/block_user.json +8 -0
  39. data/spec/fixtures/deny_user.json +8 -0
  40. data/spec/fixtures/follow_user.json +8 -0
  41. data/spec/fixtures/followed_by.json +1 -0
  42. data/spec/fixtures/follows.json +1 -0
  43. data/spec/fixtures/geography_recent_media.json +1 -0
  44. data/spec/fixtures/liked_media.json +1 -0
  45. data/spec/fixtures/location.json +1 -0
  46. data/spec/fixtures/location_recent_media.json +1 -0
  47. data/spec/fixtures/location_search.json +1 -0
  48. data/spec/fixtures/location_search_facebook.json +1 -0
  49. data/spec/fixtures/media.json +1 -0
  50. data/spec/fixtures/media_comment.json +1 -0
  51. data/spec/fixtures/media_comment_deleted.json +1 -0
  52. data/spec/fixtures/media_comments.json +1 -0
  53. data/spec/fixtures/media_liked.json +1 -0
  54. data/spec/fixtures/media_likes.json +1 -0
  55. data/spec/fixtures/media_popular.json +1 -0
  56. data/spec/fixtures/media_search.json +1 -0
  57. data/spec/fixtures/media_shortcode.json +1 -0
  58. data/spec/fixtures/media_unliked.json +1 -0
  59. data/spec/fixtures/mikeyk.json +1 -0
  60. data/spec/fixtures/oembed.json +14 -0
  61. data/spec/fixtures/recent_media.json +1 -0
  62. data/spec/fixtures/relationship.json +9 -0
  63. data/spec/fixtures/requested_by.json +12 -0
  64. data/spec/fixtures/shayne.json +1 -0
  65. data/spec/fixtures/subscription.json +12 -0
  66. data/spec/fixtures/subscription_deleted.json +1 -0
  67. data/spec/fixtures/subscription_payload.json +14 -0
  68. data/spec/fixtures/subscriptions.json +22 -0
  69. data/spec/fixtures/tag.json +1 -0
  70. data/spec/fixtures/tag_recent_media.json +1 -0
  71. data/spec/fixtures/tag_search.json +1 -0
  72. data/spec/fixtures/unblock_user.json +8 -0
  73. data/spec/fixtures/unfollow_user.json +8 -0
  74. data/spec/fixtures/user_media_feed.json +1 -0
  75. data/spec/fixtures/user_search.json +1 -0
  76. data/spec/instagram/api_spec.rb +285 -0
  77. data/spec/instagram/client/comments_spec.rb +71 -0
  78. data/spec/instagram/client/embedding_spec.rb +36 -0
  79. data/spec/instagram/client/geography_spec.rb +37 -0
  80. data/spec/instagram/client/likes_spec.rb +66 -0
  81. data/spec/instagram/client/locations_spec.rb +127 -0
  82. data/spec/instagram/client/media_spec.rb +99 -0
  83. data/spec/instagram/client/subscriptions_spec.rb +174 -0
  84. data/spec/instagram/client/tags_spec.rb +79 -0
  85. data/spec/instagram/client/users_spec.rb +432 -0
  86. data/spec/instagram/client/utils_spec.rb +32 -0
  87. data/spec/instagram/client_spec.rb +23 -0
  88. data/spec/instagram/request_spec.rb +56 -0
  89. data/spec/instagram_spec.rb +109 -0
  90. data/spec/spec_helper.rb +71 -0
  91. metadata +322 -0
@@ -0,0 +1,28 @@
1
+ module Instagram
2
+ class Client
3
+ # @private
4
+ module Utils
5
+ # Returns the raw full response including all headers. Can be used to access the values for 'X-Ratelimit-Limit' and 'X-Ratelimit-Remaining'
6
+ # ==== Examples
7
+ #
8
+ # client = Instagram.client(:access_token => session[:access_token])
9
+ # response = client.utils_raw_response
10
+ # remaining = response.headers[:x_ratelimit_remaining]
11
+ # limit = response.headers[:x_ratelimit_limit]
12
+ #
13
+ def utils_raw_response
14
+ response = get('users/self/feed',nil, false, true)
15
+ response
16
+ end
17
+
18
+ private
19
+
20
+ # Returns the configured user name or the user name of the authenticated user
21
+ #
22
+ # @return [String]
23
+ def get_username
24
+ @user_name ||= self.user.username
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,21 @@
1
+ module Instagram
2
+ # Wrapper for the Instagram REST API
3
+ #
4
+ # @note All methods have been separated into modules and follow the same grouping used in http://instagram.com/developer/
5
+ # @see http://instagram.com/developer/
6
+ class Client < API
7
+ Dir[File.expand_path('../client/*.rb', __FILE__)].each{|f| require f}
8
+
9
+ include Instagram::Client::Utils
10
+
11
+ include Instagram::Client::Users
12
+ include Instagram::Client::Media
13
+ include Instagram::Client::Locations
14
+ include Instagram::Client::Geographies
15
+ include Instagram::Client::Tags
16
+ include Instagram::Client::Comments
17
+ include Instagram::Client::Likes
18
+ include Instagram::Client::Subscriptions
19
+ include Instagram::Client::Embedding
20
+ end
21
+ end
@@ -0,0 +1,125 @@
1
+ require 'faraday'
2
+ #if using typhoeus as the adapter uncomment these two requires to avoid seeing "Ethon::Errors::InvalidOption: The option: disable_ssl_peer_verification is invalid." (https://github.com/typhoeus/typhoeus/issues/270)
3
+ #require 'typhoeus'
4
+ #require 'typhoeus/adapters/faraday'
5
+ require File.expand_path('../version', __FILE__)
6
+
7
+ module Instagram
8
+ # Defines constants and methods related to configuration
9
+ module Configuration
10
+ # An array of valid keys in the options hash when configuring a {Instagram::API}
11
+ VALID_OPTIONS_KEYS = [
12
+ :access_token,
13
+ :adapter,
14
+ :client_id,
15
+ :client_secret,
16
+ :client_ips,
17
+ :connection_options,
18
+ :scope,
19
+ :redirect_uri,
20
+ :endpoint,
21
+ :format,
22
+ :proxy,
23
+ :user_agent,
24
+ :no_response_wrapper,
25
+ :loud_logger,
26
+ :sign_requests,
27
+ ].freeze
28
+
29
+ # By default, don't set a user access token
30
+ DEFAULT_ACCESS_TOKEN = nil
31
+
32
+ # The adapter that will be used to connect if none is set
33
+ #
34
+ # @note The default faraday adapter is Net::HTTP.
35
+ DEFAULT_ADAPTER = Faraday.default_adapter
36
+
37
+ # By default, don't set an application ID
38
+ DEFAULT_CLIENT_ID = nil
39
+
40
+ # By default, don't set an application secret
41
+ DEFAULT_CLIENT_SECRET = nil
42
+
43
+ # By default, don't set application IPs
44
+ DEFAULT_CLIENT_IPS = nil
45
+
46
+ # By default, don't set any connection options
47
+ DEFAULT_CONNECTION_OPTIONS = {}
48
+
49
+ # The endpoint that will be used to connect if none is set
50
+ #
51
+ # @note There is no reason to use any other endpoint at this time
52
+ DEFAULT_ENDPOINT = 'https://api.instagram.com/v1/'.freeze
53
+
54
+ # The response format appended to the path and sent in the 'Accept' header if none is set
55
+ #
56
+ # @note JSON is the only available format at this time
57
+ DEFAULT_FORMAT = :json
58
+
59
+ # By default, don't use a proxy server
60
+ DEFAULT_PROXY = nil
61
+
62
+ # By default, don't set an application redirect uri
63
+ DEFAULT_REDIRECT_URI = nil
64
+
65
+ # By default, don't set a user scope
66
+ DEFAULT_SCOPE = nil
67
+
68
+ # By default, don't wrap responses with meta data (i.e. pagination)
69
+ DEFAULT_NO_RESPONSE_WRAPPER = false
70
+
71
+ # The user agent that will be sent to the API endpoint if none is set
72
+ DEFAULT_USER_AGENT = "Instagram Ruby Gem #{Instagram::VERSION}".freeze
73
+
74
+ # An array of valid request/response formats
75
+ #
76
+ # @note Not all methods support the XML format.
77
+ VALID_FORMATS = [
78
+ :json].freeze
79
+
80
+ # By default, don't turn on loud logging
81
+ DEFAULT_LOUD_LOGGER = nil
82
+
83
+ # By default, requests are not signed
84
+ DEFAULT_SIGN_REQUESTS = false
85
+
86
+ # @private
87
+ attr_accessor *VALID_OPTIONS_KEYS
88
+
89
+ # When this module is extended, set all configuration options to their default values
90
+ def self.extended(base)
91
+ base.reset
92
+ end
93
+
94
+ # Convenience method to allow configuration options to be set in a block
95
+ def configure
96
+ yield self
97
+ end
98
+
99
+ # Create a hash of options and their values
100
+ def options
101
+ VALID_OPTIONS_KEYS.inject({}) do |option, key|
102
+ option.merge!(key => send(key))
103
+ end
104
+ end
105
+
106
+ # Reset all configuration options to defaults
107
+ def reset
108
+ self.access_token = DEFAULT_ACCESS_TOKEN
109
+ self.adapter = DEFAULT_ADAPTER
110
+ self.client_id = DEFAULT_CLIENT_ID
111
+ self.client_secret = DEFAULT_CLIENT_SECRET
112
+ self.client_ips = DEFAULT_CLIENT_IPS
113
+ self.connection_options = DEFAULT_CONNECTION_OPTIONS
114
+ self.scope = DEFAULT_SCOPE
115
+ self.redirect_uri = DEFAULT_REDIRECT_URI
116
+ self.endpoint = DEFAULT_ENDPOINT
117
+ self.format = DEFAULT_FORMAT
118
+ self.proxy = DEFAULT_PROXY
119
+ self.user_agent = DEFAULT_USER_AGENT
120
+ self.no_response_wrapper= DEFAULT_NO_RESPONSE_WRAPPER
121
+ self.loud_logger = DEFAULT_LOUD_LOGGER
122
+ self.sign_requests = DEFAULT_SIGN_REQUESTS
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,31 @@
1
+ require 'faraday_middleware'
2
+ Dir[File.expand_path('../../faraday/*.rb', __FILE__)].each{|f| require f}
3
+
4
+ module Instagram
5
+ # @private
6
+ module Connection
7
+ private
8
+
9
+ def connection(raw=false)
10
+ options = {
11
+ :headers => {'Accept' => "application/#{format}; charset=utf-8", 'User-Agent' => user_agent},
12
+ :proxy => proxy,
13
+ :url => endpoint,
14
+ }.merge(connection_options)
15
+
16
+ Faraday::Connection.new(options) do |connection|
17
+ connection.use FaradayMiddleware::InstagramOAuth2, client_id, access_token
18
+ connection.use Faraday::Request::UrlEncoded
19
+ connection.use FaradayMiddleware::Mashify unless raw
20
+ unless raw
21
+ case format.to_s.downcase
22
+ when 'json' then connection.use Faraday::Response::ParseJson
23
+ end
24
+ end
25
+ connection.use FaradayMiddleware::RaiseHttpException
26
+ connection.use FaradayMiddleware::LoudLogger if loud_logger
27
+ connection.adapter(adapter)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,34 @@
1
+ module Instagram
2
+ # Custom error class for rescuing from all Instagram errors
3
+ class Error < StandardError; end
4
+
5
+ # Raised when Instagram returns the HTTP status code 400
6
+ class BadRequest < Error; end
7
+
8
+ # Raised when Instagram returns the HTTP status code 403
9
+ class Forbidden < Error; end
10
+
11
+ # Raised when Instagram returns the HTTP status code 404
12
+ class NotFound < Error; end
13
+
14
+ # Raised when Instagram returns the HTTP status code 429
15
+ class TooManyRequests < Error; end
16
+
17
+ # Raised when Instagram returns the HTTP status code 500
18
+ class InternalServerError < Error; end
19
+
20
+ # Raised when Instagram returns the HTTP status code 502
21
+ class BadGateway < Error; end
22
+
23
+ # Raised when Instagram returns the HTTP status code 503
24
+ class ServiceUnavailable < Error; end
25
+
26
+ # Raised when Instagram returns the HTTP status code 504
27
+ class GatewayTimeout < Error; end
28
+
29
+ # Raised when a subscription payload hash is invalid
30
+ class InvalidSignature < Error; end
31
+
32
+ # Raised when Instagram returns the HTTP status code 429
33
+ class RateLimitExceeded < Error; end
34
+ end
@@ -0,0 +1,36 @@
1
+ module Instagram
2
+ # Defines HTTP request methods
3
+ module OAuth
4
+ # Return URL for OAuth authorization
5
+ def authorize_url(options={})
6
+ options[:response_type] ||= "code"
7
+ options[:scope] ||= scope if !scope.nil? && !scope.empty?
8
+ options[:redirect_uri] ||= self.redirect_uri
9
+ params = authorization_params.merge(options)
10
+ connection.build_url("/oauth/authorize/", params).to_s
11
+ end
12
+
13
+ # Return an access token from authorization
14
+ def get_access_token(code, options={})
15
+ options[:grant_type] ||= "authorization_code"
16
+ options[:redirect_uri] ||= self.redirect_uri
17
+ params = access_token_params.merge(options)
18
+ post("/oauth/access_token/", params.merge(:code => code), signature=false, raw=false, unformatted=true, no_response_wrapper=true)
19
+ end
20
+
21
+ private
22
+
23
+ def authorization_params
24
+ {
25
+ :client_id => client_id
26
+ }
27
+ end
28
+
29
+ def access_token_params
30
+ {
31
+ :client_id => client_id,
32
+ :client_secret => client_secret
33
+ }
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,83 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ module Instagram
5
+ # Defines HTTP request methods
6
+ module Request
7
+ # Perform an HTTP GET request
8
+ def get(path, options={}, signature=false, raw=false, unformatted=false, no_response_wrapper=no_response_wrapper(), signed=sign_requests)
9
+ request(:get, path, options, signature, raw, unformatted, no_response_wrapper, signed)
10
+ end
11
+
12
+ # Perform an HTTP POST request
13
+ def post(path, options={}, signature=false, raw=false, unformatted=false, no_response_wrapper=no_response_wrapper(), signed=sign_requests)
14
+ request(:post, path, options, signature, raw, unformatted, no_response_wrapper, signed)
15
+ end
16
+
17
+ # Perform an HTTP PUT request
18
+ def put(path, options={}, signature=false, raw=false, unformatted=false, no_response_wrapper=no_response_wrapper(), signed=sign_requests)
19
+ request(:put, path, options, signature, raw, unformatted, no_response_wrapper, signed)
20
+ end
21
+
22
+ # Perform an HTTP DELETE request
23
+ def delete(path, options={}, signature=false, raw=false, unformatted=false, no_response_wrapper=no_response_wrapper(), signed=sign_requests)
24
+ request(:delete, path, options, signature, raw, unformatted, no_response_wrapper, signed)
25
+ end
26
+
27
+ private
28
+
29
+ # Perform an HTTP request
30
+ def request(method, path, options, signature=false, raw=false, unformatted=false, no_response_wrapper=false, signed=sign_requests)
31
+ response = connection(raw).send(method) do |request|
32
+ path = formatted_path(path) unless unformatted
33
+
34
+ if signed == true
35
+ if client_id != nil
36
+ sig_options = options.merge({:client_id => client_id})
37
+ end
38
+ if access_token != nil
39
+ sig_options = options.merge({:access_token => access_token})
40
+ end
41
+ sig = generate_sig("/"+path, sig_options, client_secret)
42
+ options[:sig] = sig
43
+ end
44
+
45
+ case method
46
+ when :get, :delete
47
+ request.url(URI.encode(path), options)
48
+ when :post, :put
49
+ request.path = URI.encode(path)
50
+ request.body = options unless options.empty?
51
+ end
52
+ if signature && client_ips != nil
53
+ request.headers["X-Insta-Forwarded-For"] = get_insta_fowarded_for(client_ips, client_secret)
54
+ end
55
+ end
56
+ return response if raw
57
+ return response.body if no_response_wrapper
58
+ return Response.create( response.body, {:limit => response.headers['x-ratelimit-limit'].to_i,
59
+ :remaining => response.headers['x-ratelimit-remaining'].to_i} )
60
+ end
61
+
62
+ def formatted_path(path)
63
+ [path, format].compact.join('.')
64
+ end
65
+
66
+ def get_insta_fowarded_for(ips, secret)
67
+ digest = OpenSSL::Digest.new('sha256')
68
+ signature = OpenSSL::HMAC.hexdigest(digest, secret, ips)
69
+ return [ips, signature].join('|')
70
+ end
71
+
72
+ def generate_sig(endpoint, params, secret)
73
+ sig = endpoint
74
+ params = params.sort_by{|c|c[0].to_s}
75
+ params.map do |key, val|
76
+ sig += '|%s=%s' % [key, val]
77
+ end
78
+ digest = OpenSSL::Digest.new('sha256')
79
+ return OpenSSL::HMAC.hexdigest(digest, secret, sig)
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,22 @@
1
+ module Instagram
2
+ module Response
3
+ def self.create( response_hash, ratelimit_hash )
4
+ response_hash = {} unless response_hash
5
+ data = response_hash.data.dup rescue response_hash
6
+ data.extend( self )
7
+ data.instance_exec do
8
+ %w{pagination meta}.each do |k|
9
+ response_hash.public_send(k).tap do |v|
10
+ instance_variable_set("@#{k}", v) if v
11
+ end
12
+ end
13
+ @ratelimit = ::Hashie::Mash.new(ratelimit_hash)
14
+ end
15
+ data
16
+ end
17
+
18
+ attr_reader :pagination
19
+ attr_reader :meta
20
+ attr_reader :ratelimit
21
+ end
22
+ end
@@ -0,0 +1,3 @@
1
+ module Instagram
2
+ VERSION = '2.0.0'.freeze unless defined?(::Instagram::VERSION)
3
+ end
data/lib/instagram.rb ADDED
@@ -0,0 +1,27 @@
1
+ require File.expand_path('../instagram/error', __FILE__)
2
+ require File.expand_path('../instagram/configuration', __FILE__)
3
+ require File.expand_path('../instagram/api', __FILE__)
4
+ require File.expand_path('../instagram/client', __FILE__)
5
+ require File.expand_path('../instagram/response', __FILE__)
6
+
7
+ module Instagram
8
+ extend Configuration
9
+
10
+ # Alias for Instagram::Client.new
11
+ #
12
+ # @return [Instagram::Client]
13
+ def self.client(options={})
14
+ Instagram::Client.new(options)
15
+ end
16
+
17
+ # Delegate to Instagram::Client
18
+ def self.method_missing(method, *args, &block)
19
+ return super unless client.respond_to?(method)
20
+ client.send(method, *args, &block)
21
+ end
22
+
23
+ # Delegate to Instagram::Client
24
+ def self.respond_to?(method, include_all=false)
25
+ return client.respond_to?(method, include_all) || super
26
+ end
27
+ end
@@ -0,0 +1,101 @@
1
+ require File.expand_path('../../spec_helper', __FILE__)
2
+
3
+ describe Faraday::Response do
4
+ before do
5
+ @client = Instagram::Client.new
6
+ end
7
+
8
+ {
9
+ 400 => Instagram::BadRequest,
10
+ 403 => Instagram::Forbidden,
11
+ 404 => Instagram::NotFound,
12
+ 429 => Instagram::TooManyRequests,
13
+ 500 => Instagram::InternalServerError,
14
+ 503 => Instagram::ServiceUnavailable
15
+ }.each do |status, exception|
16
+ context "when HTTP status is #{status}" do
17
+
18
+ before do
19
+ stub_get('users/self/feed.json').
20
+ to_return(:status => status)
21
+ end
22
+
23
+ it "should raise #{exception.name} error" do
24
+ expect do
25
+ @client.user_media_feed()
26
+ end.to raise_error(exception)
27
+ end
28
+
29
+ end
30
+ end
31
+
32
+ context "when a 400 is raised" do
33
+ before do
34
+ stub_get('users/self/feed.json').
35
+ to_return(:body => '{"meta":{"error_message": "Bad words are bad."}}', :status => 400)
36
+ end
37
+
38
+ it "should return the body error message" do
39
+ expect do
40
+ @client.user_media_feed()
41
+ end.to raise_error(Instagram::BadRequest, /Bad words are bad\./)
42
+ end
43
+ end
44
+
45
+ context "when a 400 is raised with no meta but an error_message" do
46
+ before do
47
+ stub_get('users/self/feed.json').
48
+ to_return(:body => '{"error_type": "OAuthException", "error_message": "No matching code found."}', :status => 400)
49
+ end
50
+
51
+ it "should return the body error type and message" do
52
+ expect do
53
+ @client.user_media_feed()
54
+ end.to raise_error(Instagram::BadRequest, /OAuthException: No matching code found\./)
55
+ end
56
+ end
57
+
58
+ context "when a 400 is raised with an HTML response" do
59
+ before do
60
+ stub_get('users/self/feed.json').to_return(
61
+ :body => '<html><body><h1>400 Bad Request</h1> The server returned an invalid or incomplete response. </body></html>',
62
+ :status => 400)
63
+ end
64
+
65
+ it "should return the body error type" do
66
+ expect do
67
+ @client.user_media_feed()
68
+ end.to raise_error(Instagram::BadRequest)
69
+ end
70
+ end
71
+
72
+ context 'when a 502 is raised with an HTML response' do
73
+ before do
74
+ stub_get('users/self/feed.json').to_return(
75
+ :body => '<html><body><h1>502 Bad Gateway</h1> The server returned an invalid or incomplete response. </body></html>',
76
+ :status => 502
77
+ )
78
+ end
79
+
80
+ it 'should raise an Instagram::BadGateway' do
81
+ expect do
82
+ @client.user_media_feed()
83
+ end.to raise_error(Instagram::BadGateway)
84
+ end
85
+ end
86
+
87
+ context 'when a 504 is raised with an HTML response' do
88
+ before do
89
+ stub_get('users/self/feed.json').to_return(
90
+ :body => '<html> <head><title>504 Gateway Time-out</title></head> <body bgcolor="white"> <center><h1>504 Gateway Time-out</h1></center> <hr><center>nginx</center> </body> </html>',
91
+ :status => 504
92
+ )
93
+ end
94
+
95
+ it 'should raise an Instagram::GatewayTimeout' do
96
+ expect do
97
+ @client.user_media_feed()
98
+ end.to raise_error(Instagram::GatewayTimeout)
99
+ end
100
+ end
101
+ end