diff --git a/lib/api/helpers/api_helpers.rb b/lib/api/helpers/api_helpers.rb index e99a511..c28471c 100644 --- a/lib/api/helpers/api_helpers.rb +++ b/lib/api/helpers/api_helpers.rb @@ -38,12 +38,12 @@ def has_data?(response) response.present? && response.body.present? end - def set_err(json_response) + def set_err(response_code, json_response) if json_response.present? if json_response["error"].present? @error = json_response["error"] elsif json_response["code"].present? - @error = get_error_message(response.code, json_response["code"]) + @error = get_error_message(response_code, json_response["code"]) else @error = DEFAULT_ERR end @@ -58,13 +58,15 @@ def parse_data(response) begin json_response = JSON.parse(response.body) rescue => e - Rails.logger.error "Failed to parse JSON return from Buffer: #{e}" + msg = "Failed to parse JSON return from Buffer: #{e}" + log_or_print msg + @error = msg end - if response.code == GOOD_RESPONSE + if response.code == GOOD_RESPONSE || json_response.nil? return json_response else - set_err(json_response) + set_err(response.code, json_response) end end diff --git a/lib/api/user_api.rb b/lib/api/user_api.rb index b9cabe7..2f420ca 100644 --- a/lib/api/user_api.rb +++ b/lib/api/user_api.rb @@ -3,7 +3,8 @@ class UserApi < BaseApi # PATHS - USER_PATH = "user.json" + USER_PATH = "user.json" + DEAUTH_PATH = "user/deauthorize.json" def get_user_id get_user_json["id"] rescue nil @@ -13,4 +14,9 @@ def get_user_json return nil unless verify_token get_get_response( build_url(USER_PATH) ) end + + def deauthorize + return nil unless verify_token + get_get_response( build_url(DEAUTH_PATH) ) + end end diff --git a/lib/buffer_client.rb b/lib/buffer_client.rb index 6fa50c0..ec5d2b8 100644 --- a/lib/buffer_client.rb +++ b/lib/buffer_client.rb @@ -82,6 +82,13 @@ def get_user_json user_json end + def deauthorize + success_json = @user_api.deauthorize + record_err(@user_api) + + is_success?(success_json) + end + ############### # PROFILE API # ############### @@ -108,6 +115,7 @@ def get_schedule(id) end def update_schedule(id, sched_array) + sched_array.deep_symbolize_keys! success_json = @profile_api.update_schedule(id, sched_array) record_err(@profile_api) @@ -153,7 +161,14 @@ def reorder_updates(id, updates_array, options = {}) new_order_json = @update_api.reorder_updates(id, updates_array, options) record_err(@update_api) - new_order_json + extract_key(new_order_json, "updates") + end + + def shuffle_updates(id, options = {}) + shuffle_json = @update_api.shuffle_updates(id, options) + record_err(@update_api) + + extract_key(shuffle_json, "updates") end def create_update(profile_ids, options = {}) @@ -229,4 +244,8 @@ def is_success?(success_json) def record_err(api) @error = api.get_error if api.has_error? end + + def extract_key(json, key) + return json[key] if json.present? + end end diff --git a/readme.md b/readme.md index 38abf9f..23adfcc 100644 --- a/readme.md +++ b/readme.md @@ -1,24 +1,23 @@ # Buffer Gem -Modelled after Octokit <3. This is a lightweight client for the Buffer API (https://buffer.com/developers/api), which is intended to return JSON as opposed to objects. +Modelled after [Octokit](https://github.com/octokit/octokit.rb) <3, this is a lightweight client for the [Buffer API](https://buffer.com/developers/api), which is intended to return (mostly) JSON as opposed to objects. [![Code Climate](https://codeclimate.com/github/kaneda/buffer-ruby/badges/gpa.svg)](https://codeclimate.com/github/kaneda/buffer-ruby) [![Test Coverage](https://codeclimate.com/github/kaneda/buffer-ruby/badges/coverage.svg)](https://codeclimate.com/github/kaneda/buffer-ruby/coverage) +[![Build Status](https://travis-ci.org/kaneda/buffer-ruby.svg?branch=master)](https://travis-ci.org/kaneda/buffer-ruby) ## Client API ### What's Imlemented? -* https://buffer.com/developers/api/oauth -* https://buffer.com/developers/api/user -* https://buffer.com/developers/api/profiles -* https://buffer.com/developers/api/updates -* https://buffer.com/developers/api/links -* https://buffer.com/developers/api/info -* https://buffer.com/developers/api/errors - - -### What's Missing? -* Rspecs +* [OAuth](https://buffer.com/developers/api/oauth) +* [Users](https://buffer.com/developers/api/user) +* [Profiles](https://buffer.com/developers/api/profiles) +* [Updates](https://buffer.com/developers/api/updates) +* [Links](https://buffer.com/developers/api/links) +* [Info](https://buffer.com/developers/api/info) +* [Errors](https://buffer.com/developers/api/errors) +* Automated builds through Travis-CI +* Automated tests using RSpec ### Installing @@ -45,39 +44,39 @@ You can define the client up front or configure it later. The options array the buffer_client = BufferClient.new({ :user_code => "yourcode" }) -``` -```ruby -buffer_client = BufferClient.new +auth_tok = buffer_client.get_auth_token buffer_client.configure({ - :user_code => "yourcode" + :auth_token => auth_tok }) ``` ### Available Calls -| Client call | Input | Description | Notes -| :---------: | :------ | :---- | :--------- -| get_auth_token | | Returns the user's long-lasting auth token | user_code must be defined in the buffer_client, as well as the ENV variables "BUFFER_KEY" and "BUFFER_SECRET" | -| get_user_id | | Returns the user's Buffer ID | | -| get_user_json | | Returns the entirety of the user JSON | | -| get_user_profiles | | Returns the entirety of the profile JSON | | -| get_user_profile | Profile ID | Get a single profile as JSON by ID | | -| get_schedule | Profile ID | Get the schedule of a profile as JSON by ID | | -| update_schedule | Profile ID, Schedule Hash | Set the schedule of a profile as JSON by ID | auth_token must be defined in the buffer client. See below for schedule representation | -| get_update | Social Media Post ID | Gets an update by post ID | | -| get_pending_updates | Profile ID, Options Hash (optional) | Gets pending updates as JSON by profile ID | Takes in hash of options, see Buffer API docs for optional parameters | -| get_sent_updates | Profile ID, Options Hash (optional) | Gets sent updates as JSON by profile ID | Takes in hash of options, see Buffer API docs for optional parameters | -| get_interactions | Social Media Post ID, Event, Options Hash (optional) | Gets interactions based on event type (see https://bufferapp.com/developers/api/info#configuration) | Takes in a hash of options, see Buffer API docs for optional parameters | -| reorder_updates | Profile ID, Updates Array, Options Hash (optional) | Updates order of updates in a profile based on updates array | | -| shuffle_updates | Profile ID, Options Hash (optional) | Randomize the order of updates to be sent | | -| create_update | Profile ID Array, Options Hash (optional) | Create a new post | Note that for the "media" option, please specify each media option in the hash separately, e.g. ```{ "media[link]" => "http%3A%2F%2Fgoogle.com", "media[description]" => "The%20google%20homepage" }``` | -| update_status | Social Media Post ID, Text, Options Hash (optional) | Update an existing status | For the "media" option see the note on create_update | -| share_update | Social Media Post ID | Share a post immediately | | -| destroy_update | Social Media Post ID| Permanently destroy an update | | -| move_to_top | Social Media Post ID| Move post to top of queue | | -| get_shares | URL (unencoded) | Gets the number of shares for a given URL through Buffer | You can pass a normal URL here, the client will encode it. This is one of the only calls to not require an auth_token | -| get_configuration | | Gets the current Buffer config | | + +| Client call | Input | Output | Notes +| :---------: | :------ | :------ | :--------- +| [get_auth_token](https://buffer.com/developers/api/oauth) | | Auth Token:String | ENV variables "BUFFER_KEY" and "BUFFER_SECRET" must be defined here | +| get_user_id | | ID:String | Convenience wrapper, calls get_user_json under the hood | +| [get_user_json](https://buffer.com/developers/api/user#user) | | User:Hash | | +| [deauthorize](https://buffer.com/developers/api/user#deauthorize) | | Success:Boolean | | +| [get_user_profiles](https://buffer.com/developers/api/profiles#profiles) | | Profiles:Array | | +| [get_user_profile](https://buffer.com/developers/api/profiles#profilesid) | Profile ID:String | Profile:Hash | | +| [get_schedule](https://buffer.com/developers/api/profiles#schedules) | Profile ID:String | Schedule:Hash | | +| [update_schedule](https://buffer.com/developers/api/profiles#schedulesupdate) | Profile ID:String, Schedule:Hash | Success:Boolean | See below for schedule representation | +| [get_update](https://buffer.com/developers/api/updates#updatesid) | Social Media Post ID:String | Update:Hash | | +| [get_pending_updates](https://buffer.com/developers/api/updates#updatespending) | Profile ID:String, Options:Hash (optional) | UpdateResult:Hash | See Buffer API doc (link to the left) for optional parameters. "updates" key contains the Updates:Array | +| [get_sent_updates](https://buffer.com/developers/api/updates#updatessent) | Profile ID:String, Options:Hash (optional) | UpdateResult:Hash | See Buffer API doc (link to the left) for optional parameters. "updates" key contains the Updates:Array | +| [get_interactions](https://buffer.com/developers/api/updates#updatesinteractions) | Social Media Post ID:String, Event:ENUM, Options:Hash (optional) | Interactions:Hash | See [event types](https://bufferapp.com/developers/api/info#configuration) for possible event values and Buffer API doc (link to the left) for optional parameters. "interactions" key contains the Interactions:Array | +| [reorder_updates](https://buffer.com/developers/api/updates#updatesreorder) | Profile ID:String, Updates:Array, Options:Hash (optional) | Updates:Array | | +| [shuffle_updates](https://buffer.com/developers/api/updates#updatesshuffle) | Profile ID:String, Options:Hash (optional) | Updates:Array | | +| [create_update](https://buffer.com/developers/api/updates#updatescreate) | Profile IDs:Array, Options:Hash (optional) | Update:Hash | Note that for the "media" option, please specify each media option in the hash separately, e.g. ```{ "media[link]" => "http%3A%2F%2Fgoogle.com", "media[description]" => "The%20google%20homepage" }```. See all available options in the Buffer docs (link to the left) | +| [update_status](https://buffer.com/developers/api/updates#updatesupdate) | Social Media Post ID:String, Text:String, Options:Hash (optional) | Update:Hash | For the "media" option see the note on create_update above | +| [share_update](https://buffer.com/developers/api/updates#updatesshare) | Social Media Post ID:String | Success:Boolean | | +| [destroy_update](https://buffer.com/developers/api/updates#updatesdestroy) | Social Media Post ID:String | Success:Boolean | | +| [move_to_top](https://buffer.com/developers/api/updates#updatesmovetotop) | Social Media Post ID:String | Success:Boolean | | +| [get_shares](https://buffer.com/developers/api/links#shares) | Unencodded URL:String | Shares:Integer | You can pass a normal URL here, the client will encode it. This is one of the only calls to not require an auth_token | +| [get_configuration](https://buffer.com/developers/api/info#configuration) | | Configuration:Hash | "services" key has internal keys for each service | ### Helper methods | Method | Description | @@ -104,6 +103,7 @@ To update a schedule the BufferClient is expecting a schedule of the form: ] ``` +Note that you can make the keys symbols or strings, the client will accept either one. ## Contributing To contribute simply: @@ -111,13 +111,13 @@ To contribute simply: 1. Fork this project 2. Make your changes in a new branch 3. Create a PR -4. Once approved squash your commits (http://davidwalsh.name/squash-commits-git) +4. Once approved [squash your commits](http://davidwalsh.name/squash-commits-git) 5. Party ## Contact Email: kanedasan@gmail.com -Twitter: @kanedasan +Twitter: [@kanedasan](https://twitter.com/kanedasan) IRC: kaneda^ on FreeNode (##hackers) diff --git a/spec/lib/api/helpers/api_helpers_spec.rb b/spec/lib/api/helpers/api_helpers_spec.rb new file mode 100644 index 0000000..d444d63 --- /dev/null +++ b/spec/lib/api/helpers/api_helpers_spec.rb @@ -0,0 +1,137 @@ +require 'action_dispatch' +require 'uri' +require_relative '../../../../lib/api/helpers/api_helpers.rb' + +describe ApiHelpers do + let(:base_api) { build(:base_api) } + let(:options) do + { + :key => "value", + :key_2 => "value_2" + } + end + + let(:err) { "An error" } + + describe "#build_options_string" do + let(:path_regex) do + /\A(\w+=[\w\d]+(&\w+=[\w\d]+)+)*\Z/ + end + + it "returns a string representing a valid path" do + expect(base_api.build_options_string(options)).to match(path_regex) + end + end + + describe "#build_url" do + let(:path) { "some_path" } + let(:auth_tok) { "123456789" } + + it "returns a valid URL when given only a path" do + expect(base_api.build_url(path, {}, false)).to match(URI::regexp) + end + + it "returns a valid URL when given a path and options" do + expect(base_api.build_url(path, options, false)).to match(URI::regexp) + end + + it "returns a valid URL when given a path and an auth token" do + base_api.instance_variable_set(:@auth_token, auth_tok) + + expect(base_api.build_url(path)).to match(URI::regexp) + end + + it "returns a valid URL when given a path, options, and an auth token" do + base_api.instance_variable_set(:@auth_token, auth_tok) + + expect(base_api.build_url(path, options)).to match(URI::regexp) + end + end + + describe "#set_err" do + let(:response_code) { "403" } + let(:err) { "An error" } + let(:err_2) { "A second err" } + let(:json_with_error) { { "error" => err } } + let(:json_with_code) { { "code" => "1001" } } + let(:json_with_other) { { "other" => "stuff" } } + + it "returns the default error when response is blank" do + expect(base_api.set_err(response_code, {})).to eq(ApiHelpers::DEFAULT_ERR) + end + + it "returns an error when the error key is present in the response" do + expect(base_api.set_err(response_code, json_with_error)).to eq(err) + end + + it "returns another error when only the code key is present" do + allow_any_instance_of(BaseApi).to receive(:get_error_message).and_return(err_2) + expect(base_api.set_err(response_code, json_with_code)).to eq(err_2) + end + + it "returns the default error when response is present but neither error nor code is present" do + expect(base_api.set_err(response_code, json_with_other)).to eq(ApiHelpers::DEFAULT_ERR) + end + end + + describe "#has_data?" do + let(:empty_response) do + ActionDispatch::Response.new(500, {}, "") + end + + let(:valid_response) do + ActionDispatch::Response.new(200, {}, "A body") + end + + it "returns false when passed a blank response" do + expect(base_api.has_data?(empty_response)).to eq(false) + end + + it "returns true when passed a response with a body" do + expect(base_api.has_data?(valid_response)).to eq(true) + end + end + + describe "#parse_data" do + let(:bad_json) { '{"foo": "bar"' } + let(:bad_json_err) { "Failed to parse JSON return from Buffer" } + let(:bad_response) do + ActionDispatch::Response.new(403, {}, bad_json) + end + + let(:good_json) { "#{bad_json} }" } + let(:good_response) do + ActionDispatch::Response.new(200, {}, good_json) + end + + it "returns nil and sets @error if the response is invalid JSON" do + json = base_api.parse_data(bad_response) + expect(json).to be_nil + expect(base_api.error).to include(bad_json_err) + end + + it "returns a hash when passed good JSON" do + json = base_api.parse_data(good_response) + expect(json).to be_present + expect(json.is_a?(Hash)).to eq(true) + end + end + + describe "#get_error_message" do + let(:event_err) { "Event type not supported." } + let(:http_code) { "400" } + let(:err_code) { "1029" } + + it "returns the event err when the given codes are passed" do + expect(base_api.get_error_message(http_code, err_code)).to eq(event_err) + end + + it "returns the default error when the http code is unknown" do + expect(base_api.get_error_message("10101", err_code)).to eq(ApiHelpers::DEFAULT_ERR) + end + + it "returns the default error when the error code is unknown" do + expect(base_api.get_error_message(http_code, "10101")).to eq(ApiHelpers::DEFAULT_ERR) + end + end +end diff --git a/spec/lib/api/update_api_spec.rb b/spec/lib/api/update_api_spec.rb new file mode 100644 index 0000000..b9f8d9b --- /dev/null +++ b/spec/lib/api/update_api_spec.rb @@ -0,0 +1,25 @@ +require_relative '../../../lib/api/update_api.rb' + +describe UpdateApi do + let(:update_api) { build(:update_api) } + let(:id) { "123456789" } + let(:path) { "some_path" } + + before(:each) do + allow_any_instance_of(UpdateApi).to receive(:verify_token).and_return(true) + end + + describe "#build_profile_url" do + it "returns a proper profile URL" do + expected_val = "#{UpdateApi::PROFILE_PATH}/#{id}/#{path}" + expect(update_api.send(:build_profile_url, id, path)).to include(expected_val) + end + end + + describe "#build_update_url" do + it "returns a proper update URL" do + expected_val = "#{UpdateApi::UPDATE_PATH}/#{id}/#{path}" + expect(update_api.send(:build_update_url, id, path)).to include(expected_val) + end + end +end