diff --git a/.rspec b/.rspec index e4d136b..c99d2e7 100644 --- a/.rspec +++ b/.rspec @@ -1 +1 @@ ---format progress +--require spec_helper diff --git a/CHANGES.md b/CHANGES.md index 44cae0d..d7f980b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,94 @@ # Changes +## Unreleased + +* Add 'references' to lazy_attributes in message.rb - Dylan Stamat + +## 1.7.2 + +* Improve error handling for better trouble shooting. - Ben Hamill + +## 1.7.1 + +* Fix bug in `ContextIO::Message#set_flags` that was failing, and also fixes the + wrong api url it was trying to call. - [Paul Bonaud](https://github.com/popox) +* Spruce up README, and add CONTRIBUTING and RELEASING guides. - Ben Hamill + +## 1.7.0 + +* Further README updates for clarity. - Johnny Goodman +* Convert to Faraday back-end for making requests. - Ben Hamill +* Fix bug in `ContextIO::Source` that caused the `sync_data` method to always + fail. - Dominik Gehl +* Improve documentation for `ContextIO::API::ResourceCollection#where` to + include a link to the general API documentation. - Chris McNair + +## 1.6.0 + +* Add `version` and `base_url` instance variables to API. - Dominik Gehl +* Don't try to JSON parse raw attachments. - Dominik Gehl +* Use symbols for options internally to avoid OAuth gem failure. - Ben Hamill +* Add `in_reply_to` to `Message`'s lazy attributes. - Asa Wilson +* Fix bug where `Hash`es with mixed `String`/`Symbol` keys would cause the OAuth + gem to explode. - Ben Hamill +* Add missing files association to `Message` class. - Ben Hamill +* Add a ton of examples to the README to help new users. - Johnny Goodman + +## 1.5.0 + +* Make `Source#sync!` and `Account#sync!` take an options hash that will be + passed as parameters in the resulting HTTP request. This is just to aid in + debugging. - Ben Hamill + +## 1.4.0 + +* Normalize key names from the API to be valid Ruby variable names. - Ben Hamill +* Fix how `Folder` objects create their associated `MessageCollection` objects, + specifically with respect to passing around the handle to an appropriate + `Account` object. - Ben Hamill +* Convenience methods: `Message#to`,`#bcc`, `#cc` and `#reply_to`. - Aubrey + Holland + +## 1.3.0 + +* Add sugar `Message#from` method. - Andrew Harrison +* Fix bug related to `nil` being passed around for associations which caused the + association not to be filled with data from the API. - Ben Hamill + +## 1.2.4 + +* Add link to gem documentation to top of README. - Ben Hamill +* Expand README to clarify gem usage. - Ben Hamill +* Make `ResourceCollection#[]` correctly pass down the "owning" associated + object to instances created with it. - Ben Hamill +* Work around the OAuth gem handling PUT request signing a bit oddly. - Ben + Hamill + +## 1.2.3 + +* Fix infinite loop in ConnectToken URL building. - Ben Hamill + +## 1.2.2 + +* Fix `PUT` and `POST` parameter submission. - Ben Hamill +* Added missing `server` argument to `SourceCollection.create`. - Dominik Gehl +* Moved erroneous `File.sync_data` and `File.sync!` over to `Source` where it + belonged in the first place. - Bram Plessers + +## 1.2.1 + +* Fixed syntax error typo in previous release. - Geoff Longman + +## 1.2.0 + +* Add `#empty?` and `#size` to resource collections so you can treat them even + *more* like arrays! - Geoff Longman + +## 1.1.0 + +* Allow passing options to `OAuth` through the gem. Notably, `:timeout` and + `:open_timeout`. - Geoff Longman + ## 1.0.1 * Updated homepage link in README distributed with the gem. - Ben Hamill diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..77052b4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,19 @@ +# Contributing + +Help is gladly welcomed. If you have a feature you'd like to add, it's much more +likely to get in (or get in faster) the closer you stick to these steps: + +1. Open an Issue to talk about it. We can discuss whether it's the right + direction or maybe help track down a bug, etc. +1. Fork the project, and make a branch to work on your feature/fix. Master is + where you'll want to start from. +1. Turn the Issue into a Pull Request. There are several ways to do this, but + [hub](https://github.com/defunkt/hub) is probably the easiest. +1. Make sure your Pull Request includes tests. +1. Bonus points if your Pull Request updates `CHANGES.md` to include a summary + of your changes and your name like the other entries. If the last entry is + the last release, add a new `## Unreleased` heading. +1. *Do not* rev the version number in your pull request. + +If you don't know how to fix something, even just a Pull Request that includes a +failing test can be helpful. If in doubt, make an Issue to discuss. diff --git a/ChangeLog.md b/ChangeLog.md deleted file mode 100644 index e7bb59a..0000000 --- a/ChangeLog.md +++ /dev/null @@ -1,5 +0,0 @@ -# Changes - -## 0.5.0 / 2012-08-18 - -* Major Refactor of all code. diff --git a/Gemfile b/Gemfile index a1b93f3..fa75df1 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,3 @@ -source :rubygems +source 'https://rubygems.org' gemspec diff --git a/README.md b/README.md index 08fece6..1ec1f11 100644 --- a/README.md +++ b/README.md @@ -2,24 +2,55 @@ * [Homepage](https://github.com/contextio/contextio-ruby#readme) * [API Documentation](http://context.io/docs/2.0/) +* [Gem Documentation](http://rubydoc.info/gems/contextio/frames) +* [API Explorer](https://console.context.io/#explore) * [Sign Up](http://context.io) + ## Description Provides a Ruby interface to [Context.IO](http://context.io). The general design was inspired by the wonderful [aws-sdk](https://github.com/aws/aws-sdk-ruby) gem. You start with an object that represents your account with Context.IO and -then you deal with collections within that going forward. Check out the example. +then you deal with collections within that going forward (see the [Quick Start +section](#quick start)). + + +## Install + + $ gem install contextio + +Or, of course, put this in your Gemfile: + + gem 'contextio' + + +## A Note On Method Names + +If you're looking at the [Context.IO docs](http://context.io/docs/2.0/), it is +important to note that there are some attributes that've been renamed to be a +bit more Ruby-friendly. In general, if the API returns a number meant to be +seconds-from-epoch, then it's been converted to return a `Time` (e.g. `updated` +has changed to `updated_at`) and a boolean has converted to something with a `?` +at the end (e.g. `HasChildren` and `initial_import_finished` are `has_children?` +and `initial_import_finished?`, respectively). See the [gem +docs](http://rubydoc.info/gems/contextio/frames) for a specific class when in +doubt. + + +## Version Numbers + +This gem adheres to [SemVer](http://semver.org/). So you should be pretty safe +upgrading from 1.0.0 to 1.9.9. Whatever as long as the major version doesn't +bump. When the major version bumps, be warned; upgrading will take some kind of +effort. -If you're looking at the Context.IO docs, it is important to note that there are -some attributes that've been renamed to be a bit more Ruby-friendly. In general, -if the API returns a number meant to be seconds-from-epoch, then it's been -converted to return a `Time` (e.g. `updated` has changed to `updated_at`) and a -boolean has converted to something with a `?` at the end (e.g. `HasChildren` and -`initial_import_finished` are `has_children?` and `initial_import_finished?`, -respectively). -## Example +## Examples + +### Quick Start + +Print the subject of the first five messages in the some@email.com account. ```ruby require 'contextio' @@ -28,48 +59,312 @@ contextio = ContextIO.new('your_api_key', 'your_api_secret') account = contextio.accounts.where(email: 'some@email.com').first -account.email_addresses # ['some@email.com', 'another@email.com'] -account.first_name # 'Bruno' -account.suspended? # False +account.messages.where(limit: 5).each do |message| + puts message.subject +end +``` + + +### Primary Key Queries + +To grab some object you already know the primary key for, you'll use the `[]` +method like you would for a `Hash` or `Array`. + +This is most helpful for accounts, but works for any resource collection. + +```ruby +require 'contextio' + +contextio = ContextIO.new('your_api_key', 'your_api_secret') + +some_account_id = "exampleaccountid12345678" + +account = contextio.accounts[some_account_id] +message = account.messages[some_message_id] +``` + + +#### Message By Key + +```ruby +message = account.messages[some_message_id] + +message.message_id #=> "examplemessageid12345678" +message.subject #=> "My Email's Subject" +message.from.class #=> Hash +message.from['email'] #=> "some@email.com" +message.from['name'] #=> "John Doe" + +message.delete #=> true +``` + +Body content must be accessed through each body part (eg: html, text, etc.). +Context.io does not store body content and so each call will source it +directly from the mail box associated with the account. This will be slow +relative to the context.io API. + +You can specify and receive a single body content type. + +```ruby +message = account.messages[some_message_id] + +message_part = message.body_parts.where(type: 'text/plain').first + +message_part.class #=> ContextIO::BodyPart +message_part.type #=> "text/plain" +message_part.content #=> "body content of text/plain body_part" +``` + +You can determine how many body parts are available and iterate over each one. + +```ruby +message = account.messages[some_message_id] -account.messages.where(folder: '\Drafts').each do |m| - puts m.subject +message.body_parts.class #=> ContextIO::BodyPartCollection +message.body_parts.count #=> 2 + +message.body_parts.each do |part| + puts part.class #=> ContextIO::BodyPart + puts part.type #=> "text/html" + puts part.content #=> "body content of text/html body_part" end ``` -## Install - $ gem install contextio +#### Account By Key -Or, of course, put this in your Gemfile: +You can specify an account id and get back information about that account. - gem contextio +```ruby +account = contextio.accounts[some_account_id] -## Version Numbers +account.email_addresses #=> ['some@email.com', 'another@email.com'] +account.id #=> "exampleaccountid12345678" +account.first_name #=> "Bruno" +account.suspended? #=> False +``` + + +### Message Collections + +#### Query Basics + +Queries to Messages return ContextIO::MessageCollection objects which can be +iterated on. + +The `where` method allows you to refine search results based on +[available filters](http://context.io/docs/2.0/accounts/messages#get). + +```ruby +#the 25 most recent messages by default, you can specify a higher limit +account.messages + +#the 50 most recent messages +account.messages.where(limit: 50) + +#recent messages sent to the account by some@email.com +account.messages.where(from: 'some@email.com') + +#multiple parameters accepted in hash format +account.messages.where(from: 'some@email.com', subject: 'hello') + +#regexp accepted as a string like '/regexp/' +account.messages.where(from: 'some@email.com', subject: '/h.llo/') + +#regexp options are supported, the /i case insensitive is often useful +account.messages.where(from: 'some@email.com', subject: '/h.llo/i') +``` + + +#### Querying Dates + +Pass dates in to message queries as Unix Epoch integers. + +```ruby +require 'active_support/all' + +account.messages.where(date_before: 3.hours.ago.to_i, date_after: 5.hours.ago.to_i).each do |message| + puts message.subject #=> "The subject of my email" + puts message.received_at #=> 2013-07-31 20:33:56 -0500 +end +``` + +You can mix date and non-date parameters. + +```ruby +account.messages.where( + date_before: 3.days.ago.to_i, + date_after: 4.weeks.ago.to_i, + subject: 'subject of email', + from: 'foo@email.com' +) +``` + + +### Individual Messages + +#### Message Basics + +```ruby +account.messages.where(limit: 1).class # ContextIO::MessageCollection + +message = account.messages.where(limit: 1).first + +message.class #=> ContextIO::Message +message.subject #=> "subject of message" +message.from #=> {"email"=>"some@email.com", "name"=>"John Doe"} +message.to #=> [{"email"=>"some@email.com", "name"=>"'John Doe'"}, {"email"=>"another@email.com", "name"=>"Jeff Mangum"}] +``` + +#### Message Dates + +##### received_at + +`received_at` is the time when your IMAP account records the message +having arrived. It is returned as a Time object. + +```ruby +m.received_at #=> 2013-04-19 08:12:04 -0500 +m.received_at.class #=> Time +``` + +##### indexed_at + +`indexed_at` is the time when the message was processed and indexed +by the Context.io sync process. + +```ruby +m.indexed_at #=> 2013-04-29 01:14:39 -0500 +m.received_at.class #=> Time +``` + +##### date + +A message's date is set by the sender, extracted from the message's Date +header and is returned as a FixNum which can be converted into a Time +object. + +```ruby +message.date #=> 1361828599 +Time.at(message.date) #=> 2013-04-19 08:11:33 -0500 +Time.at(message.date).class #=> Time +``` + +`message.date` is not reliable as it is easily spoofed. While it is made +available, `received_at` and `indexed_at` are better choices. + + +#### Messages with Body Data + +By default, Context.io's API does not return message queries with body data. + +You can include the body attribute in each individual message returned by +adding `include_body: 1` to your `messages.where` query options. + +```ruby +account.messages.where(include_body: 1, limit: 1).each do |message| + puts "#{message.subject} #{message.date} #{message.body[0]['content']}" +end +``` + +Emails that contain two or more body parts are called [multipart messages](https://en.wikipedia.org/wiki/MIME#Alternative). + +'text/plain' and 'text/html' are common body part types for multipart +messages. + +In the case a user is viewing email in a client that does not support HTML +markup, the 'text/plain' body part type will render instead. + +If you are working with multipart messages, you may want to check each +body part's content in turn. + +```ruby +account.messages.where(include_body: 1, limit: 1).each do |message| + message.body_parts.each do |body_part| + puts body_part.content + end +end +``` + +The `include_body` method queries the source IMAP box directly, which results +in slower return times. + + +### Files + +#### Files Per Message ID + +```ruby +message = account.messages[message_id] + +message.files.class #=> ContextIO::FileCollection +message.files.count #=> 2 +message.files.map { |f| f.file_name } #=> ["at_icon.png", "argyle_slides.png"] +message.files.first.content_link #=> https://contextio_to_s3_redirect_url +``` + +The file['content_link'] url is a S3 backed temporary link. It is intended to be +used promptly after being called. Do not store off this link. Instead, store off +the message_id and request on demand. + + +### On Laziness + +This gem is architected to be as lazy as possible. It won't make an HTTP request +until you ask it for some data that it knows it needs to fetch. An example might +be illustrative: + +```ruby +require 'contextio' + +contextio = ContextIO.new('your_api_key', 'your_api_secret') + +account = contextio.accounts['exampleaccountid12345678'] # No request made here. +account.last_name # Request made here. +account.first_name # No request made here. +``` + +Note that when it made the request, it stored the data it got back, which +included the first name, so it didn't need to make a second request. Asking for +the value of any attribute listed in the [gem +docs](http://rubydoc.info/gems/contextio/frames) will trigger the request. + + +### On Requests and Methods + +There are some consistent mappings between the requests documented in the +[API docs](http://context.io/docs/2.0/) and the methods implemented in the gem. + +**For collections of resources:** + +* the object its self sort of represents the collection-level `GET` (treat it + like any other `Enumerable`). +* `#where` is how you set up the filters on the collection-level `GET`. +* `#create` maps to the collection-level `POST` or `PUT`, as appropriate. +* `#[]` maps to the individual-level `GET`, but (as mentioned above) is lazy. + +**For individual resources** + +* the object its self sort of represents the individual-level `GET` (but see + `#[]` above). +* `#delete` maps to the individual-level `DELETE`. +* `#update` maps to the individual-level `POST` (except in a few cases like + `Message#copy_to` and `Message#move_to`). -This gem adheres to [SemVer](http://semver.org/). So you should be pretty safe -upgrading from 1.0.0 to 1.9.9. Whatever as long as the major version doesn't -bump. When the major version bumps, be warned; upgrading will take some kind of -effort. ## Contributing -Help is gladly welcomed. If you have a feature you'd like to add, it's much more -likely to get in (or get in faster) the closer you stick to these steps: +If you'd like to contribute, please see the [contribution guidelines](CONTRIBUTING.md). + + +## Releasing -1. Open an Issue to talk about it. We can discuss whether it's the right - direction or maybe help track down a bug, etc. -1. Fork the project, and make a branch to work on your feature/fix. Master is - where you'll want to start from. -1. Turn the Issue into a Pull Request. There are several ways to do this, but - [hub](https://github.com/defunkt/hub) is probably the easiest. -1. Make sure your Pull Request includes tests. +Maintainers: Please make sure to follow the [release steps](RELEASING.md) when +it's time to cut a new release. -If you don't know how to fix something, even just a Pull Request that includes a -failing test can be helpful. If in doubt, make an Issue to discuss. ## Copyright -Copyright (c) 2012 Context.IO +Copyright (c) 2012-2014 Context.IO -See LICENSE.md for details. +This gem is distributed under the MIT License. See LICENSE.md for details. diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..f715e2a --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,15 @@ +## Releasing + +If you want to push a new version of this gem, do this: + +1. Ideally, every Pull Request should already have included an addition to the + `CHANGES.md` file summarizing the changes and crediting the author(s). It + doesn't hurt to review this to see if anything needs adding. +1. Commit any changes you make. +1. Go into version.rb and bump the version number + [as appopriate](http://semver.org/). +1. Go into CHANGES.md and change the "Unlreleased" heading to match the new + version number. +1. Commit these changes with a message like, "Minor version bump," or similar. +1. Run `rake release`. +1. High five someone nearby. diff --git a/Rakefile b/Rakefile index 529453d..1542f7a 100644 --- a/Rakefile +++ b/Rakefile @@ -19,6 +19,7 @@ rescue Bundler::BundlerError => e end require 'rake' +require 'bundler/gem_tasks' require 'rspec/core/rake_task' RSpec::Core::RakeTask.new @@ -33,6 +34,7 @@ task :doc => :yard desc "Fire up an interactive terminal to play with" task :console do require 'pry' + require 'yaml' require File.expand_path(File.dirname(__FILE__) + '/lib/contextio') config = YAML.load_file File.expand_path(File.dirname(__FILE__) + '/spec/config.yml') diff --git a/contextio.gemspec b/contextio.gemspec index bb2e740..c1252d0 100644 --- a/contextio.gemspec +++ b/contextio.gemspec @@ -17,14 +17,16 @@ Gem::Specification.new do |gem| gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.require_paths = ['lib'] - gem.add_dependency 'oauth', '~> 0.4.5' + gem.add_dependency 'faraday', '~> 0.8.0' + gem.add_dependency 'faraday_middleware', '~> 0.9.0' + gem.add_dependency 'simple_oauth', '~> 0.2.0' gem.add_development_dependency 'bundler' gem.add_development_dependency 'rubygems-tasks', '~> 0.2' - gem.add_development_dependency 'rspec', '~> 2.4' + gem.add_development_dependency 'rspec', '~> 2.14' gem.add_development_dependency 'rake' gem.add_development_dependency 'yard' gem.add_development_dependency 'redcarpet' gem.add_development_dependency 'pry-doc' - gem.add_development_dependency 'fakeweb' + gem.add_development_dependency 'webmock' end diff --git a/lib/contextio.rb b/lib/contextio.rb index 137cdf5..0958d7f 100644 --- a/lib/contextio.rb +++ b/lib/contextio.rb @@ -17,8 +17,9 @@ class ContextIO # @param [String] key Your OAuth consumer key for your Context.IO account # @param [String] secret Your OAuth consumer secret for your Context.IO # account - def initialize(key, secret) - @api = API.new(key, secret) + # @param [Hash] opts Optional options for OAuth connections. ie. :timeout and :open_timeout are supported + def initialize(key, secret, opts={}) + @api = API.new(key, secret, opts) end # Your entry point for dealing with oauth providers. diff --git a/lib/contextio/account.rb b/lib/contextio/account.rb index 63038ad..8747851 100644 --- a/lib/contextio/account.rb +++ b/lib/contextio/account.rb @@ -121,8 +121,8 @@ def sync_data return @sync_data end - def sync! - api.request(:post, "#{resource_url}/sync")['success'] + def sync!(options={}) + api.request(:post, "#{resource_url}/sync", options)['success'] end def delete diff --git a/lib/contextio/api.rb b/lib/contextio/api.rb index 935cb3e..4d48be7 100644 --- a/lib/contextio/api.rb +++ b/lib/contextio/api.rb @@ -1,6 +1,7 @@ require 'uri' -require 'oauth' require 'json' +require 'faraday' +require 'faraday_middleware' require 'contextio/api/url_builder' @@ -52,17 +53,25 @@ def user_agent_string self.class.user_agent_string end + attr_accessor :base_url, :version + # @!attribute [r] key # @return [String] The OAuth key for the user's Context.IO account. # @!attribute [r] secret # @return [String] The OAuth secret for the user's Context.IO account. - attr_reader :key, :secret + # @!attribute [r] opts + # @return [Hash] opts Optional options for OAuth connections. + attr_reader :key, :secret, :opts # @param [String] key The user's OAuth key for their Context.IO account. # @param [String] secret The user's OAuth secret for their Context.IO account. - def initialize(key, secret) + # @param [Hash] opts Optional options for OAuth connections. ie. :timeout and :open_timeout are supported + def initialize(key, secret, opts={}) @key = key @secret = secret + @opts = opts || {} + @base_url = self.class.base_url + @version = self.class.version end # Generates the path for a resource_path and params hash for use with the API. @@ -72,7 +81,7 @@ def initialize(key, secret) # @param [{String, Symbol => String, Symbol, Array}] params # A Hash of the query parameters for the action represented by this path. def path(resource_path, params = {}) - "/#{API.version}/#{API.strip_resource_path(resource_path)}#{API.hash_to_url_params(params)}" + "/#{version}/#{strip_resource_path(resource_path)}#{API.hash_to_url_params(params)}" end # Makes a request against the Context.IO API. @@ -85,43 +94,51 @@ def path(resource_path, params = {}) # # @raise [API::Error] if the response code isn't in the 200 or 300 range. def request(method, resource_path, params = {}) - response = token.send(method, path(resource_path, params), 'Accept' => 'application/json', 'User-Agent' => user_agent_string) - body = response.body - - results = JSON.parse(body) unless response.body.empty? - - if response.code =~ /[45]\d\d/ - if results.is_a?(Hash) && results['type'] == 'error' - message = results['value'] - else - message = response.message - end + response = oauth_request(method, resource_path, params, { 'Accept' => 'application/json' }) - raise API::Error, message + with_error_handling(response) do |response| + parse_json(response.body) end - - results end def raw_request(method, resource_path, params={}) - response = token.send(method, path(resource_path, params), 'User-Agent' => user_agent_string) + response = oauth_request(method, resource_path, params) - if response.code =~ /[45]\d\d/ - raise API::Error, response.message + with_error_handling(response) do |response| + response.body end - - response.body end private + # Makes a request signed for OAuth, encoding parameters correctly, etc. + # + # @param [String, Symbol] method The HTTP verb for the request (lower case). + # @param [String] resource_path The path to the resource in question. + # @param [{String, Symbol => String, Symbol, Array}] params + # A Hash of the query parameters for the action represented by this + # request. + # @param [{String, Symbol => String, Symbol, Array}] headers + # A Hash of headers to be merged with the default headers for making + # requests. + # + # @return [Faraday::Response] The response object from the request. + def oauth_request(method, resource_path, params, headers=nil) + normalized_params = params.inject({}) do |normalized_params, (key, value)| + normalized_params[key.to_sym] = value + normalized_params + end + + connection.send(method, path(resource_path), normalized_params, headers) + end + # So that we can accept full URLs, this strips the domain and version number # out and returns just the resource path. # # @param [#to_s] resource_path The full URL or path for a resource. # # @return [String] The resource path. - def self.strip_resource_path(resource_path) + def strip_resource_path(resource_path) resource_path.to_s.gsub("#{base_url}/#{version}/", '') end @@ -145,18 +162,64 @@ def self.hash_to_url_params(params = {}) "?#{URI.encode_www_form(params)}" end - # @!attribute [r] consumer - # @return [OAuth::Consumer] An Oauth consumer object for credentials - # purposes. - def consumer - @consumer ||= OAuth::Consumer.new(key, secret, site: API.base_url) + # @!attribute [r] connection + # @return [Faraday::Connection] A handle on the Faraday connection object. + def connection + @connection ||= Faraday::Connection.new(base_url) do |faraday| + faraday.headers['User-Agent'] = user_agent_string + + faraday.request :oauth, consumer_key: key, consumer_secret: secret + faraday.request :url_encoded + + faraday.adapter Faraday.default_adapter + end + end + + # Errors can come in a few shapes and we want to detect them and extract the + # useful information. If no errors are found, it calls the provided block + # and passes the response through. + # + # @param [Faraday::Response] response A response object from making a request to the + # API with Faraday. + # + # @raise [API::Error] if the response code isn't in the 200 or 300 range. + def with_error_handling(response, &block) + return block.call(response) if response.success? + + parsed_body = parse_json(response.body) + message = determine_best_error_message(parsed_body) || "HTTP #{response.status} Error" + + raise API::Error, message + end + + # Parses JSON if there's valid JSON passed in. + # + # @param [String] document A string you suspect may be a JSON document. + # + # @return [Hash, Array, Nil] Either a parsed version of the JSON document or + # nil, if the document wasn't valid JSON. + def parse_json(document) + return JSON.parse(document.to_s) + rescue JSON::ParserError => e + return nil end - # @!attribute [r] token - # @return [Oauth::AccessToken] An Oauth token object for credentials - # purposes. - def token - @token ||= OAuth::AccessToken.new(consumer) + # Given a parsed JSON body from an error response, figures out if it can + # pull useful information therefrom. + # + # @param [Hash] parsed_body A Hash parsed from a JSON document that may + # describe an error condition. + # + # @return [String, Nil] If it can, it will return a human-readable + # error-describing String. Otherwise, nil. + def determine_best_error_message(parsed_body) + return unless parsed_body.respond_to?(:[]) + + if parsed_body['type'] == 'error' + return parsed_body['value'] + elsif parsed_body.has_key?('success') && !parsed_body['success'] + return [parsed_body['feedback_code'], parsed_body['connectionLog']].compact.join("\n") + end end end end diff --git a/lib/contextio/api/resource.rb b/lib/contextio/api/resource.rb index 1e9d842..47a757d 100644 --- a/lib/contextio/api/resource.rb +++ b/lib/contextio/api/resource.rb @@ -190,7 +190,7 @@ def lazy_attributes(*attributes) # resource. def belongs_to(association_name) define_method(association_name) do - if instance_variable_get("@#{association_name}") + if instance_variable_defined?("@#{association_name}") instance_variable_get("@#{association_name}") else association_attrs = api_attributes[association_name.to_s] @@ -219,7 +219,15 @@ def has_many(association_name) define_method(association_name) do association_class = ContextIO::API::AssociationHelpers.class_for_association_name(association_name) - instance_variable_get("@#{association_name}") || instance_variable_set("@#{association_name}", association_class.new(api, self.class.association_name => self, attribute_hashes: api_attributes[association_name.to_s])) + instance_variable_get("@#{association_name}") || + instance_variable_set( + "@#{association_name}", + association_class.new( + api, + self.class.association_name => self, + attribute_hashes: api_attributes[association_name.to_s] + ) + ) end associations << association_name.to_sym diff --git a/lib/contextio/api/resource_collection.rb b/lib/contextio/api/resource_collection.rb index 38d8157..19fb988 100644 --- a/lib/contextio/api/resource_collection.rb +++ b/lib/contextio/api/resource_collection.rb @@ -53,8 +53,25 @@ def each(&block) end end + # Returns the number of elements in self. May be zero. + # + # @note Calling this method will load the collection if not already loaded. + def size + attribute_hashes.size + end + alias :length :size + alias :count :size + + # Returns true if self contains no elements. + # + # @note Calling this method will load the collection if not already loaded. + def empty? + size == 0 + end + # Specify one or more constraints for limiting resources in this - # collection. See individual classes for the list of valid constraints. + # collection. See individual classes in the + # [Context.IO docs](http://context.io/docs/2.0/) for the list of valid constraints. # Not all collections have valid where constraints at all. # # This can be chained at need and doesn't actually cause the API to get @@ -87,7 +104,7 @@ def where(constraints) # @param [String] key The Provider Consumer Key for the # provider you want to interact with. def [](key) - resource_class.new(api, resource_class.primary_key => key) + resource_class.new(api, associations_hash.merge(resource_class.primary_key => key)) end private @@ -104,7 +121,10 @@ def attribute_hashes # associated resource of that type. def associations_hash @associations_hash ||= self.class.associations.inject({}) do |memo, association_name| - memo[association_name.to_sym] = self.send(association_name) + if (association = self.send(association_name)) + memo[association_name.to_sym] = association + end + memo end end diff --git a/lib/contextio/api/url_builder.rb b/lib/contextio/api/url_builder.rb index bb24a0e..faccc97 100644 --- a/lib/contextio/api/url_builder.rb +++ b/lib/contextio/api/url_builder.rb @@ -50,11 +50,7 @@ def self.register_url(resource_class, &block) end register_url ContextIO::ConnectToken do |connect_token| - if connect_token.account && connect_token.account.id - "accounts/#{connect_token.account.id}/connect_tokens/#{connect_token.token}" - else - "connect_tokens/#{connect_token.token}" - end + "connect_tokens/#{connect_token.token}" end register_url ContextIO::ConnectTokenCollection do |connect_tokens| diff --git a/lib/contextio/body_part.rb b/lib/contextio/body_part.rb index 29fc466..36a647c 100644 --- a/lib/contextio/body_part.rb +++ b/lib/contextio/body_part.rb @@ -24,6 +24,8 @@ def initialize(api, options = {}) @message = options.delete(:message) || options.delete('message') options.each do |key, value| + key = key.to_s.gsub('-', '_') + instance_variable_set("@#{key}", value) unless self.respond_to?(key) diff --git a/lib/contextio/email_address.rb b/lib/contextio/email_address.rb index 7df51eb..ba44995 100644 --- a/lib/contextio/email_address.rb +++ b/lib/contextio/email_address.rb @@ -24,6 +24,8 @@ def initialize(api, options = {}) @account = options.delete(:account) || options.delete('account') options.each do |key, value| + key = key.to_s.gsub('-', '_') + instance_variable_set("@#{key}", value) unless self.respond_to?(key) diff --git a/lib/contextio/email_settings.rb b/lib/contextio/email_settings.rb index 316dd16..70de0de 100644 --- a/lib/contextio/email_settings.rb +++ b/lib/contextio/email_settings.rb @@ -131,6 +131,8 @@ def fetch_attributes attr_hashes = api.request(:get, resource_url, 'email' => email, 'source_type' => source_type) attr_hashes.each do |key, value| + key = key.to_s.gsub('-', '_') + instance_variable_set("@#{key}", value) unless respond_to?(key) diff --git a/lib/contextio/file.rb b/lib/contextio/file.rb index 5a7363b..c79bac8 100644 --- a/lib/contextio/file.rb +++ b/lib/contextio/file.rb @@ -48,7 +48,7 @@ def embedded? end def content - @content ||= api.request(:get, "#{resource_url}/content") + @content ||= api.raw_request(:get, "#{resource_url}/content") end def content_link @@ -75,18 +75,5 @@ def revisions return @revisions end - def sync_data - return @sync_data if @sync_data - - sync_hashes = api.request(:get, "#{resource_url}/sync") - - @sync_data = ContextIO::SourceSyncData.new(sync_hashes) - - return @sync_data - end - - def sync! - api.request(:post, "#{resource_url}/sync")['success'] - end end end diff --git a/lib/contextio/folder.rb b/lib/contextio/folder.rb index 0de7b3c..d76dea6 100644 --- a/lib/contextio/folder.rb +++ b/lib/contextio/folder.rb @@ -25,6 +25,8 @@ def initialize(api, options = {}) @source = options.delete(:source) || options.delete('source') options.each do |key, value| + key = key.to_s.gsub('-', '_') + instance_variable_set("@#{key}", value) unless self.respond_to?(key) @@ -50,7 +52,7 @@ def imap_attributes def messages association_class = ContextIO::API::AssociationHelpers.class_for_association_name(:messages) - @messages ||= association_class.new(api, folder: self) + @messages ||= association_class.new(api, account: source.account).where(folder: self.name) end end end diff --git a/lib/contextio/message.rb b/lib/contextio/message.rb index dd4ec33..2165414 100644 --- a/lib/contextio/message.rb +++ b/lib/contextio/message.rb @@ -10,11 +10,13 @@ class Message belongs_to :account has_many :sources has_many :body_parts + has_many :files lazy_attributes :date, :folders, :addresses, :subject, :list_help, :list_unsubscribe, :message_id, :email_message_id, :gmail_message_id, :gmail_thread_id, :person_info, - :date_received, :date_indexed + :date_received, :date_indexed, :in_reply_to, :references + private :date_received, :date_indexed def received_at @@ -32,20 +34,26 @@ def flags # As of this writing, the documented valid flags are: seen, answered, # flagged, deleted, and draft. However, this will send whatever you send it. def set_flags(flag_hash) - args = flag_hash.map({}) do |memo, (flag_name, value)| + args = flag_hash.inject({}) do |memo, (flag_name, value)| memo[flag_name] = value ? 1 : 0 memo end - api.request(:post, resource_url, args)['success'] + api.request(:post, "#{resource_url}/flags", args)['success'] end def folders api.request(:get, "#{resource_url}/folders").collect { |f| f['name'] } end - def headers - api.request(:get, "#{resource_url}/headers") + # def headers + # api.request(:get, "#{resource_url}/headers") + # end + + %w(from to bcc cc reply_to).each do |f| + define_method(f) do + addresses[f] + end end def raw diff --git a/lib/contextio/source.rb b/lib/contextio/source.rb index f694c36..9f15015 100644 --- a/lib/contextio/source.rb +++ b/lib/contextio/source.rb @@ -27,20 +27,12 @@ def use_ssl? # provider_token_secret, provider_consumer_key. See the Context.IO docs # for more details on these fields. def update(options={}) - updatable_attrs = %w(status sync_period service_level password - provider_token provider_token_secret - provider_consumer_key) - - options.keep_if do |key, value| - updatable_attrs.include?(key.to_s) - end - - return nil if options.empty? - it_worked = api.request(:post, resource_url, options)['success'] if it_worked options.each do |key, value| + key = key.to_s.gsub('-', '_') + instance_variable_set("@#{key}", value) end end @@ -51,5 +43,19 @@ def update(options={}) def delete api.request(:delete, resource_url)['success'] end + + def sync_data + return @sync_data if @sync_data + + sync_hashes = api.request(:get, "#{resource_url}/sync") + + @sync_data = ContextIO::SourceSyncData.new(label, sync_hashes) + + return @sync_data + end + + def sync!(options={}) + api.request(:post, "#{resource_url}/sync", options)['success'] + end end end diff --git a/lib/contextio/source_collection.rb b/lib/contextio/source_collection.rb index 0803d83..46d0d89 100644 --- a/lib/contextio/source_collection.rb +++ b/lib/contextio/source_collection.rb @@ -24,11 +24,12 @@ class SourceCollection # required and what's optional. def create(email, server, username, use_ssl, port, type, options={}) api_args = options.merge( - 'email' => email, - 'username' => username, - 'use_ssl' => use_ssl ? '1' : '0', - 'port' => port.to_s, - 'type' => type + :email => email, + :server => server, + :username => username, + :use_ssl => use_ssl ? '1' : '0', + :port => port.to_s, + :type => type ) result_hash = api.request(:post, resource_url, api_args) diff --git a/lib/contextio/version.rb b/lib/contextio/version.rb index f8fd7e6..9fe52a9 100644 --- a/lib/contextio/version.rb +++ b/lib/contextio/version.rb @@ -1,6 +1,6 @@ class ContextIO # @private - VERSION = "1.0.1" + VERSION = "1.7.2" # The gem version. # diff --git a/spec/contextio/api_spec.rb b/spec/contextio/api_spec.rb deleted file mode 100644 index b528dc4..0000000 --- a/spec/contextio/api_spec.rb +++ /dev/null @@ -1,123 +0,0 @@ -require 'spec_helper' -require 'contextio/api' - -describe ContextIO::API do - describe ".version" do - subject { ContextIO::API } - - it "uses API version 2.0" do - expect(subject.version).to eq('2.0') - end - end - - describe ".base_url" do - subject { ContextIO::API } - - it "is https://api.context.io" do - expect(subject.base_url).to eq('https://api.context.io') - end - end - - describe ".new" do - subject { ContextIO::API.new('test_key', 'test_secret') } - - it "takes a key" do - expect(subject.key).to eq('test_key') - end - - it "takes a secret" do - expect(subject.secret).to eq('test_secret') - end - end - - describe "#path" do - context "without params" do - subject { ContextIO::API.new(nil, nil).path('test_command') } - - it "puts the command in the path" do - expect(subject).to eq('/2.0/test_command') - end - end - - context "with params" do - subject { ContextIO::API.new(nil, nil).path('test_command', foo: 1, bar: %w(a b c)) } - - it "URL encodes the params" do - expect(subject).to eq('/2.0/test_command?foo=1&bar=a%2Cb%2Cc') - end - end - - context "with a full URL" do - subject { ContextIO::API.new(nil, nil).path('https://api.context.io/2.0/test_command') } - - it "strips out the command" do - expect(subject).to eq('/2.0/test_command') - end - end - end - - describe "#request" do - subject { ContextIO::API.new(nil, nil).request(:get, 'test') } - - context "with a good response" do - before do - FakeWeb.register_uri( - :get, - 'https://api.context.io/2.0/test', - body: JSON.dump('a' => 'b', 'c' => 'd') - ) - end - - it "parses the JSON response" do - expect(subject).to eq('a' => 'b', 'c' => 'd') - end - end - - context "with a bad response that has a body" do - before do - FakeWeb.register_uri( - :get, - 'https://api.context.io/2.0/test', - status: ['400', 'Bad Request'], - body: JSON.dump('type' => 'error', 'value' => 'nope') - ) - end - - it "raises an API error with the body message" do - expect { subject }.to raise_error(ContextIO::API::Error, 'nope') - end - end - - context "with a bad response that has no body" do - before do - FakeWeb.register_uri( - :get, - 'https://api.context.io/2.0/test', - status: ['400', 'Bad Request'] - ) - end - - it "raises an API error with the header message" do - expect { subject }.to raise_error(ContextIO::API::Error, 'Bad Request') - end - end - end - - describe ".url_for" do - it "delegates to ContextIO::API::URLBuilder" do - ContextIO::API::URLBuilder.should_receive(:url_for).with('foo') - - ContextIO::API.url_for('foo') - end - end - - describe "#url_for" do - subject { ContextIO::API.new('test_key', 'test_secret') } - - it "delegates to the class" do - ContextIO::API.should_receive(:url_for).with('foo') - - subject.url_for('foo') - end - end -end diff --git a/spec/integration/accounts_messages_spec.rb b/spec/integration/accounts_messages_spec.rb new file mode 100644 index 0000000..3314e12 --- /dev/null +++ b/spec/integration/accounts_messages_spec.rb @@ -0,0 +1,11 @@ +require 'contextio' + +describe "A Message created from an Account" do + let(:api) { double(:api) } + let(:account) { ContextIO::Account.new(api, id: '1234', messages: [{}]) } + let(:message) { account.messages['4321'] } + + it "has a handle back to the account" do + expect(message.account).to be(account) + end +end diff --git a/spec/integration/folders_messages_spec.rb b/spec/integration/folders_messages_spec.rb new file mode 100644 index 0000000..da5195c --- /dev/null +++ b/spec/integration/folders_messages_spec.rb @@ -0,0 +1,12 @@ +require 'contextio' + +describe "A MessageCollection created from a Folder" do + let(:api) { double(:api) } + let(:source) { double(:source, account: 'account') } + let(:folder) { ContextIO::Folder.new(api, source: source) } + let(:message_collection) { folder.messages } + + it "has a handle on the right account" do + expect(message_collection.account).to eq('account') + end +end diff --git a/spec/integration/source_sync_data_spec.rb b/spec/integration/source_sync_data_spec.rb new file mode 100644 index 0000000..1b030b6 --- /dev/null +++ b/spec/integration/source_sync_data_spec.rb @@ -0,0 +1,14 @@ +require 'contextio' + +describe "Syncing data for a source" do + let(:api) { double(:api) } + subject { ContextIO::Source.new(api, resource_url: 'resource url') } + + before do + allow(api).to receive(:request).and_return({foo: 'bar'}) + end + + it "initializes a SourceSyncData correctly" do + expect { subject.sync_data }.to_not raise_exception + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ecba438..a376d62 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,6 @@ require 'rspec' require 'pry' -require 'fakeweb' +require 'webmock/rspec' RSpec.configure do |rspec| rspec.run_all_when_everything_filtered = true @@ -12,6 +12,10 @@ rspec.expect_with :rspec do |expectations| expectations.syntax = :expect end + + rspec.mock_with :rspec do |mocks| + mocks.syntax = :expect + end end -FakeWeb.allow_net_connect = false +WebMock.disable_net_connect! diff --git a/spec/contextio/account_collection_spec.rb b/spec/unit/contextio/account_collection_spec.rb similarity index 81% rename from spec/contextio/account_collection_spec.rb rename to spec/unit/contextio/account_collection_spec.rb index 412ef34..ce8be51 100644 --- a/spec/contextio/account_collection_spec.rb +++ b/spec/unit/contextio/account_collection_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'contextio/account_collection' describe ContextIO::AccountCollection do @@ -8,7 +7,7 @@ describe "#create" do before do - api.stub(:request).with(:post, anything, anything).and_return( + allow(api).to receive(:request).with(:post, anything, anything).and_return( 'success' => true, 'id' => '1234', 'resource_url' => 'resource_url' @@ -21,7 +20,7 @@ end it "posts to the api" do - api.should_receive(:request).with( + expect(api).to receive(:request).with( :post, 'url from api', hash_including(email: 'hello@email.com') @@ -31,7 +30,7 @@ end it "doesn't make any more API calls than it needs to" do - api.should_not_receive(:request).with(:get, anything, anything) + expect(api).to_not receive(:request).with(:get, anything, anything) subject.create(email: 'hello@email.com') end @@ -41,7 +40,7 @@ end it "takes an optional first name" do - api.should_receive(:request).with( + expect(api).to receive(:request).with( anything, anything, hash_including(first_name: 'Bruno') @@ -51,7 +50,7 @@ end it "takes an optional last name" do - api.should_receive(:request).with( + expect(api).to receive(:request).with( anything, anything, hash_including(last_name: 'Morency') @@ -65,11 +64,11 @@ subject { ContextIO::AccountCollection.new(api).where(email: 'hello@email.com')} it "allows a missing email address" do - expect { subject.create(first_name: 'Bruno') }.to_not raise_error(ArgumentError) + expect { subject.create(first_name: 'Bruno') }.to_not raise_error end it "uses the email address from the where constraints" do - api.should_receive(:request).with(anything, anything, hash_including(email: 'hello@email.com')) + expect(api).to receive(:request).with(anything, anything, hash_including(email: 'hello@email.com')) subject.create(first_name: 'Bruno') end diff --git a/spec/contextio/account_spec.rb b/spec/unit/contextio/account_spec.rb similarity index 86% rename from spec/contextio/account_spec.rb rename to spec/unit/contextio/account_spec.rb index 374a328..acf33da 100644 --- a/spec/contextio/account_spec.rb +++ b/spec/unit/contextio/account_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'contextio/account' describe ContextIO::Account do @@ -22,13 +21,13 @@ describe "#update" do before do - api.stub(:request).and_return({'success' => true}) + allow(api).to receive(:request).and_return({'success' => true}) end subject { ContextIO::Account.new(api, id: '1234', first_name: 'old first name') } it "posts to the api" do - api.should_receive(:request).with( + expect(api).to receive(:request).with( :post, 'url from api', first_name: 'new first name' @@ -44,7 +43,7 @@ end it "doesn't make any more API calls than it needs to" do - api.should_not_receive(:request).with(:get, anything, anything) + expect(api).to_not receive(:request).with(:get, anything, anything) subject.update(first_name: 'new first name') end diff --git a/spec/contextio/api/association_helpers_spec.rb b/spec/unit/contextio/api/association_helpers_spec.rb similarity index 97% rename from spec/contextio/api/association_helpers_spec.rb rename to spec/unit/contextio/api/association_helpers_spec.rb index 4d9be4e..5b6fcab 100644 --- a/spec/contextio/api/association_helpers_spec.rb +++ b/spec/unit/contextio/api/association_helpers_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'contextio/api/association_helpers' describe "ContextIO::API::AssociationHelpers" do diff --git a/spec/contextio/api/resource_collection_spec.rb b/spec/unit/contextio/api/resource_collection_spec.rb similarity index 78% rename from spec/contextio/api/resource_collection_spec.rb rename to spec/unit/contextio/api/resource_collection_spec.rb index 8dec072..78a0bc8 100644 --- a/spec/contextio/api/resource_collection_spec.rb +++ b/spec/unit/contextio/api/resource_collection_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'contextio/api/resource_collection' class SingularHelper @@ -88,7 +87,7 @@ def initialize(*args); end end it "limits the scope of subsequent #each calls" do - api.should_receive(:request).with(anything, anything, foo: 'bar').and_return([]) + expect(api).to receive(:request).with(anything, anything, foo: 'bar').and_return([]) subject.where(foo: 'bar').each { } end @@ -129,7 +128,7 @@ def initialize(*args); end end before do - api.stub(:request).and_return([{key: 'value 1'}, {key: 'value 2'}]) + allow(api).to receive(:request).and_return([{key: 'value 1'}, {key: 'value 2'}]) end it "yields instances of the singular resource class" do @@ -139,20 +138,20 @@ def initialize(*args); end end it "gets attributes for the resources from the api" do - api.should_receive(:request).exactly(:once).with(:get, 'url from api', {}) + expect(api).to receive(:request).exactly(:once).with(:get, 'url from api', {}) subject.each { } end it "passes the api to the singular resource instances" do - SingularHelper.should_receive(:new).exactly(:twice).with(api, anything) + expect(SingularHelper).to receive(:new).exactly(:twice).with(api, anything) subject.each { } end it "passes attributes to the singular resource instances" do - SingularHelper.should_receive(:new).exactly(:once).with(anything, key: 'value 1') - SingularHelper.should_receive(:new).exactly(:once).with(anything, key: 'value 2') + expect(SingularHelper).to receive(:new).exactly(:once).with(anything, key: 'value 1') + expect(SingularHelper).to receive(:new).exactly(:once).with(anything, key: 'value 2') subject.each { } end @@ -175,7 +174,7 @@ def initialize(*args); end end it "passes the belonged-to resource to the singular resource instances" do - SingularHelper.should_receive(:new).exactly(:twice).with( + expect(SingularHelper).to receive(:new).exactly(:twice).with( anything, hash_including(owner: owner) ) @@ -197,20 +196,20 @@ def initialize(*args); end end it "doesn't hit the API" do - api.should_not_receive(:request) + expect(api).to_not receive(:request) subject.each { } end it "passes the api to the singular resource instances" do - SingularHelper.should_receive(:new).exactly(:twice).with(api, anything) + expect(SingularHelper).to receive(:new).exactly(:twice).with(api, anything) subject.each { } end it "passes attributes to the singular resource instances" do - SingularHelper.should_receive(:new).exactly(:once).with(anything, foo: 'bar') - SingularHelper.should_receive(:new).exactly(:once).with(anything, foo: 'baz') + expect(SingularHelper).to receive(:new).exactly(:once).with(anything, foo: 'bar') + expect(SingularHelper).to receive(:new).exactly(:once).with(anything, foo: 'baz') subject.each { } end @@ -237,7 +236,7 @@ def initialize(*args); end end it "passes the belonged-to resource to the singular resource instances" do - SingularHelper.should_receive(:new).exactly(:twice).with( + expect(SingularHelper).to receive(:new).exactly(:twice).with( anything, hash_including(owner: owner) ) @@ -266,21 +265,38 @@ def initialize(*args); end end it "feeds the given key to the resource class" do - SingularHelper.should_receive(:new).with(anything, 'token' => 1234) + expect(SingularHelper).to receive(:new).with(anything, 'token' => 1234) subject[1234] end it "feeds the api to the resource class" do - SingularHelper.should_receive(:new).with(api, anything) + expect(SingularHelper).to receive(:new).with(api, anything) subject[1234] end it "doesn't hit the API" do - api.should_not_receive(:request) + expect(api).to_not receive(:request) subject[1234] end + + context "with an empty association" do + let(:helper_class) do + Class.new do + include ContextIO::API::ResourceCollection + + belongs_to :owner + self.resource_class = SingularHelper + end + end + + it "doesn't pass a nil association to the resouce class" do + expect(SingularHelper).to receive(:new).with(api, 'token' => 1234) + + subject[1234] + end + end end end diff --git a/spec/contextio/api/resource_spec.rb b/spec/unit/contextio/api/resource_spec.rb similarity index 86% rename from spec/contextio/api/resource_spec.rb rename to spec/unit/contextio/api/resource_spec.rb index 916bbc6..534d75f 100644 --- a/spec/contextio/api/resource_spec.rb +++ b/spec/unit/contextio/api/resource_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'contextio/api/resource' class ResourceCollection @@ -122,7 +121,7 @@ def foo end it "doesn't try to fetch from the API" do - subject.should_not_receive(:fetch_attributes) + expect(subject).to_not receive(:fetch_attributes) subject.foo end @@ -134,7 +133,7 @@ def foo context "when the attributes is not set at creation" do it "tries to fetch from the API" do - api.should_receive(:request).with(:get, 'resource_url'). + expect(api).to receive(:request).with(:get, 'resource_url'). and_return({'foo' => 'set from API', 'longer-name' => 'bar'}) subject.foo @@ -164,7 +163,7 @@ def foo subject { helper_class.new(api, resource_url: 'resource_url') } before do - api.stub(:request).and_return( + allow(api).to receive(:request).and_return( { 'resource' => { 'resource_url' => 'relation_url' @@ -178,7 +177,7 @@ def foo end it "passes keys from the api response to the new object" do - Resource.should_receive(:new).with(api, 'resource_url' => 'relation_url') + expect(Resource).to receive(:new).with(api, 'resource_url' => 'relation_url') subject.resource end @@ -192,7 +191,7 @@ def foo subject { helper_class.new(api, resource_url: 'resource_url') } before do - api.stub(:request).and_return({ }) + allow(api).to receive(:request).and_return({ }) end it "makes the resource nil" do @@ -204,7 +203,7 @@ def foo subject { helper_class.new(api, resource_url: 'resource_url') } before do - api.stub(:request).and_return( + allow(api).to receive(:request).and_return( { 'resource' => { } } @@ -220,7 +219,7 @@ def foo subject { helper_class.new(api, resource_url: 'resource_url') } before do - api.stub(:request).and_return( + allow(api).to receive(:request).and_return( { 'resource' => [ ] } @@ -243,7 +242,7 @@ def foo end it "doesn't make any API calls" do - api.should_not_receive(:request) + expect(api).to_not receive(:request) subject.resource end @@ -265,7 +264,7 @@ def foo subject { helper_class.new(api, resource_url: 'resource_url') } before do - api.stub(:request).and_return( + allow(api).to receive(:request).and_return( { 'resources' => [{ 'resource_url' => 'relation_url' @@ -279,7 +278,7 @@ def foo end it "passes keys from the api response to the new object" do - ResourceCollection.should_receive(:new).with(api, hash_including(attribute_hashes: [{'resource_url' => 'relation_url'}])) + expect(ResourceCollection).to receive(:new).with(api, hash_including(attribute_hashes: [{'resource_url' => 'relation_url'}])) subject.resources end @@ -289,7 +288,7 @@ def foo end it "passes its self to the new collection" do - ResourceCollection.should_receive(:new).with(anything, hash_including(:helper_class => subject)) + expect(ResourceCollection).to receive(:new).with(anything, hash_including(:helper_class => subject)) subject.resources end @@ -299,11 +298,11 @@ def foo subject { helper_class.new(api, resource_url: 'resource_url') } before do - api.stub(:request).and_return({ 'foo' => 'bar' }) + allow(api).to receive(:request).and_return({ 'foo' => 'bar' }) end it "tries the API only once" do - api.should_receive(:request).exactly(:once).and_return({ 'foo' => 'bar' }) + expect(api).to receive(:request).exactly(:once).and_return({ 'foo' => 'bar' }) subject.resources subject.resources @@ -314,7 +313,7 @@ def foo end it "passes its self to the new collection" do - ResourceCollection.should_receive(:new).with(anything, hash_including(:helper_class => subject)) + expect(ResourceCollection).to receive(:new).with(anything, hash_including(:helper_class => subject)) subject.resources end @@ -331,7 +330,7 @@ def foo end it "doesn't make any API calls" do - api.should_not_receive(:request) + expect(api).to_not receive(:request) subject.resources end @@ -350,13 +349,13 @@ def foo end it "makes a request to the API" do - subject.api.should_receive(:request).with(:get, 'resource_url').and_return({}) + expect(subject.api).to receive(:request).with(:get, 'resource_url').and_return({}) subject.send(:fetch_attributes) end it "defines getter methods for new attributes returned" do - subject.api.stub(:request).and_return(foo: 'bar') + allow(subject.api).to receive(:request).and_return(foo: 'bar') subject.send(:fetch_attributes) @@ -368,7 +367,7 @@ def subject.foo 'hard coded value' end - subject.api.stub(:request).and_return(foo: 'bar') + allow(subject.api).to receive(:request).and_return(foo: 'bar') subject.send(:fetch_attributes) @@ -376,7 +375,7 @@ def subject.foo end it "stores the response hash" do - subject.api.stub(:request).and_return('foo' => 'bar') + allow(subject.api).to receive(:request).and_return('foo' => 'bar') subject.send(:fetch_attributes) @@ -400,7 +399,7 @@ def subject.foo end it "hits the API only on first call" do - api.should_receive(:request).exactly(:once) + expect(api).to receive(:request).exactly(:once) subject.api_attributes subject.api_attributes @@ -436,7 +435,7 @@ def subject.foo end it "delegates URL construction to the api" do - api.should_receive(:url_for).with(subject).and_return('helpers/33f1') + expect(api).to receive(:url_for).with(subject).and_return('helpers/33f1') expect(subject.resource_url).to eq('helpers/33f1') end @@ -455,7 +454,7 @@ def subject.foo end it "makes a request to the API" do - subject.api.should_receive(:request).with(:delete, 'resource_url') + expect(subject.api).to receive(:request).with(:delete, 'resource_url') subject.delete end diff --git a/spec/contextio/api/url_builder_spec.rb b/spec/unit/contextio/api/url_builder_spec.rb similarity index 92% rename from spec/contextio/api/url_builder_spec.rb rename to spec/unit/contextio/api/url_builder_spec.rb index b803829..0db71ed 100644 --- a/spec/contextio/api/url_builder_spec.rb +++ b/spec/unit/contextio/api/url_builder_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'contextio/api/url_builder' describe ContextIO::API::URLBuilder do @@ -20,11 +19,11 @@ let(:account) { double('account', id: 'account_id') } let(:resource) { ContextIO::ConnectToken.new(api, token: 'token', account: account) } - it { should eq('accounts/account_id/connect_tokens/token') } + it { should eq('connect_tokens/token') } end context "without an account" do - let(:resource) { ContextIO::ConnectToken.new(api, token: 'token', api_attributes: { 'account' => { } }) } + let(:resource) { ContextIO::ConnectToken.new(api, token: 'token') } it { should eq('connect_tokens/token') } end @@ -38,7 +37,7 @@ it { should eq('accounts/account_id/connect_tokens') } end - context "wiehout an account" do + context "without an account" do let(:resource) { ContextIO::ConnectTokenCollection.new(api) } it { should eq('connect_tokens') } diff --git a/spec/unit/contextio/api_spec.rb b/spec/unit/contextio/api_spec.rb new file mode 100644 index 0000000..f0e2596 --- /dev/null +++ b/spec/unit/contextio/api_spec.rb @@ -0,0 +1,203 @@ +require 'contextio/api' + +describe ContextIO::API do + describe ".version" do + subject { ContextIO::API } + + it "uses API version 2.0" do + expect(subject.version).to eq('2.0') + end + end + + describe "#version" do + context "without version change" do + subject { ContextIO::API.new(nil, nil) } + + it "uses API version 2.0" do + expect(subject.version).to eq('2.0') + end + end + + context "with version change" do + subject { ContextIO::API.new(nil, nil) } + + it "changes the API version used" do + subject.version = '1.1' + expect(subject.version).to eq('1.1') + end + end + end + + describe ".base_url" do + subject { ContextIO::API } + + it "is https://api.context.io" do + expect(subject.base_url).to eq('https://api.context.io') + end + end + + describe "#base_url" do + context "without base_url change" do + subject { ContextIO::API.new(nil, nil) } + + it "is https://api.context.io" do + expect(subject.base_url).to eq('https://api.context.io') + end + end + + context "with base_url change" do + subject { ContextIO::API.new(nil, nil) } + + it "changes the base_url" do + subject.base_url = 'https://api.example.com' + expect(subject.base_url).to eq('https://api.example.com') + end + end + end + + describe ".new" do + subject { ContextIO::API.new('test_key', 'test_secret', {a:'b'}) } + + it "takes a key" do + expect(subject.key).to eq('test_key') + end + + it "takes a secret" do + expect(subject.secret).to eq('test_secret') + end + + it "takes an option hash" do + expect(subject.opts).to eq(a:'b') + end + end + + describe "#path" do + context "without params and default version" do + subject { ContextIO::API.new(nil, nil).path('test_command') } + + it "puts the command in the path" do + expect(subject).to eq('/2.0/test_command') + end + end + + context "without params and version change" do + subject { ContextIO::API.new(nil, nil) } + + it "puts the command in the path" do + subject.version = '2.5' + expect(subject.path('test_command')).to eq('/2.5/test_command') + end + end + + context "with params" do + subject { ContextIO::API.new(nil, nil).path('test_command', foo: 1, bar: %w(a b c)) } + + it "URL encodes the params" do + expect(subject).to eq('/2.0/test_command?foo=1&bar=a%2Cb%2Cc') + end + end + + context "with a full URL" do + subject { ContextIO::API.new(nil, nil).path('https://api.context.io/2.0/test_command') } + + it "strips out the command" do + expect(subject).to eq('/2.0/test_command') + end + end + + context "with a full URL and version and base_url change" do + subject { ContextIO::API.new(nil, nil) } + + it "strips out the command" do + subject.version = '2.5' + subject.base_url = 'https://api.example.com' + expect(subject.path('https://api.example.com/2.5/test_command')).to eq('/2.5/test_command') + end + end + end + + describe "#request" do + subject { ContextIO::API.new(nil, nil).request(:get, 'test') } + + context "with a good response" do + before do + WebMock.stub_request( + :get, + 'https://api.context.io/2.0/test' + ).to_return( + status: 200, + body: JSON.dump('a' => 'b', 'c' => 'd') + ) + end + + it "parses the JSON response" do + expect(subject).to eq('a' => 'b', 'c' => 'd') + end + end + + context "with a bad response that has a body" do + before do + WebMock.stub_request( + :get, + 'https://api.context.io/2.0/test' + ).to_return( + status: 400, + body: JSON.dump('type' => 'error', 'value' => 'nope') + ) + end + + it "raises an API error with the body message" do + expect { subject }.to raise_error(ContextIO::API::Error, 'nope') + end + end + + context "with a bad response that has a different body" do + before do + WebMock.stub_request( + :get, + 'https://api.context.io/2.0/test' + ).to_return( + status: 400, + body: JSON.dump('success' => false, 'feedback_code' => 'nope') + ) + end + + it "raises an API error with the body message" do + expect { subject }.to raise_error(ContextIO::API::Error, 'nope') + end + end + + context "with a bad response that has no body" do + before do + WebMock.stub_request( + :get, + 'https://api.context.io/2.0/test' + ).to_return( + status: 400 + ) + end + + it "raises an API error with the header message" do + expect { subject }.to raise_error(ContextIO::API::Error, 'HTTP 400 Error') + end + end + end + + describe ".url_for" do + it "delegates to ContextIO::API::URLBuilder" do + expect(ContextIO::API::URLBuilder).to receive(:url_for).with('foo') + + ContextIO::API.url_for('foo') + end + end + + describe "#url_for" do + subject { ContextIO::API.new('test_key', 'test_secret') } + + it "delegates to the class" do + expect(ContextIO::API).to receive(:url_for).with('foo') + + subject.url_for('foo') + end + end +end diff --git a/spec/contextio/connect_token_collection_spec.rb b/spec/unit/contextio/connect_token_collection_spec.rb similarity index 79% rename from spec/contextio/connect_token_collection_spec.rb rename to spec/unit/contextio/connect_token_collection_spec.rb index 8b8c91c..996b99e 100644 --- a/spec/contextio/connect_token_collection_spec.rb +++ b/spec/unit/contextio/connect_token_collection_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'contextio/connect_token_collection' describe ContextIO::ConnectTokenCollection do @@ -8,11 +7,11 @@ describe "#create" do before do - api.stub(:request).with(:post, anything, anything).and_return({ token: '1234' }) + allow(api).to receive(:request).with(:post, anything, anything).and_return({ token: '1234' }) end it "posts to the api" do - api.should_receive(:request).with( + expect(api).to receive(:request).with( :post, 'url from api', hash_including(callback_url: 'http://callback.com') @@ -22,7 +21,7 @@ end it "doesn't make any more API calls than it needs to" do - api.should_not_receive(:request).with(:get, anything, anything) + expect(api).to_not receive(:request).with(:get, anything, anything) subject.create('http://callback.com') end @@ -32,7 +31,7 @@ end it "takes an optional service level" do - api.should_receive(:request).with( + expect(api).to receive(:request).with( anything, anything, hash_including(service_level: 'PRO') @@ -42,7 +41,7 @@ end it "takes an optional email" do - api.should_receive(:request).with( + expect(api).to receive(:request).with( anything, anything, hash_including(email: 'person@email.com') @@ -52,7 +51,7 @@ end it "takes an optional first name" do - api.should_receive(:request).with( + expect(api).to receive(:request).with( anything, anything, hash_including(first_name: 'Bruno') @@ -62,7 +61,7 @@ end it "takes an optional last name" do - api.should_receive(:request).with( + expect(api).to receive(:request).with( anything, anything, hash_including(last_name: 'Morency') diff --git a/spec/contextio/connect_token_spec.rb b/spec/unit/contextio/connect_token_spec.rb similarity index 93% rename from spec/contextio/connect_token_spec.rb rename to spec/unit/contextio/connect_token_spec.rb index 486dff7..8c715a4 100644 --- a/spec/contextio/connect_token_spec.rb +++ b/spec/unit/contextio/connect_token_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'contextio/connect_token' describe ContextIO::ConnectToken do @@ -34,7 +33,7 @@ subject { ContextIO::ConnectToken.new(api, token: '1234') } it "uses the input key" do - api.should_not_receive(:request) + expect(api).to_not receive(:request) expect(subject.token).to eq('1234') end @@ -44,7 +43,7 @@ subject { ContextIO::ConnectToken.new(api, resource_url: 'http://example.com/hitme') } it "loads it from the API" do - api.should_receive(:request).with( + expect(api).to receive(:request).with( :get, 'http://example.com/hitme' ).and_return({ diff --git a/spec/contextio/email_settings_spec.rb b/spec/unit/contextio/email_settings_spec.rb similarity index 65% rename from spec/contextio/email_settings_spec.rb rename to spec/unit/contextio/email_settings_spec.rb index 69e8286..133b5b0 100644 --- a/spec/contextio/email_settings_spec.rb +++ b/spec/unit/contextio/email_settings_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'contextio/email_settings' describe ContextIO::EmailSettings do @@ -38,63 +37,63 @@ describe "#documentation" do it "fetches it from the api" do - api.should_receive(:request).with(:get, anything, anything).and_return({ 'documentation' => ['foo', 'bar'] }) + expect(api).to receive(:request).with(:get, anything, anything).and_return({ 'documentation' => ['foo', 'bar'] }) expect(subject.documentation).to eq(['foo', 'bar']) end end describe "#found?" do it "fetches it from the api" do - api.should_receive(:request).with(:get, anything, anything).and_return({ 'found' => true }) + expect(api).to receive(:request).with(:get, anything, anything).and_return({ 'found' => true }) expect(subject).to be_found end end describe "#type" do it "fetches it from the api" do - api.should_receive(:request).with(:get, anything, anything).and_return({ 'type' => 'gmail' }) + expect(api).to receive(:request).with(:get, anything, anything).and_return({ 'type' => 'gmail' }) expect(subject.type).to eq('gmail') end end describe "#server" do it "fetches it from the api" do - api.should_receive(:request).with(:get, anything, anything).and_return({ 'imap' => { 'server' => 'imap.gmail.com' }}) + expect(api).to receive(:request).with(:get, anything, anything).and_return({ 'imap' => { 'server' => 'imap.gmail.com' }}) expect(subject.server).to eq('imap.gmail.com') end end describe "#username" do it "fetches it from the api" do - api.should_receive(:request).with(:get, anything, anything).and_return({ 'imap' => { 'username' => 'ben' }}) + expect(api).to receive(:request).with(:get, anything, anything).and_return({ 'imap' => { 'username' => 'ben' }}) expect(subject.username).to eq('ben') end end describe "#port" do it "fetches it from the api" do - api.should_receive(:request).with(:get, anything, anything).and_return({ 'imap' => { 'port' => 993 }}) + expect(api).to receive(:request).with(:get, anything, anything).and_return({ 'imap' => { 'port' => 993 }}) expect(subject.port).to eq(993) end end describe "#oauth?" do it "fetches it from the api" do - api.should_receive(:request).with(:get, anything, anything).and_return({ 'imap' => { 'oauth' => false }}) + expect(api).to receive(:request).with(:get, anything, anything).and_return({ 'imap' => { 'oauth' => false }}) expect(subject).to_not be_oauth end end describe "#use_ssl?" do it "fetches it from the api" do - api.should_receive(:request).with(:get, anything, anything).and_return({ 'imap' => { 'use_ssl' => false }}) + expect(api).to receive(:request).with(:get, anything, anything).and_return({ 'imap' => { 'use_ssl' => false }}) expect(subject).to_not be_use_ssl end end describe "#fetch_attributes" do before do - api.stub(:request).with(:get, anything, anything).and_return({ 'foo' => 'bar' }) + allow(api).to receive(:request).with(:get, anything, anything).and_return({ 'foo' => 'bar' }) end it "defines a getter if one doesn't already exist" do @@ -104,7 +103,7 @@ end it "hits the right URL" do - api.should_receive(:request).with(:get, 'discovery', 'email' => 'email@email.com', 'source_type' => 'IMAP') + expect(api).to receive(:request).with(:get, 'discovery', 'email' => 'email@email.com', 'source_type' => 'IMAP') subject.send(:fetch_attributes) end diff --git a/spec/unit/contextio/message_spec.rb b/spec/unit/contextio/message_spec.rb new file mode 100644 index 0000000..bfa8c1f --- /dev/null +++ b/spec/unit/contextio/message_spec.rb @@ -0,0 +1,38 @@ +require 'contextio/message' + +describe ContextIO::Message do + let(:api) { double('api') } + + subject { ContextIO::Message.new(api, resource_url: 'resource/url') } + + describe "#flags" do + before do + allow(api).to receive(:request).and_return({'seen' => 0}) + end + + it "gets to the flags method api" do + expect(api).to receive(:request).with( + :get, + 'resource/url/flags' + ) + + subject.flags + end + end + + describe "#set_flags" do + before do + allow(api).to receive(:request).and_return({'seen' => 1}) + end + + it "gets to the flags method api" do + expect(api).to receive(:request).with( + :post, + 'resource/url/flags', + {:seen => 1} + ) + + subject.set_flags({:seen => true}) + end + end +end diff --git a/spec/contextio/oauth_provider_collection_spec.rb b/spec/unit/contextio/oauth_provider_collection_spec.rb similarity index 78% rename from spec/contextio/oauth_provider_collection_spec.rb rename to spec/unit/contextio/oauth_provider_collection_spec.rb index b10d56c..4ee711b 100644 --- a/spec/contextio/oauth_provider_collection_spec.rb +++ b/spec/unit/contextio/oauth_provider_collection_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'contextio/oauth_provider_collection' describe ContextIO::OAuthProviderCollection do @@ -8,11 +7,11 @@ describe "#create" do before do - api.stub(:request).with(:post, anything, anything).and_return({ provider_consumer_key: 'test_key' }) + allow(api).to receive(:request).with(:post, anything, anything).and_return({ provider_consumer_key: 'test_key' }) end it "posts to the api" do - api.should_receive(:request).with( + expect(api).to receive(:request).with( :post, 'url from api', type: 'GMAIL', @@ -24,7 +23,7 @@ end it "doesn't make any more API requests than it needs to" do - api.should_receive(:request).exactly(:once) + expect(api).to receive(:request).exactly(:once) subject.create('GMAIL', 'test_key', 'test_secret') end diff --git a/spec/contextio/oauth_provider_spec.rb b/spec/unit/contextio/oauth_provider_spec.rb similarity index 87% rename from spec/contextio/oauth_provider_spec.rb rename to spec/unit/contextio/oauth_provider_spec.rb index b5512c9..468cdaa 100644 --- a/spec/contextio/oauth_provider_spec.rb +++ b/spec/unit/contextio/oauth_provider_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'contextio/oauth_provider' describe ContextIO::OAuthProvider do @@ -42,7 +41,7 @@ subject { ContextIO::OAuthProvider.new(api, provider_consumer_key: '1234') } it "asks the api for a URL" do - api.should_receive(:url_for).with(subject) + expect(api).to receive(:url_for).with(subject) subject.resource_url end @@ -54,7 +53,7 @@ subject { ContextIO::OAuthProvider.new(api, provider_consumer_key: '1234') } it "uses the input key" do - api.should_not_receive(:request) + expect(api).to_not receive(:request) expect(subject.provider_consumer_key).to eq('1234') end @@ -64,7 +63,7 @@ subject { ContextIO::OAuthProvider.new(api, resource_url: 'http://example.com/hitme') } it "loads it from the API" do - api.should_receive(:request).with( + expect(api).to receive(:request).with( :get, 'http://example.com/hitme' ).and_return({ @@ -81,7 +80,7 @@ subject { ContextIO::OAuthProvider.new(api, provider_consumer_key: '1234', provider_consumer_secret: '0987') } it "uses the input provider_consumer_secret" do - api.should_not_receive(:request) + expect(api).to_not receive(:request) expect(subject.provider_consumer_secret).to eq('0987') end @@ -91,7 +90,7 @@ subject { ContextIO::OAuthProvider.new(api, provider_consumer_key: '1234') } it "loads it from the API" do - api.should_receive(:request).with(:get, anything).and_return({ 'provider_consumer_secret' => '1029' }) + expect(api).to receive(:request).with(:get, anything).and_return({ 'provider_consumer_secret' => '1029' }) expect(subject.provider_consumer_secret).to eq('1029') end end @@ -102,7 +101,7 @@ subject { ContextIO::OAuthProvider.new(api, provider_consumer_key: '1234', type: 'GMAIL') } it "uses the input type" do - api.should_not_receive(:request) + expect(api).to_not receive(:request) expect(subject.type).to eq('GMAIL') end @@ -112,7 +111,7 @@ subject { ContextIO::OAuthProvider.new(api, provider_consumer_key: '1234') } it "loads it from the API" do - api.should_receive(:request).with(:get, anything).and_return({ 'type' => 'GOOGLEAPPSMARKETPLACE' }) + expect(api).to receive(:request).with(:get, anything).and_return({ 'type' => 'GOOGLEAPPSMARKETPLACE' }) expect(subject.type).to eq('GOOGLEAPPSMARKETPLACE') end end diff --git a/spec/contextio/source_collection_spec.rb b/spec/unit/contextio/source_collection_spec.rb similarity index 76% rename from spec/contextio/source_collection_spec.rb rename to spec/unit/contextio/source_collection_spec.rb index f3e32a2..e6e9bc5 100644 --- a/spec/contextio/source_collection_spec.rb +++ b/spec/unit/contextio/source_collection_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'contextio/source_collection' describe ContextIO::SourceCollection do @@ -8,14 +7,14 @@ describe "#create" do before do - api.stub(:request).with(:post, anything, anything).and_return( + allow(api).to receive(:request).with(:post, anything, anything).and_return( 'success' => true, 'resource_url' => 'resource_url' ) end it "posts to the api" do - api.should_receive(:request).with( + expect(api).to receive(:request).with( :post, 'url from api', anything @@ -25,27 +24,27 @@ end it "converts boolean to number string for ssl" do - api.should_receive(:request).with( + expect(api).to receive(:request).with( anything, anything, - hash_including('use_ssl' => '1') + hash_including(:use_ssl => '1') ) subject.create('hello@gmail.com', 'imap.email.com', 'hello', true, 993, 'IMAP') end it "converts integer to number string for port" do - api.should_receive(:request).with( + expect(api).to receive(:request).with( anything, anything, - hash_including('port' => '993') + hash_including(:port => '993') ) subject.create('hello@gmail.com', 'imap.email.com', 'hello', true, 993, 'IMAP') end it "doesn't make any more API calls than it needs to" do - api.should_not_receive(:request).with(:get, anything, anything) + expect(api).to_not receive(:request).with(:get, anything, anything) subject.create('hello@gmail.com', 'imap.email.com', 'hello', true, 993, 'IMAP') end diff --git a/spec/contextio/source_spec.rb b/spec/unit/contextio/source_spec.rb similarity index 59% rename from spec/contextio/source_spec.rb rename to spec/unit/contextio/source_spec.rb index e6d2638..c4656f5 100644 --- a/spec/contextio/source_spec.rb +++ b/spec/unit/contextio/source_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'contextio/source' describe ContextIO::Source do @@ -22,13 +21,13 @@ describe "#update" do before do - api.stub(:request).and_return({'success' => true}) + allow(api).to receive(:request).and_return({'success' => true}) end subject { ContextIO::Source.new(api, resource_url: 'resource_url', sync_period: '1h') } it "posts to the api" do - api.should_receive(:request).with( + expect(api).to receive(:request).with( :post, 'resource_url', sync_period: '4h' @@ -44,9 +43,33 @@ end it "doesn't make any more API calls than it needs to" do - api.should_not_receive(:request).with(:get, anything, anything) + expect(api).to_not receive(:request).with(:get, anything, anything) subject.update(sync_period: '4h') end + + it "allows you to send arbitrary arguments to the API" do + expect(api).to receive(:request).with(:post, anything, {foo: 'bar'}) + + subject.update(foo: 'bar') + end + end + + describe ".sync!" do + before do + allow(api).to receive(:request).and_return({'success' => true}) + end + + subject { ContextIO::Source.new(api, resource_url: 'resource_url', sync_period: '1h') } + + it "syncs to the api" do + expect(api).to receive(:request).with( + :post, + 'resource_url/sync', + {} + ) + + subject.sync! + end end end diff --git a/spec/contextio/version_spec.rb b/spec/unit/contextio/version_spec.rb similarity index 88% rename from spec/contextio/version_spec.rb rename to spec/unit/contextio/version_spec.rb index ee5783d..a27870f 100644 --- a/spec/contextio/version_spec.rb +++ b/spec/unit/contextio/version_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'contextio/version' describe ContextIO do diff --git a/spec/contextio_spec.rb b/spec/unit/contextio_spec.rb similarity index 92% rename from spec/contextio_spec.rb rename to spec/unit/contextio_spec.rb index 80159b7..e9844e1 100644 --- a/spec/contextio_spec.rb +++ b/spec/unit/contextio_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'contextio' describe ContextIO do @@ -14,6 +13,11 @@ expect(api.key).to eq('1234') expect(api.secret).to eq('0987') end + + it "passes opts to its API handle" do + api = ContextIO.new('1234', '0987', {a:'b'}).api + expect(api.opts).to eq(a:'b') + end end describe "#oauth_providers" do