diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..23fcf9e1 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,35 @@ +version: 2 +jobs: + "Test against Ruby 2.4": + docker: + - image: circleci/ruby:2.4.9 + working_directory: ~/intercom-ruby + steps: + - checkout + - run: bundle install + - run: bundle exec rake + "Test against Ruby 2.5": + docker: + - image: circleci/ruby:2.5.7 + working_directory: ~/intercom-ruby + steps: + - checkout + - run: bundle install + - run: bundle exec rake + "Test against Ruby 2.6": + docker: + - image: circleci/ruby:2.6.5 + working_directory: ~/intercom-ruby + steps: + - checkout + - run: bundle install + - run: bundle exec rake + +workflows: + version: 2 + build_and_test: + jobs: + - "Test against Ruby 2.4" + - "Test against Ruby 2.5" + - "Test against Ruby 2.6" + diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..f40010de --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,19 @@ +Please use the following template to submit your issue. Following this template will allow us to quickly investigate and help you with your issue. Please be aware that issues which do not conform to this template may be closed. + +For feature requests please contact us at team@intercom.io + + +## Version info + - intercom-ruby version: + - Ruby version: + +## Expected behavior + +## Actual behavior + +## Steps to reproduce + 1. + 2. + 3. + +## Logs diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..7c94e728 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,5 @@ +#### Why? +Why are you making this change? + +#### How? +Technical details on your change diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e2f0ec2a..00000000 --- a/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -language: ruby -rvm: - - 1.9.3 - - 2.0.0 - - 2.1.0 diff --git a/Gemfile b/Gemfile index 743f2a99..4516ffc0 100644 --- a/Gemfile +++ b/Gemfile @@ -1,13 +1,10 @@ source "http://rubygems.org" +gem 'webmock' gemspec group :development, :test do platforms :jruby do - gem 'json-jruby' gem 'jruby-openssl' end - platforms :ruby_18 do - gem 'json_pure' - end end diff --git a/README.md b/README.md index 9ce999f2..8ad928a4 100644 --- a/README.md +++ b/README.md @@ -1,337 +1,767 @@ # intercom-ruby -Ruby bindings for the Intercom API (https://api.intercom.io). +[![Circle CI](https://circleci.com/gh/intercom/intercom-ruby.png?style=shield)](https://circleci.com/gh/intercom/intercom-ruby) +[![gem](https://img.shields.io/gem/v/intercom)](https://rubygems.org/gems/intercom) +![Intercom API Version](https://img.shields.io/badge/Intercom%20API%20Version-2.6-blue) -[API Documentation](https://api.intercom.io/docs) +> Ruby bindings for the [Intercom API](https://developers.intercom.io/reference). + +## Project Updates + +### Maintenance + +We're currently building a new team to provide in-depth and dedicated SDK support. + +In the meantime, we'll be operating on limited capacity, meaning all pull requests will be evaluated on a best effort basis and will be limited to critical issues. + +We'll communicate all relevant updates as we build this new team and support strategy in the coming months. + +[API Documentation](https://developers.intercom.io/docs) [Gem Documentation](http://rubydoc.info/github/intercom/intercom-ruby/master/frames) -For generating Intercom javascript script tags for Rails, please see https://github.com/intercom/intercom-rails +For generating Intercom JavaScript script tags for Rails, please see [intercom/intercom-rails](https://github.com/intercom/intercom-rails) ## Upgrading information -Version 2 of intercom-ruby is not backwards compatible with previous versions. Be sure to test this new version before deploying to production. One other change you will need to make as part of the upgrade is to set `Intercom.app_api_key` and not set `Intercom.api_key` (you can continue to use your existing API key). -This version of the gem is compatible with `Ruby 2.1`, `Ruby 2.0` & `Ruby 1.9.3` +Version 4 of intercom-ruby is not backwards compatible with previous versions. Please see our [migration guide](https://github.com/intercom/intercom-ruby/wiki/Migration-guide-for-v4) for full details of breaking changes. + +This version of the gem is compatible with `Ruby 2.1` and above. ## Installation - gem install intercom +```bash +gem install intercom +``` Using bundler: - gem 'intercom', "~> 2.4.0" +```bundler +gem 'intercom', '~> 4.1' +``` ## Basic Usage -### Configure your access credentials +### Configure your client + +> If you already have a personal access token you can find it [here](https://app.intercom.io/a/apps/_/developer-hub/). If you want to create or learn more about personal access tokens then you can find more info [here](https://developers.intercom.io/docs/personal-access-tokens). + +```ruby +# With an OAuth or Personal Access token: +intercom = Intercom::Client.new(token: 'my_token') +``` ```ruby -Intercom.app_id = "my_app_id" -Intercom.app_api_key = "my-super-crazy-api-key" +# With a versioned app: +intercom = Intercom::Client.new(token: 'my_token', api_version: '2.2') ``` +If you are building a third party application you can get your access_tokens by [setting-up-oauth](https://developers.intercom.io/page/setting-up-oauth) for Intercom. +You can also use the [omniauth-intercom lib](https://github.com/intercom/omniauth-intercom) which is a middleware helping you to handle the authentication process with Intercom. + ### Resources Resources this API supports: - https://api.intercom.io/users - https://api.intercom.io/companies - https://api.intercom.io/tags - https://api.intercom.io/notes - https://api.intercom.io/segments - https://api.intercom.io/events - https://api.intercom.io/conversations - https://api.intercom.io/messages - https://api.intercom.io/counts - https://api.intercom.io/subscriptions - -Additionally, the library can handle incoming webhooks from Intercom and convert to `Intercom::` models. +```text +https://api.intercom.io/contacts +https://api.intercom.io/visitors +https://api.intercom.io/companies +https://api.intercom.io/data_attributes +https://api.intercom.io/events +https://api.intercom.io/tags +https://api.intercom.io/notes +https://api.intercom.io/segments +https://api.intercom.io/conversations +https://api.intercom.io/messages +https://api.intercom.io/admins +https://api.intercom.io/teams +https://api.intercom.io/counts +https://api.intercom.io/subscriptions +https://api.intercom.io/jobs +https://api.intercom.io/articles +https://api.intercom.io/help_center/collections +https://api.intercom.io/help_center/sections +https://api.intercom.io/phone_call_redirects +https://api.intercom.io/subscription_types +https://api.intercom.io/export/content/data +``` ### Examples -#### Users +#### Contacts + +Note that this is a new resource compatible only with the new [Contacts API](https://developers.intercom.com/intercom-api-reference/reference#contacts-model) released in API v2.0. ```ruby -# Find user by email -user = Intercom::User.find(:email => "bob@example.com") -# Find user by user_id -user = Intercom::User.find(:user_id => "1") -# Find user by id -user = Intercom::User.find(:id => "1") -# Create a user -user = Intercom::User.create(:email => "bob@example.com", :name => "Bob Smith") -# Update custom_attributes for a user -user.custom_attributes["average_monthly_spend"] = 1234.56; user.save -# Perform incrementing -user.increment('karma'); user.save -# Iterate over all users -Intercom::User.all.each {|user| puts %Q(#{user.email} - #{user.custom_attributes["average_monthly_spend"]}) } -Intercom::User.all.map {|user| user.email } +# Create a contact with "lead" role +contact = intercom.contacts.create(email: "some_contact2@example.com", role: "lead") + +# Get a single contact using their intercom_id +intercom.contacts.find(id: contact.id) + +# Update a contact +contact.name = "New name" +intercom.contacts.save(contact) + +# Update a contact's role from "lead" to "user" +contact.role = "user" +intercom.contacts.save(contact) + +# Archive a contact +intercom.contacts.archive(contact) + +# Unarchive a contact +intercom.contacts.unarchive(contact) + +# Delete a contact permanently +intercom.contacts.delete(contact) + +# Deletes an archived contact permanently +intercom.contacts.delete_archived_contact(contact.id) + +# List all contacts +contacts = intercom.contacts.all +contacts.each { |contact| p contact.name } + +# Search for contacts by email +contacts = intercom.contacts.search( + "query": { + "field": 'email', + "operator": '=', + "value": 'some_contact@example.com' + } +) +contacts.each {|c| p c.email} +# For full detail on possible queries, please refer to the API documentation: +# https://developers.intercom.com/intercom-api-reference/reference + +# Merge a lead into an existing user +lead = intercom.contacts.create(email: "some_contact2@example.com", role: "lead") +intercom.contacts.merge(lead, intercom.contacts.find(id: "5db2e80ab1b92243d2188cfe")) + +# Add a tag to a contact +tag = intercom.tags.find(id: "123") +contact.add_tag(id: tag.id) + +# Remove a tag +contact.remove_tag(id: tag.id) + +# List tags for a contact +contact.tags.each {|t| p t.name} + +# Create a note on a contact +contact.create_note(body: "

Text for the note

") + +# List notes for a contact +contact.notes.each {|n| p n.body} + +# List segments for a contact +contact.segments.each {|segment| p segment.name} + +# Add a contact to a company +company = intercom.companies.find(id: "123") +contact.add_company(id: company.id) + +# Remove a contact from a company +contact.remove_company(id: company.id) + +# List companies for a contact +contact.companies.each {|c| p c.name} + +# attach a subscription_types on a contact +contact.create_subscription_type(id: subscription_type.id) + +# List subscription_types for a contact +contact.subscription_types.each {|n| p n.id} + +# Remove subscription_types +contact.remove_subscription_type({ "id": subscription_type.id }) + ``` -#### Admins +#### Visitors + ```ruby -# Iterate over all admins -Intercom::Admin.all.each {|admin| puts admin.email } +# Get and update a visitor +visitor = intercom.visitors.find(id: "5dd570e7b1b922452676af23") +visitor.name = "New name" +intercom.visitors.save(visitor) + +# Convert a visitor into a lead +intercom.visitors.convert(visitor) + +# Convert a visitor into a user +user = intercom.contacts.find(id: "5db2e7f5b1b92243d2188cb3") +intercom.visitors.convert(visitor, user) ``` #### Companies + ```ruby -# Add a user to one or more companies -user = Intercom::User.find(:email => "bob@example.com") -user.companies = [{:company_id => 6, :name => "Intercom"}, {:company_id => 9, :name => "Test Company"}]; user.save -# You can also pass custom attributes within a company as you do this -user.companies = [{:id => 6, :name => "Intercom", :custom_attributes => {:referral_source => "Google"} } ]; user.save # Find a company by company_id -company = Intercom::Company.find(:company_id => "44") +company = intercom.companies.find(company_id: "44") + # Find a company by name -company = Intercom::Company.find(:name => "Some company") +company = intercom.companies.find(name: "Some company") + # Find a company by id -company = Intercom::Company.find(:id => "41e66f0313708347cb0000d0") +company = intercom.companies.find(id: "41e66f0313708347cb0000d0") + # Update a company -company.name = 'Updated company name'; company.save +company.name = 'Updated company name' +intercom.companies.save(company) + +# Delete a company +intercom.companies.delete(company) + # Iterate over all companies -Intercom::Company.all.each {|company| puts %Q(#{company.name} - #{company.custom_attributes["referral_source"]}) } -Intercom::Company.all.map {|company| company.name } -# Get a list of users in a company -company.users +intercom.companies.all.each {|company| puts %Q(#{company.name} - #{company.custom_attributes["referral_source"]}) } +intercom.companies.all.map {|company| company.name } + +# Get a large list of companies using scroll +intercom.companies.scroll.each { |comp| puts comp.name} +# Please see users scroll for more details of how to use scroll +``` + +#### Data Attributes + +Data Attributes are a type of metadata used to describe your customer and company models. These include standard and custom attributes. + +```ruby +# Create a new custom data attribute +intercom.data_attributes.create({ name: "test_attribute", model: "contact", data_type: "string" }) + +# List all data attributes +attributes = intercom.data_attributes.all +attributes.each { |attribute| p attribute.name } + +# Update an attribute +attribute = intercom.data_attributes.all.first +attribute.label = "New label" +intercom.data_attributes.save(attribute) + +# Archive an attribute +attribute.archived = true +intercom.data_attributes.save(attribute) + +# Find all customer attributes including archived +customer_attributes_incl_archived = intercom.data_attributes.find_all({"model": "contact", "include_archived": true}) +customer_attributes_incl_archived.each { |attr| p attr.name } +``` + +#### Events + +```ruby +intercom.events.create( + event_name: "invited-friend", + created_at: Time.now.to_i, + email: user.email, + metadata: { + "invitee_email" => "pi@example.org", + invite_code: "ADDAFRIEND", + "found_date" => 12909364407 + } +) + +# Alternatively, use "user_id" in case your app allows multiple accounts having the same email +intercom.events.create( + event_name: "invited-friend", + created_at: Time.now.to_i, + user_id: user.uuid, +) + +# Retrieve event list for user with id:'123abc' + intercom.events.find_all("type" => "user", "intercom_user_id" => "123abc") + +# Retrieve the event summary for user with id: 'abc' this will return an event object with the following characteristics: +# name - name of the event +# first - time when event first occured. +# last - time when event last occured +# count - number of times the event occured +# description - description of the event + events = intercom.events.find_all(type: 'user',intercom_user_id: 'abc',summary: true) ``` +Metadata Objects support a few simple types that Intercom can present on your behalf + +```ruby +intercom.events.create( + event_name: "placed-order", + email: current_user.email, + created_at: 1403001013, + metadata: { + order_date: Time.now.to_i, + stripe_invoice: 'inv_3434343434', + order_number: { + value: '3434-3434', + url: 'https://example.org/orders/3434-3434' + }, + price: { + currency: 'usd', + amount: 2999 + } + } +) +``` + +The metadata key values in the example are treated as follows: + +- order_date: a Date (key ends with '_date') +- stripe_invoice: The identifier of the Stripe invoice (has a 'stripe_invoice' key) +- order_number: a Rich Link (value contains 'url' and 'value' keys) +- price: An Amount in US Dollars (value contains 'amount' and 'currency' keys) + +*NB:* This version of the gem reserves the field name `type` in Event data. + #### Tags + ```ruby -# Tag users -tag = Intercom::Tag.tag_users('blue', ["42ea2f1b93891f6a99000427"]) -# Untag users -Intercom::Tag.untag_users('blue', ["42ea2f1b93891f6a99000427"]) # Iterate over all tags -Intercom::Tag.all.each {|tag| "#{tag.id} - #{tag.name}" } -Intercom::Tag.all.map {|tag| tag.name } -# Iterate over all tags for user -Intercom::Tag.find_all_for_user(:id => '53357ddc3c776629e0000029') -Intercom::Tag.find_all_for_user(:email => 'declan+declan@intercom.io') -Intercom::Tag.find_all_for_user(:user_id => '3') +intercom.tags.all.each {|tag| "#{tag.id} - #{tag.name}" } +intercom.tags.all.map {|tag| tag.name } + # Tag companies -tag = Intercom::Tag.tag_companies('red', ["42ea2f1b93891f6a99000427"]) -# Untag companies -Intercom::Tag.untag_companies('blue', ["42ea2f1b93891f6a99000427"]) -# Iterate over all tags for company -Intercom::Tag.find_all_for_company(:id => '43357e2c3c77661e25000026') -Intercom::Tag.find_all_for_company(:company_id => '6') -``` +tag = intercom.tags.tag(name: 'blue', companies: [{company_id: "42ea2f1b93891f6a99000427"}]) -#### Segments -```ruby -# Find a segment -segment = Intercom::Segment.find(:id => segment_id) -# Update a segment -segment.name = 'Updated name'; segment.save -# Iterate over all segments -Intercom::Segment.all.each {|segment| puts "id: #{segment.id} name: #{segment.name}"} +# Untag Companies +tag = intercom.tags.untag(name: 'blue', companies: [{ company_id: "42ea2f1b93891f6a99000427" }]) + + +# Delete Tags + +# Note : If there any depedent objects for the tag we are trying to delete, then an error TagHasDependentObjects will be thrown. +tag = intercom.tags.find(id:"123") +intercom.tags.delete(tag) ``` #### Notes + ```ruby # Find a note by id -note = Intercom::Note.find(:id => note) -# Create a note for a user -note = Intercom::Note.create(:body => "

Text for the note

", :email => 'joe@example.com') -# Iterate over all notes for a user via their email address -Intercom::Note.find_all(:email => 'joe@example.com').each {|note| puts note.body} -# Iterate over all notes for a user via their user_id -Intercom::Note.find_all(:user_id => '123').each {|note| puts note.body} +note = intercom.notes.find(id: "123") +``` + +#### Segments + +```ruby +# Find a segment +segment = intercom.segments.find(id: segment_id) + +# Iterate over all segments +intercom.segments.all.each {|segment| puts "id: #{segment.id} name: #{segment.name}"} ``` #### Conversations + ```ruby +# Iterate over all conversations for your app +intercom.conversations.all.each { |convo| ... } + +# The below method of finding conversations by using the find_all method work only for API versions 2.5 and below + # FINDING CONVERSATIONS FOR AN ADMIN # Iterate over all conversations (open and closed) assigned to an admin -Intercom::Conversation.find_all(:type => 'admin', :id => '7').each do {|convo| ... } +intercom.conversations.find_all(type: 'admin', id: '7').each {|convo| ... } # Iterate over all open conversations assigned to an admin -Intercom::Conversation.find_all(:type => 'admin', :id => 7, :open => true).each do {|convo| ... } +intercom.conversations.find_all(type: 'admin', id: 7, open: true).each {|convo| ... } # Iterate over closed conversations assigned to an admin -Intercom::Conversation.find_all(:type => 'admin', :id => 7, :open => false).each do {|convo| ... } -# Iterate over closed conversations for assigned an admin, before a certain moment in time -Intercom::Conversation.find_all(:type => 'admin', :id => 7, :open => false, :before => 1374844930).each do {|convo| ... } +intercom.conversations.find_all(type: 'admin', id: 7, open: false).each {|convo| ... } +# Iterate over closed conversations which are assigned to an admin, and where updated_at is before a certain moment in time +intercom.conversations.find_all(type: 'admin', id: 7, open: false, before: 1374844930).each {|convo| ... } # FINDING CONVERSATIONS FOR A USER # Iterate over all conversations (read + unread, correct) with a user based on the users email -Intercom::Conversation.find_all(:email => 'joe@example.com', :type => 'user').each do {|convo| ... } +intercom.conversations.find_all(email: 'joe@example.com', type: 'user').each {|convo| ... } # Iterate over through all conversations (read + unread) with a user based on the users email -Intercom::Conversation.find_all(:email => 'joe@example.com', :type => 'user', :unread => false).each do {|convo| ... } +intercom.conversations.find_all(email: 'joe@example.com', type: 'user', unread: false).each {|convo| ... } # Iterate over all unread conversations with a user based on the users email -Intercom::Conversation.find_all(:email => 'joe@example.com', :type => 'user', :unread => true).each do {|convo| ... } +intercom.conversations.find_all(email: 'joe@example.com', type: 'user', unread: true).each {|convo| ... } +# Iterate over all conversations for a user with their Intercom user ID +intercom.conversations.find_all(intercom_user_id: '536e564f316c83104c000020', type: 'user').each {|convo| ... } +# Iterate over all conversations for a lead +# NOTE: to iterate over a lead's conversations you MUST use their Intercom User ID and type User +intercom.conversations.find_all(intercom_user_id: lead.id, type: 'user').each {|convo| ... } # FINDING A SINGLE CONVERSATION -conversation = Intercom::Conversation.find(:id => '1') +conversation = intercom.conversations.find(id: '1') # INTERACTING WITH THE PARTS OF A CONVERSATION # Getting the subject of a part (only applies to email-based conversations) -conversation.rendered_message.subject +conversation.source.subject + # Get the part_type of the first part -conversation.conversation_parts[0].part_type +conversation.conversation_parts.first.part_type + # Get the body of the second part conversation.conversation_parts[1].body +# Get statistics related to the conversation +conversation.statistics.time_to_admin_reply +conversation.statistics.last_assignment_at + +# Get information on the sla applied to a conversation +conversation.sla_applied.sla_name + # REPLYING TO CONVERSATIONS # User (identified by email) replies with a comment -conversation.reply(:type => 'user', :email => 'joe@example.com', :message_type => 'comment', :body => 'foo') -# Admin (identified by email) replies with a comment -conversation.reply(:type => 'admin', :email => 'bob@example.com', :message_type => 'comment', :body => 'bar') +intercom.conversations.reply(id: conversation.id, type: 'user', email: 'joe@example.com', message_type: 'comment', body: 'foo') +# Admin (identified by id) replies with a comment +intercom.conversations.reply(id: conversation.id, type: 'admin', admin_id: '123', message_type: 'comment', body: 'bar') +# User (identified by email) replies with a comment and attachment +intercom.conversations.reply(id: conversation.id, type: 'user', email: 'joe@example.com', message_type: 'comment', body: 'foo', attachment_urls: ['http://www.example.com/attachment.jpg']) + +#reply to a user's last conversation +intercom.conversations.reply_to_last(type: 'user', body: 'Thanks again', message_type: 'comment', user_id: '12345', admin_id: '123') + +# Open +intercom.conversations.open(id: conversation.id, admin_id: '123') + +# Close +intercom.conversations.close(id: conversation.id, admin_id: '123') + +# Assign +# Note: Conversations can be assigned to teams. However, the entity that performs the operation of assigning the conversation has to be an existing teammate. +# You can use `intercom.admins.all.each {|a| puts a.inspect if a.type == 'admin' }` to list all of your teammates. +intercom.conversations.assign(id: conversation.id, admin_id: '123', assignee_id: '124') + +# Snooze +intercom.conversations.snooze(id: conversation.id, admin_id: '123', snoozed_until: 9999999999) + +# Reply and Open +intercom.conversations.reply(id: conversation.id, type: 'admin', admin_id: '123', message_type: 'open', body: 'bar') + +# Reply and Close +intercom.conversations.reply(id: conversation.id, type: 'admin', admin_id: '123', message_type: 'close', body: 'bar') + +# Admin reply to last conversation +intercom.conversations.reply_to_last(intercom_user_id: '5678', type: 'admin', admin_id: '123', message_type: 'comment', body: 'bar') + +# User reply to last conversation +intercom.conversations.reply_to_last(intercom_user_id: '5678', type: 'user', message_type: 'comment', body: 'bar') + +# ASSIGNING CONVERSATIONS TO ADMINS +intercom.conversations.reply(id: conversation.id, type: 'admin', assignee_id: assignee_admin.id, admin_id: admin.id, message_type: 'assignment') # MARKING A CONVERSATION AS READ -conversation.read = true -conversation.save -``` +intercom.conversations.mark_read(conversation.id) -#### Counts -```ruby -# Get Conversation per Admin -conversation_counts_for_each_admin = Intercom::Count.conversation_counts_for_each_admin -conversation_counts_for_each_admin.each{|count| puts "Admin: #{count.name} (id: #{count.id}) Open: #{count.open} Closed: #{count.closed}" } -# Get User Tag Count Object -Intercom::Count.user_counts_for_each_tag -# Get User Segment Count Object -Intercom::Count.user_counts_for_each_segment -# Get Company Segment Count Object -Intercom::Count.company_counts_for_each_segment -# Get Company Tag Count Object -Intercom::Count.company_counts_for_each_tag -# Get Company User Count Object -Intercom::Count.company_counts_for_each_user -# Get total count of companies, users, segments or tags across app -Intercom::Company.count -Intercom::User.count -Intercom::Segment.count -Intercom::Tag.count +# RUN ASSIGNMENT RULES +intercom.conversations.run_assignment_rules(conversation.id) + +# Search for conversations +# For full detail on possible queries, please refer to the API documentation: +# https://developers.intercom.com/intercom-api-reference/reference + +# Search for open conversations sorted by the created_at date +conversations = intercom.conversations.search( + query: { + field: "open", + operator: "=", + value: true + }, + sort_field: "created_at", + sort_order: "descending" +) +conversations.each {|c| p c.id} + +# Tagging for conversations +tag = intercom.tags.find(id: "2") +conversation = intercom.conversations.find(id: "1") + +# An Admin ID is required to add or remove tag on a conversation +admin = intercom.admins.find(id: "1") + +# Add a tag to a conversation +conversation.add_tag(id: tag.id, admin_id: admin.id) + +# Remove a tag from a conversation +conversation.remove_tag(id: tag.id, admin_id: admin.id) + +# Add a contact to a conversation +conversation.add_contact(admin_id: admin.id, customer: { intercom_user_id: contact.id }) + +# Remove a contact from a conversation +conversation.remove_contact(id: contact.id, admin_id: admin.id) ``` #### Full loading of an embedded entity + ```ruby -# Given a conversation with a partial user, load the full user. This can be +# Given a conversation with a partial contact, load the full contact. This can be # done for any entity -conversation.user.load +intercom.contacts.load(conversation.contacts.first) ``` #### Sending messages + ```ruby # InApp message from admin to user -Intercom::Message.create({ - :message_type => 'inapp', - :body => "What's up :)", - :from => { - :type => 'admin', - :id => "1234" +intercom.messages.create({ + message_type: 'inapp', + body: "What's up :)", + from: { + type: 'admin', + id: "1234" }, - :to => { - :type => "user", - :id => "5678" + to: { + type: "user", + user_id: "5678" } }) # Email message from admin to user -Intercom::Message.create({ - :message_type => 'email', - :subject => 'Hey there', - :body => "What's up :)", - :template => "plain", # or "personal", - :from => { - :type => "admin", - :id => "1234" +intercom.messages.create({ + message_type: 'email', + subject: 'Hey there', + body: "What's up :)", + template: "plain", # or "personal", + from: { + type: "admin", + id: "1234" }, - :to => { - :type => "user", - :id => "536e564f316c83104c000020" + to: { + type: "user", + id: "536e564f316c83104c000020" } }) # Message from a user -Intercom::Message.create({ - :from => { - :type => "user", - :id => "536e564f316c83104c000020" +intercom.messages.create({ + from: { + type: "user", + id: "536e564f316c83104c000020" }, - :body => "halp" + body: "halp" +}) + +# Message from admin to contact + +intercom.messages.create({ + body: "How can I help :)", + from: { + type: "admin", + id: "1234" + }, + to: { + type: "contact", + id: "536e5643as316c83104c400671" + } +}) + +# Message from a contact +intercom.messages.create({ + from: { + type: "contact", + id: "536e5643as316c83104c400671" + }, + body: "halp" +}) + +#From version 2.6 the type contact is not supported and you would have to use leads to send messages to a lead. + +intercom.messages.create({ + from: { + type: "lead", + id: "536e5643as316c83104c400671" + }, + body: "halp" }) ``` -#### Events +#### Admins + ```ruby -Intercom::Event.create( - :event_name => "invited-friend", :created_at => Time.now.to_i, - :email => user.email, - :metadata => { - "invitee_email" => "pi@example.org", - :invite_code => "ADDAFRIEND", - "found_date" => 12909364407 - } -) +# Find access token owner (only with Personal Access Token and OAuth) +intercom.admins.me +# Find an admin by id +intercom.admins.find(id: admin_id) +# Iterate over all admins +intercom.admins.all.each {|admin| puts admin.email } ``` -Metadata Objects support a few simple types that Intercom can present on your behalf +#### Teams ```ruby -Intercom::Event.create(:event_name => "placed-order", :email => current_user.email, - :created_at => 1403001013, - :metadata => { - :order_date => Time.now.to_i, - :stripe_invoice => 'inv_3434343434', - :order_number => { - :value => '3434-3434', - :url => 'https://example.org/orders/3434-3434' - }, - price: { - :currency => 'usd', - :amount => 2999 - } - } -) +# Find a team by id +intercom.teams.find(id: team_id) +# Iterate over all teams +intercom.teams.all.each {|team| puts team.name } ``` -The metadata key values in the example are treated as follows- -- order_date: a Date (key ends with '_date'). -- stripe_invoice: The identifier of the Stripe invoice (has a 'stripe_invoice' key) -- order_number: a Rich Link (value contains 'url' and 'value' keys) -- price: An Amount in US Dollars (value contains 'amount' and 'currency' keys) +#### Counts + +```ruby +# App-wide counts +intercom.counts.for_app -### Subscriptions +# Users in segment counts +intercom.counts.for_type(type: 'user', count: 'segment') +``` + +#### Subscriptions Subscribe to events in Intercom to receive webhooks. ```ruby # create a subscription -Intercom::Subscription.create(:url => "http://example.com", :topics => ["user.created"]) +intercom.subscriptions.create(url: "http://example.com", topics: ["user.created"]) # fetch a subscription -Intercom::Subscription.find(:id => "nsub_123456789") +intercom.subscriptions.find(id: "nsub_123456789") + +# delete a subscription +subscription = intercom.subscriptions.find(id: "nsub_123456789") +intercom.subscriptions.delete(subscription) # list subscriptions -Intercom::Subscription.all +intercom.subscriptions.all +``` + + +#### Subscription Types + +List all the subscription types that a contact can opt in to + +```ruby + +# fetch a subscription +intercom.subscription_types.find(id: "1") + +intercom.subscription_types.all ``` -### Webhooks +#### Articles ```ruby -# create a payload from the notification hash (from json). -payload = Intercom::Notification.new(notification_hash) +# Create an article +article = intercom.articles.create(title: "New Article", author_id: "123456") + +# Create an article with translations +article = intercom.articles.create(title: "New Article", + author_id: "123456", + translated_content: {fr: {title: "Nouvel Article"}, es: {title: "Nuevo artículo"}}) + +# Fetch an article +intercom.articles.find(id: "123456") + +# List all articles +articles = intercom.articles.all +articles.each { |article| p article.title } + +# Update an article +article.title = "Article Updated!" +intercom.articles.save(article) -payload.type -# => 'user.created' +# Update an article's existing translation +article.translated_content.en.title = "English Updated!" +intercom.articles.save(article) -payload.model_type -# => Intercom::User +# Update an article by adding a new translation +article.translated_content.es = {title: "Artículo en español"} +intercom.articles.save(article) -user = payload.model -# => Instance of Intercom::User +# Delete an article +intercom.articles.delete(article) ``` -Note that models generated from webhook notifications might differ slightly from models directly acquired via the API. If this presents a problem, calling `payload.load` will load the model from the API using the `id` field. +#### Collections + +```ruby +# Create a collection +collection = intercom.collections.create(name: "New Collection") + +# Create a collection with translations +collection = intercom.collections.create(name: "New Collection", + translated_content: {fr: {name: "Nouvelle collection"}, es: {name: "Nueva colección"}}) + +# Fetch a collection +intercom.collections.find(id: "123456") + +# List all collections +collections = intercom.collections.all +collections.each { |collection| p collection.name } + +# Update a collection +collection.name = "Collection updated!" +intercom.collections.save(collection) + +# Update a collection's existing translation +collection.translated_content.en.name = "English Updated!" +intercom.collections.save(collection) + +# Update a collection by adding a new translation +collection.translated_content.es = {name: "Colección en español", description: "Descripción en español"} +intercom.collections.save(collection) + +# Delete an collection +intercom.collections.delete(collection) +``` + +#### Sections + +```ruby +# Create a section +section = intercom.sections.create(name: "New Section", parent_id: "123456") + +# Create a section with translations +section = intercom.sections.create(name: "New Section", + translated_content: {fr: {name: "Nouvelle section"}, es: {name: "Nueva sección"}}) + +# Fetch a section +intercom.sections.find(id: "123456") + +# List all sections +sections = intercom.sections.all +sections.each { |section| p section.name } + +# Update a section +section.name = "Section updated!" +intercom.sections.save(section) + +# Update a section's existing translation +section.translated_content.en.name = "English Updated!" +intercom.collections.save(section) + +# Update a section by adding a new translation +section.translated_content.es = {name: "Sección en español"} +intercom.collections.save(section) + +# Delete an section +intercom.sections.delete(section) +``` + +#### Phone Call Redirect (switch) + +```ruby +# Create a redirect +redirect = intercom.phone_call_redirect.create(phone_number: "+353871234567") + +``` + +#### Data Content Export + +```ruby +# Create a data export +export = intercom.export_content.create(created_at_after: 1667566801, created_at_before: 1668085202) + + +#View a data export +export = intercom.export_content.find(id: 'k0e27ohsyvh8ef3m') + +# Cancel a data export +export = intercom.export_content.cancel('k0e27ohsyvh8ef3m') + +``` ### Errors -You do not need to deal with the HTTP response from an API call directly. If there is an unsuccessful response then an error that is a subclass of Intercom:Error will be raised. If desired, you can get at the http_code of an Intercom::Error via its `http_code` method. -The list of different error subclasses are listed below. As they all inherit off Intercom::Error you can choose to rescue Intercom::Error or -else rescue the more specific error subclass. +There are different styles for error handling - some people prefer exceptions; some prefer nil and check; some prefer error objects/codes. Balancing these preferences alongside our wish to provide an idiomatic gem has brought us to use the current mechanism of throwing specific exceptions. Our approach in the client is to propagate errors and signal our failure loudly so that erroneous data does not get propagated through our customers' systems - in other words, if you see a `Intercom::ServiceUnavailableError` you know where the problem is. + +You do not need to deal with the HTTP response from an API call directly. If there is an unsuccessful response then an error that is a subclass of `Intercom::IntercomError` will be raised. If desired, you can get at the http_code of an `Intercom::IntercomError` via its `http_code` method. + +The list of different error subclasses are listed below. As they all inherit off Intercom::IntercomError you can choose to rescue Intercom::IntercomError or else rescue the more specific error subclass. ```ruby Intercom::AuthenticationError @@ -339,19 +769,64 @@ Intercom::ServerError Intercom::ServiceUnavailableError Intercom::ServiceConnectionError Intercom::ResourceNotFound +Intercom::BlockedUserError Intercom::BadRequestError Intercom::RateLimitExceeded Intercom::AttributeNotSetError # Raised when you try to call a getter that does not exist on an object Intercom::MultipleMatchingUsersError Intercom::HttpError # Raised when response object is unexpectedly nil +Intercom::GatewayTimeoutError ``` ### Rate Limiting -Calling `Intercom.rate_limit_details` returns a Hash that contains details about your app's current rate limit. +Calling your client's `rate_limit_details` returns a Hash that contains details about your app's current rate limit. ```ruby -Intercom.rate_limit_details +intercom.rate_limit_details #=> {:limit=>180, :remaining=>179, :reset_at=>2014-10-07 14:58:00 +0100} ``` +You can handle the rate limits yourself but a simple option is to use the handle_rate_limit flag. +This will automatically catch the 429 rate limit exceeded error and wait until the reset time to retry. After three retries a rate limit exception will be raised. Encountering this error frequently may require a revisiting of your usage of the API. + +```ruby +intercom = Intercom::Client.new(token: ENV['AT'], handle_rate_limit: true) +``` + +### Pull Requests + +- **Add tests!** Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour**. Make sure the README and any other + relevant documentation are kept up-to-date. + +- **Create topic branches**. Don't ask us to pull from your master branch. + +- **One pull request per feature**. If you want to do more than one thing, send + multiple pull requests. + +- **Send coherent history**. Make sure each individual commit in your pull + request is meaningful. If you had to make multiple intermediate commits while + developing, please squash them before sending them to us. + +### Development + +#### Running tests + +```bash +# all tests +bundle exec rake spec + +# unit tests +bundle exec rake spec:unit + +# integration tests +bundle exec rake spec:integration + +# single test file +bundle exec m spec/unit/intercom/job_spec.rb + +# single test +bundle exec m spec/unit/intercom/job_spec.rb:49 +``` diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 00000000..e1819060 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,9 @@ +# Releasing Intercom + +We use https://github.com/svenfuchs/gem-release to tag, bump, and release new versions + +Please add a line to changes.txt before you release a new version. + +``` +gem bump --tag --release +``` diff --git a/Rakefile b/Rakefile index 1f14f39b..c1b11b09 100644 --- a/Rakefile +++ b/Rakefile @@ -18,4 +18,4 @@ Rake::TestTask.new("spec:integration") do |spec| end task :spec => "spec:unit" -task :default => :spec \ No newline at end of file +task :default => :spec diff --git a/changes.txt b/changes.txt index 08ba6429..b9f617f3 100644 --- a/changes.txt +++ b/changes.txt @@ -1,3 +1,241 @@ +4.2.2 +- Fixed FlatStore to skip hash values when building API request payloads +- Removed hash validation on FlatStore reads to allow for custom objects + +4.1.3 +- Updated ReadMe with more errors. +- Fixed issue where paginated requests could only be iterated through once. +- Moved Dynamic accessors from class level to instance level. + +4.1.2 +- Adding support for company delete. +- Adding support for archiving/unarchiving contacts. +- Adding support for listing contact segments. +- Fixed issue with scroll collection proxy. +- Fixed issue with running assignment rules on a conversation. + +4.1.1 +- Fixed bug with deprecated lead resource. + +4.1.0 +- Added support for new Articles API. +- Added support for new Collections API. +- Added support for new Sections API. +- Added support to equate two resources. +- Fixed issue for dirty tracking nested typed objects. + +4.0.1 +- Fixed bug with nested resources. +- Support for add/remove contact on conversation object. + +4.0.0 +New version to support API version 2.0. +- Added support for new Contacts API. +- Added support for Conversation Search and for Conversation model changes. +- New DataAttribute class to support the Data Attributes. See README for details on usage. +- New method to run assignment rules on a conversation: `intercom.conversations.run_assignment_rules()`. +- See Migration guide for breaking changes: https://github.com/intercom/intercom-ruby/wiki/Migration-guide-for-v4 + +3.9.5 +Add Unstable version support + +3.9.4 +Add handling for Gateway Timeouts + +3.9.3 +Fix regression added in 3.9.2 + +3.9.2 +Added error handling for malformed responses + +3.9.1 +Version skipped in error + +3.9.0 +Added Teams endpoint functionality + +3.8.1 +Added error handling for company_not_found + +3.8.0 +Add support for Customer Search (currently in Unstable API Version) +https://developers.intercom.com/intercom-api-reference/v0/reference#customers + +3.7.7 +Remove deprecated features from Gemspec + +3.7.6 +Added error handling for invalid_document error state + +3.7.5 +Added error handling for scroll_exists error state + +3.7.4 +Added support for API versioning via +Intercom::Client.new(token: "token", api_version "1.1") + +3.7.3 +Added error handling for when an admin cannot be found. + +3.7.2 +Added error handling for when an app's custom attribute limits have been reached. + +3.7.1 +Extra version bump after faulty previous bump + +3.7.0 +Providing the ability to hard delete users as described here: +https://developers.intercom.com/intercom-api-reference/reference#archive-a-user + +This chaged the previous delete action to an archive action and added a new hard delete option +You can still use the delete method but it will archive a user, we added an alias for delete. +#442 archiving alias +#410 add ability to hard delete users + +Alos enabling reply to last from the SDK +#443 Residently conversations last reply + +3.6.2 +#384 Add ability to snooze conversation +You can now snooze conversations in your app via: +intercom.conversations.snooze(...) + +3.6.1 +#430 Allow all conversations to be listed +You can now iterate over all conversations for your app via: +intercom.conversations.all.each { |convo| ... } + +3.6.0 +BREAKING CHANGE companies +We updated companies to be able to list users via company_id as well as id (#428 ) +Note that this is a breaking change as we had to remove the old way of listing users via company. + +Previously it was: +intercom.companies.users(company.id) + +Now you get a list of users in a company by Intercom Company ID +intercom.companies.users_by_intercom_company_id(company.id) + +Now you get a list of users in a company by external company_id +intercom.companies.users_by_company_id(company.company_id) + +Rate limit handling +We also improved the way we handle rate limits in PR #409 which was related to issue #405 + +3.5.23 + - New type of error (ResourceNotUniqueError). Thrown when trying to create a resource that already exists in Intercom + +3.5.22 + - Return object type + +3.5.21 + - Fix for PR-353 which addressed "NoMethodError in intercom/request" + - There were issues on older versions of Ruby (<2.3) + - This PR does not use lonely operator and instead simple checks for nil parsed_body + +3.5.17 + - Fix BlockedUserError typo + +3.5.16 + - Standardize comparison of attribute as string when input is Hash or JSON + +3.5.15 +- UnauthorizedError on invalid token +- BlockerUserError on restoring blocked user + +3.5.14 +- Rate Limit Exception (@jaimeiniesta) + +3.5.12 +- Use base_url in initialize parameter + +3.5.11 +- Add scroll api for companies + +3.5.10 + - Add Support for find_all events pagination (@jkeyes) + +3.5.9 + - Fix event create method + +3.5.8 + - Add admins.me method + +3.5.7 +- Add method to find all events for a user (@reidab) + +3.5.6 + +3.5.5 +- Add scroll api for contacts +- Add extra context to IntercomError +- Add support to find admin by id +- Add decrement method to incrementable traits +- Suppress printing of users during test runs + + +3.5.4 +- Add support for scoll API feature + +3.5.3 +- Add support for global conversation counts + +3.5.2 +- Add Support for pagination + +3.5.1 + - Support for 'visitors' + - Fix utf8 body parsing + +3.4.0 + - Add a "token" keyword for OAuth clients + +3.3.0 + - Add Bulk API support + +3.2.0 + - Add attachment support for conversations + - Fix puts'ing api resources + +3.1.0 + - Support opening, closing, and assigning conversations + +3.0.6 + - Support the `delete` resource on Contacts + +3.0.5 + - Fix id-based updates on Contacts (thanks @gevans) + +3.0.4 + - Support the `all` resource on Contacts + +3.0.3 + - Fix untagging + +3.0.2 + - Fix bad .gem push :( + +3.0.1 + - Fix circular dependency warning in Ruby 2.2. + +3.0.0 + - New version, client-based access. + +2.5.4 + - Acquire support + +2.4.4 + - Fix parsing nil lists from notifications + +2.4.3 + - Updates to remove warning when running in Ruby 2.2.0 (thanks @pat @jwaldrip) + +2.4.2 + - Add nil guard around decode_body to fix potential issue. + +2.4.1 + - Add 'update_last_requst_at=true' as an attribute to set on a User. + 2.4.0 - Support for Ruby 1.9.3 (thanks @Chocksy) diff --git a/intercom.gemspec b/intercom.gemspec index efaef311..e5ed7737 100644 --- a/intercom.gemspec +++ b/intercom.gemspec @@ -12,18 +12,19 @@ Gem::Specification.new do |spec| spec.summary = %q{Ruby bindings for the Intercom API} spec.description = %Q{Intercom (https://www.intercom.io) is a customer relationship management and messaging tool for web app owners. This library wraps the api provided by Intercom. See http://docs.intercom.io/api for more details. } spec.license = "MIT" - spec.rubyforge_project = "intercom" spec.files = `git ls-files`.split("\n") spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") spec.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } spec.require_paths = ["lib"] - spec.add_development_dependency 'minitest' - spec.add_development_dependency 'rake' - spec.add_development_dependency 'mocha' + spec.add_development_dependency 'minitest', '~> 5.4' + spec.add_development_dependency "m", "~> 1.5.0" + spec.add_development_dependency 'rake', '~> 10.3' + spec.add_development_dependency 'mocha', '~> 2.0' spec.add_development_dependency "fakeweb", ["~> 1.3"] + spec.add_development_dependency "pry" - spec.add_dependency 'json' - spec.required_ruby_version = '>= 1.9.3' + spec.required_ruby_version = '>= 2.1.0' + spec.add_development_dependency 'gem-release' end diff --git a/lib/intercom.rb b/lib/intercom.rb index e4cb39b8..520b9036 100644 --- a/lib/intercom.rb +++ b/lib/intercom.rb @@ -1,175 +1,62 @@ -require "intercom/version" -require "intercom/user" -require "intercom/company" -require "intercom/note" -require "intercom/tag" -require "intercom/segment" -require "intercom/event" -require "intercom/conversation" -require "intercom/message" -require "intercom/admin" -require "intercom/count" -require "intercom/request" -require "intercom/subscription" -require "intercom/notification" -require "intercom/utils" -require "intercom/errors" -require "json" +# frozen_string_literal: true + +require 'intercom/version' +require 'intercom/service/admin' +require 'intercom/service/article' +require 'intercom/service/collection' +require 'intercom/service/company' +require 'intercom/service/contact' +require 'intercom/service/conversation' +require 'intercom/service/count' +require 'intercom/service/event' +require 'intercom/service/message' +require 'intercom/service/note' +require 'intercom/service/job' +require 'intercom/service/subscription_type' +require 'intercom/service/subscription' +require 'intercom/service/segment' +require 'intercom/service/section' +require 'intercom/service/tag' +require 'intercom/service/team' +require 'intercom/service/visitor' +require 'intercom/service/user' +require 'intercom/service/lead' +require 'intercom/deprecated_resources.rb' +require 'intercom/service/export_content' +require 'intercom/service/phone_call_redirect' +require 'intercom/options' +require 'intercom/client' +require 'intercom/contact' +require 'intercom/user' +require 'intercom/lead' +require 'intercom/count' +require 'intercom/collection' +require 'intercom/company' +require 'intercom/service/data_attribute' +require 'intercom/note' +require 'intercom/job' +require 'intercom/tag' +require 'intercom/segment' +require 'intercom/section' +require 'intercom/event' +require 'intercom/conversation' +require 'intercom/message' +require 'intercom/admin' +require 'intercom/article' +require 'intercom/request' +require 'intercom/subscription' +require 'intercom/subscription_type' +require 'intercom/team' +require 'intercom/errors' +require 'intercom/visitor' +require 'intercom/data_attribute' +require 'intercom/export_content' +require 'intercom/phone_call_redirect' +require 'json' ## # Intercom is a customer relationship management and messaging tool for web app owners # # This library provides ruby bindings for the Intercom API (https://api.intercom.io) -# -# == Basic Usage -# === Configure Intercom with your access credentials -# Intercom.app_id = "my_app_id" -# Intercom.app_api_key = "my_api_key" -# === Make requests to the API -# Intercom::User.find(:email => "bob@example.com") -# module Intercom - @hostname = "api.intercom.io" - @protocol = "https" - - @endpoints = nil - @current_endpoint = nil - @app_id = nil - @app_api_key = nil - @rate_limit_details = {} - - def self.app_id=(app_id) - @app_id = app_id - end - - def self.app_id - @app_id - end - - def self.app_api_key=(app_api_key) - @app_api_key = app_api_key - end - - def self.app_api_key - @app_api_key - end - - def self.rate_limit_details=(rate_limit_details) - @rate_limit_details = rate_limit_details - end - - def self.rate_limit_details - @rate_limit_details - end - - # This method is obsolete and used to warn of backwards incompatible changes on upgrading - def self.api_key=(val) - raise ArgumentError, "#{compatibility_warning_text} #{compatibility_workaround_text} #{related_docs_text}" - end - - private - - def self.target_base_url - raise ArgumentError, "#{configuration_required_text} #{related_docs_text}" if [@app_id, @app_api_key].any?(&:nil?) - basic_auth_part = "#{@app_id}:#{@app_api_key}@" - current_endpoint.gsub(/(https?:\/\/)(.*)/, "\\1#{basic_auth_part}\\2") - end - - def self.related_docs_text - "See https://github.com/intercom/intercom-ruby for usage examples." - end - - def self.compatibility_warning_text - "It looks like you are upgrading from an older version of the intercom-ruby gem. Please note that this new version (#{Intercom::VERSION}) is not backwards compatible. " - end - - def self.compatibility_workaround_text - "To get rid of this error please set Intercom.app_api_key and don't set Intercom.api_key." - end - - def self.configuration_required_text - "You must set both Intercom.app_id and Intercom.app_api_key to use this client." - end - - def self.send_request_to_path(request) - request.execute(target_base_url) - rescue Intercom::ServiceUnavailableError => e - if endpoints.length > 1 - retry_on_alternative_endpoint(request) - else - raise e - end - end - - def self.retry_on_alternative_endpoint(request) - @current_endpoint = alternative_random_endpoint - request.execute(target_base_url) - end - - def self.current_endpoint - return @current_endpoint if @current_endpoint && @endpoint_randomized_at > (Time.now - (60 * 5)) - @endpoint_randomized_at = Time.now - @current_endpoint = random_endpoint - end - - def self.random_endpoint - endpoints.shuffle.first - end - - def self.alternative_random_endpoint - (endpoints.shuffle - [@current_endpoint]).first - end - - def self.post(path, payload_hash) - send_request_to_path(Intercom::Request.post(path, payload_hash)) - end - - def self.delete(path, payload_hash) - send_request_to_path(Intercom::Request.delete(path, payload_hash)) - end - - def self.put(path, payload_hash) - send_request_to_path(Intercom::Request.put(path, payload_hash)) - end - - def self.get(path, params) - send_request_to_path(Intercom::Request.get(path, params)) - end - - def self.check_required_params(params, path=nil) #nodoc - return if path.eql?("users") - raise ArgumentError.new("Expected params Hash, got #{params.class}") unless params.is_a?(Hash) - raise ArgumentError.new("Either email or user_id must be specified") unless params.keys.any? { |key| %W(email user_id).include?(key.to_s) } - end - - def self.protocol #nodoc - @protocol - end - - def self.protocol=(override) #nodoc - @protocol = override - end - - def self.hostname #nodoc - @hostname - end - - def self.hostname=(override) #nodoc - @hostname = override - end - - def self.endpoint=(endpoint) #nodoc - self.endpoints = [endpoint] - @current_endpoint = nil - end - - def self.endpoints=(endpoints) #nodoc - @endpoints = endpoints - @current_endpoint = nil - end - - def self.endpoints - @endpoints || ["#{@protocol}://#{hostname}"] - end - end diff --git a/lib/intercom/admin.rb b/lib/intercom/admin.rb index 0736c2cf..eaa12fb9 100644 --- a/lib/intercom/admin.rb +++ b/lib/intercom/admin.rb @@ -1,9 +1,7 @@ -require 'intercom/api_operations/list' require 'intercom/traits/api_resource' module Intercom class Admin - include ApiOperations::List include Traits::ApiResource end end diff --git a/lib/intercom/api_operations/archive.rb b/lib/intercom/api_operations/archive.rb new file mode 100644 index 00000000..8e6de554 --- /dev/null +++ b/lib/intercom/api_operations/archive.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'intercom/utils' + +module Intercom + module ApiOperations + module Archive + def archive(object) + @client.delete("/#{collection_name}/#{object.id}", {}) + object + end + + alias_method 'delete', 'archive' + end + end +end diff --git a/lib/intercom/api_operations/bulk/load_error_feed.rb b/lib/intercom/api_operations/bulk/load_error_feed.rb new file mode 100644 index 00000000..d2b99cb4 --- /dev/null +++ b/lib/intercom/api_operations/bulk/load_error_feed.rb @@ -0,0 +1,13 @@ +module Intercom + module ApiOperations + module Bulk + module LoadErrorFeed + def errors(params) + response = @client.get("/jobs/#{params.fetch(:id)}/error", {}) + raise Intercom::HttpError.new('Http Error - No response entity returned') unless response + from_api(response) + end + end + end + end +end diff --git a/lib/intercom/api_operations/bulk/submit.rb b/lib/intercom/api_operations/bulk/submit.rb new file mode 100644 index 00000000..7336e2ec --- /dev/null +++ b/lib/intercom/api_operations/bulk/submit.rb @@ -0,0 +1,37 @@ +require 'intercom/utils' + +module Intercom + module ApiOperations + module Bulk + module Submit + def submit_bulk_job(params) + raise(ArgumentError, "events do not support bulk delete operations") if collection_class == Intercom::Event && !params.fetch(:delete_items, []).empty? + data_type = Utils.resource_class_to_singular_name(collection_class) + collection_name = Utils.resource_class_to_collection_name(collection_class) + create_items = params.fetch(:create_items, []).map { |item| item_for_api("post", data_type, item) } + delete_items = params.fetch(:delete_items, []).map { |item| item_for_api("delete", data_type, item) } + existing_job_id = params.fetch(:job_id, '') + + bulk_request = { + items: create_items + delete_items + } + bulk_request[:job] = { id: existing_job_id } unless existing_job_id.empty? + + response = @client.post("/bulk/#{collection_name}", bulk_request) + raise Intercom::HttpError.new('Http Error - No response entity returned') unless response + Intercom::Job.new.from_response(response) + end + + private + + def item_for_api(method, data_type, item) + { + method: method, + data_type: data_type, + data: item + } + end + end + end + end +end diff --git a/lib/intercom/api_operations/convert.rb b/lib/intercom/api_operations/convert.rb new file mode 100644 index 00000000..73ab15ac --- /dev/null +++ b/lib/intercom/api_operations/convert.rb @@ -0,0 +1,36 @@ +require 'intercom/traits/api_resource' + +module Intercom + module ApiOperations + module Convert + def convert(contact, user = false) + if contact.class == Intercom::Visitor + visitor = contact + req = { + visitor: { user_id: visitor.user_id }, + } + if user + req[:user] = identity_hash(user) + req[:type] = 'user' + else + req[:type] = 'lead' + end + Intercom::User.new.from_response( + @client.post( + "/visitors/convert", req + ) + ) + else + Intercom::User.new.from_response( + @client.post( + "/contacts/convert", { + contact: { user_id: contact.user_id }, + user: identity_hash(user) + } + ) + ) + end + end + end + end +end diff --git a/lib/intercom/api_operations/count.rb b/lib/intercom/api_operations/count.rb deleted file mode 100644 index 5335f5ad..00000000 --- a/lib/intercom/api_operations/count.rb +++ /dev/null @@ -1,16 +0,0 @@ -module Intercom - module ApiOperations - module Count - module ClassMethods - def count - singular_resource_name = Utils.resource_class_to_singular_name(self) - Intercom::Count.send("#{singular_resource_name}_count") - end - end - - def self.included(base) - base.extend(ClassMethods) - end - end - end -end diff --git a/lib/intercom/api_operations/delete.rb b/lib/intercom/api_operations/delete.rb index 2ea8b1af..bdbe6fab 100644 --- a/lib/intercom/api_operations/delete.rb +++ b/lib/intercom/api_operations/delete.rb @@ -1,15 +1,16 @@ -require 'intercom/traits/api_resource' +# frozen_string_literal: true + +require 'intercom/utils' module Intercom module ApiOperations module Delete - - def delete - collection_name = Utils.resource_class_to_collection_name(self.class) - Intercom.delete("/#{collection_name}/#{id}", {}) - self + def delete(object) + @client.delete("/#{collection_name}/#{object.id}", {}) + object end + alias_method 'archive', 'delete' end end end diff --git a/lib/intercom/api_operations/find.rb b/lib/intercom/api_operations/find.rb index c07b1df2..460adaeb 100644 --- a/lib/intercom/api_operations/find.rb +++ b/lib/intercom/api_operations/find.rb @@ -1,22 +1,22 @@ +# frozen_string_literal: true + +require 'intercom/utils' + module Intercom module ApiOperations module Find - module ClassMethods - def find(params) - raise BadRequestError, "#{self}#find takes a hash as its parameter but you supplied #{params.inspect}" unless params.is_a? Hash - collection_name = Utils.resource_class_to_collection_name(self) - if params[:id] - response = Intercom.get("/#{collection_name}/#{params[:id]}", {}) - else - response = Intercom.get("/#{collection_name}", params) - end - raise Intercom::HttpError.new('Http Error - No response entity returned') unless response - from_api(response) + def find(params) + raise BadRequestError, "#{self}#find takes a hash as its parameter but you supplied #{params.inspect}" unless params.is_a? Hash + + if params[:id] + id = params.delete(:id) + response = @client.get("/#{collection_name}/#{id}", params) + else + response = @client.get("/#{collection_name}", params) end - end + raise Intercom::HttpError, 'Http Error - No response entity returned' unless response - def self.included(base) - base.extend(ClassMethods) + from_api(response) end end end diff --git a/lib/intercom/api_operations/find_all.rb b/lib/intercom/api_operations/find_all.rb index 719d917f..bdecd23b 100644 --- a/lib/intercom/api_operations/find_all.rb +++ b/lib/intercom/api_operations/find_all.rb @@ -1,32 +1,29 @@ -require 'intercom/collection_proxy' +# frozen_string_literal: true + +require 'intercom/client_collection_proxy' +require 'intercom/utils' module Intercom module ApiOperations module FindAll - module ClassMethods - def find_all(params) - raise BadRequestError, "#{self}#find takes a hash as its parameter but you supplied #{params.inspect}" unless params.is_a? Hash - collection_name = Utils.resource_class_to_collection_name(self) - finder_details = {} - if params[:id] && !type_switched_finder?(params) - finder_details[:url] = "/#{collection_name}/#{params[:id]}" - finder_details[:params] = {} - else - finder_details[:url] = "/#{collection_name}" - finder_details[:params] = params - end - CollectionProxy.new(collection_name, finder_details) - end + def find_all(params) + raise BadRequestError, "#find takes a hash as its parameter but you supplied #{params.inspect}" unless params.is_a? Hash - private - - def type_switched_finder?(params) - params.include?(:type) + finder_details = {} + if params[:id] && !type_switched_finder?(params) + finder_details[:url] = "/#{collection_name}/#{params[:id]}" + finder_details[:params] = {} + else + finder_details[:url] = "/#{collection_name}" + finder_details[:params] = params end + collection_proxy_class.new(collection_name, collection_class, details: finder_details, client: @client) end - def self.included(base) - base.extend(ClassMethods) + private + + def type_switched_finder?(params) + params.include?(:type) end end end diff --git a/lib/intercom/api_operations/list.rb b/lib/intercom/api_operations/list.rb index 9ecfa0c9..34c5927c 100644 --- a/lib/intercom/api_operations/list.rb +++ b/lib/intercom/api_operations/list.rb @@ -1,16 +1,14 @@ -require 'intercom/collection_proxy' +# frozen_string_literal: true + +require 'intercom/client_collection_proxy' +require 'intercom/base_collection_proxy' +require 'intercom/utils' module Intercom module ApiOperations - module List # TODO: Should we rename to All - module ClassMethods - def all - CollectionProxy.new(Utils.resource_class_to_collection_name(self)) - end - end - - def self.included(base) - base.extend(ClassMethods) + module List + def all + collection_proxy_class.new(collection_name, collection_class, client: @client) end end end diff --git a/lib/intercom/api_operations/load.rb b/lib/intercom/api_operations/load.rb index 15c305c1..8f91dc99 100644 --- a/lib/intercom/api_operations/load.rb +++ b/lib/intercom/api_operations/load.rb @@ -1,15 +1,19 @@ +# frozen_string_literal: true + +require 'intercom/utils' + module Intercom module ApiOperations module Load - def load - collection_name = Utils.resource_class_to_collection_name(self.class) - if id - response = Intercom.get("/#{collection_name}/#{id}", {}) + def load(object) + if object.id + response = @client.get("/#{collection_name}/#{object.id}", {}) else - raise "Cannot load #{self.class} as it does not have a valid id." + raise "Cannot load #{collection_class} as it does not have a valid id." end - raise Intercom::HttpError.new('Http Error - No response entity returned') unless response - from_response(response) + raise Intercom::HttpError, 'Http Error - No response entity returned' unless response + + object.from_response(response) end end end diff --git a/lib/intercom/api_operations/nested_resource.rb b/lib/intercom/api_operations/nested_resource.rb new file mode 100644 index 00000000..961d7362 --- /dev/null +++ b/lib/intercom/api_operations/nested_resource.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module Intercom + module ApiOperations + module NestedResource + module ClassMethods + def nested_resource_methods(resource, + path: nil, + operations: nil, + resource_plural: nil) + resource_plural ||= Utils.pluralize(resource.to_s) + path ||= resource_plural + raise ArgumentError, 'operations array required' if operations.nil? + + resource_url_method = :"#{resource_plural}_url" + resource_name = Utils.resource_class_to_collection_name(self) + define_method(resource_url_method.to_sym) do |id, nested_id = nil| + url = "/#{resource_name}/#{id}/#{path}" + url += "/#{nested_id}" unless nested_id.nil? + url + end + + operations.each do |operation| + case operation + when :create + define_method(:"create_#{resource}") do |params| + url = send(resource_url_method, self.id) + response = client.post(url, params) + raise_no_response_error unless response + self.class.from_api(response) + end + when :add + define_method(:"add_#{resource}") do |params| + url = send(resource_url_method, self.id) + response = client.post(url, params) + raise_no_response_error unless response + self.class.from_api(response) + end + when :delete + define_method(:"remove_#{resource}") do |params| + url = send(resource_url_method, self.id, params[:id]) + response = client.delete(url, params) + raise_no_response_error unless response + self.class.from_api(response) + end + when :list + define_method(resource_plural.to_sym) do + url = send(resource_url_method, self.id) + resource_class = Utils.constantize_resource_name(resource.to_s) + resource_class.collection_proxy_class.new(resource_plural, resource_class, details: { url: url }, client: client) + end + else + raise ArgumentError, "Unknown operation: #{operation.inspect}" + end + end + end + end + + def self.included(base) + base.extend(ClassMethods) + end + + private def raise_no_response_error + raise Intercom::HttpError, 'Http Error - No response entity returned' + end + end + end +end diff --git a/lib/intercom/api_operations/request_hard_delete.rb b/lib/intercom/api_operations/request_hard_delete.rb new file mode 100644 index 00000000..77ae09bd --- /dev/null +++ b/lib/intercom/api_operations/request_hard_delete.rb @@ -0,0 +1,12 @@ +require 'intercom/utils' + +module Intercom + module ApiOperations + module RequestHardDelete + def request_hard_delete(object) + @client.post("/user_delete_requests", {intercom_user_id: object.id}) + object + end + end + end +end diff --git a/lib/intercom/api_operations/save.rb b/lib/intercom/api_operations/save.rb index 732c13ae..8bab9c05 100644 --- a/lib/intercom/api_operations/save.rb +++ b/lib/intercom/api_operations/save.rb @@ -1,43 +1,48 @@ -require 'intercom/traits/api_resource' +# frozen_string_literal: true + +require 'intercom/utils' +require 'ext/sliceable_hash' module Intercom module ApiOperations module Save - - module ClassMethods - def create(params) - instance = self.new(params) - instance.mark_fields_as_changed!(params.keys) - instance.save + PARAMS_NOT_PROVIDED = Object.new + private_constant :PARAMS_NOT_PROVIDED + + def create(params = PARAMS_NOT_PROVIDED) + if collection_class.ancestors.include?(Intercom::Lead) && params == PARAMS_NOT_PROVIDED + params = {} + elsif params == PARAMS_NOT_PROVIDED + raise ArgumentError, '.create requires 1 parameter' end - end - def self.included(base) - base.extend(ClassMethods) + instance = collection_class.new(params) + instance.mark_fields_as_changed!(params.keys) + save(instance) end - def save - collection_name = Utils.resource_class_to_collection_name(self.class) - if id_present? && !posted_updates? - response = Intercom.put("/#{collection_name}/#{id}", to_submittable_hash) + def save(object) + if id_present?(object) && !posted_updates?(object) + response = @client.put("/#{collection_name}/#{object.id}", object.to_submittable_hash) else - response = Intercom.post("/#{collection_name}", to_submittable_hash.merge(identity_hash)) + response = @client.post("/#{collection_name}", object.to_submittable_hash.merge(identity_hash(object))) end - from_response(response) if response # may be nil we received back a 202 + object.client = @client + object.from_response(response) if response # may be nil we received back a 202 end - private - - def id_present? - id && id.to_s != '' + def identity_hash(object) + object.respond_to?(:identity_vars) ? SliceableHash.new(object.to_hash).slice(*object.identity_vars.map(&:to_s)) : {} end - def posted_updates? - respond_to?(:update_verb) && update_verb == 'post' + private + + def id_present?(object) + object.id && object.id.to_s != '' end - def identity_hash - respond_to?(:identity_vars) ? SliceableHash.new(to_hash).slice(*(identity_vars.map(&:to_s))) : {} + def posted_updates?(object) + object.respond_to?(:update_verb) && object.update_verb == 'post' end end end diff --git a/lib/intercom/api_operations/scroll.rb b/lib/intercom/api_operations/scroll.rb new file mode 100644 index 00000000..4d372d4a --- /dev/null +++ b/lib/intercom/api_operations/scroll.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'intercom/scroll_collection_proxy' +require 'intercom/utils' + +module Intercom + module ApiOperations + module Scroll + def scroll + finder_details = {} + finder_details[:url] = "/#{collection_name}" + ScrollCollectionProxy.new(collection_name, collection_class, details: finder_details, client: @client) + end + end + end +end diff --git a/lib/intercom/api_operations/search.rb b/lib/intercom/api_operations/search.rb new file mode 100644 index 00000000..1eafc456 --- /dev/null +++ b/lib/intercom/api_operations/search.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'intercom/search_collection_proxy' +require 'intercom/utils' + +module Intercom + module ApiOperations + module Search + def search(params) + search_details = { + url: "/#{collection_name}/search", + params: params + } + SearchCollectionProxy.new(collection_name, collection_class, details: search_details, client: @client) + end + end + end +end diff --git a/lib/intercom/article.rb b/lib/intercom/article.rb new file mode 100644 index 00000000..d1ace3bc --- /dev/null +++ b/lib/intercom/article.rb @@ -0,0 +1,7 @@ +require 'intercom/traits/api_resource' + +module Intercom + class Article + include Traits::ApiResource + end +end diff --git a/lib/intercom/base_collection_proxy.rb b/lib/intercom/base_collection_proxy.rb new file mode 100644 index 00000000..8fb59451 --- /dev/null +++ b/lib/intercom/base_collection_proxy.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'intercom/utils' + +module Intercom + class BaseCollectionProxy + attr_reader :resource_name, :url, :resource_class + + def initialize(resource_name, resource_class, details: {}, client:, method: 'get') + @resource_name = resource_name + @resource_class = resource_class + @url = (details[:url] || "/#{@resource_name}") + @params = (details[:params] || {}) + @client = client + @method = method + end + + def each(&block) + loop do + response_hash = @client.public_send(@method, @url, payload) + raise Intercom::HttpError, 'Http Error - No response entity returned' unless response_hash + + deserialize_response_hash(response_hash, block) + break unless has_next_link?(response_hash) + end + self + end + + def [](target_index) + each_with_index do |item, index| + return item if index == target_index + end + nil + end + + include Enumerable + + private + + def deserialize_response_hash(response_hash, block) + top_level_type = response_hash.delete('type') + top_level_entity_key = if resource_name == 'subscriptions' + 'items' + else + Utils.entity_key_from_type(top_level_type) + end + response_hash[top_level_entity_key].each do |object_json| + if top_level_type == 'event.summary' + block.call Lib::TypedJsonDeserializer.new(object_json, @client, top_level_type).deserialize + else + block.call Lib::TypedJsonDeserializer.new(object_json, @client).deserialize + end + end + end + + def has_next_link?(response_hash) + paging_info = response_hash.delete('pages') + return false unless paging_info + + paging_next = paging_info['next'] + if paging_next + @params[:starting_after] = paging_next['starting_after'] + return true + else + @params[:starting_after] = nil + return false + end + end + + def payload + @params.keep_if { |k, v| !v.nil? }.to_h + end + end +end diff --git a/lib/intercom/client.rb b/lib/intercom/client.rb new file mode 100644 index 00000000..5b3f607f --- /dev/null +++ b/lib/intercom/client.rb @@ -0,0 +1,180 @@ +# frozen_string_literal: true + +module Intercom + class MisconfiguredClientError < StandardError; end + class Client + include Options + include DeprecatedResources + attr_reader :base_url, :rate_limit_details, :token, :handle_rate_limit, :timeouts, :api_version + + class << self + def set_base_url(base_url) + proc do |o| + old_url = o.base_url + o.send(:base_url=, base_url) + proc { |_obj| set_base_url(old_url).call(o) } + end + end + + def set_timeouts(open_timeout: nil, read_timeout: nil) + proc do |o| + old_timeouts = o.timeouts + timeouts = {} + timeouts[:open_timeout] = open_timeout if open_timeout + timeouts[:read_timeout] = read_timeout if read_timeout + o.send(:timeouts=, timeouts) + proc { |_obj| set_timeouts(old_timeouts).call(o) } + end + end + end + + def initialize(token: nil, base_url: 'https://api.intercom.io', handle_rate_limit: false, api_version: nil) + @token = token + validate_credentials! + + @api_version = api_version + validate_api_version! + + @base_url = base_url + @rate_limit_details = {} + @handle_rate_limit = handle_rate_limit + @timeouts = { + open_timeout: 30, + read_timeout: 90 + } + end + + def admins + Intercom::Service::Admin.new(self) + end + + def articles + Intercom::Service::Article.new(self) + end + + def companies + Intercom::Service::Company.new(self) + end + + def contacts + Intercom::Service::Contact.new(self) + end + + def conversations + Intercom::Service::Conversation.new(self) + end + + def counts + Intercom::Service::Counts.new(self) + end + + def events + Intercom::Service::Event.new(self) + end + + def messages + Intercom::Service::Message.new(self) + end + + def notes + Intercom::Service::Note.new(self) + end + + def subscriptions + Intercom::Service::Subscription.new(self) + end + + def subscription_types + Intercom::Service::SubscriptionType.new(self) + end + + def segments + Intercom::Service::Segment.new(self) + end + + def sections + Intercom::Service::Section.new(self) + end + + def tags + Intercom::Service::Tag.new(self) + end + + def teams + Intercom::Service::Team.new(self) + end + + def users + Intercom::Service::User.new(self) + end + + def leads + Intercom::Service::Lead.new(self) + end + + def visitors + Intercom::Service::Visitor.new(self) + end + + def jobs + Intercom::Service::Job.new(self) + end + + def data_attributes + Intercom::Service::DataAttribute.new(self) + end + + def collections + Intercom::Service::Collection.new(self) + end + + def export_content + Intercom::Service::ExportContent.new(self) + end + + def phone_call_redirect + Intercom::Service::PhoneCallRedirect.new(self) + end + + def get(path, params) + execute_request Intercom::Request.get(path, params) + end + + def post(path, payload_hash) + execute_request Intercom::Request.post(path, payload_hash) + end + + def put(path, payload_hash) + execute_request Intercom::Request.put(path, payload_hash) + end + + def delete(path, payload_hash) + execute_request Intercom::Request.delete(path, payload_hash) + end + + private + + def validate_credentials! + error = MisconfiguredClientError.new('an access token must be provided') + raise error if @token.nil? + end + + def validate_api_version! + error = MisconfiguredClientError.new('api_version must be either nil or a valid API version') + raise error if @api_version && @api_version != 'Unstable' && Gem::Version.new(@api_version) < Gem::Version.new('1.0') + end + + def execute_request(request) + request.handle_rate_limit = handle_rate_limit + request.execute(@base_url, token: @token, api_version: @api_version, **timeouts) + ensure + @rate_limit_details = request.rate_limit_details + end + + attr_writer :base_url + + def timeouts=(timeouts) + @timeouts = @timeouts.merge(timeouts) + end + end +end diff --git a/lib/intercom/client_collection_proxy.rb b/lib/intercom/client_collection_proxy.rb new file mode 100644 index 00000000..f08b9a3e --- /dev/null +++ b/lib/intercom/client_collection_proxy.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'intercom/utils' +require 'intercom/base_collection_proxy' + +module Intercom + class ClientCollectionProxy < BaseCollectionProxy + def each(&block) + next_page = nil + current_page = nil + loop do + response_hash = fetch(next_page) + raise Intercom::HttpError, 'Http Error - No response entity returned' unless response_hash + + current_page = extract_current_page(response_hash) + deserialize_response_hash(response_hash, block) + next_page = extract_next_link(response_hash) + break if next_page.nil? || (@params[:page] && (current_page >= @params[:page])) + end + self + end + + def fetch(next_page) + if next_page + @client.get(next_page, {}) + else + @client.get(@url, @params) + end + end + + private + + def paging_info_present?(response_hash) + !!(response_hash['pages'] && response_hash['pages']['type']) + end + + def extract_next_link(response_hash) + return nil unless paging_info_present?(response_hash) + + paging_info = response_hash.delete('pages') + paging_info['next'] + end + + def extract_current_page(response_hash) + return nil unless paging_info_present?(response_hash) + + response_hash['pages']['page'] + end + end +end diff --git a/lib/intercom/collection.rb b/lib/intercom/collection.rb new file mode 100644 index 00000000..3fc7ab26 --- /dev/null +++ b/lib/intercom/collection.rb @@ -0,0 +1,7 @@ +require 'intercom/traits/api_resource' + +module Intercom + class Collection + include Traits::ApiResource + end +end diff --git a/lib/intercom/collection_proxy.rb b/lib/intercom/collection_proxy.rb deleted file mode 100644 index 340ce557..00000000 --- a/lib/intercom/collection_proxy.rb +++ /dev/null @@ -1,71 +0,0 @@ -require "intercom/utils" -require "ext/sliceable_hash" - -module Intercom - class CollectionProxy - - attr_reader :resource_name - - def initialize(resource_name, finder_details = {}) - @resource_name = resource_name - @resource_class = Utils.constantize_resource_name(resource_name) - @finder_url = (finder_details[:url] || "/#{@resource_name}") - @finder_params = (finder_details[:params] || {}) - end - - def each(&block) - next_page = nil - loop do - if next_page - response_hash = Intercom.get(next_page, {}) - else - response_hash = Intercom.get(@finder_url, @finder_params) - end - raise Intercom::HttpError.new('Http Error - No response entity returned') unless response_hash - deserialize_response_hash(response_hash, block) - next_page = extract_next_link(response_hash) - break if next_page.nil? - end - self - end - - def [](target_index) - self.each_with_index do |item, index| - return item if index == target_index - end - nil - end - - include Enumerable - - def count - raise NoMethodError, "undefined method `count' for #{self.class}. Consider using the dedicated Intercom::Count interface if suitable" - end - - private - - def resource_class; @resource_class; end - - def deserialize_response_hash(response_hash, block) - top_level_type = response_hash.delete('type') - if resource_name == 'subscriptions' - top_level_entity_key = 'items' - else - top_level_entity_key = Utils.entity_key_from_type(top_level_type) - end - response_hash[top_level_entity_key].each do |object_json| - block.call Lib::TypedJsonDeserializer.new(object_json).deserialize - end - end - - def paging_info_present?(response_hash) - !!(response_hash['pages'] && response_hash['pages']['type']) - end - - def extract_next_link(response_hash) - return nil unless paging_info_present?(response_hash) - paging_info = response_hash.delete('pages') - paging_info["next"] - end - end -end diff --git a/lib/intercom/company.rb b/lib/intercom/company.rb index 701c63f0..61e8ca5d 100644 --- a/lib/intercom/company.rb +++ b/lib/intercom/company.rb @@ -1,26 +1,18 @@ -require 'intercom/api_operations/count' -require 'intercom/api_operations/list' -require 'intercom/api_operations/find' -require 'intercom/api_operations/find_all' -require 'intercom/api_operations/save' -require 'intercom/api_operations/load' -require 'intercom/extended_api_operations/users' -require 'intercom/extended_api_operations/tags' require 'intercom/traits/incrementable_attributes' require 'intercom/traits/api_resource' +require 'intercom/api_operations/nested_resource' module Intercom class Company - include ApiOperations::Count - include ApiOperations::List - include ApiOperations::Find - include ApiOperations::FindAll - include ApiOperations::Save - include ApiOperations::Load - include ExtendedApiOperations::Users - include ExtendedApiOperations::Tags include Traits::IncrementableAttributes include Traits::ApiResource + include ApiOperations::NestedResource + + nested_resource_methods :contact, operations: %i[list] + + def self.collection_proxy_class + Intercom::ClientCollectionProxy + end def identity_vars ; [:id, :company_id] ; end def flat_store_attributes ; [:custom_attributes] ; end diff --git a/lib/intercom/contact.rb b/lib/intercom/contact.rb new file mode 100644 index 00000000..6f656d9e --- /dev/null +++ b/lib/intercom/contact.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'intercom/traits/incrementable_attributes' +require 'intercom/traits/api_resource' +require 'intercom/api_operations/nested_resource' + +module Intercom + class Contact + include Traits::IncrementableAttributes + include Traits::ApiResource + include ApiOperations::NestedResource + + nested_resource_methods :tag, operations: %i[add delete list] + nested_resource_methods :note, operations: %i[create list] + nested_resource_methods :subscription_type, path: 'subscriptions', operations: %i[create delete list] + nested_resource_methods :company, operations: %i[add delete list] + nested_resource_methods :segment, operations: %i[list] + + def self.collection_proxy_class + Intercom::BaseCollectionProxy + end + + def identity_vars + [:id] + end + + def flat_store_attributes + [:custom_attributes] + end + end +end diff --git a/lib/intercom/conversation.rb b/lib/intercom/conversation.rb index 9a4e9885..64d7b843 100644 --- a/lib/intercom/conversation.rb +++ b/lib/intercom/conversation.rb @@ -1,17 +1,12 @@ -require 'intercom/extended_api_operations/reply' -require 'intercom/api_operations/find_all' -require 'intercom/api_operations/find' -require 'intercom/api_operations/load' -require 'intercom/api_operations/save' require 'intercom/traits/api_resource' +require 'intercom/api_operations/nested_resource' module Intercom class Conversation - include ExtendedApiOperations::Reply - include ApiOperations::FindAll - include ApiOperations::Find - include ApiOperations::Load - include ApiOperations::Save include Traits::ApiResource + include ApiOperations::NestedResource + + nested_resource_methods :tag, operations: %i[add delete] + nested_resource_methods :contact, operations: %i[add delete], path: :customers end end diff --git a/lib/intercom/count.rb b/lib/intercom/count.rb index 8201e661..99a5945f 100644 --- a/lib/intercom/count.rb +++ b/lib/intercom/count.rb @@ -1,21 +1,7 @@ require 'intercom/traits/api_resource' -require 'intercom/api_operations/find' -require 'intercom/traits/generic_handler_binding' -require 'intercom/generic_handlers/count' module Intercom class Count - include ApiOperations::Find include Traits::ApiResource - include Traits::GenericHandlerBinding - include GenericHandlers::Count - - def self.fetch_for_app - Intercom::Count.find({}) - end - - def self.fetch_broken_down_count(entity_to_count, count_context) - Intercom::Count.find(:type => entity_to_count, :count => count_context) - end end end diff --git a/lib/intercom/data_attribute.rb b/lib/intercom/data_attribute.rb new file mode 100644 index 00000000..cda919b3 --- /dev/null +++ b/lib/intercom/data_attribute.rb @@ -0,0 +1,7 @@ +require 'intercom/traits/api_resource' + +module Intercom + class DataAttribute + include Traits::ApiResource + end +end diff --git a/lib/intercom/deprecated_leads_collection_proxy.rb b/lib/intercom/deprecated_leads_collection_proxy.rb new file mode 100644 index 00000000..8a96165c --- /dev/null +++ b/lib/intercom/deprecated_leads_collection_proxy.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Intercom + class DeprecatedLeadsCollectionProxy < ClientCollectionProxy + def fetch(next_page) + response_hash = if next_page + @client.get(next_page, {}) + else + @client.get(@url, @params) + end + transform(response_hash) + end + + def transform(response_hash) + response_hash['type'] = 'lead.list' + leads_list = response_hash.delete('contacts') + leads_list.each { |lead| lead['type'] = 'lead' } + response_hash['leads'] = leads_list + response_hash + end + end +end diff --git a/lib/intercom/deprecated_resources.rb b/lib/intercom/deprecated_resources.rb new file mode 100644 index 00000000..bbc178fa --- /dev/null +++ b/lib/intercom/deprecated_resources.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Intercom + module DeprecatedResources + def deprecated__leads + Intercom::Service::Lead.new(self) + end + + def deprecated__users + Intercom::Service::User.new(self) + end + end +end diff --git a/lib/intercom/errors.rb b/lib/intercom/errors.rb index d51befd2..b3af0bd5 100644 --- a/lib/intercom/errors.rb +++ b/lib/intercom/errors.rb @@ -2,24 +2,49 @@ module Intercom # Base class exception from which all public Intercom exceptions will be derived class IntercomError < StandardError - attr_reader :http_code, :application_error_code - def initialize(message, http_code = nil, application_error_code = application_error_code) - @http_code = http_code - @application_error_code = application_error_code + attr_reader :http_code, :application_error_code, :field, :request_id + def initialize(message, context={}) + @http_code = context[:http_code] + @application_error_code = context[:application_error_code] + @field = context[:field] + @request_id = context[:request_id] super(message) end + def inspect + attributes = instance_variables.map do |var| + value = instance_variable_get(var).inspect + "#{var}=#{value}" + end + "##{self.class.name}:#{message} #{attributes.join(' ')}" + end + def to_hash + {message: message} + .merge(Hash[instance_variables.map{ |var| [var[1..-1], instance_variable_get(var)] }]) + end end - # Raised when the credentials you provide don't match a valid account on Intercom. - # Check that you have set Intercom.app_id= and Intercom.app_api_key= correctly. + # Raised when the token you provided is incorrect or not authorized to access certain type of data. + # Check that you have set Intercom.token correctly. class AuthenticationError < IntercomError; end - # Raised when something does wrong on within the Intercom API service. + # Raised when the token provided is linked to a deleted application. + class AppSuspendedError < AuthenticationError; end + + # Raised when the token provided has been revoked. + class TokenRevokedError < AuthenticationError; end + + # Raised when the token provided can't be decoded, and is most likely invalid. + class TokenUnauthorizedError < AuthenticationError; end + + # Raised when something goes wrong on within the Intercom API service. class ServerError < IntercomError; end # Raised when we have bad gateway errors. class BadGatewayError < IntercomError; end + # Raised when we have gateway timeout errors. + class GatewayTimeoutError < IntercomError; end + # Raised when we experience a socket read timeout class ServiceUnavailableError < IntercomError; end @@ -29,33 +54,64 @@ class ServiceConnectionError < IntercomError; end # Raised when requesting resources on behalf of a user that doesn't exist in your application on Intercom. class ResourceNotFound < IntercomError; end - # Raised when the request has a bad syntax + # Raised when requesting an admin that doesn't exist in your Intercom workspace. + class AdminNotFound < IntercomError; end + + # Raised when trying to create a resource that already exists in Intercom. + class ResourceNotUniqueError < IntercomError; end + + # Raised when the request has bad syntax class BadRequestError < IntercomError; end - # Raised when you have exceed the API rate limit + # Raised when you have exceeded the API rate limit class RateLimitExceeded < IntercomError; end + # Raised when some attribute of the response cannot be handled + class UnexpectedResponseError < IntercomError; end + # Raised when the request throws an error not accounted for class UnexpectedError < IntercomError; end - + + # Raised when the CDA limit for the app has been reached + class CDALimitReachedError < IntercomError; end + # Raised when multiple users match the query (typically duplicate email addresses) class MultipleMatchingUsersError < IntercomError; end + # Raised when restoring a blocked user + class BlockedUserError < IntercomError; end + # Raised when you try to call a non-setter method that does not exist on an object - class Intercom::AttributeNotSetError < IntercomError ; end - + class Intercom::AttributeNotSetError < IntercomError; end + # Raised when unexpected nil returned from server - class Intercom::HttpError < IntercomError ; end + class Intercom::HttpError < IntercomError; end + + # Raised when an invalid api version is used + class ApiVersionInvalid < IntercomError; end + + # Raised when an creating a scroll if one already exists + class ScrollAlreadyExistsError < IntercomError; end + + # Raised when a CDA is invalid + class InvalidDocumentError < IntercomError; end + + # Raised when a merge is invalid + class InvalidMergeError < IntercomError; end + + # Raised when a tag has dependent objects + class TagHasDependentObjects < IntercomError; end # # Non-public errors (internal to the gem) # - # Base class exception from which all public Intercom exceptions will be derived + # Base class exception from which all private Intercom exceptions will be derived class IntercomInternalError < StandardError; end # Raised when we attempt to handle a method missing but are unsuccessful class Intercom::NoMethodMissingHandler < IntercomInternalError; end class Intercom::DeserializationError < IntercomInternalError; end + end diff --git a/lib/intercom/event.rb b/lib/intercom/event.rb index 8cf6b817..697d4ae5 100644 --- a/lib/intercom/event.rb +++ b/lib/intercom/event.rb @@ -1,9 +1,8 @@ -require 'intercom/api_operations/save' require 'intercom/traits/api_resource' module Intercom class Event - include ApiOperations::Save include Traits::ApiResource + def update_verb; 'post' ; end end end diff --git a/lib/intercom/export_content.rb b/lib/intercom/export_content.rb new file mode 100644 index 00000000..9ea7c3d5 --- /dev/null +++ b/lib/intercom/export_content.rb @@ -0,0 +1,7 @@ +require 'intercom/traits/api_resource' + +module Intercom + class ExportContent + include Traits::ApiResource + end +end diff --git a/lib/intercom/extended_api_operations/reply.rb b/lib/intercom/extended_api_operations/reply.rb deleted file mode 100644 index f74526bf..00000000 --- a/lib/intercom/extended_api_operations/reply.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'intercom/traits/api_resource' - -module Intercom - module ExtendedApiOperations - module Reply - - def reply(reply_data) - collection_name = Utils.resource_class_to_collection_name(self.class) - # TODO: For server, we should not need to merge in :conversation_id here (already in the URL) - response = Intercom.post("/#{collection_name}/#{id}/reply", reply_data.merge(:conversation_id => id)) - from_response(response) - end - - end - end -end diff --git a/lib/intercom/extended_api_operations/segments.rb b/lib/intercom/extended_api_operations/segments.rb new file mode 100644 index 00000000..d3ee8fe1 --- /dev/null +++ b/lib/intercom/extended_api_operations/segments.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'intercom/client_collection_proxy' +require 'intercom/utils' + +module Intercom + module ExtendedApiOperations + module Segments + def by_segment(id) + collection_name = Utils.resource_class_to_collection_name(collection_class) + ClientCollectionProxy.new(collection_name, collection_class, details: { url: "/#{collection_name}?segment_id=#{id}" }, client: @client) + end + end + end +end diff --git a/lib/intercom/extended_api_operations/tags.rb b/lib/intercom/extended_api_operations/tags.rb index cc32ce00..d252e254 100644 --- a/lib/intercom/extended_api_operations/tags.rb +++ b/lib/intercom/extended_api_operations/tags.rb @@ -1,14 +1,15 @@ -require 'intercom/traits/api_resource' +# frozen_string_literal: true + +require 'intercom/client_collection_proxy' +require 'intercom/utils' module Intercom module ExtendedApiOperations module Tags - - def tags - collection_name = Utils.resource_class_to_collection_name(self.class) - self.id ? Intercom::Tag.send("find_all_for_#{collection_name}", :id => id) : [] + def by_tag(id) + collection_name = Utils.resource_class_to_collection_name(collection_class) + ClientCollectionProxy.new(collection_name, collection_class, details: { url: "/#{collection_name}?tag_id=#{id}" }, client: @client) end - end end end diff --git a/lib/intercom/extended_api_operations/users.rb b/lib/intercom/extended_api_operations/users.rb deleted file mode 100644 index 32102468..00000000 --- a/lib/intercom/extended_api_operations/users.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'intercom/traits/api_resource' - -module Intercom - module ExtendedApiOperations - module Users - - def users - collection_name = Utils.resource_class_to_collection_name(self.class) - finder_details = {} - finder_details[:url] = "/#{collection_name}/#{id}/users" - finder_details[:params] = {} - CollectionProxy.new("users", finder_details) - end - - end - end -end diff --git a/lib/intercom/generic_handlers/base_handler.rb b/lib/intercom/generic_handlers/base_handler.rb deleted file mode 100644 index 3ebe2285..00000000 --- a/lib/intercom/generic_handlers/base_handler.rb +++ /dev/null @@ -1,22 +0,0 @@ -module Intercom - module GenericHandlers - class BaseHandler - attr_reader :method_sym, :arguments, :entity - - def initialize(method_sym, arguments, entity) - @method_sym = method_sym - @arguments = arguments - @entity = entity - end - - def method_string - method_sym.to_s - end - - def raise_no_method_missing_handler - raise Intercom::NoMethodMissingHandler, "Could not handle '#{method_string}'" - end - end - - end -end diff --git a/lib/intercom/generic_handlers/count.rb b/lib/intercom/generic_handlers/count.rb deleted file mode 100644 index 88478964..00000000 --- a/lib/intercom/generic_handlers/count.rb +++ /dev/null @@ -1,59 +0,0 @@ -require 'intercom/generic_handlers/base_handler' - -module Intercom - module GenericHandlers - module Count - module ClassMethods - def generic_count(method_sym, *arguments, &block) - - handler_class = Class.new(GenericHandlers::BaseHandler) do - def handle - match = method_string.match(GenericHandlers::Count.count_breakdown_matcher) - if match && match[1] && match[2] && match[3].nil? - do_broken_down_count(match[1], match[2]) - elsif method_string.end_with? '_count' - return do_count - else - raise_no_method_missing_handler - end - end - - private - - def do_count - entity.fetch_for_app.send(appwide_entity_to_count)['count'] - rescue Intercom::AttributeNotSetError - # Indicates this this kind of counting is not supported - raise_no_method_missing_handler - end - - def do_broken_down_count(entity_to_count, count_context) - result = entity.fetch_broken_down_count(entity_to_count, count_context) - result.send(entity_to_count)[count_context] - rescue Intercom::BadRequestError => ex - # Indicates this this kind of counting is not supported - ex.application_error_code == 'parameter_invalid' ? raise_no_method_missing_handler : raise - end - - def appwide_entity_to_count; method_string.gsub(/_count$/, ''); end - end - - handler_class.new(method_sym, arguments, self).handle - end - - end - - def self.count_breakdown_matcher - /([[:alnum:]]+)_counts_for_each_([[:alnum:]]+)/ - end - - def self.handles_method?(method_sym) - method_sym.to_s.end_with? '_count' or method_sym.to_s.match(count_breakdown_matcher) - end - - def self.included(base) - base.extend(ClassMethods) - end - end - end -end diff --git a/lib/intercom/generic_handlers/tag.rb b/lib/intercom/generic_handlers/tag.rb deleted file mode 100644 index f22fb12f..00000000 --- a/lib/intercom/generic_handlers/tag.rb +++ /dev/null @@ -1,71 +0,0 @@ -require 'intercom/generic_handlers/base_handler' - -module Intercom - module GenericHandlers - module Tag - module ClassMethods - def generic_tag(method_sym, *arguments, &block) - - handler_class = Class.new(GenericHandlers::BaseHandler) do - def handle - if method_string.start_with? 'tag_' - return do_tagging - elsif method_string.start_with? 'untag_' - return do_untagging - else - raise_no_method_missing_handler - end - end - - private - - def do_tagging - entity.create(:name => arguments[0], tagging_context.to_sym => tag_object_list(arguments)) - end - - def do_untagging - current_tag = find_tag(arguments[0]) - return untag_via_save(current_tag) if current_tag - end - - def find_tag(name_arg) - Intercom::Tag.find(:name => name_arg) - rescue Intercom::ResourceNotFound - return nil # Ignore if tag has since been deleted - end - - def untag_via_save(current_tag) - current_tag.name = arguments[0] - current_tag.send("#{untagging_context}=", untag_object_list(arguments)) - current_tag.save - end - - def tag_object_list(args) - args[1].map { |id| { :id => id } } - end - - def untag_object_list(args) - to_tag = tag_object_list(args) - to_tag.map { |tag_object| tag_object[:untag] = true } - to_tag - end - - def tagging_context; method_string.gsub(/^tag_/, ''); end - def untagging_context; method_string.gsub(/^untag_/, ''); end - end - - handler_class.new(method_sym, arguments, self).handle - end - - end - - def self.handles_method?(method_sym) - method_sym.to_s.start_with?('tag_') || method_sym.to_s.start_with?('untag_') - end - - def self.included(base) - base.extend(ClassMethods) - end - end - end -end diff --git a/lib/intercom/generic_handlers/tag_find_all.rb b/lib/intercom/generic_handlers/tag_find_all.rb deleted file mode 100644 index 16b5fa9c..00000000 --- a/lib/intercom/generic_handlers/tag_find_all.rb +++ /dev/null @@ -1,47 +0,0 @@ -require 'intercom/generic_handlers/base_handler' - -module Intercom - module GenericHandlers - module TagFindAll - module ClassMethods - def generic_tag_find_all(method_sym, *arguments, &block) - - handler_class = Class.new(GenericHandlers::BaseHandler) do - def handle - if method_string.start_with? 'find_all_for_' - return do_tag_find_all - else - raise_no_method_missing_handler - end - end - - private - - def do_tag_find_all - Intercom::Tag.find_all(cleaned_arguments.merge(:taggable_type => Utils.singularize(context))) - end - - def cleaned_arguments - cleaned_args = arguments[0] - cleaned_args[:taggable_id] = cleaned_args.delete(:id) if cleaned_args.has_key?(:id) - cleaned_args - end - - def context; method_string.gsub(/^find_all_for_/, ''); end - end - - handler_class.new(method_sym, arguments, self).handle - end - - end - - def self.handles_method?(method_sym) - method_sym.to_s.start_with? 'find_all_for_' - end - - def self.included(base) - base.extend(ClassMethods) - end - end - end -end diff --git a/lib/intercom/job.rb b/lib/intercom/job.rb new file mode 100644 index 00000000..2263dac4 --- /dev/null +++ b/lib/intercom/job.rb @@ -0,0 +1,7 @@ +require 'intercom/traits/api_resource' + +module Intercom + class Job + include Traits::ApiResource + end +end diff --git a/lib/intercom/lead.rb b/lib/intercom/lead.rb new file mode 100644 index 00000000..022b3534 --- /dev/null +++ b/lib/intercom/lead.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'intercom/traits/api_resource' + +module Intercom + class Lead + include Traits::ApiResource + + def identity_vars + %i[email user_id] + end + + def flat_store_attributes + [:custom_attributes] + end + + def update_verb + 'put' + end + end +end diff --git a/lib/intercom/lib/dynamic_accessors.rb b/lib/intercom/lib/dynamic_accessors.rb index c8cda291..9d26b4fc 100644 --- a/lib/intercom/lib/dynamic_accessors.rb +++ b/lib/intercom/lib/dynamic_accessors.rb @@ -5,20 +5,19 @@ module DynamicAccessors class << self def define_accessors(attribute, value, object) - klass = object.class - if attribute.to_s.end_with? '_at' - define_date_based_accessors(attribute, value, klass) + if attribute.to_s.end_with?('_at') && attribute.to_s != 'update_last_request_at' + define_date_based_accessors(attribute, value, object) elsif object.flat_store_attribute?(attribute) - define_flat_store_based_accessors(attribute, value, klass) + define_flat_store_based_accessors(attribute, value, object) else - define_standard_accessors(attribute, value, klass) + define_standard_accessors(attribute, value, object) end end private - def define_flat_store_based_accessors(attribute, value, klass) - klass.class_eval %Q" + def define_flat_store_based_accessors(attribute, value, object) + object.instance_eval %Q" def #{attribute}=(value) mark_field_as_changed!(:#{attribute}) @#{attribute} = Intercom::Lib::FlatStore.new(value) @@ -29,8 +28,8 @@ def #{attribute} " end - def define_date_based_accessors(attribute, value, klass) - klass.class_eval %Q" + def define_date_based_accessors(attribute, value, object) + object.instance_eval %Q" def #{attribute}=(value) mark_field_as_changed!(:#{attribute}) @#{attribute} = value.nil? ? nil : value.to_i @@ -41,8 +40,8 @@ def #{attribute} " end - def define_standard_accessors(attribute, value, klass) - klass.class_eval %Q" + def define_standard_accessors(attribute, value, object) + object.instance_eval %Q" def #{attribute}=(value) mark_field_as_changed!(:#{attribute}) @#{attribute} = value diff --git a/lib/intercom/lib/dynamic_accessors_on_method_missing.rb b/lib/intercom/lib/dynamic_accessors_on_method_missing.rb index 4af59dac..c268f252 100644 --- a/lib/intercom/lib/dynamic_accessors_on_method_missing.rb +++ b/lib/intercom/lib/dynamic_accessors_on_method_missing.rb @@ -18,7 +18,7 @@ def define_accessors_or_call(&block) Lib::DynamicAccessors.define_accessors(attribute_name, *arguments, object) object.send(method_sym, *arguments) else # getter - if trying_to_access_private_variable? + if trying_to_access_private_variable? || trying_to_access_print_method? yield else raise Intercom::AttributeNotSetError, attribute_not_set_error_message @@ -44,6 +44,10 @@ def trying_to_access_private_variable? object.instance_variable_defined?("@#{method_string}") end + def trying_to_access_print_method? + [:to_ary, :to_s].include? method_sym + end + def attribute_not_set_error_message "'#{method_string}' called on #{klass} but it has not been set an " + "attribute or does not exist as a method" diff --git a/lib/intercom/lib/flat_store.rb b/lib/intercom/lib/flat_store.rb index 6971ca75..2077a2bd 100644 --- a/lib/intercom/lib/flat_store.rb +++ b/lib/intercom/lib/flat_store.rb @@ -2,7 +2,7 @@ module Intercom module Lib # Sub-class of {Hash} for storing custom data attributes. - # Doesn't allow nested Hashes or Arrays. And requires {String} or {Symbol} keys. + # Doesn't allow Arrays. And requires {String} or {Symbol} keys. class FlatStore < Hash def initialize(attributes={}) @@ -21,9 +21,16 @@ def [](key) super(key.to_s) end + def to_submittable_hash + # Filter out Custom Object references when submitting to API + self.reject do |key, value| + value.is_a?(Hash) + end + end + private def validate_key_and_value(key, value) - raise ArgumentError.new("This does not support nested data structures (key: #{key}, value: #{value}") if value.is_a?(Array) || value.is_a?(Hash) + raise ArgumentError.new("This does not support nested data structures (key: #{key}, value: #{value}") if value.is_a?(Array) raise ArgumentError.new("Key must be String or Symbol: #{key}") unless key.is_a?(String) || key.is_a?(Symbol) end end diff --git a/lib/intercom/lib/typed_json_deserializer.rb b/lib/intercom/lib/typed_json_deserializer.rb index ddbda689..155d2325 100644 --- a/lib/intercom/lib/typed_json_deserializer.rb +++ b/lib/intercom/lib/typed_json_deserializer.rb @@ -1,52 +1,64 @@ +# frozen_string_literal: true + +require 'intercom/utils' + module Intercom module Lib + # Responsibility: To decide whether we are deserializing a collection or an + # entity of a particular type and to dispatch deserialization + class TypedJsonDeserializer + attr_reader :json - # Responsibility: To decide whether we are deserializing a collection or an - # entity of a particular type and to dispatch deserialization - class TypedJsonDeserializer - attr_reader :json - - def initialize(json) - @json = json - end + def initialize(json, client, type = nil) + @json = json + @client = client + @type = type + end - def deserialize - if blank_object_type?(object_type) - raise DeserializationError, "No type field was found to facilitate deserialization" - elsif list_object_type?(object_type) - deserialize_collection(json[object_entity_key]) - else # singular object type - deserialize_object(json) - end + def deserialize + if blank_object_type?(object_type) + raise DeserializationError, 'No type field was found to facilitate deserialization' + elsif list_object_type?(object_type) + deserialize_collection(json[object_entity_key]) + else # singular object type + deserialize_object(json) end + end - private + private - def blank_object_type?(object_type) - object_type.nil? || object_type == '' - end + def blank_object_type?(object_type) + object_type.nil? || object_type == '' && @type.nil? + end - def list_object_type?(object_type) - object_type.end_with?('.list') - end + def list_object_type?(object_type) + object_type.end_with?('.list') + end - def deserialize_collection(collection_json) - collection_json.map { |item_json| TypedJsonDeserializer.new(item_json).deserialize } - end + def deserialize_collection(collection_json) + return [] if collection_json.nil? - def deserialize_object(object_json) - entity_class = Utils.constantize_singular_resource_name(object_entity_key) - entity_class.from_api(object_json) - end + collection_json.map { |item_json| TypedJsonDeserializer.new(item_json, @client).deserialize } + end - def object_type - @object_type ||= json['type'] - end + def deserialize_object(object_json) + entity_class = Utils.constantize_singular_resource_name(object_entity_key) + deserialized = entity_class.from_api(object_json) + deserialized.client = @client + deserialized + end - def object_entity_key - @object_entity_key ||= Utils.entity_key_from_type(object_type) + def object_type + if !@type.nil? + @object_type = @type + else + @object_type ||= json['type'] end + end + def object_entity_key + @object_entity_key ||= Utils.entity_key_from_type(object_type) end + end end end diff --git a/lib/intercom/message.rb b/lib/intercom/message.rb index 62952195..0d29b78b 100644 --- a/lib/intercom/message.rb +++ b/lib/intercom/message.rb @@ -1,9 +1,7 @@ -require 'intercom/api_operations/save' require 'intercom/traits/api_resource' module Intercom class Message - include ApiOperations::Save include Traits::ApiResource end end diff --git a/lib/intercom/note.rb b/lib/intercom/note.rb index 68ac17bc..ec8a0e01 100644 --- a/lib/intercom/note.rb +++ b/lib/intercom/note.rb @@ -1,17 +1,12 @@ -require 'intercom/api_operations/save' -require 'intercom/api_operations/list' -require 'intercom/api_operations/find_all' -require 'intercom/api_operations/find' -require 'intercom/api_operations/load' + require 'intercom/traits/api_resource' module Intercom class Note - include ApiOperations::Save - include ApiOperations::List - include ApiOperations::FindAll - include ApiOperations::Find - include ApiOperations::Load include Traits::ApiResource + + def self.collection_proxy_class + Intercom::BaseCollectionProxy + end end end diff --git a/lib/intercom/notification.rb b/lib/intercom/notification.rb deleted file mode 100644 index 02811bda..00000000 --- a/lib/intercom/notification.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'intercom/traits/api_resource' - -module Intercom - class Notification - include Traits::ApiResource - - def model - data.item - end - - def model_type - model.class - end - - def load - model.load - end - - end -end diff --git a/lib/intercom/options.rb b/lib/intercom/options.rb new file mode 100644 index 00000000..024ee256 --- /dev/null +++ b/lib/intercom/options.rb @@ -0,0 +1,11 @@ +module Intercom + module Options + def options(*opts) + previous = nil + opts.each do |opt| + previous = opt.call(self) + end + previous + end + end +end diff --git a/lib/intercom/phone_call_redirect.rb b/lib/intercom/phone_call_redirect.rb new file mode 100644 index 00000000..61db81bb --- /dev/null +++ b/lib/intercom/phone_call_redirect.rb @@ -0,0 +1,7 @@ +require 'intercom/traits/api_resource' + +module Intercom + class PhoneCallRedirect + include Traits::ApiResource + end +end diff --git a/lib/intercom/request.rb b/lib/intercom/request.rb index ad1cdf38..40379939 100644 --- a/lib/intercom/request.rb +++ b/lib/intercom/request.rb @@ -1,148 +1,229 @@ +# frozen_string_literal: true + require 'cgi' require 'net/https' module Intercom class Request - attr_accessor :path, :net_http_method + class << self + def get(path, params) + new(path, Net::HTTP::Get.new(append_query_string_to_url(path, params), default_headers)) + end - def initialize(path, net_http_method) - self.path = path - self.net_http_method = net_http_method - end + def post(path, form_data) + new(path, method_with_body(Net::HTTP::Post, path, form_data)) + end - def set_common_headers(method, base_uri) - method.basic_auth(CGI.unescape(base_uri.user), CGI.unescape(base_uri.password)) - method.add_field('AcceptEncoding', 'gzip, deflate') - end + def delete(path, params) + new(path, method_with_body(Net::HTTP::Delete, path, params)) + end - def self.get(path, params) - new(path, Net::HTTP::Get.new(append_query_string_to_url(path, params), default_headers)) - end + def put(path, form_data) + new(path, method_with_body(Net::HTTP::Put, path, form_data)) + end - def self.post(path, form_data) - new(path, method_with_body(Net::HTTP::Post, path, form_data)) - end + private def method_with_body(http_method, path, params) + request = http_method.send(:new, path, default_headers) + request.body = params.to_json + request['Content-Type'] = 'application/json' + request + end - def self.delete(path, params) - new(path, method_with_body(Net::HTTP::Delete, path, params)) - end + private def default_headers + { 'Accept-Encoding' => 'gzip, deflate', 'Accept' => 'application/vnd.intercom.3+json', 'User-Agent' => "Intercom-Ruby/#{Intercom::VERSION}" } + end - def self.put(path, form_data) - new(path, method_with_body(Net::HTTP::Put, path, form_data)) - end + private def append_query_string_to_url(url, params) + return url if params.empty? - def self.method_with_body(http_method, path, params) - request = http_method.send(:new, path, default_headers) - request.body = params.to_json - request["Content-Type"] = "application/json" - request + query_string = params.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&') + url + "?#{query_string}" + end end - def self.default_headers - {'Accept-Encoding' => 'gzip, deflate', 'Accept' => 'application/vnd.intercom.3+json', 'User-Agent' => "Intercom-Ruby/#{Intercom::VERSION}"} + def initialize(path, net_http_method) + self.path = path + self.net_http_method = net_http_method + self.handle_rate_limit = false end - def client(uri) - net = Net::HTTP.new(uri.host, uri.port) - if uri.is_a?(URI::HTTPS) - net.use_ssl = true - net.verify_mode = OpenSSL::SSL::VERIFY_PEER - net.ca_file = File.join(File.dirname(__FILE__), '../data/cacert.pem') - end - net.read_timeout = 90 - net.open_timeout = 30 - net - end + attr_accessor :handle_rate_limit - def execute(target_base_url=nil) + def execute(target_base_url = nil, token:, read_timeout: 90, open_timeout: 30, api_version: nil) + retries = 3 base_uri = URI.parse(target_base_url) set_common_headers(net_http_method, base_uri) + set_auth_header(net_http_method, token) + set_api_version(net_http_method, api_version) if api_version begin - client(base_uri).start do |http| + client(base_uri, read_timeout: read_timeout, open_timeout: open_timeout).start do |http| begin response = http.request(net_http_method) + set_rate_limit_details(response) - decoded_body = decode_body(response) - parsed_body = parse_body(decoded_body, response) raise_errors_on_failure(response) + + parsed_body = extract_response_body(response) + + return nil if parsed_body.nil? + + raise_application_errors_on_failure(parsed_body, response.code.to_i) if parsed_body['type'] == 'error.list' + parsed_body + rescue Intercom::RateLimitExceeded => e + if @handle_rate_limit + seconds_to_retry = (@rate_limit_details[:reset_at] - Time.now.utc).ceil + if (retries -= 1) < 0 + raise Intercom::RateLimitExceeded, 'Rate limit retries exceeded. Please examine current API Usage.' + else + sleep seconds_to_retry unless seconds_to_retry < 0 + retry + end + else + raise e + end rescue Timeout::Error - raise Intercom::ServiceUnavailableError.new('Service Unavailable [request timed out]') + raise Intercom::ServiceUnavailableError, 'Service Unavailable [request timed out]' end end rescue Timeout::Error - raise Intercom::ServiceConnectionError.new('Failed to connect to service [connection attempt timed out]') + raise Intercom::ServiceConnectionError, 'Failed to connect to service [connection attempt timed out]' end end - - def decode_body(response) - decode(response['content-encoding'], response.body) - end - def parse_body(decoded_body, response) - parsed_body = nil - unless decoded_body.strip.empty? - begin - parsed_body = JSON.parse(decoded_body) - rescue JSON::ParserError => e - raise_errors_on_failure(response) - end - raise_application_errors_on_failure(parsed_body, response.code.to_i) if parsed_body['type'] == 'error.list' + attr_accessor :path, + :net_http_method, + :rate_limit_details + + private :path, + :net_http_method + + private def client(uri, read_timeout:, open_timeout:) + net = Net::HTTP.new(uri.host, uri.port) + if uri.is_a?(URI::HTTPS) + net.use_ssl = true + net.verify_mode = OpenSSL::SSL::VERIFY_PEER + net.ca_file = File.join(File.dirname(__FILE__), '../data/cacert.pem') end - parsed_body + net.read_timeout = read_timeout + net.open_timeout = open_timeout + net + end + + private def extract_response_body(response) + decoded_body = decode(response['content-encoding'], response.body) + + json_parse_response(decoded_body, response.code) end - def set_rate_limit_details(response) + private def decode(content_encoding, body) + return body if !body || body.empty? || content_encoding != 'gzip' + + Zlib::GzipReader.new(StringIO.new(body)).read.force_encoding('utf-8') + end + + private def json_parse_response(str, code) + return nil if str.to_s.empty? + + JSON.parse(str) + rescue JSON::ParserError + msg = <<~MSG.gsub(/[[:space:]]+/, ' ').strip # #squish from ActiveSuppor + Expected a JSON response body. Instead got '#{str}' + with status code '#{code}'. + MSG + + raise UnexpectedResponseError, msg + end + + private def set_rate_limit_details(response) rate_limit_details = {} rate_limit_details[:limit] = response['X-RateLimit-Limit'].to_i if response['X-RateLimit-Limit'] rate_limit_details[:remaining] = response['X-RateLimit-Remaining'].to_i if response['X-RateLimit-Remaining'] rate_limit_details[:reset_at] = Time.at(response['X-RateLimit-Reset'].to_i) if response['X-RateLimit-Reset'] - Intercom.rate_limit_details = rate_limit_details + @rate_limit_details = rate_limit_details end - def decode(content_encoding, body) - return body if (!body) || body.empty? || content_encoding != 'gzip' - Zlib::GzipReader.new(StringIO.new(body)).read + private def set_common_headers(method, _base_uri) + method.add_field('AcceptEncoding', 'gzip, deflate') end - def raise_errors_on_failure(res) - if res.code.to_i.eql?(404) - raise Intercom::ResourceNotFound.new('Resource Not Found') - elsif res.code.to_i.eql?(401) - raise Intercom::AuthenticationError.new('Unauthorized') - elsif res.code.to_i.eql?(403) - raise Intercom::AuthenticationError.new('Forbidden') - elsif res.code.to_i.eql?(500) - raise Intercom::ServerError.new('Server Error') - elsif res.code.to_i.eql?(502) - raise Intercom::BadGatewayError.new('Bad Gateway Error') - elsif res.code.to_i.eql?(503) - raise Intercom::ServiceUnavailableError.new('Service Unavailable') + private def set_auth_header(method, token) + method.add_field('Authorization', "Bearer #{token}") + end + + private def set_api_version(method, api_version) + method.add_field('Intercom-Version', api_version) + end + + private def raise_errors_on_failure(res) + code = res.code.to_i + + if code == 404 + raise Intercom::ResourceNotFound, 'Resource Not Found' + elsif code == 401 + raise Intercom::AuthenticationError, 'Unauthorized' + elsif code == 403 + raise Intercom::AuthenticationError, 'Forbidden' + elsif code == 429 + raise Intercom::RateLimitExceeded, 'Rate Limit Exceeded' + elsif code == 500 + raise Intercom::ServerError, 'Server Error' + elsif code == 502 + raise Intercom::BadGatewayError, 'Bad Gateway Error' + elsif code == 503 + raise Intercom::ServiceUnavailableError, 'Service Unavailable' + elsif code == 504 + raise Intercom::GatewayTimeoutError, 'Gateway Timeout' end end - def raise_application_errors_on_failure(error_list_details, http_code) + private def raise_application_errors_on_failure(error_list_details, http_code) # Currently, we don't support multiple errors error_details = error_list_details['errors'].first error_code = error_details['type'] || error_details['code'] + error_field = error_details['field'] parsed_http_code = (http_code > 0 ? http_code : nil) error_context = { - :http_code => parsed_http_code, - :application_error_code => error_code + http_code: parsed_http_code, + application_error_code: error_code, + field: error_field, + request_id: error_list_details['request_id'] } case error_code - when 'unauthorized', 'forbidden' + when 'unauthorized', 'forbidden', 'token_not_found' raise Intercom::AuthenticationError.new(error_details['message'], error_context) - when "bad_request", "missing_parameter", 'parameter_invalid' + when 'token_suspended' + raise Intercom::AppSuspendedError.new(error_details['message'], error_context) + when 'token_revoked' + raise Intercom::TokenRevokedError.new(error_details['message'], error_context) + when 'token_unauthorized' + raise Intercom::TokenUnauthorizedError.new(error_details['message'], error_context) + when 'bad_request', 'missing_parameter', 'parameter_invalid', 'parameter_not_found' raise Intercom::BadRequestError.new(error_details['message'], error_context) - when "not_found" + when 'not_restorable' + raise Intercom::BlockedUserError.new(error_details['message'], error_context) + when 'not_found', 'company_not_found' raise Intercom::ResourceNotFound.new(error_details['message'], error_context) - when "rate_limit_exceeded" + when 'admin_not_found' + raise Intercom::AdminNotFound.new(error_details['message'], error_context) + when 'rate_limit_exceeded' raise Intercom::RateLimitExceeded.new(error_details['message'], error_context) + when 'custom_data_limit_reached' + raise Intercom::CDALimitReachedError.new(error_details['message'], error_context) + when 'invalid_document' + raise Intercom::InvalidDocumentError.new(error_details['message'], error_context) when 'service_unavailable' raise Intercom::ServiceUnavailableError.new(error_details['message'], error_context) - when 'conflict' + when 'conflict', 'unique_user_constraint' raise Intercom::MultipleMatchingUsersError.new(error_details['message'], error_context) + when 'resource_conflict' + raise Intercom::ResourceNotUniqueError.new(error_details['message'], error_context) + when 'intercom_version_invalid' + raise Intercom::ApiVersionInvalid.new(error_details['message'], error_context) + when 'scroll_exists' + raise Intercom::ScrollAlreadyExistsError.new(error_details['message'], error_context) + when 'tag_has_dependent_objects' + raise Intercom::TagHasDependentObjects.new(error_details['message'], error_context) when nil, '' raise Intercom::UnexpectedError.new(message_for_unexpected_error_without_type(error_details, parsed_http_code), error_context) else @@ -150,18 +231,12 @@ def raise_application_errors_on_failure(error_list_details, http_code) end end - def message_for_unexpected_error_with_type(error_details, parsed_http_code) + private def message_for_unexpected_error_with_type(error_details, parsed_http_code) "The error of type '#{error_details['type']}' is not recognized. It occurred with the message: #{error_details['message']} and http_code: '#{parsed_http_code}'. Please contact Intercom with these details." end - def message_for_unexpected_error_without_type(error_details, parsed_http_code) + private def message_for_unexpected_error_without_type(error_details, parsed_http_code) "An unexpected error occured. It occurred with the message: #{error_details['message']} and http_code: '#{parsed_http_code}'. Please contact Intercom with these details." end - - def self.append_query_string_to_url(url, params) - return url if params.empty? - query_string = params.map { |k, v| "#{k.to_s}=#{CGI::escape(v.to_s)}" }.join('&') - url + "?#{query_string}" - end end end diff --git a/lib/intercom/scroll_collection_proxy.rb b/lib/intercom/scroll_collection_proxy.rb new file mode 100644 index 00000000..48129b3b --- /dev/null +++ b/lib/intercom/scroll_collection_proxy.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'intercom/utils' +require 'intercom/base_collection_proxy' + +module Intercom + class ScrollCollectionProxy < BaseCollectionProxy + attr_reader :scroll_url, :scroll_param, :records + + def initialize(resource_name, resource_class, details: {}, client:) + @resource_name = resource_name + @resource_class = resource_class + @scroll_url = (details[:url] || "/#{@resource_name}") + '/scroll' + @client = client + end + + def next(scroll_parameter = nil) + @records = [] + response_hash = if !scroll_parameter + # First time so do initial get without scroll_param + @client.get(@scroll_url, '') + else + # Not first call so use get next page + @client.get(@scroll_url, scroll_param: scroll_parameter) + end + raise Intercom::HttpError, 'Http Error - No response entity returned' unless response_hash + + @scroll_param = extract_scroll_param(response_hash) + top_level_entity_key = entity_key_from_response(response_hash) + response_hash[top_level_entity_key] = response_hash[top_level_entity_key].map do |object_json| + Lib::TypedJsonDeserializer.new(object_json, @client).deserialize + end + @records = response_hash[@resource_name] + self + end + + def each(&block) + scroll_param = nil + loop do + response_hash = if !scroll_param + @client.get(@scroll_url, '') + else + @client.get(@scroll_url, scroll_param: scroll_param) + end + raise Intercom::HttpError, 'Http Error - No response entity returned' unless response_hash + + top_level_entity_key = entity_key_from_response(response_hash) + response_hash[top_level_entity_key].each do |object_json| + block.call Lib::TypedJsonDeserializer.new(object_json, @client).deserialize + end + scroll_param = extract_scroll_param(response_hash) + break unless records_present?(response_hash) + end + self + end + + private + + def entity_key_from_response(response_hash) + top_level_type = response_hash['type'] + if resource_name == 'subscriptions' + 'items' + else + Utils.entity_key_from_type(top_level_type) + end + end + + def records_present?(response_hash) + !response_hash[entity_key_from_response(response_hash)].empty? + end + + def extract_scroll_param(response_hash) + return nil unless records_present?(response_hash) + + response_hash['scroll_param'] + end + end +end diff --git a/lib/intercom/search_collection_proxy.rb b/lib/intercom/search_collection_proxy.rb new file mode 100644 index 00000000..01e5643a --- /dev/null +++ b/lib/intercom/search_collection_proxy.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'intercom/utils' +require 'intercom/base_collection_proxy' + +module Intercom + class SearchCollectionProxy < BaseCollectionProxy + def initialize(resource_name, resource_class, details: {}, client:) + super(resource_name, resource_class, details: details, client: client, method: 'post') + end + + private + + def payload + payload = { + query: @params[:query] + } + if sort_field || sort_order + payload[:sort] = {} + payload[:sort][:field] = sort_field if sort_field + payload[:sort][:order] = sort_order if sort_order + end + if per_page || starting_after + payload[:pagination] = {} + payload[:pagination][:per_page] = per_page if per_page + payload[:pagination][:starting_after] = starting_after if starting_after + end + payload + end + + def sort_field + @params[:sort_field] + end + + def sort_order + @params[:sort_order] + end + + def per_page + @params[:per_page] + end + + def starting_after + @params[:starting_after] + end + end +end diff --git a/lib/intercom/section.rb b/lib/intercom/section.rb new file mode 100644 index 00000000..75e387ed --- /dev/null +++ b/lib/intercom/section.rb @@ -0,0 +1,23 @@ +require 'intercom/api_operations/list' +require 'intercom/api_operations/find' +require 'intercom/api_operations/save' +require 'intercom/api_operations/delete' + +module Intercom + module Service + class Section < BaseService + include ApiOperations::List + include ApiOperations::Find + include ApiOperations::Save + include ApiOperations::Delete + + def collection_class + Intercom::Section + end + + def collection_name + 'help_center/sections' + end + end + end +end diff --git a/lib/intercom/segment.rb b/lib/intercom/segment.rb index 33ef5769..589ae474 100644 --- a/lib/intercom/segment.rb +++ b/lib/intercom/segment.rb @@ -1,14 +1,11 @@ -require 'intercom/api_operations/count' -require 'intercom/api_operations/find' -require 'intercom/api_operations/save' require 'intercom/traits/api_resource' module Intercom class Segment - include ApiOperations::List - include ApiOperations::Find - include ApiOperations::Save - include ApiOperations::Count include Traits::ApiResource + + def self.collection_proxy_class + Intercom::BaseCollectionProxy + end end end diff --git a/lib/intercom/service/admin.rb b/lib/intercom/service/admin.rb new file mode 100644 index 00000000..fbb57c49 --- /dev/null +++ b/lib/intercom/service/admin.rb @@ -0,0 +1,22 @@ +require 'intercom/service/base_service' +require 'intercom/api_operations/list' +require 'intercom/api_operations/find' + +module Intercom + module Service + class Admin < BaseService + include ApiOperations::List + include ApiOperations::Find + + def collection_class + Intercom::Admin + end + + def me + response = @client.get("/me", {}) + raise Intercom::HttpError.new('Http Error - No response entity returned') unless response + from_api(response) + end + end + end +end diff --git a/lib/intercom/service/article.rb b/lib/intercom/service/article.rb new file mode 100644 index 00000000..d21bae74 --- /dev/null +++ b/lib/intercom/service/article.rb @@ -0,0 +1,20 @@ +require 'intercom/service/base_service' +require 'intercom/api_operations/find' +require 'intercom/api_operations/list' +require 'intercom/api_operations/delete' +require 'intercom/api_operations/save' + +module Intercom + module Service + class Article < BaseService + include ApiOperations::Find + include ApiOperations::List + include ApiOperations::Delete + include ApiOperations::Save + + def collection_class + Intercom::Article + end + end + end +end diff --git a/lib/intercom/service/base_service.rb b/lib/intercom/service/base_service.rb new file mode 100644 index 00000000..caadafa3 --- /dev/null +++ b/lib/intercom/service/base_service.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'intercom/client_collection_proxy' + +module Intercom + module Service + class BaseService + attr_reader :client + + def initialize(client) + @client = client + end + + def collection_class + raise NotImplementedError + end + + def collection_proxy_class + Intercom::ClientCollectionProxy + end + + def collection_name + @collection_name ||= Utils.resource_class_to_collection_name(collection_class) + end + + def from_api(api_response) + object = collection_class.new + object.client = @client + object.from_response(api_response) + object + end + end + end +end diff --git a/lib/intercom/service/collection.rb b/lib/intercom/service/collection.rb new file mode 100644 index 00000000..cbe7a6f2 --- /dev/null +++ b/lib/intercom/service/collection.rb @@ -0,0 +1,24 @@ +require 'intercom/service/base_service' +require 'intercom/api_operations/list' +require 'intercom/api_operations/find' +require 'intercom/api_operations/delete' +require 'intercom/api_operations/save' + +module Intercom + module Service + class Collection < BaseService + include ApiOperations::List + include ApiOperations::Find + include ApiOperations::Delete + include ApiOperations::Save + + def collection_class + Intercom::Collection + end + + def collection_name + "help_center/collections" + end + end + end +end diff --git a/lib/intercom/service/company.rb b/lib/intercom/service/company.rb new file mode 100644 index 00000000..b627c452 --- /dev/null +++ b/lib/intercom/service/company.rb @@ -0,0 +1,30 @@ +require 'intercom/service/base_service' +require 'intercom/api_operations/delete' +require 'intercom/api_operations/list' +require 'intercom/api_operations/scroll' +require 'intercom/api_operations/find' +require 'intercom/api_operations/find_all' +require 'intercom/api_operations/save' +require 'intercom/api_operations/load' +require 'intercom/extended_api_operations/tags' +require 'intercom/extended_api_operations/segments' + +module Intercom + module Service + class Company < BaseService + include ApiOperations::Delete + include ApiOperations::Find + include ApiOperations::FindAll + include ApiOperations::Load + include ApiOperations::List + include ApiOperations::Scroll + include ApiOperations::Save + include ExtendedApiOperations::Tags + include ExtendedApiOperations::Segments + + def collection_class + Intercom::Company + end + end + end +end diff --git a/lib/intercom/service/contact.rb b/lib/intercom/service/contact.rb new file mode 100644 index 00000000..3d01bf32 --- /dev/null +++ b/lib/intercom/service/contact.rb @@ -0,0 +1,55 @@ +require 'intercom/service/base_service' +require 'intercom/api_operations/load' +require 'intercom/api_operations/list' +require 'intercom/api_operations/find' +require 'intercom/api_operations/save' +require 'intercom/api_operations/delete' +require 'intercom/api_operations/search' + +module Intercom + module Service + class Contact < BaseService + include ApiOperations::Load + include ApiOperations::List + include ApiOperations::Find + include ApiOperations::Save + include ApiOperations::Delete + include ApiOperations::Search + + def collection_class + Intercom::Contact + end + + def collection_proxy_class + Intercom::BaseCollectionProxy + end + + def merge(lead, user) + raise_invalid_merge_error unless lead.role == 'lead' && user.role == 'user' + + response = @client.post('/contacts/merge', from: lead.id, into: user.id) + raise Intercom::HttpError, 'Http Error - No response entity returned' unless response + + user.from_response(response) + end + + def archive(contact) + @client.post("/#{collection_name}/#{contact.id}/archive", {}) + contact + end + + def unarchive(contact) + @client.post("/#{collection_name}/#{contact.id}/unarchive", {}) + contact + end + + def delete_archived_contact(id) + @client.delete("/#{collection_name}/#{id}", {}) + end + + private def raise_invalid_merge_error + raise Intercom::InvalidMergeError, 'Merging can only be performed on a lead into a user' + end + end + end +end diff --git a/lib/intercom/service/conversation.rb b/lib/intercom/service/conversation.rb new file mode 100644 index 00000000..77685db0 --- /dev/null +++ b/lib/intercom/service/conversation.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'intercom/service/base_service' +require 'intercom/api_operations/find_all' +require 'intercom/api_operations/find' +require 'intercom/api_operations/load' +require 'intercom/api_operations/save' +require 'intercom/utils' + +module Intercom + module Service + class Conversation < BaseService + include ApiOperations::FindAll + include ApiOperations::List + include ApiOperations::Find + include ApiOperations::Load + include ApiOperations::Save + include ApiOperations::Search + + def collection_class + Intercom::Conversation + end + + def collection_proxy_class + Intercom::BaseCollectionProxy + end + + def mark_read(id) + @client.put("/conversations/#{id}", read: true) + end + + def reply(reply_data) + id = reply_data.delete(:id) + collection_name = Utils.resource_class_to_collection_name(collection_class) + response = @client.post("/#{collection_name}/#{id}/reply", reply_data.merge(conversation_id: id)) + collection_class.new.from_response(response) + end + + def reply_to_last(reply_data) + collection_name = Utils.resource_class_to_collection_name(collection_class) + response = @client.post("/#{collection_name}/last/reply", reply_data) + collection_class.new.from_response(response) + end + + def open(reply_data) + reply reply_data.merge(message_type: 'open', type: 'admin') + end + + def close(reply_data) + reply reply_data.merge(message_type: 'close', type: 'admin') + end + + def snooze(reply_data) + reply_data.fetch(:snoozed_until) { raise 'snoozed_until field is required' } + reply reply_data.merge(message_type: 'snoozed', type: 'admin') + end + + def assign(reply_data) + assignee_id = reply_data.fetch(:assignee_id) { raise 'assignee_id is required' } + reply reply_data.merge(message_type: 'assignment', assignee_id: assignee_id, type: 'admin') + end + + def run_assignment_rules(id) + collection_name = Utils.resource_class_to_collection_name(collection_class) + response = @client.post("/#{collection_name}/#{id}/run_assignment_rules", {}) + collection_class.new.from_response(response) + end + end + end +end diff --git a/lib/intercom/service/count.rb b/lib/intercom/service/count.rb new file mode 100644 index 00000000..3fc206b1 --- /dev/null +++ b/lib/intercom/service/count.rb @@ -0,0 +1,24 @@ +require 'intercom/service/base_service' +require 'intercom/api_operations/find' + +module Intercom + module Service + class Counts < BaseService + include ApiOperations::Find + + def collection_class + Intercom::Count + end + + def for_app + find({}) + end + + def for_type(type:, count: nil) + params = {type: type} + params[:count] = count if count + find(params) + end + end + end +end diff --git a/lib/intercom/service/data_attribute.rb b/lib/intercom/service/data_attribute.rb new file mode 100644 index 00000000..40f20a61 --- /dev/null +++ b/lib/intercom/service/data_attribute.rb @@ -0,0 +1,20 @@ +require 'intercom/service/base_service' +require 'intercom/api_operations/load' +require 'intercom/api_operations/list' +require 'intercom/api_operations/find_all' +require 'intercom/api_operations/save' + +module Intercom + module Service + class DataAttribute < BaseService + include ApiOperations::Load + include ApiOperations::List + include ApiOperations::FindAll + include ApiOperations::Save + + def collection_class + Intercom::DataAttribute + end + end + end +end diff --git a/lib/intercom/service/event.rb b/lib/intercom/service/event.rb new file mode 100644 index 00000000..33371014 --- /dev/null +++ b/lib/intercom/service/event.rb @@ -0,0 +1,29 @@ +require 'intercom/client_collection_proxy' +require 'intercom/service/base_service' +require 'intercom/api_operations/save' +require 'intercom/api_operations/bulk/submit' + +module Intercom + class EventCollectionProxy < ClientCollectionProxy + + def paging_info_present?(response_hash) + !!(response_hash['pages']) + end + end + + module Service + class Event < BaseService + include ApiOperations::FindAll + include ApiOperations::Save + include ApiOperations::Bulk::Submit + + def collection_class + Intercom::Event + end + + def collection_proxy_class + Intercom::EventCollectionProxy + end + end + end +end diff --git a/lib/intercom/service/export_content.rb b/lib/intercom/service/export_content.rb new file mode 100644 index 00000000..7c1e3897 --- /dev/null +++ b/lib/intercom/service/export_content.rb @@ -0,0 +1,30 @@ +require 'intercom/service/base_service' +require 'intercom/api_operations/find' +require 'intercom/api_operations/list' +require 'intercom/api_operations/save' + +module Intercom + module Service + class ExportContent < BaseService + include ApiOperations::Load + include ApiOperations::List + include ApiOperations::Find + include ApiOperations::Save + + def collection_class + Intercom::ExportContent + end + + def collection_name + 'export/content/data' + end + + def cancel(id) + response = @client.post("/export/cancel/#{id}", {}) + collection_class.new.from_response(response) + end + + end + end +end + diff --git a/lib/intercom/service/job.rb b/lib/intercom/service/job.rb new file mode 100644 index 00000000..c8133699 --- /dev/null +++ b/lib/intercom/service/job.rb @@ -0,0 +1,24 @@ +require 'intercom/service/base_service' +require 'intercom/api_operations/list' +require 'intercom/api_operations/find_all' +require 'intercom/api_operations/find' +require 'intercom/api_operations/load' +require 'intercom/api_operations/save' +require 'intercom/api_operations/bulk/load_error_feed' + +module Intercom + module Service + class Job < BaseService + include ApiOperations::Save + include ApiOperations::List + include ApiOperations::FindAll + include ApiOperations::Find + include ApiOperations::Load + include ApiOperations::Bulk::LoadErrorFeed + + def collection_class + Intercom::Job + end + end + end +end diff --git a/lib/intercom/service/lead.rb b/lib/intercom/service/lead.rb new file mode 100644 index 00000000..7c243e9f --- /dev/null +++ b/lib/intercom/service/lead.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'intercom/service/base_service' +require 'intercom/api_operations/load' +require 'intercom/api_operations/list' +require 'intercom/api_operations/find' +require 'intercom/api_operations/find_all' +require 'intercom/api_operations/save' +require 'intercom/api_operations/scroll' +require 'intercom/api_operations/convert' +require 'intercom/api_operations/archive' +require 'intercom/api_operations/request_hard_delete' +require 'intercom/deprecated_leads_collection_proxy' + +module Intercom + module Service + class Lead < BaseService + include ApiOperations::Load + include ApiOperations::List + include ApiOperations::Find + include ApiOperations::FindAll + include ApiOperations::Save + include ApiOperations::Scroll + include ApiOperations::Convert + include ApiOperations::Archive + include ApiOperations::RequestHardDelete + + def collection_proxy_class + Intercom::DeprecatedLeadsCollectionProxy + end + + def collection_class + Intercom::Lead + end + + def collection_name + 'contacts' + end + end + end +end diff --git a/lib/intercom/service/message.rb b/lib/intercom/service/message.rb new file mode 100644 index 00000000..e893913c --- /dev/null +++ b/lib/intercom/service/message.rb @@ -0,0 +1,14 @@ +require 'intercom/service/base_service' +require 'intercom/api_operations/save' + +module Intercom + module Service + class Message < BaseService + include ApiOperations::Save + + def collection_class + Intercom::Message + end + end + end +end diff --git a/lib/intercom/service/note.rb b/lib/intercom/service/note.rb new file mode 100644 index 00000000..1f220c25 --- /dev/null +++ b/lib/intercom/service/note.rb @@ -0,0 +1,18 @@ +require 'intercom/service/base_service' +require 'intercom/api_operations/find' + +module Intercom + module Service + class Note < BaseService + include ApiOperations::Find + + def collection_class + Intercom::Note + end + + def collection_proxy_class + Intercom::BaseCollectionProxy + end + end + end +end diff --git a/lib/intercom/service/phone_call_redirect.rb b/lib/intercom/service/phone_call_redirect.rb new file mode 100644 index 00000000..425b57d2 --- /dev/null +++ b/lib/intercom/service/phone_call_redirect.rb @@ -0,0 +1,15 @@ +require 'intercom/service/base_service' +require 'intercom/api_operations/save' + +module Intercom + module Service + class PhoneCallRedirect < BaseService + include ApiOperations::Save + + def collection_class + Intercom::PhoneCallRedirect + end + + end + end +end diff --git a/lib/intercom/service/section.rb b/lib/intercom/service/section.rb new file mode 100644 index 00000000..2f03e746 --- /dev/null +++ b/lib/intercom/service/section.rb @@ -0,0 +1,7 @@ +require 'intercom/traits/api_resource' + +module Intercom + class Section + include Traits::ApiResource + end +end diff --git a/lib/intercom/service/segment.rb b/lib/intercom/service/segment.rb new file mode 100644 index 00000000..d51ab4db --- /dev/null +++ b/lib/intercom/service/segment.rb @@ -0,0 +1,16 @@ +require 'intercom/service/base_service' +require 'intercom/api_operations/list' +require 'intercom/api_operations/find' + +module Intercom + module Service + class Segment < BaseService + include ApiOperations::List + include ApiOperations::Find + + def collection_class + Intercom::Segment + end + end + end +end diff --git a/lib/intercom/service/subscription.rb b/lib/intercom/service/subscription.rb new file mode 100644 index 00000000..eb5ea139 --- /dev/null +++ b/lib/intercom/service/subscription.rb @@ -0,0 +1,21 @@ +require 'intercom/api_operations/list' +require 'intercom/api_operations/find_all' +require 'intercom/api_operations/find' +require 'intercom/api_operations/save' +require 'intercom/api_operations/delete' + +module Intercom + module Service + class Subscription < BaseService + include ApiOperations::List + include ApiOperations::Find + include ApiOperations::FindAll + include ApiOperations::Save + include ApiOperations::Delete + + def collection_class + Intercom::Subscription + end + end + end +end diff --git a/lib/intercom/service/subscription_type.rb b/lib/intercom/service/subscription_type.rb new file mode 100644 index 00000000..c1de4879 --- /dev/null +++ b/lib/intercom/service/subscription_type.rb @@ -0,0 +1,18 @@ +require 'intercom/api_operations/list' +require 'intercom/api_operations/find_all' +require 'intercom/api_operations/find' + +module Intercom + module Service + class SubscriptionType < BaseService + include ApiOperations::List + include ApiOperations::Find + include ApiOperations::FindAll + include ApiOperations::Delete + + def collection_class + Intercom::SubscriptionType + end + end + end +end diff --git a/lib/intercom/service/tag.rb b/lib/intercom/service/tag.rb new file mode 100644 index 00000000..42b8f00e --- /dev/null +++ b/lib/intercom/service/tag.rb @@ -0,0 +1,38 @@ +require 'intercom/service/base_service' +require 'intercom/api_operations/save' +require 'intercom/api_operations/list' +require 'intercom/api_operations/find_all' +require 'intercom/api_operations/find' + +module Intercom + module Service + class Tag < BaseService + include ApiOperations::Save + include ApiOperations::List + include ApiOperations::FindAll + include ApiOperations::Delete + include ApiOperations::Find + + def collection_class + Intercom::Tag + end + + def collection_proxy_class + Intercom::BaseCollectionProxy + end + + def tag(params) + params['tag_or_untag'] = 'tag' + create(params) + end + + def untag(params) + params['tag_or_untag'] = 'untag' + params[:companies].each do |company| + company[:untag] = true + end + create(params) + end + end + end +end diff --git a/lib/intercom/service/team.rb b/lib/intercom/service/team.rb new file mode 100644 index 00000000..f5dea932 --- /dev/null +++ b/lib/intercom/service/team.rb @@ -0,0 +1,17 @@ +require 'intercom/service/base_service' +require 'intercom/api_operations/list' +require 'intercom/api_operations/find' + +module Intercom + module Service + class Team < BaseService + include ApiOperations::List + include ApiOperations::Find + + def collection_class + Intercom::Team + end + + end + end +end diff --git a/lib/intercom/service/user.rb b/lib/intercom/service/user.rb new file mode 100644 index 00000000..63198862 --- /dev/null +++ b/lib/intercom/service/user.rb @@ -0,0 +1,34 @@ +require 'intercom/service/base_service' +require 'intercom/api_operations/load' +require 'intercom/api_operations/list' +require 'intercom/api_operations/scroll' +require 'intercom/api_operations/find' +require 'intercom/api_operations/find_all' +require 'intercom/api_operations/save' +require 'intercom/api_operations/archive' +require 'intercom/api_operations/bulk/submit' +require 'intercom/api_operations/request_hard_delete' +require 'intercom/extended_api_operations/tags' +require 'intercom/extended_api_operations/segments' + +module Intercom + module Service + class User < BaseService + include ApiOperations::Load + include ApiOperations::List + include ApiOperations::Scroll + include ApiOperations::Find + include ApiOperations::FindAll + include ApiOperations::Save + include ApiOperations::Archive + include ApiOperations::RequestHardDelete + include ApiOperations::Bulk::Submit + include ExtendedApiOperations::Tags + include ExtendedApiOperations::Segments + + def collection_class + Intercom::User + end + end + end +end diff --git a/lib/intercom/service/visitor.rb b/lib/intercom/service/visitor.rb new file mode 100644 index 00000000..8b8578a2 --- /dev/null +++ b/lib/intercom/service/visitor.rb @@ -0,0 +1,35 @@ +require 'intercom/service/base_service' +require 'intercom/api_operations/load' +require 'intercom/api_operations/find' +require 'intercom/api_operations/save' +require 'intercom/api_operations/delete' + +module Intercom + module Service + class Visitor < BaseService + include ApiOperations::Load + include ApiOperations::Find + include ApiOperations::Save + include ApiOperations::Delete + + def collection_class + Intercom::Visitor + end + + def convert(visitor, contact = false) + req = { visitor: { user_id: visitor.user_id } } + if contact + req[:user] = identity_hash(contact) + req[:type] = 'user' + else + req[:type] = 'lead' + end + Intercom::Contact.new.from_response( + @client.post( + "/visitors/convert", req + ) + ) + end + end + end +end diff --git a/lib/intercom/subscription.rb b/lib/intercom/subscription.rb index 38644ad3..0e4062f8 100644 --- a/lib/intercom/subscription.rb +++ b/lib/intercom/subscription.rb @@ -1,15 +1,7 @@ -require 'intercom/api_operations/list' -require 'intercom/api_operations/find_all' -require 'intercom/api_operations/save' -require 'intercom/api_operations/delete' require 'intercom/traits/api_resource' module Intercom class Subscription - include ApiOperations::List - include ApiOperations::Find - include ApiOperations::Save - include ApiOperations::Delete include Traits::ApiResource end end diff --git a/lib/intercom/subscription_type.rb b/lib/intercom/subscription_type.rb new file mode 100644 index 00000000..d53c2762 --- /dev/null +++ b/lib/intercom/subscription_type.rb @@ -0,0 +1,12 @@ + +require 'intercom/traits/api_resource' + +module Intercom + class SubscriptionType + include Traits::ApiResource + + def self.collection_proxy_class + Intercom::BaseCollectionProxy + end + end +end diff --git a/lib/intercom/tag.rb b/lib/intercom/tag.rb index 81ace737..eaeaee59 100644 --- a/lib/intercom/tag.rb +++ b/lib/intercom/tag.rb @@ -1,23 +1,11 @@ -require 'intercom/api_operations/count' -require 'intercom/api_operations/save' -require 'intercom/api_operations/list' -require 'intercom/api_operations/find' -require 'intercom/api_operations/find_all' require 'intercom/traits/api_resource' -require 'intercom/traits/generic_handler_binding' -require 'intercom/generic_handlers/tag' -require 'intercom/generic_handlers/tag_find_all' module Intercom class Tag - include ApiOperations::Count - include ApiOperations::Save - include ApiOperations::List - include ApiOperations::Find - include ApiOperations::FindAll include Traits::ApiResource - include Traits::GenericHandlerBinding - include GenericHandlers::Tag - include GenericHandlers::TagFindAll + + def self.collection_proxy_class + Intercom::BaseCollectionProxy + end end end diff --git a/lib/intercom/team.rb b/lib/intercom/team.rb new file mode 100644 index 00000000..d476ae8b --- /dev/null +++ b/lib/intercom/team.rb @@ -0,0 +1,7 @@ +require 'intercom/traits/api_resource' + +module Intercom + class Team + include Traits::ApiResource + end +end diff --git a/lib/intercom/traits/api_resource.rb b/lib/intercom/traits/api_resource.rb index 175277ca..8749c196 100644 --- a/lib/intercom/traits/api_resource.rb +++ b/lib/intercom/traits/api_resource.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'intercom/lib/flat_store' require 'intercom/lib/dynamic_accessors' require 'intercom/lib/dynamic_accessors_on_method_missing' @@ -6,16 +8,19 @@ module Intercom module Traits - module ApiResource include DirtyTracking - attr_accessor :id + attr_accessor :id, :client def initialize(attributes = {}) from_hash(attributes) end + def ==(other) + self.class == other.class && to_json == other.to_json + end + def from_response(response) from_hash(response) reset_changed_fields! @@ -24,7 +29,6 @@ def from_response(response) def from_hash(hash) hash.each do |attribute, value| - next if type_field?(attribute) initialize_property(attribute, value) end initialize_missing_flat_store_attributes if respond_to? :flat_store_attributes @@ -32,74 +36,100 @@ def from_hash(hash) end def to_hash - instance_variables_excluding_dirty_tracking_field.inject({}) do |hash, variable| - hash[variable.to_s.delete("@")] = instance_variable_get(variable) - hash + instance_variables_excluding_dirty_tracking_field.each_with_object({}) do |variable, hash| + hash[variable.to_s.delete('@')] = instance_variable_get(variable) + end + end + + def to_json(*args) + instance_variables_excluding_dirty_tracking_field.each_with_object({}) do |variable, hash| + next if variable == :@client + + value = instance_variable_get(variable) + hash[variable.to_s.delete('@')] = value.respond_to?(:to_json) ? value.to_json(*args) : value end end def to_submittable_hash submittable_hash = {} to_hash.each do |attribute, value| - submittable_hash[attribute] = value if submittable_attribute?(attribute, value) + next unless submittable_attribute?(attribute, value) + + submittable_hash[attribute] = value.respond_to?(:to_submittable_hash) ? value.to_submittable_hash : value end submittable_hash end def method_missing(method_sym, *arguments, &block) - Lib::DynamicAccessorsOnMethodMissing.new(method_sym, *arguments, self). - define_accessors_or_call { super } + Lib::DynamicAccessorsOnMethodMissing.new(method_sym, *arguments, self) + .define_accessors_or_call { super } end def flat_store_attribute?(attribute) - (respond_to?(:flat_store_attributes)) && (flat_store_attributes.map(&:to_s).include?(attribute.to_s)) + respond_to?(:flat_store_attributes) && flat_store_attributes.map(&:to_s).include?(attribute.to_s) end private def initialize_property(attribute, value) + return if addressable_list?(attribute, value) + Lib::DynamicAccessors.define_accessors(attribute, value, self) unless accessors_already_defined?(attribute) set_property(attribute, value) end + def addressable_list?(attribute, value) + return false unless typed_property?(attribute, value) + + value['type'] == 'list' && value['url'] + end + def accessors_already_defined?(attribute) respond_to?(attribute) && respond_to?("#{attribute}=") end def set_property(attribute, value) - if typed_value?(value) && !custom_attribute_field?(attribute) && !message_from_field?(attribute, value) && !message_to_field?(attribute, value) - value_to_set = Intercom::Lib::TypedJsonDeserializer.new(value).deserialize + value_to_set = parsed_value_for_attribute(attribute, value) + call_setter_for_attribute(attribute, value_to_set) + end + + def parsed_value_for_attribute(attribute, value) + if typed_property?(attribute, value) + Intercom::Lib::TypedJsonDeserializer.new(value, client).deserialize elsif flat_store_attribute?(attribute) - value_to_set = Intercom::Lib::FlatStore.new(value) + Intercom::Lib::FlatStore.new(value) else - value_to_set = value + value end - call_setter_for_attribute(attribute, value_to_set) end def custom_attribute_field?(attribute) - attribute == 'custom_attributes' + attribute.to_s == 'custom_attributes' end - + def message_from_field?(attribute, value) attribute.to_s == 'from' && value.is_a?(Hash) && value['type'] end - + def message_to_field?(attribute, value) attribute.to_s == 'to' && value.is_a?(Hash) && value['type'] end - def typed_value?(value) - value.is_a? Hash and !!value['type'] + def typed_property?(attribute, value) + typed_value?(value) && + !custom_attribute_field?(attribute) && + !message_from_field?(attribute, value) && + !message_to_field?(attribute, value) && + attribute.to_s != 'metadata' end - def call_setter_for_attribute(attribute, value) - setter_method = "#{attribute.to_s}=" - self.send(setter_method, value) + def typed_value?(value) + value.is_a?(Hash) && !!value['type'] end - def type_field?(attribute) - attribute == 'type' + def call_setter_for_attribute(attribute, value) + setter_method = "#{attribute}=" + send(setter_method, value) end def initialize_missing_flat_store_attributes @@ -117,7 +147,7 @@ def submittable_attribute?(attribute, value) module ClassMethods def from_api(api_response) - object = self.new + object = new object.from_response(api_response) object end @@ -126,7 +156,6 @@ def from_api(api_response) def self.included(base) base.extend(ClassMethods) end - end end end diff --git a/lib/intercom/traits/dirty_tracking.rb b/lib/intercom/traits/dirty_tracking.rb index 0d120094..d155c2d9 100644 --- a/lib/intercom/traits/dirty_tracking.rb +++ b/lib/intercom/traits/dirty_tracking.rb @@ -22,7 +22,14 @@ def mark_field_as_changed!(field_name) def field_changed?(field_name) @changed_fields ||= Set.new - @changed_fields.include?(field_name.to_s) + field = instance_variable_get("@#{field_name}") + if field.respond_to?(:field_changed?) + field.to_hash.any? do |attribute, _| + field.field_changed?(attribute) + end + else + @changed_fields.include?(field_name.to_s) + end end def instance_variables_excluding_dirty_tracking_field diff --git a/lib/intercom/traits/generic_handler_binding.rb b/lib/intercom/traits/generic_handler_binding.rb deleted file mode 100644 index bf4db0ac..00000000 --- a/lib/intercom/traits/generic_handler_binding.rb +++ /dev/null @@ -1,29 +0,0 @@ -module Intercom - module Traits - - # Allows us to have one class level method missing handler across all entities - # which can dispatch to the appropriate function based on the method name - module GenericHandlerBinding - - module ClassMethods - def method_missing(method_sym, *arguments, &block) - if respond_to? :generic_tag and GenericHandlers::Tag.handles_method?(method_sym) - return generic_tag(method_sym, *arguments, block) - elsif respond_to? :generic_tag_find_all and GenericHandlers::TagFindAll.handles_method?(method_sym) - return generic_tag_find_all(method_sym, *arguments, block) - elsif respond_to? :generic_count and GenericHandlers::Count.handles_method?(method_sym) - return generic_count(method_sym, *arguments, block) - end - super - rescue Intercom::NoMethodMissingHandler - super - end - end - - def self.included(base) - base.extend(ClassMethods) - end - - end - end -end diff --git a/lib/intercom/traits/incrementable_attributes.rb b/lib/intercom/traits/incrementable_attributes.rb index b3b671f0..d9452a5e 100644 --- a/lib/intercom/traits/incrementable_attributes.rb +++ b/lib/intercom/traits/incrementable_attributes.rb @@ -7,6 +7,12 @@ def increment(key, value=1) existing_value ||= 0 self.custom_attributes[key] = existing_value + value end + + def decrement(key, value=1) + existing_value = self.custom_attributes[key] || 0 + self.custom_attributes[key] = existing_value - value + end + end end end diff --git a/lib/intercom/user.rb b/lib/intercom/user.rb index 38702860..2cdc9de3 100644 --- a/lib/intercom/user.rb +++ b/lib/intercom/user.rb @@ -1,30 +1,23 @@ -require 'intercom/api_operations/count' -require 'intercom/api_operations/list' -require 'intercom/api_operations/load' -require 'intercom/api_operations/find' -require 'intercom/api_operations/find_all' -require 'intercom/api_operations/save' -require 'intercom/api_operations/delete' -require 'intercom/extended_api_operations/tags' +# frozen_string_literal: true + require 'intercom/traits/incrementable_attributes' require 'intercom/traits/api_resource' module Intercom class User - include ApiOperations::Count - include ApiOperations::List - include ApiOperations::Load - include ApiOperations::Find - include ApiOperations::FindAll - include ApiOperations::Save - include ApiOperations::Delete - include ExtendedApiOperations::Tags include Traits::IncrementableAttributes include Traits::ApiResource - def identity_vars ; [:id, :email, :user_id] ; end - def flat_store_attributes ; [:custom_attributes] ; end - def update_verb ; 'post' ; end + def identity_vars + %i[id email user_id] + end + + def flat_store_attributes + [:custom_attributes] + end + def update_verb + 'post' + end end end diff --git a/lib/intercom/utils.rb b/lib/intercom/utils.rb index da6fe334..6ccba8cc 100644 --- a/lib/intercom/utils.rb +++ b/lib/intercom/utils.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Intercom module Utils class << self @@ -7,6 +9,7 @@ def singularize(str) def pluralize(str) return str.gsub(/y$/, 'ies') if str =~ /y$/ + "#{str}s" end @@ -22,8 +25,18 @@ def constantize(camel_cased_word) constant end + def camelize(snake_cased_word) + snake_cased_word.split(/_/).map(&:capitalize).join + end + def resource_class_to_singular_name(resource_class) - resource_class.to_s.split('::')[-1].downcase + resource_name = resource_class.to_s.split('::')[-1] + resource_name = maybe_underscore_name(resource_name) + resource_name.downcase + end + + def maybe_underscore_name(resource_name) + resource_name.gsub!(/(.)([A-Z])/, '\1_\2') || resource_name end def resource_class_to_collection_name(resource_class) @@ -31,7 +44,7 @@ def resource_class_to_collection_name(resource_class) end def constantize_resource_name(resource_name) - class_name = Utils.singularize(resource_name.capitalize) + class_name = camelize Utils.singularize(resource_name.capitalize) define_lightweight_class(class_name) unless Intercom.const_defined?(class_name, false) namespaced_class_name = "Intercom::#{class_name}" constantize namespaced_class_name @@ -45,7 +58,6 @@ def constantize_singular_resource_name(resource_name) end def define_lightweight_class(class_name) - #File.open('./intercom_ruby_dynamically_defined_classes.log', 'a') {|f| f.puts("Dynamically defining the class Intercom::#{class_name}") } #HACK new_class_definition = Class.new(Object) do include Traits::ApiResource end @@ -53,9 +65,12 @@ def define_lightweight_class(class_name) end def entity_key_from_type(type) + return 'data' if type == 'list' + is_list = type.split('.')[1] == 'list' entity_name = type.split('.')[0] - is_list ? Utils.pluralize(entity_name) : entity_name + return Utils.pluralize(entity_name) if entity_name == 'event' + is_list ? Utils.pluralize(entity_name) : entity_name end end end diff --git a/lib/intercom/version.rb b/lib/intercom/version.rb index df81a68c..7406b336 100644 --- a/lib/intercom/version.rb +++ b/lib/intercom/version.rb @@ -1,3 +1,3 @@ module Intercom #:nodoc: - VERSION = "2.4.0" + VERSION = "4.2.2" end diff --git a/lib/intercom/visitor.rb b/lib/intercom/visitor.rb new file mode 100644 index 00000000..099c2021 --- /dev/null +++ b/lib/intercom/visitor.rb @@ -0,0 +1,12 @@ +require 'intercom/traits/incrementable_attributes' +require 'intercom/traits/api_resource' + +module Intercom + class Visitor + include Traits::IncrementableAttributes + include Traits::ApiResource + + def identity_vars ; [:id, :email, :user_id] ; end + def flat_store_attributes ; [:custom_attributes] ; end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 095452e9..1dc07c62 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,60 +1,333 @@ +# frozen_string_literal: true + require 'intercom' require 'minitest/autorun' -require 'mocha/setup' - -def test_user(email="bob@example.com") - { - "type" =>"user", - "id" =>"aaaaaaaaaaaaaaaaaaaaaaaa", - "user_id" => 'id-from-customers-app', - "email" => email, - "name" => "Joe Schmoe", - "avatar" => {"type"=>"avatar", "image_url"=>"https://graph.facebook.com/1/picture?width=24&height=24"}, - "app_id" => "the-app-id", - "created_at" => 1323422442, - "custom_attributes" => {"a" => "b", "b" => 2}, - "companies" => - {"type"=>"company.list", - "companies"=> - [{"type"=>"company", - "company_id"=>"123", - "id"=>"bbbbbbbbbbbbbbbbbbbbbbbb", - "app_id"=>"the-app-id", - "name"=>"Company 1", - "remote_created_at"=>1390936440, - "created_at"=>1401970114, - "updated_at"=>1401970114, - "last_request_at"=>1401970113, - "monthly_spend"=>0, - "session_count"=>0, - "user_count"=>1, - "tag_ids"=>[], - "custom_attributes"=>{"category"=>"Tech"}}]}, - "session_count" => 123, - "unsubscribed_from_emails" => true, - "last_request_at" =>1401970113, - "created_at" =>1401970114, - "remote_created_at" =>1393613864, - "updated_at" =>1401970114, - "user_agent_data" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11", - "social_profiles" =>{"type"=>"social_profile.list", - "social_profiles" => [ - {"type" => "social_profile", "name" => "twitter", "url" => "http://twitter.com/abc", "username" => "abc", "id" => nil}, - {"type" => "social_profile", "name" => "twitter", "username" => "abc2", "url" => "http://twitter.com/abc2", "id" => nil}, - {"type" => "social_profile", "name" => "facebook", "url" => "http://facebook.com/abc", "username" => "abc", "id" => "1234242"}, - {"type" => "social_profile", "name" => "quora", "url" => "http://facebook.com/abc", "username" => "abc", "id" => "1234242"} - ]}, - "location_data"=> - {"type"=>"location_data", - "city_name"=> 'Dublin', - "continent_code"=> 'EU', - "country_name"=> 'Ireland', - "latitude"=> '90', - "longitude"=> '10', - "postal_code"=> 'IE', - "region_name"=> 'Europe', - "timezone"=> '+1000', - "country_code" => "IRL"} +require 'mocha/minitest' +require 'webmock' +require 'time' +require 'pry' +include WebMock::API + +def test_article + { + "id": "1", + "type": "article", + "workspace_id": "tx2p130c", + "title": "new title", + "description": "test Finished articles are visible when they're added to a Help Center collection", + "body": "

thingbop

", + "author_id": 1, + "state": "draft" + } +end + +def test_updated_article + { + "id": "1", + "type": "article", + "workspace_id": "tx2p130c", + "title": "new updated title", + "description": "test Finished articles are visible when they're added to a Help Center collection", + "body": "

thingbop

", + "author_id": 1, + "state": "draft" + } +end + +def test_user(email = 'bob@example.com') + { + 'type' => 'user', + 'id' => 'aaaaaaaaaaaaaaaaaaaaaaaa', + 'user_id' => 'id-from-customers-app', + 'email' => email, + 'name' => 'Joe Schmoe', + 'avatar' => { 'type' => 'avatar', 'image_url' => 'https://graph.facebook.com/1/picture?width=24&height=24' }, + 'app_id' => 'the-app-id', + 'custom_attributes' => { 'a' => 'b', 'b' => 2 }, + 'companies' => + { 'type' => 'company.list', + 'companies' => + [{ 'type' => 'company', + 'company_id' => '123', + 'id' => 'bbbbbbbbbbbbbbbbbbbbbbbb', + 'app_id' => 'the-app-id', + 'name' => 'Company 1', + 'remote_created_at' => 1_390_936_440, + 'created_at' => 1_401_970_114, + 'updated_at' => 1_401_970_114, + 'last_request_at' => 1_401_970_113, + 'monthly_spend' => 0, + 'session_count' => 0, + 'user_count' => 1, + 'tag_ids' => [], + 'custom_attributes' => { 'category' => 'Tech' } }] }, + 'session_count' => 123, + 'unsubscribed_from_emails' => true, + 'last_request_at' => 1_401_970_113, + 'created_at' => 1_401_970_114, + 'remote_created_at' => 1_393_613_864, + 'updated_at' => 1_401_970_114, + 'user_agent_data' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11', + 'social_profiles' => { 'type' => 'social_profile.list', + 'social_profiles' => [ + { 'type' => 'social_profile', 'name' => 'twitter', 'url' => 'http://twitter.com/abc', 'username' => 'abc', 'id' => nil }, + { 'type' => 'social_profile', 'name' => 'twitter', 'username' => 'abc2', 'url' => 'http://twitter.com/abc2', 'id' => nil }, + { 'type' => 'social_profile', 'name' => 'facebook', 'url' => 'http://facebook.com/abc', 'username' => 'abc', 'id' => '1234242' }, + { 'type' => 'social_profile', 'name' => 'quora', 'url' => 'http://facebook.com/abc', 'username' => 'abc', 'id' => '1234242' } + ] }, + 'location_data' => + { 'type' => 'location_data', + 'city_name' => 'Dublin', + 'continent_code' => 'EU', + 'country_name' => 'Ireland', + 'latitude' => '90', + 'longitude' => '10', + 'postal_code' => 'IE', + 'region_name' => 'Europe', + 'timezone' => '+1000', + 'country_code' => 'IRL' } + } +end + +def test_contact(email = 'bob@example.com', role = 'user') + { + 'type' => 'contact', + 'id' => 'aaaaaaaaaaaaaaaaaaaaaaaa', + 'external_id' => 'id-from-customers-app', + 'role' => role, + 'email' => email, + 'name' => 'Joe Schmoe', + 'avatar' => { 'type' => 'avatar', 'image_url' => 'https://graph.facebook.com/1/picture?width=24&height=24' }, + 'workspace_id' => 'the-workspace-id', + 'custom_attributes' => { 'a' => 'b', 'b' => 2 }, + 'companies' => { + 'type' => 'list', + 'data' => + [{ 'type' => 'company', + 'company_id' => '123', + 'id' => 'bbbbbbbbbbbbbbbbbbbbbbbb', + 'workspace_id' => 'the-workspace-id', + 'name' => 'Company 1', + 'remote_created_at' => 1_390_936_440, + 'created_at' => 1_401_970_114, + 'updated_at' => 1_401_970_114, + 'last_request_at' => 1_401_970_113, + 'monthly_spend' => 0, + 'session_count' => 0, + 'contact_count' => 1, + 'tag_ids' => [], + 'custom_attributes' => { 'category' => 'Tech' } }], + 'url' => '/contacts/12345/companies' + }, + 'tags' => { + 'type' => 'list', + 'data' => [], + 'url' => '/contacts/12345/tags' + }, + 'notes' => { + 'type' => 'list', + 'data' => [], + 'url' => '/contacts/12345/notes' + }, + 'session_count' => 123, + 'unsubscribed_from_emails' => true, + 'last_request_at' => 1_401_970_113, + 'created_at' => 1_401_970_114, + 'remote_created_at' => 1_393_613_864, + 'updated_at' => 1_401_970_114, + 'user_agent_data' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11', + 'social_profiles' => { 'type' => 'social_profile.list', + 'social_profiles' => [ + { 'type' => 'social_profile', 'name' => 'twitter', 'url' => 'http://twitter.com/abc', 'username' => 'abc', 'id' => nil }, + { 'type' => 'social_profile', 'name' => 'twitter', 'username' => 'abc2', 'url' => 'http://twitter.com/abc2', 'id' => nil }, + { 'type' => 'social_profile', 'name' => 'facebook', 'url' => 'http://facebook.com/abc', 'username' => 'abc', 'id' => '1234242' }, + { 'type' => 'social_profile', 'name' => 'quora', 'url' => 'http://facebook.com/abc', 'username' => 'abc', 'id' => '1234242' } + ] }, + 'location_data' => + { 'type' => 'location_data', + 'city_name' => 'Dublin', + 'continent_code' => 'EU', + 'country_name' => 'Ireland', + 'latitude' => '90', + 'longitude' => '10', + 'postal_code' => 'IE', + 'region_name' => 'Europe', + 'timezone' => '+1000', + 'country_code' => 'IRL' } + } +end + +def test_collection + { + 'id' => '1', + 'workspace_id' => 'tx2p130c', + 'name' => 'Collection 1', + 'url' => 'http://www.intercom.test/help/', + 'order' => 1, + 'type' => 'collection', + 'description' => 'Collection desc', + 'icon' => 'book-bookmark' + } +end + +def test_collection_list + { + 'type' => 'list', + 'total_count' => 1, + 'pages' => { + 'page' => 1, + 'per_page' => 20, + 'total_pages' => 1 + }, + 'data' => [{ + 'id' => '1', + 'workspace_id' => 'tx2p130c', + 'name' => 'Collection 1', + 'url' => 'http://www.intercom.test/help/', + 'order' => 1, + 'type' => 'collection', + 'description' => 'Collection desc', + 'icon' => 'book-bookmark' + }] + } +end + +def test_visitor + { + 'type' => 'visitor', + 'id' => '123', + 'user_id' => '12334', + 'anonymous' => true, + 'email' => nil, + 'phone' => nil, + 'name' => nil, + 'pseudonym' => nil, + 'app_id' => 'abcd1234', + 'companies' => { 'type' => 'company.list', 'companies' => [] }, + 'location_data' => {}, + 'last_request_at' => nil, + 'created_at' => 1_401_970_114, + 'remote_created_at' => 1_401_970_114, + 'signed_up_at' => 1_401_970_114, + 'updated_at' => 1_401_970_114, + 'session_count' => 0, + 'social_profiles' => { 'type' => 'social_profile.list', 'social_profiles' => [] }, + 'owner_id' => nil, + 'unsubscribed_from_emails' => false, + 'marked_email_as_spam' => false, + 'has_hard_bounced' => false, + 'tags' => { 'type' => 'tag.list', 'tags' => [] }, + 'segments' => { 'type' => 'segment.list', 'segments' => [] }, + 'custom_attributes' => {}, + 'referrer' => nil, + 'utm_campaign' => nil, + 'utm_content' => nil, + 'utm_medium' => nil, + 'utm_source' => nil, + 'utm_term' => nil, + 'do_not_track' => nil + } +end + +def test_admin_list + { + 'type' => 'admin.list', + 'admins' => [ + { + 'type' => 'admin', + 'id' => '1234', + 'name' => 'Hoban Washburne', + 'email' => 'wash@serenity.io' + } + ] + } +end + +def test_admin + { + 'type' => 'admin', + 'id' => '1234', + 'name' => 'Hoban Washburne', + 'email' => 'wash@serenity.io' + } +end + +def test_team_list + { + 'type' => 'team.list', + 'teams' => [ + { + 'type' => 'team', + 'id' => '2744328', + 'name' => 'the_a_team', + 'admin_ids' => [646_303, 814_860] + }, + { + 'type' => 'team', + 'id' => '814865', + 'name' => 'BA_App', + 'admin_ids' => [492_881, 1_195_856] + } + ] + } +end + +def test_team + { + 'type' => 'team', + 'id' => '2744328', + 'name' => 'the_a_team', + 'admin_ids' => [646_303, 814_860] + } +end + +def test_company(name = 'Blue Sun') + { + 'type' => 'company', + 'id' => '531ee472cce572a6ec000006', + 'name' => name, + 'plan' => { + 'type' => 'plan', + 'id' => '1', + 'name' => 'Paid' + }, + 'company_id' => '6', + 'remote_created_at' => 1_394_531_169, + 'created_at' => 1_394_533_506, + 'updated_at' => 1_396_874_658, + 'last_request_at' => 1_396_874_658, + 'monthly_spend' => 49, + 'session_count' => 26, + 'user_count' => 10, + 'custom_attributes' => { + 'paid_subscriber' => true, + 'team_mates' => 0 + } + } +end + +def test_company_dates(name = 'Blue Sun', created_at = 1_401_970_114, last_request_at = 1_401_970_113) + { + 'type' => 'company', + 'id' => '531ee472cce572a6ec000006', + 'name' => name, + 'plan' => { + 'type' => 'plan', + 'id' => '1', + 'name' => 'Paid' + }, + 'company_id' => '6', + 'remote_created_at' => 1_394_531_169, + 'created_at' => created_at, + 'updated_at' => 1_396_874_658, + 'last_request_at' => last_request_at, + 'monthly_spend' => 49, + 'session_count' => 26, + 'user_count' => 10, + 'custom_attributes' => { + 'paid_subscriber' => true, + 'team_mates' => 0 + } } end @@ -64,229 +337,687 @@ def test_messages def test_message { - "created_at" => 1329837506, - "updated_at" => 1329664706, - "read" => true, - "created_by_user" => true, - "thread_id" => 5591, - "messages" => [ - { - "created_at" => 1329837506, - "html" => "

Hey Intercom, What is up?

\n", - "from" => { - "email" => "bob@example.com", - "name" => "Bob", - "user_id" => "123", - "is_admin" => false - } + 'created_at' => 1_329_837_506, + 'updated_at' => 1_329_664_706, + 'read' => true, + 'created_by_user' => true, + 'thread_id' => 5591, + 'messages' => [ + { + 'created_at' => 1_329_837_506, + 'html' => "

Hey Intercom, What is up?

\n", + 'from' => { + 'email' => 'bob@example.com', + 'name' => 'Bob', + 'user_id' => '123', + 'is_admin' => false + } + }, + { + 'created_at' => 1_329_664_706, + 'rendered_body' => "

Not much, you?

\n", + 'from' => { + 'name' => 'Super Duper Admin', + 'avatar' => { + 'square_25' => 'https://static.intercomcdn.com/avatars/13347/square_25/Ruairi_Profile.png?1375368166', + 'square_50' => 'https://static.intercomcdn.com/avatars/13347/square_50/Ruairi_Profile.png?1375368166', + 'square_128' => 'https://static.intercomcdn.com/avatars/13347/square_128/Ruairi_Profile.png?1375368166' + }, + 'is_admin' => true + } + }, + { + 'created_at' => 1_329_664_806, + 'rendered_body' => "

Not much either :(

\n", + 'from' => { + 'email' => 'bob@example.com', + 'name' => 'Bob', + 'user_id' => '123', + 'is_admin' => false + } + } + ] + } +end + +def page_of_users(include_next_link = false) + { + 'type' => 'user.list', + 'pages' => + { + 'type' => 'pages', + 'next' => (include_next_link ? 'https://api.intercom.io/users?per_page=50&page=2' : nil), + 'page' => 1, + 'per_page' => 50, + 'total_pages' => 7 + }, + 'users' => [test_user('user1@example.com'), test_user('user2@example.com'), test_user('user3@example.com')], + 'total_count' => 314 + } +end + +def page_of_contacts(include_starting_after = false) + { 'type' => 'list', + 'data' => [ + { + 'type' => 'contact', + 'id' => '123', + 'workspace_id' => 'abc', + 'external_id' => '12345', + 'role' => 'lead', + 'email' => 'test1@example.com', + 'name' => 'Test', + 'unsubscribed_from_emails' => false, + 'created_at' => 1_573_035_771, + 'updated_at' => 1_573_035_771, + 'custom_attributes' => {}, + 'tags' => { + 'type' => 'list', + 'data' => [], + 'url' => '/contacts/12345/tags' + }, + 'notes' => { + 'type' => 'list', + 'data' => [], + 'url' => '/contacts/12345/notes' + }, + 'companies' => { + 'type' => 'list', + 'data' => [], + 'url' => '/contacts/12345/companies' + } + }, + { + 'type' => 'contact', + 'id' => '321', + 'workspace_id' => 'abc', + 'external_id' => '54321', + 'role' => 'user', + 'email' => 'test2@example.com', + 'name' => 'Test', + 'unsubscribed_from_emails' => false, + 'created_at' => 1_573_035_771, + 'updated_at' => 1_573_035_771, + 'custom_attributes' => {}, + 'tags' => { + 'type' => 'list', + 'data' => [], + 'url' => '/contacts/54321/tags' + }, + 'notes' => { + 'type' => 'list', + 'data' => [], + 'url' => '/contacts/54321/notes' + }, + 'companies' => { + 'type' => 'list', + 'data' => [], + 'url' => '/contacts/54321/companies' + } + }, + { + 'type' => 'contact', + 'id' => '111', + 'workspace_id' => 'abc', + 'external_id' => '111', + 'role' => 'lead', + 'email' => 'test3@example.com', + 'name' => 'Test', + 'unsubscribed_from_emails' => false, + 'created_at' => 1_573_035_771, + 'updated_at' => 1_573_035_771, + 'custom_attributes' => {}, + 'tags' => { + 'type' => 'list', + 'data' => [], + 'url' => '/contacts/111/tags' }, + 'notes' => { + 'type' => 'list', + 'data' => [], + 'url' => '/contacts/111/notes' + }, + 'companies' => { + 'type' => 'list', + 'data' => [], + 'url' => '/contacts/111/companies' + } + } + ], + 'total_count' => 3, + 'pages' => { + 'type' => 'pages', + 'next' => (include_starting_after ? { 'page' => 2, 'starting_after' => 'EnCrYpTeDsTrInG' } : nil), + 'page' => 1, + 'per_page' => 50, + 'total_pages' => 1 + } } +end + +def page_of_companies(include_next_link = false) + { + 'type' => 'company.list', + 'pages' => { - "created_at" => 1329664706, - "rendered_body" => "

Not much, you?

\n", - "from" => { - "name" => "Super Duper Admin", - "avatar" => { - "square_25" => "https://static.intercomcdn.com/avatars/13347/square_25/Ruairi_Profile.png?1375368166", - "square_50" => "https://static.intercomcdn.com/avatars/13347/square_50/Ruairi_Profile.png?1375368166", - "square_128" => "https://static.intercomcdn.com/avatars/13347/square_128/Ruairi_Profile.png?1375368166" - }, - "is_admin" => true - } + 'type' => 'pages', + 'next' => (include_next_link ? 'https://api.intercom.io/companies?per_page=50&page=2' : nil), + 'page' => 1, + 'per_page' => 50, + 'total_pages' => 7 }, + 'companies' => [test_company('company1'), test_company('company2'), test_company('company3')], + 'total_count' => 314 + } +end + +def companies_scroll(include_companies = false) + { + 'type' => 'company.list', + 'scroll_param' => 'da6bbbac-25f6-4f07-866b-b911082d7', + 'companies' => (include_companies ? [test_company('company1'), test_company('company2'), test_company('company3')] : []) + } +end + +def companies_pagination(include_next_link:, per_page:, page:, total_pages:, total_count:, company_list:) + { + 'type' => 'company.list', + 'pages' => { - "created_at" => 1329664806, - "rendered_body" => "

Not much either :(

\n", - "from" => { - "email" => "bob@example.com", - "name" => "Bob", - "user_id" => "123", - "is_admin" => false - } - } + 'type' => 'pages', + 'next' => (include_next_link ? 'https://api.intercom.io/companies?per_page=' \ + + per_page.to_s + '&page=' + (page + 1).to_s : nil), + 'page' => page, + 'per_page' => per_page, + 'total_pages' => total_pages + }, + 'companies' => company_list, + 'total_count' => total_count + } +end + +def test_conversation + { + 'type' => 'conversation', + 'id' => '147', + 'created_at' => 1_400_850_973, + 'updated_at' => 1_400_857_494, + 'conversation_message' => { + 'type' => 'conversation_message', + 'subject' => '', + 'body' => "

Hi Alice,

\n\n

We noticed you using our Product, do you have any questions?

\n

- Jane

", + 'author' => { + 'type' => 'admin', + 'id' => '25' + }, + 'attachments' => [ + { + 'name' => 'signature', + 'url' => 'http =>//someurl.com/signature.jpg' + } ] + }, + 'user' => { + 'type' => 'user', + 'id' => '536e564f316c83104c000020' + }, + 'assignee' => { + 'type' => 'admin', + 'id' => '25' + }, + 'open' => true, + 'read' => true, + 'conversation_parts' => { + 'type' => 'conversation_part.list', + 'conversation_parts' => [ + ] + } } end -def page_of_users(include_next_link= false) +def test_conversation_list { - "type"=>"user.list", - "pages"=> + 'type' => 'conversation.list', + 'pages' => { + 'type' => 'pages', + 'page' => 1, + 'per_page' => 20, + 'total_pages' => 1 + }, + 'conversations' => [ { - "type"=>"pages", - "next"=> (include_next_link ? "https://api.intercom.io/users?per_page=50&page=2" : nil), - "page"=>1, - "per_page"=>50, - "total_pages"=>7 + 'type' => 'conversation', + 'id' => '147', + 'created_at' => 1_400_850_973, + 'updated_at' => 1_400_857_494, + 'conversation_message' => { + 'type' => 'conversation_message', + 'subject' => '', + 'body' => "

Hi Alice,

\n\n

We noticed you using our Product, do you have any questions?

\n

- Jane

", + 'author' => { + 'type' => 'admin', + 'id' => '25' + }, + 'attachments' => [ + { + 'name' => 'signature', + 'url' => 'http =>//someurl.com/signature.jpg' + } + ] + }, + 'user' => { + 'type' => 'user', + 'id' => '536e564f316c83104c000020' + }, + 'assignee' => { + 'type' => 'admin', + 'id' => '25' + }, + 'open' => true, + 'read' => true, + 'conversation_parts' => { + 'type' => 'conversation_part.list', + 'conversation_parts' => [ + ] + } + } + ] + } +end + +def segment + { + 'type' => 'segment', + 'id' => '5310d8e7598c9a0b24000002', + 'name' => 'Active', + 'created_at' => 1_393_613_031, + 'updated_at' => 1_393_613_031 + } +end + +def segment_list + { + 'type' => 'segment.list', + 'segments' => [ + { + 'created_at' => 1_393_613_031, + 'id' => '5310d8e7598c9a0b24000002', + 'name' => 'Active', + 'type' => 'segment', + 'updated_at' => 1_393_613_031 + }, + { + 'created_at' => 1_393_613_030, + 'id' => '5310d8e6598c9a0b24000001', + 'name' => 'New', + 'type' => 'segment', + 'updated_at' => 1_393_613_030 }, - "users"=> [test_user("user1@example.com"), test_user("user2@example.com"), test_user("user3@example.com")], - "total_count"=>314 + { + 'created_at' => 1_393_613_031, + 'id' => '5310d8e7598c9a0b24000003', + 'name' => 'Slipping Away', + 'type' => 'segment', + 'updated_at' => 1_393_613_031 + } + ] } end def test_tag { - "id" => "4f73428b5e4dfc000b000112", - "name" => "Test Tag", - "segment" => false, - "tagged_user_count" => 2 + 'id' => '4f73428b5e4dfc000b000112', + 'name' => 'Test Tag', + 'segment' => false, + 'tagged_company_count' => 2 } end def test_user_notification { - "type" => "notification_event", - "id" => "notif_123456-56465-546546", - "topic" => "user.created", - "app_id" => "aaaaaa", - "data" => - { - "type" => "notification_event_data", - "item" => - { - "type" => "user", - "id" => "aaaaaaaaaaaaaaaaaaaaaaaa", - "user_id" => nil, - "email" => "joe@example.com", - "name" => "Joe Schmoe", - "avatar" => - { - "type" => "avatar", - "image_url" => nil - }, - "app_id" => "aaaaa", - "companies" => - { - "type" => "company.list", - "companies" => [ ] - }, - "location_data" => + 'type' => 'notification_event', + 'id' => 'notif_123456-56465-546546', + 'topic' => 'user.created', + 'app_id' => 'aaaaaa', + 'data' => + { + 'type' => 'notification_event_data', + 'item' => + { + 'type' => 'user', + 'id' => 'aaaaaaaaaaaaaaaaaaaaaaaa', + 'user_id' => nil, + 'email' => 'joe@example.com', + 'name' => 'Joe Schmoe', + 'avatar' => + { + 'type' => 'avatar', + 'image_url' => nil + }, + 'app_id' => 'aaaaa', + 'companies' => + { + 'type' => 'company.list', + 'companies' => [] + }, + 'location_data' => + { + }, + 'last_request_at' => nil, + 'created_at' => '1401970114', + 'remote_created_at' => nil, + 'updated_at' => '1401970114', + 'session_count' => 0, + 'social_profiles' => + { + 'type' => 'social_profile.list', + 'social_profiles' => [] + }, + 'unsubscribed_from_emails' => false, + 'user_agent_data' => nil, + 'tags' => + { + 'type' => 'tag.list', + 'tags' => [] + }, + 'segments' => + { + 'type' => 'segment.list', + 'segments' => [] + }, + 'custom_attributes' => + { + } + } + }, + 'delivery_status' => nil, + 'delivery_attempts' => 1, + 'delivered_at' => 0, + 'first_sent_at' => 1_410_188_629, + 'created_at' => 1_410_188_628, + 'links' => {}, + 'self' => nil + } +end + +def test_conversation_notification + { + 'type' => 'notification_event', + 'id' => 'notif_123456-56465-546546', + 'topic' => 'conversation.user.created', + 'app_id' => 'aaaaa', + 'data' => + { + 'type' => 'notification_event_data', + 'item' => + { + 'type' => 'conversation', + 'id' => '123456789', + 'created_at' => '1410335293', + 'updated_at' => '1410335293', + 'user' => + { + 'type' => 'user', + 'id' => '540f1de7112d3d1d51001637', + 'name' => 'Kill Bill', + 'email' => 'bill@bill.bill' + }, + 'assignee' => + { + 'type' => 'nobody_admin', + 'id' => nil + }, + 'conversation_message' => + { + 'type' => 'conversation_message', + 'id' => '321546', + 'subject' => '', + 'body' => '

An important message

', + 'author' => + { + 'type' => 'user', + 'id' => 'aaaaaaaaaaaaaaaaaaaaaa', + 'name' => 'Kill Bill', + 'email' => 'bill@bill.bill' + }, + 'attachments' => [] + }, + 'conversation_parts' => + { + 'type' => 'conversation_part.list', + 'conversation_parts' => [] + }, + 'open' => nil, + 'read' => true, + 'links' => + { + 'conversation_web' => + 'https://app.intercom.io/a/apps/aaaaaa/inbox/all/conversations/123456789' + } + } + }, + 'delivery_status' => nil, + 'delivery_attempts' => 1, + 'delivered_at' => 0, + 'first_sent_at' => 1_410_335_293, + 'created_at' => 1_410_335_293, + 'links' => {}, + 'self' => nil + } +end + +def test_subscription + { 'request' => + { 'type' => 'notification_subscription', + 'id' => 'nsub_123456789', + 'created_at' => 1_410_368_642, + 'updated_at' => 1_410_368_642, + 'service_type' => 'web', + 'app_id' => '3qmk5gyg', + 'url' => + 'http://example.com', + 'self' => + 'https://api.intercom.io/subscriptions/nsub_123456789', + 'topics' => ['user.created', 'conversation.user.replied', 'conversation.admin.replied'], + 'active' => true, + 'metadata' => {}, + 'hub_secret' => nil, + 'mode' => 'point', + 'links' => + { 'sent' => + 'https://api.intercom.io/subscriptions/nsub_123456789/sent', + 'retry' => + 'https://api.intercom.io/subscriptions/nsub_123456789/retry', + 'errors' => + 'https://api.intercom.io/subscriptions/nsub_123456789/errors' }, + 'notes' => [] } } +end + +def test_app_count + { + 'type' => 'count.hash', + 'company' => { + 'count' => 8 + }, + 'segment' => { + 'count' => 47 + }, + 'tag' => { + 'count' => 341 + }, + 'user' => { + 'count' => 12_239 + } + } +end + +def test_section + { + 'id' => '18', + 'workspace_id' => 'tx2p130c', + 'name' => 'Section 1', + 'url' => 'http://www.intercom.test/help/', + 'order' => 0, + 'created_at' => 1_589_801_953, + 'updated_at' => 1_589_801_953, + 'type' => 'section', + 'parent_id' => 1 + } +end + +def test_section_list + { + 'type' => 'list', + 'total_count' => 1, + 'pages' => { + 'page' => 1, + 'per_page' => 20, + 'total_pages' => 1 + }, + 'data' => [{ + 'id' => '18', + 'workspace_id' => 'tx2p130c', + 'name' => 'Section 1', + 'url' => 'http://www.intercom.test/help/', + 'order' => 0, + 'created_at' => 1_589_801_953, + 'updated_at' => 1_589_801_953, + 'type' => 'section', + 'parent_id' => 1 + }] + } +end + +def test_segment_count + { + 'type' => 'count', + 'user' => { + 'segment' => [ { + 'Active' => 1 }, - "last_request_at" => nil, - "created_at" => "1401970114", - "remote_created_at" => nil, - "updated_at" => "1401970114", - "session_count" => 0, - "social_profiles" => { - "type" => "social_profile.list", - "social_profiles" => [ ] + 'New' => 0 }, - "unsubscribed_from_emails" => false, - "user_agent_data" => nil, - "tags" => { - "type" => "tag.list", - "tags" => [ ] + 'VIP' => 0 }, - "segments" => { - "type" => "segment.list", - "segments" => [ ] + 'Slipping Away' => 0 }, - "custom_attributes" => { + 'segment 1' => 1 } + ] + } + } +end + +def test_conversation_count + { + 'type' => 'count', + 'conversation' => { + 'assigned' => 1, + 'closed' => 15, + 'open' => 1, + 'unassigned' => 0 + } + } +end + +def test_event + { + 'type' => 'event', + 'event_name' => 'invited-friend', + 'created_at' => 1_389_913_941, + 'user_id' => '314159', + 'metadata' => { + 'type' => 'user', + 'invitee_email' => 'pi@example.org', + 'invite_code' => 'ADDAFRIEND' + } + } +end + +def test_event_list + { + 'type' => 'event.list', + 'events' => [test_event], + 'pages' => { + 'next' => 'https://api.intercom.io/events?type=user&intercom_user_id=55a3b&before=144474756550' + } + } +end + +def tomorrow + (DateTime.now.to_time + 1).to_i +end + +def page_of_events(include_next_link = false) + { + 'type' => 'event.list', + 'events' => [test_event], + 'pages' => + { + 'next' => (include_next_link ? 'https://api.intercom.io/events?type=user&intercom_user_id=55a3b&before=144474756550' : nil) } - }, - "delivery_status" => nil, - "delivery_attempts" => 1, - "delivered_at" => 0, - "first_sent_at" => 1410188629, - "created_at" => 1410188628, - "links" => { }, - "self" => nil } end -def test_conversation_notification +def test_data_attribute { - "type"=>"notification_event", - "id"=>"notif_123456-56465-546546", - "topic"=>"conversation.user.created", - "app_id"=>"aaaaa", - "data"=> - { - "type"=>"notification_event_data", - "item"=> - { - "type"=>"conversation", - "id"=>"123456789", - "created_at"=>"1410335293", - "updated_at"=>"1410335293", - "user"=> - { - "type"=>"user", - "id"=>"540f1de7112d3d1d51001637", - "name"=>"Kill Bill", - "email"=>"bill@bill.bill"}, - "assignee"=> - { - "type"=>"nobody_admin", - "id"=>nil - }, - "conversation_message"=> - { - "type"=>"conversation_message", - "id"=>"321546", - "subject"=>"", - "body"=>"

An important message

", - "author"=> - { - "type"=>"user", - "id"=>"aaaaaaaaaaaaaaaaaaaaaa", - "name"=>"Kill Bill", - "email"=>"bill@bill.bill"}, - "attachments"=>[] - }, - "conversation_parts"=> - { - "type"=>"conversation_part.list", - "conversation_parts"=>[] - }, - "open"=>nil, - "read"=>true, - "links"=> - { - "conversation_web"=> - "https://app.intercom.io/a/apps/aaaaaa/inbox/all/conversations/123456789"} - } - }, - "delivery_status"=>nil, - "delivery_attempts"=>1, - "delivered_at"=>0, - "first_sent_at"=>1410335293, - "created_at"=>1410335293, - "links"=>{}, - "self"=>nil + 'type' => 'data_attribute', + 'model' => 'contact', + 'name' => 'region_name', + 'full_name' => 'location_data.region_name', + 'label' => 'Region', + 'description' => '', + 'data_type' => 'string', + 'api_writable' => false, + 'ui_writable' => true, + 'custom' => false, + 'archived' => false } end -def test_subscription - {"request"=> - {"type"=>"notification_subscription", - "id"=>"nsub_123456789", - "created_at"=>1410368642, - "updated_at"=>1410368642, - "service_type"=>"web", - "app_id"=>"3qmk5gyg", - "url"=> - "http://example.com", - "self"=> - "https://api.intercom.io/subscriptions/nsub_123456789", - "topics"=>["user.created", "conversation.user.replied", "conversation.admin.replied"], - "active"=>true, - "metadata"=>{}, - "hub_secret"=>nil, - "mode"=>"point", - "links"=> - {"sent"=> - "https://api.intercom.io/subscriptions/nsub_123456789/sent", - "retry"=> - "https://api.intercom.io/subscriptions/nsub_123456789/retry", - "errors"=> - "https://api.intercom.io/subscriptions/nsub_123456789/errors"}, - "notes"=>[]}} +def test_data_attribute_list + { + 'type' => 'data_attribute.list', + 'data_attributes' => [ + { + 'type' => 'data_attribute', + 'model' => 'customer', + 'name' => 'paid_subscriber', + 'full_name' => 'custom_attributes.paid_subscriber', + 'label' => 'paid_subscriber', + 'description' => '', + 'data_type' => 'string', + 'options' => %w[ + pick_value_1 + pick_value_2 + ], + 'api_writable' => true, + 'ui_writable' => true, + 'custom' => true, + 'archived' => false, + 'admin_id' => '1', + 'created_at' => 1_392_734_388, + 'updated_at' => 1_392_734_388 + }, + { + 'type' => 'data_attribute', + 'model' => 'customer', + 'name' => 'region_name', + 'full_name' => 'location_data.region_name', + 'label' => 'Region', + 'description' => '', + 'data_type' => 'string', + 'api_writable' => false, + 'ui_writable' => true, + 'custom' => false, + 'archived' => false + } + ] + } end def error_on_modify_frozen @@ -294,11 +1025,9 @@ def error_on_modify_frozen end def capture_exception(&block) - begin - block.call - rescue => e - return e - end + block.call +rescue StandardError => e + e end def unshuffleable_array(array) diff --git a/spec/unit/intercom/admin_spec.rb b/spec/unit/intercom/admin_spec.rb index ea3bc487..4c633b21 100644 --- a/spec/unit/intercom/admin_spec.rb +++ b/spec/unit/intercom/admin_spec.rb @@ -1,9 +1,26 @@ require "spec_helper" describe "Intercom::Admin" do + let(:client) { Intercom::Client.new(token: 'token') } + it "returns a CollectionProxy for all without making any requests" do - Intercom.expects(:execute_request).never - all = Intercom::Admin.all - all.must_be_instance_of(Intercom::CollectionProxy) + client.expects(:execute_request).never + all = client.admins.all + _(all).must_be_instance_of(Intercom::ClientCollectionProxy) + end + + it "gets me (access token method only)" do + client.expects(:get).with("/me", {}).returns(test_admin) + client.admins.me + end + + it 'gets an admin list' do + client.expects(:get).with("/admins", {}).returns(test_admin_list) + client.admins.all.each { |a| } + end + + it "gets an admin" do + client.expects(:get).with("/admins/1234", {}).returns(test_admin) + client.admins.find(:id => "1234") end -end \ No newline at end of file +end diff --git a/spec/unit/intercom/article_spec.rb b/spec/unit/intercom/article_spec.rb new file mode 100644 index 00000000..9fa3a695 --- /dev/null +++ b/spec/unit/intercom/article_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe "Intercom::Article" do + let(:client) { Intercom::Client.new(token: 'token') } + + describe "Getting an Article" do + it "successfully finds an article" do + client.expects(:get).with("/articles/1", {}).returns(test_article) + client.articles.find(id: "1") + end + end + + describe "Creating an Article" do + it "successfully creates and article with information passed individually" do + client.expects(:post).with("/articles", {"title" => "new title", "author_id" => 1, "body" => "

thingbop

", "state" => "draft"}).returns(test_article) + client.articles.create(:title => "new title", :author_id => 1, :body => "

thingbop

", :state => "draft") + end + + it "successfully creates and article with information in json" do + client.expects(:post).with("/articles", {"title" => "new title", "author_id" => 1, "body" => "

thingbop

", "state" => "draft"}).returns(test_article) + client.articles.create({title: "new title", author_id: 1, body: "

thingbop

", state: "draft"}) + end + end + + describe "Updating an article" do + it "successfully updates an article" do + article = Intercom::Article.new(id: 12345) + client.expects(:put).with('/articles/12345', {}) + client.articles.save(article) + end + end + + describe "Deleting an article" do + it "successfully deletes an article" do + article = Intercom::Article.new(id: 12345) + client.expects(:delete).with('/articles/12345', {}) + client.articles.delete(article) + end + end +end \ No newline at end of file diff --git a/spec/unit/intercom/base_collection_proxy_spec.rb b/spec/unit/intercom/base_collection_proxy_spec.rb new file mode 100644 index 00000000..02efed90 --- /dev/null +++ b/spec/unit/intercom/base_collection_proxy_spec.rb @@ -0,0 +1,52 @@ +require "spec_helper" + +describe Intercom::BaseCollectionProxy do + let(:client) { Intercom::Client.new(token: 'token') } + + it "stops iterating if no starting after value" do + client.expects(:get).with("/contacts", {}).returns(page_of_contacts(false)) + emails = [] + client.contacts.all.each { |contact| emails << contact.email } + _(emails).must_equal %w[test1@example.com test2@example.com test3@example.com] + end + + it "keeps iterating if starting after value" do + client.expects(:get).with("/contacts", {}).returns(page_of_contacts(true)) + client.expects(:get).with('/contacts', { starting_after: "EnCrYpTeDsTrInG" }).returns(page_of_contacts(false)) + emails = [] + client.contacts.all.each { |contact| emails << contact.email } + _(emails).must_equal %w[test1@example.com test2@example.com test3@example.com test1@example.com test2@example.com test3@example.com] + end + + it "supports indexed array access" do + client.expects(:get).with("/contacts", {}).returns(page_of_contacts(false)) + _(client.contacts.all[0].email).must_equal "test1@example.com" + end + + it "supports map" do + client.expects(:get).with("/contacts", {}).returns(page_of_contacts(false)) + emails = client.contacts.all.map { |contact| contact.email } + _(emails).must_equal %w[test1@example.com test2@example.com test3@example.com] + end + + it "keeps entire collection iterable after first iteration" do + contacts = client.contacts.all + emails_iter1 = [] + emails_iter2 = [] + expects_pagination = proc do + client.expects(:get).with("/contacts", {}).returns(page_of_contacts(true)) + client.expects(:get).with("/contacts", { starting_after: "EnCrYpTeDsTrInG" }).returns(page_of_contacts(false)) + end + + expects_pagination.call + contacts.each { |contact| emails_iter1 << contact.email } + expects_pagination.call + contacts.each { |contact| emails_iter2 << contact.email } + _(emails_iter1).must_equal emails_iter2 + end + + it "supports query params" do + client.expects(:get).with("/conversations", {:intercom_user_id => 'abcdef0000'}).returns(test_conversation_list) + _(client.conversations.find_all(:intercom_user_id => 'abcdef0000').map(&:id)).must_equal %w[147] + end +end diff --git a/spec/unit/intercom/client_collection_proxy_spec.rb b/spec/unit/intercom/client_collection_proxy_spec.rb new file mode 100644 index 00000000..40431636 --- /dev/null +++ b/spec/unit/intercom/client_collection_proxy_spec.rb @@ -0,0 +1,80 @@ +require "spec_helper" + +describe Intercom::ClientCollectionProxy do + let(:client) { Intercom::Client.new(token: 'token') } + + it "stops iterating if no next link" do + client.expects(:get).with("/companies", {}).returns(page_of_companies(false)) + names = [] + client.companies.all.each { |company| names << company.name } + _(names).must_equal %W(company1 company2 company3) + end + + it "keeps iterating if next link" do + client.expects(:get).with("/companies", {}).returns(page_of_companies(true)) + client.expects(:get).with('https://api.intercom.io/companies?per_page=50&page=2', {}).returns(page_of_companies(false)) + names = [] + client.companies.all.each { |company| names << company.name } + end + + it "supports indexed array access" do + client.expects(:get).with("/companies", {}).returns(page_of_companies(false)) + _(client.companies.all[0].name).must_equal 'company1' + end + + it "supports map" do + client.expects(:get).with("/companies", {}).returns(page_of_companies(false)) + names = client.companies.all.map { |company| company.name } + _(names).must_equal %W(company1 company2 company3) + end + + it "supports querying" do + client.expects(:get).with("/companies", {:tag_name => 'Taggart J'}).returns(page_of_companies(false)) + _(client.companies.find_all(:tag_name => 'Taggart J').map(&:name)).must_equal %W(company1 company2 company3) + end + + it "supports single page pagination" do + companies = [test_company("company1"), test_company("company2"), test_company("company3"), + test_company("company4"), test_company("company5"), test_company("company6"), + test_company("company7"), test_company("company8"), test_company("company9"), + test_company("company10")] + client.expects(:get).with("/companies", {:type=>'companies', :per_page => 10, :page => 1}).returns(companies_pagination(include_next_link: false, per_page: 10, page: 1, total_pages: 1, total_count: 10, company_list: companies)) + result = client.companies.find_all(:type=>'companies', :per_page => 10, :page => 1).map {|company| company.name } + _(result).must_equal %W(company1 company2 company3 company4 company5 company6 company7 company8 company9 company10) + end + + it "supports multi page pagination" do + companies = [test_company("company3"), test_company("company4")] + client.expects(:get).with("/companies", {:type=>'companies', :per_page => 2, :page => 3}).returns(companies_pagination(include_next_link: true, per_page: 2, page: 3, total_pages: 5, total_count: 10, company_list: companies)) + result = client.companies.find_all(:type=>'companies', :per_page => 2, :page => 3).map {|company| company.name } + _(result).must_equal %W(company3 company4) + end + + it "works with page out of range request" do + companies = [] + client.expects(:get).with("/companies", {:type=>'companies', :per_page => 2, :page => 30}).returns(companies_pagination(include_next_link: true, per_page: 2, page: 30, total_pages: 2, total_count: 3, company_list: companies)) + result = client.companies.find_all(:type=>'companies', :per_page => 2, :page => 30).map {|company| company.name } + _(result).must_equal %W() + end + + it "works with asc order" do + test_date=1457337600 + time_increment=1000 + companies = [test_company_dates(name="company1", created_at=test_date), test_company_dates(name="company2", created_at=test_date-time_increment), + test_company_dates(name="company3", created_at=test_date-2*time_increment), test_company_dates(name="company4", created_at=test_date-3*time_increment)] + client.expects(:get).with("/companies", {:type=>'companies', :per_page => 4, :page => 5, :order => "asc", :sort => "created_at"}).returns(companies_pagination(include_next_link: true, per_page: 4, page: 5, total_pages: 6, total_count: 30, company_list: companies)) + result = client.companies.find_all(:type=>'companies', :per_page => 4, :page => 5, :order => "asc", :sort => "created_at").map(&:name) + _(result).must_equal %W(company1 company2 company3 company4) + end + + it "works with desc order" do + test_date=1457337600 + time_increment=1000 + companies = [test_company_dates(name="company4", created_at=3*test_date), test_company_dates(name="company3", created_at=test_date-2*time_increment), + test_company_dates(name="company2", created_at=test_date-time_increment), test_company_dates(name="company1", created_at=test_date)] + client.expects(:get).with("/companies", {:type=>'companies', :per_page => 4, :page => 5, :order => "desc", :sort => "created_at"}).returns(companies_pagination(include_next_link: true, per_page: 4, page: 5, total_pages: 6, total_count: 30, company_list: companies)) + result = client.companies.find_all(:type=>'companies', :per_page => 4, :page => 5, :order => "desc", :sort => "created_at").map {|company| company.name } + _(result).must_equal %W(company4 company3 company2 company1) + end + +end diff --git a/spec/unit/intercom/client_spec.rb b/spec/unit/intercom/client_spec.rb new file mode 100644 index 00000000..660c49d9 --- /dev/null +++ b/spec/unit/intercom/client_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Intercom + describe Client do + let(:token) { 'my_access_token' } + let(:client) do + Client.new( + token: token, + handle_rate_limit: true + ) + end + + it 'should set the base url' do + _(client.base_url).must_equal('https://api.intercom.io') + end + + it 'should have handle_rate_limit set' do + _(client.handle_rate_limit).must_equal(true) + end + + it 'should be able to change the base url' do + prev = client.options(Intercom::Client.set_base_url('https://mymockintercom.io')) + _(client.base_url).must_equal('https://mymockintercom.io') + client.options(prev) + _(client.base_url).must_equal('https://api.intercom.io') + end + + it 'should be able to change the timeouts' do + prev = client.options(Intercom::Client.set_timeouts(open_timeout: 10, read_timeout: 15)) + _(client.timeouts).must_equal(open_timeout: 10, read_timeout: 15) + client.options(prev) + _(client.timeouts).must_equal(open_timeout: 30, read_timeout: 90) + end + + it 'should be able to change the open timeout individually' do + prev = client.options(Intercom::Client.set_timeouts(open_timeout: 50)) + _(client.timeouts).must_equal(open_timeout: 50, read_timeout: 90) + client.options(prev) + _(client.timeouts).must_equal(open_timeout: 30, read_timeout: 90) + end + + it 'should be able to change the read timeout individually' do + prev = client.options(Intercom::Client.set_timeouts(read_timeout: 50)) + _(client.timeouts).must_equal(open_timeout: 30, read_timeout: 50) + client.options(prev) + _(client.timeouts).must_equal(open_timeout: 30, read_timeout: 90) + end + + it 'should raise on nil credentials' do + _(proc { Client.new(token: nil) }).must_raise MisconfiguredClientError + end + + describe 'API version' do + it 'does not set the api version by default' do + assert_nil(client.api_version) + end + + it 'allows api version to be provided' do + _(Client.new(token: token, api_version: '2.0').api_version).must_equal('2.0') + end + + it 'allows api version to be nil' do + # matches default behavior, and will honor version set in the Developer Hub + assert_nil(Client.new(token: token, api_version: nil).api_version) + end + + it 'allows api version to be Unstable' do + _(Client.new(token: token, api_version: 'Unstable').api_version).must_equal('Unstable') + end + + it 'raises on invalid api version' do + _(proc { Client.new(token: token, api_version: '0.2') }).must_raise MisconfiguredClientError + end + + it 'raises on empty api version' do + _(proc { Client.new(token: token, api_version: '') }).must_raise MisconfiguredClientError + end + + it 'assigns works' do + stub_request(:any, 'https://api.intercom.io/contacts?id=123').to_return( + status: [200, 'OK'], + headers: { 'X-RateLimit-Reset' => Time.now.utc + 10 }, + body: { "test": 'testing' }.to_json + ) + + client.get('/contacts', id: '123') + end + end + + describe 'OAuth clients' do + it 'supports "token"' do + client = Client.new(token: 'foo') + _(client.token).must_equal('foo') + end + end + end +end diff --git a/spec/unit/intercom/collection_proxy_spec.rb b/spec/unit/intercom/collection_proxy_spec.rb deleted file mode 100644 index 25a93313..00000000 --- a/spec/unit/intercom/collection_proxy_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -require "spec_helper" - -describe Intercom::CollectionProxy do - - it "stops iterating if no next link" do - Intercom.expects(:get).with("/users", {}).returns(page_of_users(false)) - emails = [] - Intercom::User.all.each { |user| emails << user.email } - emails.must_equal %W(user1@example.com user2@example.com user3@example.com) - end - - it "keeps iterating if next link" do - Intercom.expects(:get).with("/users", {}).returns(page_of_users(true)) - Intercom.expects(:get).with('https://api.intercom.io/users?per_page=50&page=2', {}).returns(page_of_users(false)) - emails = [] - Intercom::User.all.each { |user| emails << user.email } - end - - it "supports indexed array access" do - Intercom.expects(:get).with("/users", {}).returns(page_of_users(false)) - Intercom::User.all[0].email.must_equal 'user1@example.com' - end - - it "supports map" do - Intercom.expects(:get).with("/users", {}).returns(page_of_users(false)) - emails = Intercom::User.all.map { |user| user.email } - emails.must_equal %W(user1@example.com user2@example.com user3@example.com) - end - - it "supports querying" do - Intercom.expects(:get).with("/users", {:tag_name => 'Taggart J'}).returns(page_of_users(false)) - Intercom::User.find_all(:tag_name => 'Taggart J').map(&:email).must_equal %W(user1@example.com user2@example.com user3@example.com) - end -end diff --git a/spec/unit/intercom/collection_spec.rb b/spec/unit/intercom/collection_spec.rb new file mode 100644 index 00000000..87388c9c --- /dev/null +++ b/spec/unit/intercom/collection_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Intercom::Collection do + let(:client) { Intercom::Client.new(token: 'token') } + + it 'creates a collection' do + client.expects(:post).with('/help_center/collections', { 'name' => 'Collection 1', 'description' => 'Collection desc' }).returns(test_collection) + client.collections.create(:name => 'Collection 1', :description => 'Collection desc') + end + + it 'lists collections' do + client.expects(:get).with('/help_center/collections', {}).returns(test_collection_list) + client.collections.all.each { |t| } + end + + it 'finds a collection' do + client.expects(:get).with('/help_center/collections/1', {}).returns(test_collection) + client.collections.find(id: '1') + end + + it 'updates a collection' do + collection = Intercom::Collection.new(id: '12345') + client.expects(:put).with('/help_center/collections/12345', {}) + client.collections.save(collection) + end + + it 'deletes a collection' do + collection = Intercom::Collection.new(id: '12345') + client.expects(:delete).with('/help_center/collections/12345', {}) + client.collections.delete(collection) + end +end \ No newline at end of file diff --git a/spec/unit/intercom/company_spec.rb b/spec/unit/intercom/company_spec.rb index fe357687..ac0f0e11 100644 --- a/spec/unit/intercom/company_spec.rb +++ b/spec/unit/intercom/company_spec.rb @@ -1,23 +1,44 @@ -require 'spec_helper' +require "spec_helper" describe Intercom::Company do - - describe 'when no response raises error' do - it 'on find' do - Intercom.expects(:get).with("/companies", {:company_id => '4'}).returns(nil) - proc {company = Intercom::Company.find(:company_id => '4')}.must_raise Intercom::HttpError + let(:client) { Intercom::Client.new(token: 'token') } + + describe "when no response raises error" do + it "on find" do + client.expects(:get).with("/companies", {:company_id => "4"}).returns(nil) + _(proc { client.companies.find(:company_id => "4")}).must_raise Intercom::HttpError end - - it 'on find_all' do - Intercom.expects(:get).with("/companies", {}).returns(nil) - proc {Intercom::Company.all.each {|company| }}.must_raise Intercom::HttpError + + it "on all" do + client.expects(:get).with("/companies", {}).returns(nil) + _(proc { client.companies.all.each {|company| }}).must_raise Intercom::HttpError end - - it 'on load' do - Intercom.expects(:get).with("/companies", {:company_id => '4'}).returns({'type' =>'user', 'id' =>'aaaaaaaaaaaaaaaaaaaaaaaa', 'company_id' => '4', 'name' => 'MyCo'}) - company = Intercom::Company.find(:company_id => '4') - Intercom.expects(:get).with('/companies/aaaaaaaaaaaaaaaaaaaaaaaa', {}).returns(nil) - proc {company.load}.must_raise Intercom::HttpError + + it "on load" do + client.expects(:get).with("/companies", {:company_id => "4"}).returns({"type" =>"user", "id" =>"aaaaaaaaaaaaaaaaaaaaaaaa", "company_id" => "4", "name" => "MyCo"}) + company = client.companies.find(:company_id => "4") + client.expects(:get).with("/companies/aaaaaaaaaaaaaaaaaaaaaaaa", {}).returns(nil) + _(proc { client.companies.load(company)}).must_raise Intercom::HttpError end end + + it "finds a company" do + client.expects(:get).with("/companies/531ee472cce572a6ec000006", {}).returns(test_company) + company = client.companies.find(id: "531ee472cce572a6ec000006") + _(company.name).must_equal("Blue Sun") + end + + it "returns a collection proxy for listing contacts" do + company = Intercom::Company.new("id" => "1") + proxy = company.contacts + _(proxy.resource_name).must_equal 'contacts' + _(proxy.url).must_equal '/companies/1/contacts' + _(proxy.resource_class).must_equal Intercom::Contact + end + + it "deletes a company" do + company = Intercom::Company.new("id" => "1") + client.expects(:delete).with("/companies/1", {}) + client.companies.delete(company) + end end diff --git a/spec/unit/intercom/contact_spec.rb b/spec/unit/intercom/contact_spec.rb new file mode 100644 index 00000000..6361905d --- /dev/null +++ b/spec/unit/intercom/contact_spec.rb @@ -0,0 +1,426 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Intercom::Contact do + let(:client) { Intercom::Client.new(token: 'token') } + + it 'should be listable' do + proxy = client.contacts.all + _(proxy.resource_name).must_equal 'contacts' + _(proxy.url).must_equal '/contacts' + _(proxy.resource_class).must_equal Intercom::Contact + end + + it 'should throw an ArgumentError when there are no parameters' do + _(proc { client.contacts.create }).must_raise(ArgumentError) + end + + it "to_hash'es itself" do + created_at = Time.now + contact = Intercom::Contact.new(email: 'jim@example.com', contact_id: '12345', created_at: created_at, name: 'Jim Bob') + as_hash = contact.to_hash + _(as_hash['email']).must_equal 'jim@example.com' + _(as_hash['contact_id']).must_equal '12345' + _(as_hash['created_at']).must_equal created_at.to_i + _(as_hash['name']).must_equal 'Jim Bob' + end + + it 'presents created_at and last_impression_at as Date' do + now = Time.now + contact = Intercom::Contact.new(created_at: now, last_impression_at: now) + _(contact.created_at).must_be_kind_of Time + _(contact.created_at.to_s).must_equal now.to_s + _(contact.last_impression_at).must_be_kind_of Time + _(contact.last_impression_at.to_s).must_equal now.to_s + end + + it 'is throws a Intercom::AttributeNotSetError on trying to access an attribute that has not been set' do + contact = Intercom::Contact.new + _(proc { contact.foo_property }).must_raise Intercom::AttributeNotSetError + end + + it 'presents a complete contact record correctly' do + contact = Intercom::Contact.new(test_contact) + _(contact.external_id).must_equal 'id-from-customers-app' + _(contact.email).must_equal 'bob@example.com' + _(contact.name).must_equal 'Joe Schmoe' + _(contact.workspace_id).must_equal 'the-workspace-id' + _(contact.session_count).must_equal 123 + _(contact.created_at.to_i).must_equal 1_401_970_114 + _(contact.remote_created_at.to_i).must_equal 1_393_613_864 + _(contact.updated_at.to_i).must_equal 1_401_970_114 + + _(contact.avatar).must_be_kind_of Intercom::Avatar + _(contact.avatar.image_url).must_equal 'https://graph.facebook.com/1/picture?width=24&height=24' + + _(contact.notes).must_be_kind_of Intercom::BaseCollectionProxy + _(contact.tags).must_be_kind_of Intercom::BaseCollectionProxy + _(contact.companies).must_be_kind_of Intercom::ClientCollectionProxy + + _(contact.custom_attributes).must_be_kind_of Intercom::Lib::FlatStore + _(contact.custom_attributes['a']).must_equal 'b' + _(contact.custom_attributes['b']).must_equal 2 + + _(contact.social_profiles.size).must_equal 4 + twitter_account = contact.social_profiles.first + _(twitter_account).must_be_kind_of Intercom::SocialProfile + _(twitter_account.name).must_equal 'twitter' + _(twitter_account.username).must_equal 'abc' + _(twitter_account.url).must_equal 'http://twitter.com/abc' + + _(contact.location_data).must_be_kind_of Intercom::LocationData + _(contact.location_data.city_name).must_equal 'Dublin' + _(contact.location_data.continent_code).must_equal 'EU' + _(contact.location_data.country_name).must_equal 'Ireland' + _(contact.location_data.latitude).must_equal '90' + _(contact.location_data.longitude).must_equal '10' + _(contact.location_data.country_code).must_equal 'IRL' + + _(contact.unsubscribed_from_emails).must_equal true + _(contact.user_agent_data).must_equal 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11' + end + + it 'allows easy setting of custom data' do + now = Time.now + contact = Intercom::Contact.new + contact.custom_attributes['mad'] = 123 + contact.custom_attributes['other'] = now.to_i + contact.custom_attributes['thing'] = 'yay' + _(contact.to_hash['custom_attributes']).must_equal 'mad' => 123, 'other' => now.to_i, 'thing' => 'yay' + end + + it 'rejects lists in custom_attributes' do + contact = Intercom::Contact.new + + _(proc { contact.custom_attributes['thing'] = [1] }).must_raise(ArgumentError) + + contact = Intercom::Contact.new(test_contact) + _(proc { contact.custom_attributes['thing'] = [1] }).must_raise(ArgumentError) + end + + describe 'incrementing custom_attributes fields' do + before :each do + @now = Time.now + @contact = Intercom::Contact.new('email' => 'jo@example.com', :external_id => 'i-1224242', :custom_attributes => { 'mad' => 123, 'another' => 432, 'other' => @now.to_i, :thing => 'yay' }) + end + + it 'increments up by 1 with no args' do + @contact.increment('mad') + _(@contact.to_hash['custom_attributes']['mad']).must_equal 124 + end + + it 'increments up by given value' do + @contact.increment('mad', 4) + _(@contact.to_hash['custom_attributes']['mad']).must_equal 127 + end + + it 'increments down by given value' do + @contact.increment('mad', -1) + _(@contact.to_hash['custom_attributes']['mad']).must_equal 122 + end + + it 'can increment new custom data fields' do + @contact.increment('new_field', 3) + _(@contact.to_hash['custom_attributes']['new_field']).must_equal 3 + end + + it 'can call increment on the same key twice and increment by 2' do + @contact.increment('mad') + @contact.increment('mad') + _(@contact.to_hash['custom_attributes']['mad']).must_equal 125 + end + end + + describe 'decrementing custom_attributes fields' do + before :each do + @now = Time.now + @contact = Intercom::Contact.new('email' => 'jo@example.com', :external_id => 'i-1224242', :custom_attributes => { 'mad' => 123, 'another' => 432, 'other' => @now.to_i, :thing => 'yay' }) + end + + it 'decrements down by 1 with no args' do + @contact.decrement('mad') + _(@contact.to_hash['custom_attributes']['mad']).must_equal 122 + end + + it 'decrements down by given value' do + @contact.decrement('mad', 3) + _(@contact.to_hash['custom_attributes']['mad']).must_equal 120 + end + + it 'can decrement new custom data fields' do + @contact.decrement('new_field', 5) + _(@contact.to_hash['custom_attributes']['new_field']).must_equal(-5) + end + + it 'can call decrement on the same key twice and decrement by 2' do + @contact.decrement('mad') + @contact.decrement('mad') + _(@contact.to_hash['custom_attributes']['mad']).must_equal 121 + end + end + + it 'saves a contact (always sends custom_attributes)' do + contact = Intercom::Contact.new('email' => 'jo@example.com', :external_id => 'i-1224242') + client.expects(:post).with('/contacts', 'email' => 'jo@example.com', 'external_id' => 'i-1224242', 'custom_attributes' => {}).returns('email' => 'jo@example.com', 'external_id' => 'i-1224242') + client.contacts.save(contact) + end + + it 'can save a contact with a nil email' do + contact = Intercom::Contact.new('email' => nil, :external_id => 'i-1224242') + client.expects(:post).with('/contacts', 'custom_attributes' => {}, 'email' => nil, 'external_id' => 'i-1224242').returns('email' => nil, 'external_id' => 'i-1224242') + client.contacts.save(contact) + end + + it 'can use client.contacts.create for convenience' do + client.expects(:post).with('/contacts', 'custom_attributes' => {}, 'email' => 'jo@example.com', 'external_id' => 'i-1224242').returns('email' => 'jo@example.com', 'external_id' => 'i-1224242') + contact = client.contacts.create('email' => 'jo@example.com', :external_id => 'i-1224242') + _(contact.email).must_equal 'jo@example.com' + end + + it 'updates the contact with attributes as set by the server' do + client.expects(:post).with('/contacts', 'email' => 'jo@example.com', 'external_id' => 'i-1224242', 'custom_attributes' => {}).returns('email' => 'jo@example.com', 'external_id' => 'i-1224242', 'session_count' => 4) + contact = client.contacts.create('email' => 'jo@example.com', :external_id => 'i-1224242') + _(contact.session_count).must_equal 4 + end + + it 'allows setting dates to nil without converting them to 0' do + client.expects(:post).with('/contacts', 'email' => 'jo@example.com', 'custom_attributes' => {}, 'remote_created_at' => nil).returns('email' => 'jo@example.com') + contact = client.contacts.create('email' => 'jo@example.com', 'remote_created_at' => nil) + assert_nil contact.remote_created_at + end + + it 'sets/gets rw keys' do + params = { 'email' => 'me@example.com', :external_id => 'abc123', 'name' => 'Bob Smith', 'last_seen_ip' => '1.2.3.4', 'last_seen_contact_agent' => 'ie6', 'created_at' => Time.now } + contact = Intercom::Contact.new(params) + custom_attributes = (params.keys + ['custom_attributes']).map(&:to_s).sort + _(contact.to_hash.keys.sort).must_equal custom_attributes + params.keys.each do |key| + _(contact.send(key).to_s).must_equal params[key].to_s + end + end + + it 'will allow extra attributes in response from api' do + contact = Intercom::Contact.send(:from_api, 'new_param' => 'some value') + _(contact.new_param).must_equal 'some value' + end + + it 'returns a BaseCollectionProxy for all without making any requests' do + client.expects(:execute_request).never + all = client.contacts.all + _(all).must_be_instance_of(Intercom::BaseCollectionProxy) + end + + it 'can print contacts without crashing' do + client.expects(:get).with('/contacts', 'email' => 'bo@example.com').returns(test_contact) + contact = client.contacts.find('email' => 'bo@example.com') + + begin + orignal_stdout = $stdout + $stdout = StringIO.new + + puts contact + p contact + ensure + $stdout = orignal_stdout + end + end + + it 'fetches a contact' do + client.expects(:get).with('/contacts', 'email' => 'bo@example.com').returns(test_contact) + contact = client.contacts.find('email' => 'bo@example.com') + _(contact.email).must_equal 'bob@example.com' + _(contact.name).must_equal 'Joe Schmoe' + _(contact.session_count).must_equal 123 + end + + it 'can update a contact with an id' do + contact = Intercom::Contact.new(id: 'de45ae78gae1289cb') + client.expects(:put).with('/contacts/de45ae78gae1289cb', 'custom_attributes' => {}) + client.contacts.save(contact) + end + + it 'deletes a contact' do + contact = Intercom::Contact.new('id' => '1') + client.expects(:delete).with('/contacts/1', {}).returns(contact) + client.contacts.delete(contact) + end + + it 'archives a contact' do + contact = Intercom::Contact.new('id' => '1') + client.expects(:post).with('/contacts/1/archive', {}) + client.contacts.archive(contact) + end + + it 'unarchives a contact' do + contact = Intercom::Contact.new('id' => '1') + client.expects(:post).with('/contacts/1/unarchive', {}) + client.contacts.unarchive(contact) + end + + it 'deletes an archived contact' do + contact = Intercom::Contact.new('id' => '1','archived' =>true) + client.expects(:delete).with('/contacts/1', {}) + client.contacts.delete_archived_contact("1") + end + + describe 'merging' do + let(:lead) { Intercom::Contact.from_api(external_id: 'contact_id', role: 'lead') } + let(:user) { Intercom::Contact.from_api(id: 'external_id', role: 'user') } + + it 'should be successful with a lead and user' do + client.expects(:post).with('/contacts/merge', + from: lead.id, into: user.id).returns(test_contact) + + client.contacts.merge(lead, user) + end + end + + describe 'nested resources' do + let(:contact) { Intercom::Contact.new(id: '1', client: client) } + let(:contact_no_tags) { Intercom::Contact.new(id: '2', client: client, tags: []) } + let(:company) { Intercom::Company.new(id: '1') } + let(:subscription) { Intercom::Subscription.new(id: '1', client: client) } + let(:tag) { Intercom::Tag.new(id: '1') } + let(:note) { Intercom::Note.new(body: "

Text for the note

") } + + it 'returns a collection proxy for listing notes' do + proxy = contact.notes + _(proxy.resource_name).must_equal 'notes' + _(proxy.url).must_equal '/contacts/1/notes' + _(proxy.resource_class).must_equal Intercom::Note + end + + it 'returns a collection proxy for listing segments' do + proxy = contact.segments + _(proxy.resource_name).must_equal 'segments' + _(proxy.url).must_equal '/contacts/1/segments' + _(proxy.resource_class).must_equal Intercom::Segment + end + + it 'returns a collection proxy for listing tags' do + proxy = contact.tags + _(proxy.resource_name).must_equal 'tags' + _(proxy.url).must_equal '/contacts/1/tags' + _(proxy.resource_class).must_equal Intercom::Tag + end + + it 'returns correct tags from differring contacts' do + client.expects(:get).with('/contacts/1/tags', {}).returns({ + 'type' => 'tag.list', + 'tags' => [ + { + 'type' => 'tag', + 'id' => '1', + 'name' => 'VIP Customer' + }, + { + 'type' => 'tag', + 'id' => '2', + 'name' => 'Test tag' + } + ] + }) + + _(contact_no_tags.tags.map{ |t| t.id }).must_equal [] + _(contact.tags.map{ |t| t.id }).must_equal ['1', '2'] + end + + it 'returns a collection proxy for listing companies' do + proxy = contact.companies + _(proxy.resource_name).must_equal 'companies' + _(proxy.url).must_equal '/contacts/1/companies' + _(proxy.resource_class).must_equal Intercom::Company + end + + it 'adds a note to a contact' do + client.expects(:post).with('/contacts/1/notes', {body: note.body}).returns(note.to_hash) + contact.create_note({body: note.body}) + end + + it 'adds a tag to a contact' do + client.expects(:post).with('/contacts/1/tags', "id": tag.id).returns(tag.to_hash) + contact.add_tag({ "id": tag.id }) + end + + it 'removes a subscription to a contact' do + client.expects(:delete).with("/contacts/1/subscriptions/#{subscription.id}", "id": subscription.id).returns(subscription.to_hash) + contact.remove_subscription_type({ "id": subscription.id }) + end + + it 'removes a tag from a contact' do + client.expects(:delete).with("/contacts/1/tags/#{tag.id}", "id": tag.id ).returns(tag.to_hash) + contact.remove_tag({ "id": tag.id }) + end + + it 'adds a contact to a company' do + client.expects(:post).with('/contacts/1/companies', "id": company.id).returns(test_company) + contact.add_company({ "id": tag.id }) + end + + it 'removes a contact from a company' do + client.expects(:delete).with("/contacts/1/companies/#{company.id}", "id": tag.id ).returns(test_company) + contact.remove_company({ "id": tag.id }) + end + + describe 'just after creating the contact' do + let(:contact) do + contact = Intercom::Contact.new('email' => 'jo@example.com', :external_id => 'i-1224242') + client.expects(:post).with('/contacts', 'email' => 'jo@example.com', 'external_id' => 'i-1224242', 'custom_attributes' => {}) + .returns('id' => 1, 'email' => 'jo@example.com', 'external_id' => 'i-1224242') + client.contacts.save(contact) + end + + it 'returns a collection proxy for listing notes' do + proxy = contact.notes + _(proxy.resource_name).must_equal 'notes' + _(proxy.url).must_equal '/contacts/1/notes' + _(proxy.resource_class).must_equal Intercom::Note + end + + it 'returns a collection proxy for listing tags' do + proxy = contact.tags + _(proxy.resource_name).must_equal 'tags' + _(proxy.url).must_equal '/contacts/1/tags' + _(proxy.resource_class).must_equal Intercom::Tag + end + + it 'returns a collection proxy for listing companies' do + proxy = contact.companies + _(proxy.resource_name).must_equal 'companies' + _(proxy.url).must_equal '/contacts/1/companies' + _(proxy.resource_class).must_equal Intercom::Company + end + + it 'adds a note to a contact' do + client.expects(:post).with('/contacts/1/notes', {body: note.body}).returns(note.to_hash) + contact.create_note({body: note.body}) + end + + it 'adds a subscription to a contact' do + client.expects(:post).with('/contacts/1/subscriptions', "id": subscription.id).returns(subscription.to_hash) + contact.create_subscription_type({ "id": subscription.id }) + end + + it 'adds a tag to a contact' do + client.expects(:post).with('/contacts/1/tags', "id": tag.id).returns(tag.to_hash) + contact.add_tag({ "id": tag.id }) + end + + it 'removes a tag from a contact' do + client.expects(:delete).with("/contacts/1/tags/#{tag.id}", "id": tag.id ).returns(tag.to_hash) + contact.remove_tag({ "id": tag.id }) + end + + it 'adds a contact to a company' do + client.expects(:post).with('/contacts/1/companies', "id": company.id).returns(test_company) + contact.add_company({ "id": tag.id }) + end + + it 'removes a contact from a company' do + client.expects(:delete).with("/contacts/1/companies/#{company.id}", "id": tag.id ).returns(test_company) + contact.remove_company({ "id": tag.id }) + end + end + end +end diff --git a/spec/unit/intercom/conversation_spec.rb b/spec/unit/intercom/conversation_spec.rb new file mode 100644 index 00000000..d585eea6 --- /dev/null +++ b/spec/unit/intercom/conversation_spec.rb @@ -0,0 +1,116 @@ +require 'spec_helper' + +describe "Intercom::Conversation" do + let(:client) { Intercom::Client.new(token: 'token') } + + it "gets a conversation" do + client.expects(:get).with("/conversations/147", {}).returns(test_conversation) + client.conversations.find(:id => "147") + end + + it "gets all conversations" do + client.expects(:get).with("/conversations", {}).returns(test_conversation_list) + client.conversations.all.each { |c| } + end + + it "can filter conversations based on params" do + client.expects(:get).with("/conversations", {type: 'user', intercom_user_id: '123456789'}).returns(test_conversation_list) + client.conversations.find_all(type: 'user', intercom_user_id: '123456789').each { |c| } + end + + it 'marks a conversation as read' do + client.expects(:put).with('/conversations/147', { read: true }) + client.conversations.mark_read('147') + end + + it 'replies to a conversation' do + client.expects(:post).with('/conversations/147/reply', { type: 'user', body: 'Thanks again', message_type: 'comment', user_id: 'ac4', conversation_id: '147' }).returns(test_conversation) + client.conversations.reply(id: '147', type: 'user', body: 'Thanks again', message_type: 'comment', user_id: 'ac4') + end + + it 'replies to a conversation with an attachment' do + client.expects(:post).with('/conversations/147/reply', { type: 'user', body: 'Thanks again', message_type: 'comment', user_id: 'ac4', conversation_id: '147', attachment_urls: ["http://www.example.com/attachment.jpg"] }).returns(test_conversation) + client.conversations.reply(id: '147', type: 'user', body: 'Thanks again', message_type: 'comment', user_id: 'ac4', attachment_urls: ["http://www.example.com/attachment.jpg"]) + end + + it 'sends a reply to a users last conversation from an admin' do + client.expects(:post).with('/conversations/last/reply', { type: 'admin', body: 'Thanks again', message_type: 'comment', user_id: 'ac4', admin_id: '123' }).returns(test_conversation) + client.conversations.reply_to_last(type: 'admin', body: 'Thanks again', message_type: 'comment', user_id: 'ac4', admin_id: '123') + end + + it 'opens a conversation' do + client.expects(:post).with('/conversations/147/reply', { type: 'admin', message_type: 'open', conversation_id: '147', admin_id: '123'}).returns(test_conversation) + client.conversations.open(id: '147', admin_id: '123') + end + + it 'closes a conversation' do + client.expects(:post).with('/conversations/147/reply', { type: 'admin', message_type: 'close', conversation_id: '147', admin_id: '123'}).returns(test_conversation) + client.conversations.close(id: '147', admin_id: '123') + end + + it 'assigns a conversation' do + client.expects(:post).with('/conversations/147/reply', { type: 'admin', message_type: 'assignment', conversation_id: '147', admin_id: '123', assignee_id: '124'}).returns(test_conversation) + client.conversations.assign(id: '147', admin_id: '123', assignee_id: '124') + end + + it 'snoozes a conversation' do + client.expects(:post).with('/conversations/147/reply', { type: 'admin', message_type: 'snoozed', conversation_id: '147', admin_id: '123', snoozed_until: tomorrow}).returns(test_conversation) + client.conversations.snooze(id: '147', admin_id: '123', snoozed_until: tomorrow) + end + + it 'runs assignment rules on a conversation' do + client.expects(:post).with('/conversations/147/run_assignment_rules', {}).returns(test_conversation) + client.conversations.run_assignment_rules('147') + end + + describe 'nested resources' do + let(:conversation) { Intercom::Conversation.new('id' => '1', 'client' => client) } + let(:tag) { Intercom::Tag.new('id' => '1') } + + it 'adds a tag to a conversation' do + client.expects(:post).with("/conversations/1/tags", { 'id': tag.id, 'admin_id': test_admin['id'] }).returns(tag.to_hash) + conversation.add_tag({ "id": tag.id, "admin_id": test_admin['id'] }) + end + + it 'does not add a tag to a conversation if no admin_id is passed' do + client.expects(:post).with("/conversations/1/tags", { 'id': tag.id }).returns(nil) + _(proc { conversation.add_tag({ "id": tag.id }) }).must_raise Intercom::HttpError + end + + it 'removes a tag from a conversation' do + client.expects(:delete).with("/conversations/1/tags/1", { "id": tag.id, "admin_id": test_admin['id'] }).returns(tag.to_hash) + conversation.remove_tag({ "id": tag.id, "admin_id": test_admin['id'] }) + end + + it 'does not remove a tag from a conversation if no admin_id is passed' do + client.expects(:delete).with("/conversations/1/tags/1", { "id": tag.id }).returns(nil) + _(proc { conversation.remove_tag({ "id": tag.id }) }).must_raise Intercom::HttpError + end + + describe 'contacts' do + let(:response) do + { + customers: [ + { type: test_contact['type'], id: test_contact['id'] } + ] + } + end + + it 'adds a contact to a conversation' do + client.expects(:post) + .with("/conversations/1/customers", + { admin_id: test_admin['id'], customer: { intercom_user_id: test_contact['id'] } }) + .returns(response.to_hash) + conversation.add_contact(admin_id: test_admin['id'], customer: { intercom_user_id: test_contact['id'] }) + end + + it 'removes a contact from a conversation' do + client.expects(:delete) + .with("/conversations/1/customers/aaaaaaaaaaaaaaaaaaaaaaaa", + { id: 'aaaaaaaaaaaaaaaaaaaaaaaa', admin_id: '1234' }) + .returns(response.to_hash) + conversation.remove_contact(id: test_contact['id'], admin_id: test_admin['id']) + end + end + end +end diff --git a/spec/unit/intercom/count_spec.rb b/spec/unit/intercom/count_spec.rb new file mode 100644 index 00000000..208e0790 --- /dev/null +++ b/spec/unit/intercom/count_spec.rb @@ -0,0 +1,28 @@ +require "spec_helper" + +describe "Intercom::Count" do + let(:client) { Intercom::Client.new(token: 'token') } + + it 'should get app wide counts' do + client.expects(:get).with("/counts", {}).returns(test_app_count) + counts = client.counts.for_app + _(counts.tag['count']).must_equal(341) + end + + it 'should get type counts' do + client.expects(:get).with("/counts", {type: 'user', count: 'segment'}).returns(test_segment_count) + counts = client.counts.for_type(type: 'user', count: 'segment') + _(counts.user['segment'][4]["segment 1"]).must_equal(1) + end + + it 'should not include count param when nil' do + client.expects(:get).with("/counts", {type: 'conversation'}).returns(test_conversation_count) + counts = client.counts.for_type(type: 'conversation') + _(counts.conversation).must_equal({ + "assigned" => 1, + "closed" => 15, + "open" => 1, + "unassigned" => 0 + }) + end +end diff --git a/spec/unit/intercom/data_attribute_spec.rb b/spec/unit/intercom/data_attribute_spec.rb new file mode 100644 index 00000000..c8d0b3e4 --- /dev/null +++ b/spec/unit/intercom/data_attribute_spec.rb @@ -0,0 +1,40 @@ +require "spec_helper" + +describe "Intercom::DataAttribute" do + let(:client) { Intercom::Client.new(token: 'token') } + + it "returns a CollectionProxy for all without making any requests" do + client.expects(:execute_request).never + all = client.data_attributes.all + _(all).must_be_instance_of(Intercom::ClientCollectionProxy) + end + + + it "creates a new data attribute" do + client.expects(:post).with("/data_attributes", { "name" => "blah", "model" => "contact", "data_type" => "string" }).returns(status: 200) + client.data_attributes.create("name": "blah", + "model": "contact", + "data_type": "string" ) + end + + it "updates an existing attribute" do + attribute = Intercom::DataAttribute.new("id": 123, + "name": "blah", + "model": "contact", + "data_type": "string") + client.expects(:put).with("/data_attributes/#{attribute.id}", { "name" => "New name", "model" => "contact", "data_type" => "string" }) + attribute.name = "New name" + client.data_attributes.save(attribute) + _(attribute.name).must_equal "New name" + end + + it 'gets a list of attributes' do + client.expects(:get).with("/data_attributes", {}).returns(test_data_attribute_list) + client.data_attributes.all.each { |d| } + end + + it 'finds all customer or company attributes' do + client.expects(:get).with("/data_attributes", { "model": "contact" }).returns(test_data_attribute_list) + client.data_attributes.find_all({"model": "contact"}).each { |d| } + end +end diff --git a/spec/unit/intercom/deprecated_leads_collection_proxy_spec.rb b/spec/unit/intercom/deprecated_leads_collection_proxy_spec.rb new file mode 100644 index 00000000..9c90a9e1 --- /dev/null +++ b/spec/unit/intercom/deprecated_leads_collection_proxy_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Intercom::ClientCollectionProxy do + let(:client) { Intercom::Client.new(token: 'token') } + let(:lead_json) do + { 'type' => 'contact.list', 'contacts' => [{ 'type' => 'contact', 'id' => 'id' }] } + end + + it 'stops iterating if no next link' do + client.expects(:get).with('/contacts', {}).returns(lead_json) + client.deprecated__leads.all.each do |company| + _(company.class).must_equal Intercom::Lead + end + end +end diff --git a/spec/unit/intercom/event_spec.rb b/spec/unit/intercom/event_spec.rb index e8e37e27..5d62551f 100644 --- a/spec/unit/intercom/event_spec.rb +++ b/spec/unit/intercom/event_spec.rb @@ -4,11 +4,43 @@ let(:user) {Intercom::User.new("email" => "jim@example.com", :user_id => "12345", :created_at => Time.now, :name => "Jim Bob")} let(:created_time) {Time.now - 300} + let(:client) { Intercom::Client.new(token: 'token') } + + it 'gets events for a user' do + client.expects(:get).with('/events', type: 'user', email: 'joe@example.com').returns(test_event_list) + client.events.find_all(type: 'user', email: 'joe@example.com').first + end + + it "has the correct collection proxy class" do + _(client.events.collection_proxy_class).must_equal Intercom::EventCollectionProxy + end + + it "stops iterating if no next link" do + client.expects(:get).with("/events", type: 'user', email: 'joe@example.com').returns(page_of_events(false)) + event_names = [] + client.events.find_all(type: 'user', email: 'joe@example.com').each { |event| event_names << event.event_name } + _(event_names).must_equal %W(invited-friend) + end + + it "able to fetch event summary" do + client.expects(:get).with("/events", type: 'user', email: 'joe@example.com', summary: true).returns(page_of_events(false)) + event_names = [] + client.events.find_all(type: 'user', email: 'joe@example.com',summary: true).each { |event| event_names << event.event_name } + _(event_names).must_equal %W(invited-friend) + end + + it "keeps iterating if next link" do + client.expects(:get).with("/events", type: 'user', email: 'joe@example.com').returns(page_of_events(true)) + client.expects(:get).with("https://api.intercom.io/events?type=user&intercom_user_id=55a3b&before=144474756550", {}).returns(page_of_events(false)) + event_names = [] + client.events.find_all(type: 'user', email: 'joe@example.com').each { |event| event_names << event.event_name } + _(event_names).must_equal %W(invited-friend invited-friend) + end it "creates an event with metadata" do - Intercom.expects(:post).with('/events', {'event_name' => 'Eventful 1', 'created_at' => created_time.to_i, 'email' => 'joe@example.com', 'metadata' => {'invitee_email' => 'pi@example.org', :invite_code => 'ADDAFRIEND', 'found_date' => 12909364407}}).returns(:status => 202) + client.expects(:post).with('/events', {'event_name' => 'Eventful 1', 'created_at' => created_time.to_i, 'email' => 'joe@example.com', 'metadata' => {'invitee_email' => 'pi@example.org', :invite_code => 'ADDAFRIEND', 'found_date' => 12909364407}}).returns(:status => 202) - Intercom::Event.create(:event_name => "Eventful 1", :created_at => created_time, + client.events.create(:event_name => "Eventful 1", :created_at => created_time, :email => 'joe@example.com', :metadata => { "invitee_email" => "pi@example.org", @@ -18,8 +50,107 @@ end it "creates an event without metadata" do - Intercom.expects(:post).with('/events', {'event_name' => 'sale of item', 'email' => 'joe@example.com'}) - Intercom::Event.create(:event_name => "sale of item", :email => 'joe@example.com') + client.expects(:post).with('/events', {'event_name' => 'sale of item', 'id' => 123}) + client.events.create(:event_name => "sale of item", :id => 123) + end + + describe 'bulk operations' do + let (:job) { + { + "app_id"=>"app_id", + "id"=>"super_awesome_job", + "created_at"=>1446033421, + "completed_at"=>1446048736, + "closing_at"=>1446034321, + "updated_at"=>1446048736, + "name"=>"api_bulk_job", + "state"=>"completed", + "links"=> + { + "error"=>"https://api.intercom.io/jobs/super_awesome_job/error", + "self"=>"https://api.intercom.io/jobs/super_awesome_job" + }, + "tasks"=> + [ + { + "id"=>"super_awesome_task", + "item_count"=>2, + "created_at"=>1446033421, + "started_at"=>1446033709, + "completed_at"=>1446033709, + "state"=>"completed" + } + ] + } + } + let(:bulk_request) { + { + items: [ + { + method: "post", + data_type: "event", + data: { + event_name: "ordered-item", + created_at: 1438944980, + user_id: "314159", + metadata: { + order_date: 1438944980, + stripe_invoice: "inv_3434343434" + } + } + }, + { + method: "post", + data_type: "event", + data: { + event_name: "invited-friend", + created_at: 1438944979, + user_id: "314159", + metadata: { + invitee_email: "pi@example.org", + invite_code: "ADDAFRIEND" + } + } + } + ] + } + } + let(:events) { + [ + { + event_name: "ordered-item", + created_at: 1438944980, + user_id: "314159", + metadata: { + order_date: 1438944980, + stripe_invoice: "inv_3434343434" + } + }, + { + event_name: "invited-friend", + created_at: 1438944979, + user_id: "314159", + metadata: { + invitee_email: "pi@example.org", + invite_code: "ADDAFRIEND" + } + } + ] + } + + it "submits a bulk job" do + client.expects(:post).with("/bulk/events", bulk_request).returns(job) + client.events.submit_bulk_job(create_items: events) + end + + it "adds events to an existing bulk job" do + bulk_request[:job] = {id: 'super_awesome_job'} + client.expects(:post).with("/bulk/events", bulk_request).returns(job) + client.events.submit_bulk_job(create_items: events, job_id: 'super_awesome_job') + end + + it "does not submit delete jobs" do + _(lambda { client.events.submit_bulk_job(delete_items: events) }).must_raise ArgumentError + end end - end diff --git a/spec/unit/intercom/export_content_spec.rb b/spec/unit/intercom/export_content_spec.rb new file mode 100644 index 00000000..6c7787b7 --- /dev/null +++ b/spec/unit/intercom/export_content_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe "Intercom::ExportContent" do + let(:client) { Intercom::Client.new(token: 'token') } + let(:job) { + { + job_identifier: "k0e27ohsyvh8ef3m", + status: "no_data", + download_url: "", + download_expires_at: 0 + } + } + + it "creates an export job" do + client.expects(:post).with("/export/content/data", {"created_at_after" => 1667566801, "created_at_before" => 1668085202}).returns(job) + client.export_content.create({"created_at_after" => 1667566801, "created_at_before" => 1668085202}) + end + + it "can view an export job" do + client.expects(:get).with("/export/content/data/#{job[:job_identifier]}", {}).returns(job) + client.export_content.find(id: job[:job_identifier]) + end + + it "Cancels a export job redirect" do + client.expects(:post).with("/export/cancel/#{job[:job_identifier]}", {}).returns(job) + client.export_content.cancel(job[:job_identifier]) + end +end diff --git a/spec/unit/intercom/job_spec.rb b/spec/unit/intercom/job_spec.rb new file mode 100644 index 00000000..f21a49ee --- /dev/null +++ b/spec/unit/intercom/job_spec.rb @@ -0,0 +1,51 @@ +require "spec_helper" + +describe "jobs" do + let(:client) { Intercom::Client.new(token: 'token') } + let (:job) { + { + "app_id" => "app_id", + "id" => "super_awesome_job", + "created_at" => 1446033421, + "completed_at" => 1446048736, + "closing_at" => 1446034321, + "updated_at" => 1446048736, + "name" => "api_bulk_job", + "state" => "completed", + "links" => + { + "error" => "https://api.intercom.io/jobs/super_awesome_job/error", + "self" => "https://api.intercom.io/jobs/super_awesome_job" + }, + "tasks" => + [ + { + "id" => "super_awesome_task", + "item_count" => 2, + "created_at" => 1446033421, + "started_at" => 1446033709, + "completed_at" => 1446033709, + "state" => "completed" + } + ] + } + } + let (:error_feed) { + { + "app_id" => "app_id", + "job_id" => "super_awesome_job", + "pages" => {}, + "items" => [] + } + } + + it 'gets a job' do + client.expects(:get).with("/jobs/super_awesome_job", {}).returns(job) + client.jobs.find(id: 'super_awesome_job') + end + + it 'gets a job\'s error feed' do + client.expects(:get).with("/jobs/super_awesome_job/error", {}).returns(error_feed) + client.jobs.errors(id: 'super_awesome_job') + end +end diff --git a/spec/unit/intercom/lead_spec.rb b/spec/unit/intercom/lead_spec.rb new file mode 100644 index 00000000..4bbcd989 --- /dev/null +++ b/spec/unit/intercom/lead_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Intercom::Lead' do + let (:client) { Intercom::Client.new(token: 'token') } + + it 'should be listable' do + proxy = client.deprecated__leads.all + _(proxy.resource_name).must_equal 'contacts' + _(proxy.resource_class).must_equal Intercom::Lead + end + + it 'should not throw ArgumentErrors when there are no parameters' do + client.expects(:post) + client.deprecated__leads.create + end + + it 'can update a lead with an id' do + lead = Intercom::Lead.new(id: 'de45ae78gae1289cb') + client.expects(:put).with('/contacts/de45ae78gae1289cb', 'custom_attributes' => {}) + client.deprecated__leads.save(lead) + end + + describe 'converting' do + let(:lead) { Intercom::Lead.from_api(user_id: 'contact_id') } + let(:user) { Intercom::User.from_api(id: 'user_id') } + + it do + client.expects(:post).with( + '/contacts/convert', + contact: { user_id: lead.user_id }, + user: { 'id' => user.id } + ).returns(test_user) + + client.deprecated__leads.convert(lead, user) + end + end + + it 'returns a DeprecatedLeadsCollectionProxy for all without making any requests' do + client.expects(:execute_request).never + all = client.deprecated__leads.all + _(all).must_be_instance_of(Intercom::DeprecatedLeadsCollectionProxy) + end + + it 'deletes a lead' do + lead = Intercom::Lead.new('id' => '1') + client.expects(:delete).with('/contacts/1', {}).returns(lead) + client.deprecated__leads.delete(lead) + end + + it 'sends a request for a hard deletion' do + lead = Intercom::Lead.new('id' => '1') + client.expects(:post).with('/user_delete_requests', intercom_user_id: '1').returns(id: lead.id) + client.deprecated__leads.request_hard_delete(lead) + end +end diff --git a/spec/unit/intercom/lib/flat_store_spec.rb b/spec/unit/intercom/lib/flat_store_spec.rb index 435cc1eb..412ed717 100644 --- a/spec/unit/intercom/lib/flat_store_spec.rb +++ b/spec/unit/intercom/lib/flat_store_spec.rb @@ -1,29 +1,61 @@ -require "spec_helper" +# frozen_string_literal: true + +require 'spec_helper' describe Intercom::Lib::FlatStore do - it "raises if you try to set or merge in nested hash structures" do - data = Intercom::Lib::FlatStore.new() - proc { data["thing"] = [1] }.must_raise ArgumentError - proc { data["thing"] = {1 => 2} }.must_raise ArgumentError - proc { Intercom::Lib::FlatStore.new({1 => {2 => 3}}) }.must_raise ArgumentError + it 'raises if you try to set arrays but allows hashes' do + data = Intercom::Lib::FlatStore.new + _(proc { data['thing'] = [1] }).must_raise ArgumentError + + data['thing'] = { 'key' => 'value' } + _(data['thing']).must_equal({ 'key' => 'value' }) + + flat_store = Intercom::Lib::FlatStore.new('custom_object' => { 'type' => 'Order.list', 'instances' => [{'id' => '123'}] }) + _(flat_store['custom_object']).must_equal({ 'type' => 'Order.list', 'instances' => [{'id' => '123'}] }) end - it "raises if you try to use a non string key" do - data =Intercom::Lib::FlatStore.new() - proc { data[1] = "something" }.must_raise ArgumentError + it 'raises if you try to use a non string key' do + data = Intercom::Lib::FlatStore.new + _(proc { data[1] = 'something' }).must_raise ArgumentError end - it "sets and merges valid entries" do - data = Intercom::Lib::FlatStore.new() - data["a"] = 1 + it 'sets and merges valid entries' do + data = Intercom::Lib::FlatStore.new + data['a'] = 1 data[:b] = 2 - data[:a].must_equal 1 - data["b"].must_equal 2 - data[:b].must_equal 2 - data = Intercom::Lib::FlatStore.new({"a" => 1, :b => 2}) - data["a"].must_equal 1 - data[:a].must_equal 1 - data["b"].must_equal 2 - data[:b].must_equal 2 + _(data[:a]).must_equal 1 + _(data['b']).must_equal 2 + _(data[:b]).must_equal 2 + data = Intercom::Lib::FlatStore.new('a' => 1, :b => 2) + _(data['a']).must_equal 1 + _(data[:a]).must_equal 1 + _(data['b']).must_equal 2 + _(data[:b]).must_equal 2 + end + + describe '#to_submittable_hash' do + it 'filters out all hash values' do + data = Intercom::Lib::FlatStore.new( + 'regular_attr' => 'value', + 'number_attr' => 42, + 'custom_object' => { + 'type' => 'Order.list', + 'instances' => [ + { 'id' => '31', 'external_id' => 'ext_123' } + ] + }, + 'regular_hash' => { 'key' => 'value' }, + 'metadata' => { 'source' => 'api', 'version' => 2 } + ) + + submittable = data.to_submittable_hash + + _(submittable['regular_attr']).must_equal 'value' + _(submittable['number_attr']).must_equal 42 + + _(submittable.key?('custom_object')).must_equal false + _(submittable.key?('regular_hash')).must_equal false + _(submittable.key?('metadata')).must_equal false + end end end diff --git a/spec/unit/intercom/message_spec.rb b/spec/unit/intercom/message_spec.rb index 36e5cae4..f027a1e3 100644 --- a/spec/unit/intercom/message_spec.rb +++ b/spec/unit/intercom/message_spec.rb @@ -3,19 +3,20 @@ describe "Intercom::Message" do let (:user) {Intercom::User.new("email" => "jim@example.com", :user_id => "12345", :created_at => Time.now, :name => "Jim Bob")} + let(:client) { Intercom::Client.new(token: 'token') } it 'creates an user message with symbol keys' do - Intercom.expects(:post).with('/messages', {'from' => { :type => 'user', :email => 'jim@example.com'}, 'body' => 'halp'}).returns(:status => 200) - Intercom::Message.create(:from => { :type => "user", :email => "jim@example.com" }, :body => "halp") + client.expects(:post).with('/messages', {'from' => { :type => 'user', :email => 'jim@example.com'}, 'body' => 'halp'}).returns(:status => 200) + client.messages.create(:from => { :type => "user", :email => "jim@example.com" }, :body => "halp") end - + it "creates an user message with string keys" do - Intercom.expects(:post).with('/messages', {'from' => { 'type' => 'user', 'email' => 'jim@example.com'}, 'body' => 'halp'}).returns(:status => 200) - Intercom::Message.create('from' => { 'type' => "user", 'email' => "jim@example.com" }, 'body' => "halp") + client.expects(:post).with('/messages', {'from' => { 'type' => 'user', 'email' => 'jim@example.com'}, 'body' => 'halp'}).returns(:status => 200) + client.messages.create('from' => { 'type' => "user", 'email' => "jim@example.com" }, 'body' => "halp") end - + it "creates a admin message" do - Intercom.expects(:post).with('/messages', {'from' => { 'type' => "admin", 'id' => "1234" }, 'to' => { 'type' => 'user', 'id' => '5678' }, 'body' => 'halp', 'message_type' => 'inapp'}).returns(:status => 200) - Intercom::Message.create('from' => { 'type' => "admin", 'id' => "1234" }, :to => { 'type' => 'user', 'id' => '5678' }, 'body' => "halp", 'message_type' => 'inapp') + client.expects(:post).with('/messages', {'from' => { 'type' => "admin", 'id' => "1234" }, 'to' => { 'type' => 'user', 'id' => '5678' }, 'body' => 'halp', 'message_type' => 'inapp'}).returns(:status => 200) + client.messages.create('from' => { 'type' => "admin", 'id' => "1234" }, :to => { 'type' => 'user', 'id' => '5678' }, 'body' => "halp", 'message_type' => 'inapp') end end diff --git a/spec/unit/intercom/note_spec.rb b/spec/unit/intercom/note_spec.rb index a2a6165b..1715b5ff 100644 --- a/spec/unit/intercom/note_spec.rb +++ b/spec/unit/intercom/note_spec.rb @@ -1,19 +1,20 @@ require "spec_helper" describe "notes" do - it "creates a note" do - Intercom.expects(:post).with("/notes", {"body" => "Note to leave on user"}).returns({"body" => "

Note to leave on user

", "created_at" => 1234567890}) - note = Intercom::Note.create("body" => "Note to leave on user") - note.body.must_equal "

Note to leave on user

" + let(:client) { Intercom::Client.new(token: 'token') } + + it 'gets a note' do + client.expects(:get).with("/notes/123", {}).returns({"id" => "123", "body" => "

Note to leave on user

", "created_at" => 1234567890}) + client.notes.find(id: '123') end it "sets/gets allowed keys" do params = {"body" => "Note body", "email" => "me@example.com", :user_id => "abc123"} note = Intercom::Note.new(params) - note.to_hash.keys.sort.must_equal params.keys.map(&:to_s).sort - params.keys.each do | key| - note.send(key).must_equal params[key] + _(note.to_hash.keys.sort).must_equal params.keys.map(&:to_s).sort + params.keys.each do |key| + _(note.send(key)).must_equal params[key] end end end diff --git a/spec/unit/intercom/notification_spec.rb b/spec/unit/intercom/notification_spec.rb deleted file mode 100644 index 0297cb74..00000000 --- a/spec/unit/intercom/notification_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -require 'spec_helper' - -describe "Intercom::Notification" do - - it "converts notification hash to object" do - payload = Intercom::Notification.new(test_user_notification) - payload.must_be_instance_of Intercom::Notification - end - - it "returns correct model type for User" do - payload = Intercom::Notification.new(test_user_notification) - payload.model_type.must_equal Intercom::User - end - - it "returns correct User notification topic" do - payload = Intercom::Notification.new(test_user_notification) - payload.topic.must_equal "user.created" - end - - it "returns instance of User" do - payload = Intercom::Notification.new(test_user_notification) - payload.model.must_be_instance_of Intercom::User - end - - it "returns instance of Conversation" do - payload = Intercom::Notification.new(test_conversation_notification) - payload.model.must_be_instance_of Intercom::Conversation - end - - it "returns correct model type for Conversation" do - payload = Intercom::Notification.new(test_conversation_notification) - payload.model_type.must_equal Intercom::Conversation - end - - it "returns correct Conversation notification topic" do - payload = Intercom::Notification.new(test_conversation_notification) - payload.topic.must_equal "conversation.user.created" - end - - it "returns inner User object for Conversation" do - payload = Intercom::Notification.new(test_conversation_notification) - payload.model.user.must_be_instance_of Intercom::User - end - -end \ No newline at end of file diff --git a/spec/unit/intercom/phone_call_redirect.rb b/spec/unit/intercom/phone_call_redirect.rb new file mode 100644 index 00000000..02a38863 --- /dev/null +++ b/spec/unit/intercom/phone_call_redirect.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe "Intercom::PhoneCallRedirect" do + let(:client) { Intercom::Client.new(token: 'token') } + + it "creates a phone redirect" do + + client.expects(:post).with("/phone_call_redirect", {phone_number: "+353871234567"}) + client.phone_call_redirect.create(phone_number: "+353871234567") + end + +end diff --git a/spec/unit/intercom/request_spec.rb b/spec/unit/intercom/request_spec.rb index 76a41c49..6ec61cb8 100644 --- a/spec/unit/intercom/request_spec.rb +++ b/spec/unit/intercom/request_spec.rb @@ -1,10 +1,170 @@ require 'spec_helper' require 'ostruct' -describe 'Intercom::Request' do - it 'raises an error when a html error page rendered' do - response = OpenStruct.new(:code => 500) - req = Intercom::Request.new('path/', 'GET') - proc {req.parse_body('somethjing', response)}.must_raise(Intercom::ServerError) +WebMock.enable! + +describe 'Intercom::Request', '#execute' do + let(:uri) {"https://api.intercom.io/users"} + let(:req) { Intercom::Request.get(uri, {}) } + let(:default_body) { { data: "test" }.to_json } + + def execute! + req.execute(uri, token: 'test-token') + end + + it 'should call sleep for rate limit error three times and raise a rate limit error otherwise' do + stub_request(:any, uri).to_return( + status: [429, "Too Many Requests"], + headers: { 'X-RateLimit-Reset' => (Time.now.utc + 10).to_i.to_s }, + body: default_body + ) + + req.handle_rate_limit=true + + req.expects(:sleep).times(3).with(any_parameters) + + expect { execute! }.must_raise(Intercom::RateLimitExceeded) + end + + it 'should not call sleep for rate limit error' do + stub_request(:any, uri).to_return( + status: [200, "OK"], + headers: { 'X-RateLimit-Reset' => Time.now.utc + 10 }, + body: default_body + ) + + req.handle_rate_limit=true + req.expects(:sleep).never.with(any_parameters) + + execute! + end + + it 'should call sleep for rate limit error just once' do + stub_request(:any, uri).to_return( + status: [429, "Too Many Requests"], + headers: { 'X-RateLimit-Reset' => (Time.now.utc + 10).to_i.to_s }, + ).then.to_return(status: [200, "OK"], body: default_body) + + req.handle_rate_limit=true + req.expects(:sleep).with(any_parameters) + + execute! + end + + it 'should not sleep if rate limit reset time has passed' do + stub_request(:any, uri).to_return( + status: [429, "Too Many Requests"], + headers: { 'X-RateLimit-Reset' => Time.parse("February 25 2010").utc.to_i.to_s }, + body: default_body + ).then.to_return(status: [200, "OK"], body: default_body) + + req.handle_rate_limit=true + req.expects(:sleep).never.with(any_parameters) + + execute! + end + + it 'handles an empty body gracefully' do + stub_request(:any, uri).to_return( + status: 200, + body: nil + ) + + assert_nil(execute!) + end + + describe 'HTTP error handling' do + it 'raises an error when the response is successful but the body is not JSON' do + stub_request(:any, uri).to_return( + status: 200, + body: 'something' + ) + + expect { execute! }.must_raise(Intercom::UnexpectedResponseError) + end + + it 'raises an error when an html error page rendered' do + stub_request(:any, uri).to_return( + status: 500, + body: 'something' + ) + + expect { execute! }.must_raise(Intercom::ServerError) + end + + it 'raises an error if the decoded_body is "null"' do + stub_request(:any, uri).to_return( + status: 500, + body: 'null' + ) + + expect { execute! }.must_raise(Intercom::ServerError) + end + + it 'raises a RateLimitExceeded error when the response code is 429' do + stub_request(:any, uri).to_return( + status: 429, + body: 'null' + ) + + expect { execute! }.must_raise(Intercom::RateLimitExceeded) + end + + it 'raises a GatewayTimeoutError error when the response code is 504' do + stub_request(:any, uri).to_return( + status: 504, + body: ' 504 Gateway Time-out

504 Gateway Time-out

' + ) + + expect { execute! }.must_raise(Intercom::GatewayTimeoutError) + end + end + + describe "application error handling" do + let(:uri) {"https://api.intercom.io/conversations/reply"} + let(:req) { Intercom::Request.put(uri, {}) } + + let(:tag_uri) {"https://api.intercom.io/tags/"} + let(:del_req) { Intercom::Request.delete(tag_uri, {}) } + + it 'should raise ResourceNotUniqueError error on resource_conflict code' do + stub_request(:put, uri).to_return( + status: [409, "Resource Already Exists"], + headers: { 'X-RateLimit-Reset' => (Time.now.utc + 10).to_i.to_s }, + body: { type: "error.list", errors: [ code: "resource_conflict" ] }.to_json + ) + + expect { execute! }.must_raise(Intercom::ResourceNotUniqueError) + end + + it 'should raise ApiVersionInvalid error on intercom_version_invalid code' do + stub_request(:put, uri).to_return( + status: [400, "Bad Request"], + headers: { 'X-RateLimit-Reset' => (Time.now.utc + 10).to_i.to_s }, + body: { type: "error.list", errors: [ code: "intercom_version_invalid" ] }.to_json + ) + + expect { execute! }.must_raise(Intercom::ApiVersionInvalid) + end + + it 'should raise ResourceNotFound error on company_not_found code' do + stub_request(:put, uri).to_return( + status: [404, "Not Found"], + headers: { 'X-RateLimit-Reset' => (Time.now.utc + 10).to_i.to_s }, + body: { type: "error.list", errors: [ code: "company_not_found" ] }.to_json + ) + + expect { execute! }.must_raise(Intercom::ResourceNotFound) + end + + it 'should raise TagHasDependentObjects error on tag_has_dependent_objects code' do + stub_request(:delete, tag_uri).to_return( + status: [400, "Bad Request"], + headers: { 'X-RateLimit-Reset' => (Time.now.utc + 10).to_i.to_s }, + body: { type: "error.list", errors: [ code: "tag_has_dependent_objects" ] }.to_json + ) + + expect { del_req.execute(tag_uri, token: 'test-token') }.must_raise(Intercom::TagHasDependentObjects) + end end -end \ No newline at end of file +end diff --git a/spec/unit/intercom/scroll_collection_proxy_spec.rb b/spec/unit/intercom/scroll_collection_proxy_spec.rb new file mode 100644 index 00000000..49ab7aaf --- /dev/null +++ b/spec/unit/intercom/scroll_collection_proxy_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Intercom::ScrollCollectionProxy do + let(:client) { Intercom::Client.new(token: 'token') } + + it 'stops iterating if no companies returned' do + client.expects(:get).with('/companies/scroll', '').returns(companies_scroll(false)) + names = [] + client.companies.scroll.each { |company| names << company.name } + _(names).must_equal %w[] + end + + it 'keeps iterating if companies returned' do + client.expects(:get).with('/companies/scroll', '').returns(companies_scroll(true)) + client.expects(:get).with('/companies/scroll', scroll_param: 'da6bbbac-25f6-4f07-866b-b911082d7').returns(companies_scroll(false)) + names = [] + client.companies.scroll.each { |company| names << company.name } + end + + it 'supports indexed array access' do + client.expects(:get).with('/companies/scroll', '').returns(companies_scroll(true)) + _(client.companies.scroll[0].name).must_equal 'company1' + end + + it 'supports map' do + client.expects(:get).with('/companies/scroll', '').returns(companies_scroll(true)) + client.expects(:get).with('/companies/scroll', scroll_param: 'da6bbbac-25f6-4f07-866b-b911082d7').returns(companies_scroll(false)) + names = client.companies.scroll.map(&:name) + _(names).must_equal %w[company1 company2 company3] + end + + it 'returns one page scroll' do + client.expects(:get).with('/companies/scroll', '').returns(companies_scroll(true)) + scroll = client.companies.scroll.next + names = [] + scroll.records.each { |usr| names << usr.name } + _(names).must_equal %w[company1 company2 company3] + end + + it 'keeps iterating if called with scroll_param' do + client.expects(:get).with('/companies/scroll', '').returns(companies_scroll(true)) + client.expects(:get).with('/companies/scroll', scroll_param: 'da6bbbac-25f6-4f07-866b-b911082d7').returns(companies_scroll(true)) + scroll = client.companies.scroll.next + scroll = client.companies.scroll.next('da6bbbac-25f6-4f07-866b-b911082d7') + scroll.records.each(&:name) + end + + it 'works with an empty list' do + client.expects(:get).with('/companies/scroll', '').returns(companies_scroll(false)) + scroll = client.companies.scroll.next + names = [] + scroll.records.each { |usr| names << usr.name } + _(names).must_equal %w[] + end +end diff --git a/spec/unit/intercom/search_collection_proxy_spec.rb b/spec/unit/intercom/search_collection_proxy_spec.rb new file mode 100644 index 00000000..ffc2ee3c --- /dev/null +++ b/spec/unit/intercom/search_collection_proxy_spec.rb @@ -0,0 +1,60 @@ +require "spec_helper" + +describe Intercom::SearchCollectionProxy do + let(:client) { Intercom::Client.new(token: 'token') } + + it "sends query to the contact search endpoint" do + client.expects(:post).with("/contacts/search", { query: {} }).returns(page_of_contacts(false)) + client.contacts.search(query: {}).first + end + + it "sends query to the conversation search endpoint" do + client.expects(:post).with("/conversations/search", { query: {} }).returns(test_conversation_list) + client.conversations.search(query: {}).first + end + + it "sends query to the contact search endpoint with sort_field" do + client.expects(:post).with("/contacts/search", { query: {}, sort: { field: "name" } }).returns(page_of_contacts(false)) + client.contacts.search(query: {}, sort_field: "name").first + end + + it "sends query to the contact search endpoint with sort_field and sort_order" do + client.expects(:post).with("/contacts/search", { query: {}, sort: { field: "name", order: "ascending" } }).returns(page_of_contacts(false)) + client.contacts.search(query: {}, sort_field: "name", sort_order: "ascending").first + end + + it "sends query to the contact search endpoint with per_page" do + client.expects(:post).with("/contacts/search", { query: {}, pagination: { per_page: 10 }}).returns(page_of_contacts(false)) + client.contacts.search(query: {}, per_page: 10).first + end + + it "sends query to the contact search endpoint with starting_after" do + client.expects(:post).with("/contacts/search", { query: {}, pagination: { starting_after: "EnCrYpTeDsTrInG" }}).returns(page_of_contacts(false)) + client.contacts.search(query: {}, starting_after: "EnCrYpTeDsTrInG").first + end + + it "stops iterating if no starting_after value" do + client.expects(:post).with("/contacts/search", { query: {} }).returns(page_of_contacts(false)) + emails = [] + client.contacts.search(query: {}).each { |contact| emails << contact.email } + _(emails).must_equal %w[test1@example.com test2@example.com test3@example.com] + end + + it "keeps iterating if starting_after value" do + client.expects(:post).with("/contacts/search", { query: {} }).returns(page_of_contacts(true)) + client.expects(:post).with("/contacts/search", { query: {}, pagination: { starting_after: "EnCrYpTeDsTrInG" }}).returns(page_of_contacts(false)) + emails = [] + client.contacts.search(query: {}).each { |contact| emails << contact.email } + end + + it "supports indexed array access" do + client.expects(:post).with("/contacts/search", { query: {} }).returns(page_of_contacts(false)) + _(client.contacts.search(query: {})[0].email).must_equal 'test1@example.com' + end + + it "supports map" do + client.expects(:post).with("/contacts/search", { query: {} }).returns(page_of_contacts(false)) + emails = client.contacts.search(query: {}).map { |contact| contact.email } + _(emails).must_equal %w[test1@example.com test2@example.com test3@example.com] + end +end diff --git a/spec/unit/intercom/section_spec.rb b/spec/unit/intercom/section_spec.rb new file mode 100644 index 00000000..ff73f672 --- /dev/null +++ b/spec/unit/intercom/section_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Intercom::Section do + let(:client) { Intercom::Client.new(token: 'token') } + + it 'creates a section' do + client.expects(:post).with('/help_center/sections', { 'name' => 'Section 1', 'parent_id' => '1' }).returns(test_section) + client.sections.create(:name => 'Section 1', :parent_id => '1') + end + + it 'lists sections' do + client.expects(:get).with('/help_center/sections', {}).returns(test_section_list) + client.sections.all.each { |t| } + end + + it 'finds a section' do + client.expects(:get).with('/help_center/sections/1', {}).returns(test_section) + client.sections.find(id: '1') + end + + it 'updates a section' do + section = Intercom::Section.new(id: '12345') + client.expects(:put).with('/help_center/sections/12345', {}) + client.sections.save(section) + end + + it 'deletes a section' do + section = Intercom::Section.new(id: '12345') + client.expects(:delete).with('/help_center/sections/12345', {}) + client.sections.delete(section) + end +end \ No newline at end of file diff --git a/spec/unit/intercom/segment_spec.rb b/spec/unit/intercom/segment_spec.rb new file mode 100644 index 00000000..def1dfea --- /dev/null +++ b/spec/unit/intercom/segment_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe "Intercom::Segment" do + + let(:client) { Intercom::Client.new(token: 'token') } + + it 'lists segments' do + client.expects(:get).with('/segments', {}).returns(segment_list) + segments = client.segments.all.to_a + _(segments[0].name).must_equal('Active') + end +end diff --git a/spec/unit/intercom/subscription_spec.rb b/spec/unit/intercom/subscription_spec.rb index 53012faa..42c869cd 100644 --- a/spec/unit/intercom/subscription_spec.rb +++ b/spec/unit/intercom/subscription_spec.rb @@ -1,18 +1,19 @@ require 'spec_helper' describe "Intercom::Subscription" do + let(:client) { Intercom::Client.new(token: 'token') } + it "gets a subscription" do - Intercom.expects(:get).with("/subscriptions/nsub_123456789", {}).returns(test_subscription) - subscription = Intercom::Subscription.find(:id => "nsub_123456789") - subscription.request.topics[0].must_equal "user.created" - subscription.request.topics[1].must_equal "conversation.user.replied" + client.expects(:get).with("/subscriptions/nsub_123456789", {}).returns(test_subscription) + subscription = client.subscriptions.find(:id => "nsub_123456789") + _(subscription.request.topics[0]).must_equal "user.created" + _(subscription.request.topics[1]).must_equal "conversation.user.replied" end it "creates a subscription" do - Intercom.expects(:post).with("/subscriptions", {'url' => "http://example.com", 'topics' => ["user.created"]}).returns(test_subscription) - subscription = Intercom::Subscription.create(:url => "http://example.com", :topics => ["user.created"]) - subscription.request.topics[0].must_equal "user.created" - subscription.request.url.must_equal "http://example.com" + client.expects(:post).with("/subscriptions", {'url' => "http://example.com", 'topics' => ["user.created"]}).returns(test_subscription) + subscription = client.subscriptions.create(:url => "http://example.com", :topics => ["user.created"]) + _(subscription.request.topics[0]).must_equal "user.created" + _(subscription.request.url).must_equal "http://example.com" end - -end \ No newline at end of file +end diff --git a/spec/unit/intercom/tag_spec.rb b/spec/unit/intercom/tag_spec.rb index c4ee06ed..b18c97a7 100644 --- a/spec/unit/intercom/tag_spec.rb +++ b/spec/unit/intercom/tag_spec.rb @@ -1,23 +1,37 @@ +# frozen_string_literal: true + require 'spec_helper' -describe "Intercom::Tag" do - it "gets a tag" do - Intercom.expects(:get).with("/tags", {:name => "Test Tag"}).returns(test_tag) - tag = Intercom::Tag.find(:name => "Test Tag") - tag.name.must_equal "Test Tag" +describe 'Intercom::Tag' do + let(:client) { Intercom::Client.new(token: 'token') } + + it 'creates a tag' do + client.expects(:post).with('/tags', 'name' => 'Test Tag').returns(test_tag) + tag = client.tags.create(name: 'Test Tag') + _(tag.name).must_equal 'Test Tag' end - it "creates a tag" do - Intercom.expects(:post).with("/tags", {'name' => "Test Tag"}).returns(test_tag) - tag = Intercom::Tag.create(:name => "Test Tag") - tag.name.must_equal "Test Tag" + it 'finds a tag by id' do + client.expects(:get).with('/tags/4f73428b5e4dfc000b000112', {}).returns(test_tag) + tag = client.tags.find(id: '4f73428b5e4dfc000b000112') + _(tag.name).must_equal 'Test Tag' end - it "tags users" do - Intercom.expects(:post).with("/tags", {'name' => "Test Tag", 'user_ids' => ["abc123", "def456"], 'tag_or_untag' => "tag"}).returns(test_tag) - tag = Intercom::Tag.create(:name => "Test Tag", :user_ids => ["abc123", "def456"], :tag_or_untag => "tag") - tag.name.must_equal "Test Tag" - tag.tagged_user_count.must_equal 2 + it 'tags companies' do + client.expects(:post).with('/tags', 'name' => 'Test Tag', 'companies' => [{ company_id: 'abc123' }, { company_id: 'def456' }], 'tag_or_untag' => 'tag').returns(test_tag) + tag = client.tags.tag(name: 'Test Tag', companies: [{ company_id: 'abc123' }, { company_id: 'def456' }]) + _(tag.name).must_equal 'Test Tag' + _(tag.tagged_company_count).must_equal 2 end -end \ No newline at end of file + it 'untags companies' do + client.expects(:post).with('/tags', 'name' => 'Test Tag', 'companies' => [{ company_id: 'abc123', untag: true }, { company_id: 'def456', untag: true }], 'tag_or_untag' => 'untag').returns(test_tag) + client.tags.untag(name: 'Test Tag', companies: [{ company_id: 'abc123' }, { company_id: 'def456' }]) + end + + it 'delete tags' do + tag = Intercom::Tag.new('id' => '1') + client.expects(:delete).with('/tags/1', {}).returns(tag) + client.tags.delete(tag) + end +end diff --git a/spec/unit/intercom/team_spec.rb b/spec/unit/intercom/team_spec.rb new file mode 100644 index 00000000..a3b3b785 --- /dev/null +++ b/spec/unit/intercom/team_spec.rb @@ -0,0 +1,21 @@ +require "spec_helper" + +describe "Intercom::Team" do + let(:client) { Intercom::Client.new(token: 'token') } + + it "returns a CollectionProxy for all without making any requests" do + client.expects(:execute_request).never + all = client.teams.all + _(all).must_be_instance_of(Intercom::ClientCollectionProxy) + end + + it 'gets an team list' do + client.expects(:get).with("/teams", {}).returns(test_team_list) + client.teams.all.each { |t| } + end + + it "gets an team" do + client.expects(:get).with("/teams/1234", {}).returns(test_team) + client.teams.find(:id => "1234") + end +end diff --git a/spec/unit/intercom/traits/api_resource_spec.rb b/spec/unit/intercom/traits/api_resource_spec.rb index 2a485f78..7afc144f 100644 --- a/spec/unit/intercom/traits/api_resource_spec.rb +++ b/spec/unit/intercom/traits/api_resource_spec.rb @@ -1,85 +1,175 @@ -require "spec_helper" +# frozen_string_literal: true + +require 'spec_helper' describe Intercom::Traits::ApiResource do let(:object_json) do - {"type"=>"company", - "id"=>"aaaaaaaaaaaaaaaaaaaaaaaa", - "app_id"=>"some-app-id", - "name"=>"SuperSuite", - "plan_id"=>1, - "remote_company_id"=>"8", - "remote_created_at"=>103201, - "created_at"=>1374056196, - "user_count"=>1, - "custom_attributes"=>{}} - end - let(:api_resource) { DummyClass.new.extend(Intercom::Traits::ApiResource)} + { 'type' => 'company', + 'id' => 'aaaaaaaaaaaaaaaaaaaaaaaa', + 'app_id' => 'some-app-id', + 'name' => 'SuperSuite', + 'plan_id' => 1, + 'remote_company_id' => '8', + 'remote_created_at' => 103_201, + 'created_at' => 1_374_056_196, + 'user_count' => 1, + 'custom_attributes' => {}, + 'metadata' => { + 'type' => 'user', + 'color' => 'cyan' + }, + 'nested_fields' => { + 'type' => 'nested_fields_content', + 'field_1' => { + 'type' => 'field_content', + 'name' => 'Nested Field' + } + } + } + end + + let(:object_hash) do + { + type: 'company', + id: 'aaaaaaaaaaaaaaaaaaaaaaaa', + app_id: 'some-app-id', + name: 'SuperSuite', + plan_id: 1, + remote_company_id: '8', + remote_created_at: 103_201, + created_at: 1_374_056_196, + user_count: 1, + custom_attributes: { type: 'ping' }, + metadata: { + type: 'user', + color: 'cyan' + }, + nested_fields: { + type: 'nested_fields_content', + field_1: { + type: 'field_content', + name: 'Nested Field' + } + } + } + end + + let(:api_resource) { DummyClass.new.extend(Intercom::Traits::ApiResource) } before(:each) { api_resource.from_response(object_json) } - it "does not set type on parsing json" do - api_resource.wont_respond_to :type + it 'coerces time on parsing json' do + assert_equal Time.at(1_374_056_196), api_resource.created_at end - it "coerces time on parsing json" do - assert_equal Time.at(1374056196), api_resource.created_at + it 'exposes string' do + assert_equal Time.at(1_374_056_196), api_resource.created_at end - it "exposes string" do - assert_equal Time.at(1374056196), api_resource.created_at + it "treats 'metadata' as a plain hash, not a typed object" do + assert_equal Hash, api_resource.metadata.class end - it "dynamically defines accessors when a non-existent property is called that looks like a setter" do - api_resource.wont_respond_to :spiders + it 'dynamically defines accessors when a non-existent property is called that looks like a setter' do + _(api_resource).wont_respond_to :spiders api_resource.spiders = 4 - api_resource.must_respond_to :spiders + _(api_resource).must_respond_to :spiders end - it "calls dynamically defined getter when asked" do + it 'calls dynamically defined getter when asked' do api_resource.foo = 4 assert_equal 4, api_resource.foo end - it "accepts unix timestamps into dynamically defined date setters" do - api_resource.foo_at = 1401200468 - assert_equal 1401200468, api_resource.instance_variable_get(:@foo_at) + it 'accepts unix timestamps into dynamically defined date setters' do + api_resource.foo_at = 1_401_200_468 + assert_equal 1_401_200_468, api_resource.instance_variable_get(:@foo_at) end - it "exposes dates correctly for dynamically defined getters" do - api_resource.foo_at = 1401200468 - assert_equal Time.at(1401200468), api_resource.foo_at + it 'exposes dates correctly for dynamically defined getters' do + api_resource.foo_at = 1_401_200_468 + assert_equal Time.at(1_401_200_468), api_resource.foo_at end - it "throws regular method missing error when non-existent getter is called that is backed by an instance variable" do + it 'throws regular method missing error when non-existent getter is called that is backed by an instance variable' do api_resource.instance_variable_set(:@bar, 'you cant see me') - proc { api_resource.bar }.must_raise NoMethodError + _(proc { api_resource.bar }).must_raise NoMethodError end - it "throws attribute not set error when non-existent getter is called that is not backed by an instance variable" do - proc { api_resource.flubber }.must_raise Intercom::AttributeNotSetError + it 'throws attribute not set error when non-existent getter is called that is not backed by an instance variable' do + _(proc { api_resource.flubber }).must_raise Intercom::AttributeNotSetError end - it "throws regular method missing error when non-existent method is called that cannot be an accessor" do - proc { api_resource.flubber! }.must_raise NoMethodError - proc { api_resource.flubber? }.must_raise NoMethodError + it 'throws regular method missing error when non-existent method is called that cannot be an accessor' do + _(proc { api_resource.flubber! }).must_raise NoMethodError + _(proc { api_resource.flubber? }).must_raise NoMethodError end - it "throws regular method missing error when non-existent setter is called with multiple arguments" do - proc { api_resource.send(:flubber=, 'a', 'b') }.must_raise NoMethodError + it 'throws regular method missing error when non-existent setter is called with multiple arguments' do + _(proc { api_resource.send(:flubber=, 'a', 'b') }).must_raise NoMethodError end - it "an initialized ApiResource is equal to on generated from a response" do + it 'an initialized ApiResource is equal to one generated from a response' do class ConcreteApiResource; include Intercom::Traits::ApiResource; end initialized_api_resource = ConcreteApiResource.new(object_json) except(object_json, 'type').keys.each do |attribute| assert_equal initialized_api_resource.send(attribute), api_resource.send(attribute) end end - + + it 'initialized ApiResource using hash is equal to one generated from response' do + class ConcreteApiResource; include Intercom::Traits::ApiResource; end + + api_resource.from_hash(object_hash) + initialized_api_resource = ConcreteApiResource.new(object_hash) + except(object_json, 'type').keys.each do |attribute| + assert_equal initialized_api_resource.send(attribute), api_resource.send(attribute) + end + end + + describe 'correctly equates two resources' do + class DummyResource; include Intercom::Traits::ApiResource; end + + specify 'if each resource has the same class and same value' do + api_resource1 = DummyResource.new(object_json) + api_resource2 = DummyResource.new(object_json) + assert_equal (api_resource1 == api_resource2), true + end + + specify 'if each resource has the same class and different value' do + object2_json = object_json.merge('id' => 'bbbbbb') + api_resource1 = DummyResource.new(object_json) + api_resource2 = DummyResource.new(object2_json) + assert_equal (api_resource1 == api_resource2), false + end + + specify 'if each resource has a different class' do + dummy_resource = DummyResource.new(object_json) + assert_equal (dummy_resource == api_resource), false + end + end + + it 'correctly generates submittable hash when no updates' do + assert_equal api_resource.to_submittable_hash, {} + end + + it 'correctly generates submittable hash when there are updates' do + api_resource.name = 'SuperSuite updated' + api_resource.nested_fields.field_1.name = 'Updated Name' + assert_equal api_resource.to_submittable_hash, { + 'name' => 'SuperSuite updated', + 'nested_fields' => { + 'field_1' => { + 'name' => 'Updated Name' + } + } + } + end + def except(h, *keys) keys.each { |key| h.delete(key) } h end - + class DummyClass; end end diff --git a/spec/unit/intercom/user_spec.rb b/spec/unit/intercom/user_spec.rb index 5ad034c1..a570abfe 100644 --- a/spec/unit/intercom/user_spec.rb +++ b/spec/unit/intercom/user_spec.rb @@ -1,225 +1,371 @@ -require "spec_helper" +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Intercom::User' do + let (:client) { Intercom::Client.new(token: 'token') } -describe "Intercom::User" do it "to_hash'es itself" do created_at = Time.now - user = Intercom::User.new(:email => "jim@example.com", :user_id => "12345", :created_at => created_at, :name => "Jim Bob") + user = Intercom::User.new(email: 'jim@example.com', user_id: '12345', created_at: created_at, name: 'Jim Bob') as_hash = user.to_hash - as_hash["email"].must_equal "jim@example.com" - as_hash["user_id"].must_equal "12345" - as_hash["created_at"].must_equal created_at.to_i - as_hash["name"].must_equal "Jim Bob" + _(as_hash['email']).must_equal 'jim@example.com' + _(as_hash['user_id']).must_equal '12345' + _(as_hash['created_at']).must_equal created_at.to_i + _(as_hash['name']).must_equal 'Jim Bob' end - it "presents created_at and last_impression_at as Date" do + it 'presents created_at and last_impression_at as Date' do now = Time.now - user = Intercom::User.from_api(:created_at => now, :last_impression_at => now) - user.created_at.must_be_kind_of Time - user.created_at.to_s.must_equal now.to_s - user.last_impression_at.must_be_kind_of Time - user.last_impression_at.to_s.must_equal now.to_s + user = Intercom::User.from_api(created_at: now, last_impression_at: now) + _(user.created_at).must_be_kind_of Time + _(user.created_at.to_s).must_equal now.to_s + _(user.last_impression_at).must_be_kind_of Time + _(user.last_impression_at.to_s).must_equal now.to_s end - it "is throws a Intercom::AttributeNotSetError on trying to access an attribute that has not been set" do + it 'is throws a Intercom::AttributeNotSetError on trying to access an attribute that has not been set' do user = Intercom::User.new - proc { user.foo_property }.must_raise Intercom::AttributeNotSetError + _(proc { user.foo_property }).must_raise Intercom::AttributeNotSetError end - it "presents a complete user record correctly" do + it 'presents a complete user record correctly' do user = Intercom::User.from_api(test_user) - user.user_id.must_equal 'id-from-customers-app' - user.email.must_equal 'bob@example.com' - user.name.must_equal 'Joe Schmoe' - user.app_id.must_equal 'the-app-id' - user.session_count.must_equal 123 - user.created_at.to_i.must_equal 1401970114 - user.remote_created_at.to_i.must_equal 1393613864 - user.updated_at.to_i.must_equal 1401970114 - - user.avatar.must_be_kind_of Intercom::Avatar - user.avatar.image_url.must_equal 'https://graph.facebook.com/1/picture?width=24&height=24' - - user.companies.must_be_kind_of Array - user.companies.size.must_equal 1 - user.companies[0].must_be_kind_of Intercom::Company - user.companies[0].company_id.must_equal "123" - user.companies[0].id.must_equal "bbbbbbbbbbbbbbbbbbbbbbbb" - user.companies[0].app_id.must_equal "the-app-id" - user.companies[0].name.must_equal "Company 1" - user.companies[0].remote_created_at.to_i.must_equal 1390936440 - user.companies[0].created_at.to_i.must_equal 1401970114 - user.companies[0].updated_at.to_i.must_equal 1401970114 - user.companies[0].last_request_at.to_i.must_equal 1401970113 - user.companies[0].monthly_spend.must_equal 0 - user.companies[0].session_count.must_equal 0 - user.companies[0].user_count.must_equal 1 - user.companies[0].tag_ids.must_equal [] - - user.custom_attributes.must_be_kind_of Intercom::Lib::FlatStore - user.custom_attributes["a"].must_equal "b" - user.custom_attributes["b"].must_equal 2 - - user.social_profiles.size.must_equal 4 + _(user.user_id).must_equal 'id-from-customers-app' + _(user.email).must_equal 'bob@example.com' + _(user.name).must_equal 'Joe Schmoe' + _(user.app_id).must_equal 'the-app-id' + _(user.session_count).must_equal 123 + _(user.created_at.to_i).must_equal 1_401_970_114 + _(user.remote_created_at.to_i).must_equal 1_393_613_864 + _(user.updated_at.to_i).must_equal 1_401_970_114 + + _(user.avatar).must_be_kind_of Intercom::Avatar + _(user.avatar.image_url).must_equal 'https://graph.facebook.com/1/picture?width=24&height=24' + + _(user.companies).must_be_kind_of Array + _(user.companies.size).must_equal 1 + _(user.companies[0]).must_be_kind_of Intercom::Company + _(user.companies[0].company_id).must_equal '123' + _(user.companies[0].id).must_equal 'bbbbbbbbbbbbbbbbbbbbbbbb' + _(user.companies[0].app_id).must_equal 'the-app-id' + _(user.companies[0].name).must_equal 'Company 1' + _(user.companies[0].remote_created_at.to_i).must_equal 1_390_936_440 + _(user.companies[0].created_at.to_i).must_equal 1_401_970_114 + _(user.companies[0].updated_at.to_i).must_equal 1_401_970_114 + _(user.companies[0].last_request_at.to_i).must_equal 1_401_970_113 + _(user.companies[0].monthly_spend).must_equal 0 + _(user.companies[0].session_count).must_equal 0 + _(user.companies[0].user_count).must_equal 1 + _(user.companies[0].tag_ids).must_equal [] + + _(user.custom_attributes).must_be_kind_of Intercom::Lib::FlatStore + _(user.custom_attributes['a']).must_equal 'b' + _(user.custom_attributes['b']).must_equal 2 + + _(user.social_profiles.size).must_equal 4 twitter_account = user.social_profiles.first - twitter_account.must_be_kind_of Intercom::SocialProfile - twitter_account.name.must_equal "twitter" - twitter_account.username.must_equal "abc" - twitter_account.url.must_equal "http://twitter.com/abc" - - user.location_data.must_be_kind_of Intercom::LocationData - user.location_data.city_name.must_equal "Dublin" - user.location_data.continent_code.must_equal "EU" - user.location_data.country_name.must_equal "Ireland" - user.location_data.latitude.must_equal "90" - user.location_data.longitude.must_equal "10" - user.location_data.country_code.must_equal "IRL" + _(twitter_account).must_be_kind_of Intercom::SocialProfile + _(twitter_account.name).must_equal 'twitter' + _(twitter_account.username).must_equal 'abc' + _(twitter_account.url).must_equal 'http://twitter.com/abc' + + _(user.location_data).must_be_kind_of Intercom::LocationData + _(user.location_data.city_name).must_equal 'Dublin' + _(user.location_data.continent_code).must_equal 'EU' + _(user.location_data.country_name).must_equal 'Ireland' + _(user.location_data.latitude).must_equal '90' + _(user.location_data.longitude).must_equal '10' + _(user.location_data.country_code).must_equal 'IRL' + + _(user.unsubscribed_from_emails).must_equal true + _(user.user_agent_data).must_equal 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11' + end - user.unsubscribed_from_emails.must_equal true - user.user_agent_data.must_equal "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11" + it 'allows update_last_request_at' do + client.expects(:post).with('/users', 'user_id' => '1224242', 'update_last_request_at' => true, 'custom_attributes' => {}).returns('user_id' => 'i-1224242', 'last_request_at' => 1_414_509_439) + client.deprecated__users.create(user_id: '1224242', update_last_request_at: true) end - it "allows easy setting of custom data" do + it 'allows easy setting of custom data' do now = Time.now user = Intercom::User.new - user.custom_attributes["mad"] = 123 - user.custom_attributes["other"] = now.to_i - user.custom_attributes["thing"] = "yay" - user.to_hash["custom_attributes"].must_equal "mad" => 123, "other" => now.to_i, "thing" => "yay" + user.custom_attributes['mad'] = 123 + user.custom_attributes['other'] = now.to_i + user.custom_attributes['thing'] = 'yay' + _(user.to_hash['custom_attributes']).must_equal 'mad' => 123, 'other' => now.to_i, 'thing' => 'yay' end - it "allows easy setting of multiple companies" do - user = Intercom::User.new() + it 'allows easy setting of multiple companies' do + user = Intercom::User.new companies = [ - {"name" => "Intercom", "company_id" => "6"}, - {"name" => "Test", "company_id" => "9"} + { 'name' => 'Intercom', 'company_id' => '6' }, + { 'name' => 'Test', 'company_id' => '9' } ] user.companies = companies - user.to_hash["companies"].must_equal companies + _(user.to_hash['companies']).must_equal companies end - it "rejects nested data structures in custom_attributes" do - user = Intercom::User.new() - - proc { user.custom_attributes["thing"] = [1] }.must_raise(ArgumentError) - proc { user.custom_attributes["thing"] = {1 => 2} }.must_raise(ArgumentError) - proc { user.custom_attributes["thing"] = {1 => {2 => 3}} }.must_raise(ArgumentError) + it 'rejects lists in custom_attributes' do + user = Intercom::User.new + + _(proc { user.custom_attributes['thing'] = [1] }).must_raise(ArgumentError) user = Intercom::User.from_api(test_user) - proc { user.custom_attributes["thing"] = [1] }.must_raise(ArgumentError) + _(proc { user.custom_attributes['thing'] = [1] }).must_raise(ArgumentError) end - describe "incrementing custom_attributes fields" do + describe 'incrementing custom_attributes fields' do before :each do @now = Time.now - @user = Intercom::User.new("email" => "jo@example.com", :user_id => "i-1224242", :custom_attributes => {"mad" => 123, "another" => 432, "other" => @now.to_i, :thing => "yay"}) + @user = Intercom::User.new('email' => 'jo@example.com', :user_id => 'i-1224242', :custom_attributes => { 'mad' => 123, 'another' => 432, 'other' => @now.to_i, :thing => 'yay' }) end - it "increments up by 1 with no args" do - @user.increment("mad") - @user.to_hash["custom_attributes"]["mad"].must_equal 124 + it 'increments up by 1 with no args' do + @user.increment('mad') + _(@user.to_hash['custom_attributes']['mad']).must_equal 124 end - it "increments up by given value" do - @user.increment("mad", 4) - @user.to_hash["custom_attributes"]["mad"].must_equal 127 + it 'increments up by given value' do + @user.increment('mad', 4) + _(@user.to_hash['custom_attributes']['mad']).must_equal 127 end - it "increments down by given value" do - @user.increment("mad", -1) - @user.to_hash["custom_attributes"]["mad"].must_equal 122 + it 'increments down by given value' do + @user.increment('mad', -1) + _(@user.to_hash['custom_attributes']['mad']).must_equal 122 end - it "can increment new custom data fields" do - @user.increment("new_field", 3) - @user.to_hash["custom_attributes"]["new_field"].must_equal 3 + it 'can increment new custom data fields' do + @user.increment('new_field', 3) + _(@user.to_hash['custom_attributes']['new_field']).must_equal 3 end - it "can call increment on the same key twice and increment by 2" do - @user.increment("mad") - @user.increment("mad") - @user.to_hash["custom_attributes"]["mad"].must_equal 125 + it 'can call increment on the same key twice and increment by 2' do + @user.increment('mad') + @user.increment('mad') + _(@user.to_hash['custom_attributes']['mad']).must_equal 125 end end - it "fetches a user" do - Intercom.expects(:get).with("/users", {"email" => "bo@example.com"}).returns(test_user) - user = Intercom::User.find("email" => "bo@example.com") - user.email.must_equal "bob@example.com" - user.name.must_equal "Joe Schmoe" - user.session_count.must_equal 123 + describe 'decrementing custom_attributes fields' do + before :each do + @now = Time.now + @user = Intercom::User.new('email' => 'jo@example.com', :user_id => 'i-1224242', :custom_attributes => { 'mad' => 123, 'another' => 432, 'other' => @now.to_i, :thing => 'yay' }) + end + + it 'decrements down by 1 with no args' do + @user.decrement('mad') + _(@user.to_hash['custom_attributes']['mad']).must_equal 122 + end + + it 'decrements down by given value' do + @user.decrement('mad', 3) + _(@user.to_hash['custom_attributes']['mad']).must_equal 120 + end + + it 'can decrement new custom data fields' do + @user.decrement('new_field', 5) + _(@user.to_hash['custom_attributes']['new_field']).must_equal(-5) + end + + it 'can call decrement on the same key twice and decrement by 2' do + @user.decrement('mad') + @user.decrement('mad') + _(@user.to_hash['custom_attributes']['mad']).must_equal 121 + end + end + + it 'fetches a user' do + client.expects(:get).with('/users', 'email' => 'bo@example.com').returns(test_user) + user = client.deprecated__users.find('email' => 'bo@example.com') + _(user.email).must_equal 'bob@example.com' + _(user.name).must_equal 'Joe Schmoe' + _(user.session_count).must_equal 123 + end + + it 'gets users by tag' do + client.expects(:get).with('/users?tag_id=124', {}).returns(page_of_users(false)) + client.deprecated__users.by_tag(124).each { |t| } + end + + it 'gets users by segment' do + client.expects(:get).with('/users?segment_id=124', {}).returns(page_of_users(false)) + client.deprecated__users.by_segment(124).each { |t| } end - it "saves a user (always sends custom_attributes)" do - user = Intercom::User.new("email" => "jo@example.com", :user_id => "i-1224242") - Intercom.expects(:post).with("/users", {"email" => "jo@example.com", "user_id" => "i-1224242", "custom_attributes" => {}}).returns({"email" => "jo@example.com", "user_id" => "i-1224242"}) - user.save + it 'saves a user (always sends custom_attributes)' do + user = Intercom::User.new('email' => 'jo@example.com', :user_id => 'i-1224242') + client.expects(:post).with('/users', 'email' => 'jo@example.com', 'user_id' => 'i-1224242', 'custom_attributes' => {}).returns('email' => 'jo@example.com', 'user_id' => 'i-1224242') + client.deprecated__users.save(user) end - it "saves a user with a company" do - user = Intercom::User.new("email" => "jo@example.com", :user_id => "i-1224242", :company => {'company_id' => 6, 'name' => "Intercom"}) - Intercom.expects(:post).with("/users", {'custom_attributes' => {}, "user_id" => "i-1224242", "email" => "jo@example.com", "company" => {"company_id" => 6, "name" => "Intercom"}}).returns({"email" => "jo@example.com", "user_id" => "i-1224242"}) - user.save + it 'saves a user with a company' do + user = Intercom::User.new('email' => 'jo@example.com', :user_id => 'i-1224242', :company => { 'company_id' => 6, 'name' => 'Intercom' }) + client.expects(:post).with('/users', 'custom_attributes' => {}, 'user_id' => 'i-1224242', 'email' => 'jo@example.com', 'company' => { 'company_id' => 6, 'name' => 'Intercom' }).returns('email' => 'jo@example.com', 'user_id' => 'i-1224242') + client.deprecated__users.save(user) end - it "saves a user with a companies" do - user = Intercom::User.new("email" => "jo@example.com", :user_id => "i-1224242", :companies => [{'company_id' => 6, 'name' => "Intercom"}]) - Intercom.expects(:post).with("/users", {'custom_attributes' => {}, "email" => "jo@example.com", "user_id" => "i-1224242", "companies" => [{"company_id" => 6, "name" => "Intercom"}]}).returns({"email" => "jo@example.com", "user_id" => "i-1224242"}) - user.save + it 'saves a user with a companies' do + user = Intercom::User.new('email' => 'jo@example.com', :user_id => 'i-1224242', :companies => [{ 'company_id' => 6, 'name' => 'Intercom' }]) + client.expects(:post).with('/users', 'custom_attributes' => {}, 'email' => 'jo@example.com', 'user_id' => 'i-1224242', 'companies' => [{ 'company_id' => 6, 'name' => 'Intercom' }]).returns('email' => 'jo@example.com', 'user_id' => 'i-1224242') + client.deprecated__users.save(user) end - + it 'can save a user with a nil email' do - user = Intercom::User.new("email" => nil, :user_id => "i-1224242", :companies => [{'company_id' => 6, 'name' => "Intercom"}]) - Intercom.expects(:post).with("/users", {'custom_attributes' => {}, "email" => nil, "user_id" => "i-1224242", "companies" => [{"company_id" => 6, "name" => "Intercom"}]}).returns({"email" => nil, "user_id" => "i-1224242"}) - user.save + user = Intercom::User.new('email' => nil, :user_id => 'i-1224242', :companies => [{ 'company_id' => 6, 'name' => 'Intercom' }]) + client.expects(:post).with('/users', 'custom_attributes' => {}, 'email' => nil, 'user_id' => 'i-1224242', 'companies' => [{ 'company_id' => 6, 'name' => 'Intercom' }]).returns('email' => nil, 'user_id' => 'i-1224242') + client.deprecated__users.save(user) end - it "deletes a user" do - user = Intercom::User.new("id" => "1") - Intercom.expects(:delete).with("/users/1", {}).returns(user) - user.delete + it 'archives a user' do + user = Intercom::User.new('id' => '1') + client.expects(:delete).with('/users/1', {}).returns(user) + client.deprecated__users.archive(user) + end + it 'has an alias to archive a user' do + user = Intercom::User.new('id' => '1') + client.expects(:delete).with('/users/1', {}).returns(user) + client.deprecated__users.delete(user) end - it "can use User.create for convenience" do - Intercom.expects(:post).with("/users", {'custom_attributes' => {}, "email" => "jo@example.com", "user_id" => "i-1224242"}).returns({"email" => "jo@example.com", "user_id" => "i-1224242"}) - user = Intercom::User.create("email" => "jo@example.com", :user_id => "i-1224242") - user.email.must_equal "jo@example.com" + it 'sends a request for a hard deletion' do + user = Intercom::User.new('id' => '1') + client.expects(:post).with('/user_delete_requests', intercom_user_id: '1').returns(id: user.id) + client.deprecated__users.request_hard_delete(user) end - it "updates the @user with attributes as set by the server" do - Intercom.expects(:post).with("/users", {"email" => "jo@example.com", "user_id" => "i-1224242", 'custom_attributes' => {}}).returns({"email" => "jo@example.com", "user_id" => "i-1224242", "session_count" => 4}) - user = Intercom::User.create("email" => "jo@example.com", :user_id => "i-1224242") - user.session_count.must_equal 4 + it 'can use client.users.create for convenience' do + client.expects(:post).with('/users', 'custom_attributes' => {}, 'email' => 'jo@example.com', 'user_id' => 'i-1224242').returns('email' => 'jo@example.com', 'user_id' => 'i-1224242') + user = client.deprecated__users.create('email' => 'jo@example.com', :user_id => 'i-1224242') + _(user.email).must_equal 'jo@example.com' end - it "allows setting dates to nil without converting them to 0" do - Intercom.expects(:post).with("/users", {"email" => "jo@example.com", 'custom_attributes' => {}, "remote_created_at" => nil}).returns({"email" => "jo@example.com"}) - user = Intercom::User.create("email" => "jo@example.com", "remote_created_at" => nil) - user.remote_created_at.must_equal nil + it 'updates the user with attributes as set by the server' do + client.expects(:post).with('/users', 'email' => 'jo@example.com', 'user_id' => 'i-1224242', 'custom_attributes' => {}).returns('email' => 'jo@example.com', 'user_id' => 'i-1224242', 'session_count' => 4) + user = client.deprecated__users.create('email' => 'jo@example.com', :user_id => 'i-1224242') + _(user.session_count).must_equal 4 end - it "sets/gets rw keys" do - params = {"email" => "me@example.com", :user_id => "abc123", "name" => "Bob Smith", "last_seen_ip" => "1.2.3.4", "last_seen_user_agent" => "ie6", "created_at" => Time.now} + it 'allows setting dates to nil without converting them to 0' do + client.expects(:post).with('/users', 'email' => 'jo@example.com', 'custom_attributes' => {}, 'remote_created_at' => nil).returns('email' => 'jo@example.com') + user = client.deprecated__users.create('email' => 'jo@example.com', 'remote_created_at' => nil) + assert_nil user.remote_created_at + end + + it 'sets/gets rw keys' do + params = { 'email' => 'me@example.com', :user_id => 'abc123', 'name' => 'Bob Smith', 'last_seen_ip' => '1.2.3.4', 'last_seen_user_agent' => 'ie6', 'created_at' => Time.now } user = Intercom::User.new(params) custom_attributes = (params.keys + ['custom_attributes']).map(&:to_s).sort - user.to_hash.keys.sort.must_equal custom_attributes + _(user.to_hash.keys.sort).must_equal custom_attributes params.keys.each do |key| - user.send(key).to_s.must_equal params[key].to_s + _(user.send(key).to_s).must_equal params[key].to_s end end - it "will allow extra attributes in response from api" do - user = Intercom::User.send(:from_api, {"new_param" => "some value"}) - user.new_param.must_equal "some value" + it 'will allow extra attributes in response from api' do + user = Intercom::User.send(:from_api, 'new_param' => 'some value') + _(user.new_param).must_equal 'some value' end - it "returns a CollectionProxy for all without making any requests" do - Intercom.expects(:execute_request).never - all = Intercom::User.all - all.must_be_instance_of(Intercom::CollectionProxy) + it 'returns a ClientCollectionProxy for all without making any requests' do + client.expects(:execute_request).never + all = client.deprecated__users.all + _(all).must_be_instance_of(Intercom::ClientCollectionProxy) end - it "returns the total number of users" do - Intercom::Count.expects(:user_count).returns('count_info') - Intercom::User.count - end + it 'can print users without crashing' do + client.expects(:get).with('/users', 'email' => 'bo@example.com').returns(test_user) + user = client.deprecated__users.find('email' => 'bo@example.com') + + begin + orignal_stdout = $stdout + $stdout = StringIO.new + + puts user + p user + ensure + $stdout = orignal_stdout + end + end + + describe 'bulk operations' do + let (:job) do + { + 'app_id' => 'app_id', + 'id' => 'super_awesome_job', + 'created_at' => 1_446_033_421, + 'completed_at' => 1_446_048_736, + 'closing_at' => 1_446_034_321, + 'updated_at' => 1_446_048_736, + 'name' => 'api_bulk_job', + 'state' => 'completed', + 'links' => + { + 'error' => 'https://api.intercom.io/jobs/super_awesome_job/error', + 'self' => 'https://api.intercom.io/jobs/super_awesome_job' + }, + 'tasks' => + [ + { + 'id' => 'super_awesome_task', + 'item_count' => 2, + 'created_at' => 1_446_033_421, + 'started_at' => 1_446_033_709, + 'completed_at' => 1_446_033_709, + 'state' => 'completed' + } + ] + } + end + let(:bulk_request) do + { + items: [ + { + method: 'post', + data_type: 'user', + data: { + user_id: 25, + email: 'alice@example.com' + } + }, + { + method: 'delete', + data_type: 'user', + data: { + user_id: 26, + email: 'bob@example.com' + } + } + ] + } + end + let(:users_to_create) do + [ + { + user_id: 25, + email: 'alice@example.com' + } + ] + end + let(:users_to_delete) do + [ + { + user_id: 26, + email: 'bob@example.com' + } + ] + end + + it 'submits a bulk job' do + client.expects(:post).with('/bulk/users', bulk_request).returns(job) + client.deprecated__users.submit_bulk_job(create_items: users_to_create, delete_items: users_to_delete) + end + + it 'adds users to an existing bulk job' do + bulk_request[:job] = { id: 'super_awesome_job' } + client.expects(:post).with('/bulk/users', bulk_request).returns(job) + client.deprecated__users.submit_bulk_job(create_items: users_to_create, delete_items: users_to_delete, job_id: 'super_awesome_job') + end + end end diff --git a/spec/unit/intercom/visitor_spec.rb b/spec/unit/intercom/visitor_spec.rb new file mode 100644 index 00000000..ebabb7ec --- /dev/null +++ b/spec/unit/intercom/visitor_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe 'Intercom::Visitor' do + let (:client) { Intercom::Client.new(token: 'token') } + + it 'can update a visitor with an id' do + visitor = Intercom::Visitor.new(:id => 'de45ae78gae1289cb') + client.expects(:put).with('/visitors/de45ae78gae1289cb', 'custom_attributes' => {}) + client.visitors.save(visitor) + end + + it 'can get a visitor' do + visitor = Intercom::Visitor.new(:id => 'de45ae78gae1289cb') + client.expects(:get).with('/visitors/de45ae78gae1289cb', {}).returns(test_visitor) + client.visitors.find(id: visitor.id) + end + + describe 'converting' do + let(:visitor) { Intercom::Visitor.from_api(user_id: 'visitor_id') } + let(:user) { Intercom::Contact.from_api(id: 'user_id', role: 'user') } + + it 'visitor to user' do + client.expects(:post).with( + '/visitors/convert', + visitor: { user_id: visitor.user_id }, + user: { 'id' => user.id }, + type: 'user' + ).returns(test_contact) + + client.visitors.convert(visitor, user) + end + + it 'visitor to lead' do + client.expects(:post).with( + '/visitors/convert', + visitor: { user_id: visitor.user_id }, + type: 'lead' + ).returns(test_contact(role: 'lead')) + + client.visitors.convert(visitor) + end + end + + it 'deletes a visitor' do + visitor = Intercom::Visitor.new('id' => '1') + client.expects(:delete).with('/visitors/1', {}).returns(visitor) + client.visitors.delete(visitor) + end +end diff --git a/spec/unit/intercom_spec.rb b/spec/unit/intercom_spec.rb index a43b4ed6..4c305d23 100644 --- a/spec/unit/intercom_spec.rb +++ b/spec/unit/intercom_spec.rb @@ -1,90 +1,9 @@ -require "spec_helper" +# frozen_string_literal: true -describe Intercom do - it "has a version number" do - Intercom::VERSION.must_match(/\d+\.\d+\.\d+/) - end - - describe "API" do - before do - Intercom.app_id = "abc123" - Intercom.app_api_key = "super-secret-key" - end - - it "raises ArgumentError if no app_id or app_api_key specified" do - Intercom.app_id = nil - Intercom.app_api_key = nil - proc { Intercom.target_base_url }.must_raise ArgumentError, "You must set both Intercom.app_id and Intercom.app_api_key to use this client. See https://github.com/intercom/intercom-ruby for usage examples." - end - - it 'raises an Intercom::MultipleMatchingUsers error when receiving a conflict' do - multiple_matching_error = { 'type' => 'error.list', 'errors' => [{ 'code' => 'conflict', 'message' => 'Multiple existing users match this email address - must be more specific using user_id' }]} - proc {Intercom::Request.new('/users', :get).raise_application_errors_on_failure(multiple_matching_error, 400)}.must_raise(Intercom::MultipleMatchingUsersError) - end - - it "returns the app_id and app_api_key previously set" do - Intercom.app_id.must_equal "abc123" - Intercom.app_api_key.must_equal "super-secret-key" - end - - it "defaults to https to api.intercom.io" do - Intercom.target_base_url.must_equal "https://abc123:super-secret-key@api.intercom.io" - end - - it "raises ResourceNotFound if get a 404" do - - end - - describe "overriding protocol/hostname" do - before do - @protocol = Intercom.protocol - @hostname = Intercom.hostname - Intercom.endpoints = nil - end +require 'spec_helper' - after do - Intercom.protocol = @protocol - Intercom.hostname = @hostname - Intercom.endpoints = ["https://api.intercom.io"] - end - - it "allows overriding of the endpoint and protocol" do - Intercom.protocol = "http" - Intercom.hostname = "localhost:3000" - Intercom.target_base_url.must_equal "http://abc123:super-secret-key@localhost:3000" - end - - it "prefers endpoints" do - Intercom.endpoint = "https://localhost:7654" - Intercom.target_base_url.must_equal "https://abc123:super-secret-key@localhost:7654" - Intercom.endpoints = unshuffleable_array(["http://example.com","https://localhost:7654"]) - Intercom.target_base_url.must_equal "http://abc123:super-secret-key@example.com" - end - - it "has endpoints" do - Intercom.endpoints.must_equal ["https://api.intercom.io"] - Intercom.endpoints = ["http://example.com","https://localhost:7654"] - Intercom.endpoints.must_equal ["http://example.com","https://localhost:7654"] - end - - it "should randomize endpoints if last checked endpoint is > 5 minutes ago" do - Intercom.instance_variable_set(:@current_endpoint, "http://start") - Intercom.instance_variable_set(:@endpoints, ["http://alternative"]) - Intercom.instance_variable_set(:@endpoint_randomized_at, Time.now - 120) - Intercom.current_endpoint.must_equal "http://start" - Intercom.instance_variable_set(:@endpoint_randomized_at, Time.now - 360) - Intercom.current_endpoint.must_equal "http://alternative" - Intercom.instance_variable_get(:@endpoint_randomized_at).to_i.must_be_close_to Time.now.to_i - end - end - end - - - it "checks for email or user id" do - proc { Intercom.check_required_params("else") }.must_raise ArgumentError, "Expected params Hash, got String" - proc { Intercom.check_required_params(:something => "else") }.must_raise ArgumentError, "Either email or user_id must be specified" - Intercom.check_required_params(:email => "bob@example.com", :something => "else") - Intercom.check_required_params("email" => "bob@example.com", :something => "else") - Intercom.check_required_params(:user_id => "123") +describe Intercom do + it 'has a version number' do + _(Intercom::VERSION).must_match(/\d+\.\d+\.\d+/) end end