├── README.md ├── .rspec ├── .gitignore ├── lib ├── lib │ ├── formats.rb │ └── icalendar.rb ├── models │ ├── customer.rb │ ├── calendar.rb │ └── event.rb └── api │ ├── application_api.rb │ ├── calendar_api.rb │ └── event_api.rb ├── Gemfile ├── config.ru ├── spec ├── models │ ├── customer_spec.rb │ └── event_spec.rb ├── factories.rb ├── support │ ├── error_matcher.rb │ └── event_matcher.rb ├── spec_helper.rb └── api │ ├── calendar_api_spec.rb │ ├── application_api_spec.rb │ └── event_api_spec.rb ├── Rakefile └── Gemfile.lock /README.md: -------------------------------------------------------------------------------- 1 | calendar_api 2 | ============ -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format=progress 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | coverage 6 | InstalledFiles 7 | lib/bundler/man 8 | pkg 9 | rdoc 10 | spec/reports 11 | test/tmp 12 | test/version_tmp 13 | tmp 14 | 15 | # YARD artifacts 16 | .yardoc 17 | _yardoc 18 | doc/ 19 | -------------------------------------------------------------------------------- /lib/lib/formats.rb: -------------------------------------------------------------------------------- 1 | module Grape 2 | module Middleware 3 | class Base 4 | module Formats 5 | CONTENT_TYPES[:ical] = "text/calendar" 6 | FORMATTERS[:ical] = :encode_ical 7 | 8 | def encode_ical(object) 9 | object.respond_to?(:to_ical) ? object.to_ical : object.to_s 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem 'grape' 4 | gem 'mongo_mapper' 5 | gem 'bson_ext' 6 | gem 'icalendar' 7 | 8 | gem 'rake' 9 | 10 | group :test, :development do 11 | gem 'rspec' 12 | gem 'rack-test' 13 | gem 'database_cleaner' 14 | gem 'factory_girl', '~> 4.0' 15 | 16 | gem 'pry' 17 | gem 'pry-nav' 18 | gem 'pry-remote' 19 | 20 | gem 'ffaker' 21 | end 22 | 23 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require "grape" 2 | require "mongo_mapper" 3 | MongoMapper.database = "calendar_api" 4 | 5 | Dir.glob('lib/lib/**/*.rb') { |f| require File.expand_path(f) } 6 | Dir.glob('lib/models/**/*.rb') { |f| require File.expand_path(f) } 7 | require File.expand_path("lib/api/application_api.rb") 8 | Dir.glob('lib/api/**/*.rb') { |f| require File.expand_path(f) } 9 | 10 | run CalendarAPI 11 | 12 | -------------------------------------------------------------------------------- /spec/models/customer_spec.rb: -------------------------------------------------------------------------------- 1 | describe Customer do 2 | let!(:customer1) { create(:customer) } 3 | let!(:customer2) { create(:customer) } 4 | let(:calendar1) { attributes_for(:calendar) } 5 | 6 | it "has many calendars" do 7 | calendar = customer1.calendars.create!(calendar1.slice(:title)) 8 | calendar.reload 9 | Calendar.count.should == 1 10 | customer1.calendars.should == [calendar] 11 | customer2.calendars.should == [] 12 | end 13 | end 14 | 15 | -------------------------------------------------------------------------------- /spec/factories.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | 3 | FactoryGirl.define do 4 | factory :calendar do 5 | sequence(:title) { |n| "title #{n}" } 6 | 7 | customer 8 | end 9 | 10 | start_at = proc { rand(11..20).days.ago.to_i } 11 | end_at = proc { rand(1..10).days.ago.to_i } 12 | 13 | factory :event do 14 | sequence(:title) { |n| "title #{n}" } 15 | 16 | sequence(:start) { start_at.call } 17 | sequence(:end) { start_at.call } 18 | 19 | calendar 20 | end 21 | 22 | factory :customer do 23 | sequence(:api_key) { SecureRandom.hex } 24 | end 25 | end 26 | 27 | -------------------------------------------------------------------------------- /lib/models/customer.rb: -------------------------------------------------------------------------------- 1 | require "securerandom" 2 | 3 | class Customer 4 | include MongoMapper::Document 5 | 6 | key :api_key, String 7 | 8 | many :calendars 9 | 10 | before_create :generate_api_key 11 | 12 | def has?(resource) 13 | self == resource.customer 14 | end 15 | 16 | def self.authorize!(api_key) 17 | Customer.find_by_api_key(api_key) 18 | end 19 | 20 | private 21 | 22 | def generate_api_key 23 | begin 24 | self.api_key = SecureRandom.hex 25 | end while Customer.find_by_api_key(api_key) 26 | end 27 | end 28 | 29 | Customer.ensure_index(:api_key) 30 | 31 | -------------------------------------------------------------------------------- /lib/models/calendar.rb: -------------------------------------------------------------------------------- 1 | class Calendar 2 | include MongoMapper::Document 3 | 4 | key :title, String 5 | key :description, String 6 | key :owner_id, ObjectId 7 | 8 | validates_presence_of :title 9 | 10 | attr_accessible :title, :description 11 | 12 | many :events 13 | belongs_to :customer 14 | 15 | scope :for_customer, lambda { |customer| where(:customer_id => customer.id) } 16 | 17 | def serializable_hash(options = {}) 18 | super({:only => ["title", "description"]}.merge(options)) 19 | end 20 | 21 | def is_accessible?(current_user, params = nil) 22 | current_user.has?(self) 23 | end 24 | 25 | def to_ical 26 | IcalendarEvents.new(self.events).to_ical 27 | end 28 | end 29 | 30 | -------------------------------------------------------------------------------- /spec/support/error_matcher.rb: -------------------------------------------------------------------------------- 1 | module RSpec 2 | module CustomMatchers 3 | def response_with_error(status_code, message) 4 | ErrorMatcher.new(status_code, message, last_response) 5 | end 6 | 7 | class ErrorMatcher 8 | def initialize(status_code, error_message, last_response) 9 | @status_code = status_code 10 | @error_message = { :error => error_message }.to_json 11 | @last_response = last_response 12 | end 13 | 14 | def failure_message_for_should 15 | "Expected #{@last_response.body} with code #{@last_response.status} 16 | to contain an error #{@error_message} with code #{@status_code}" 17 | end 18 | 19 | def failure_message_for_should_not 20 | "Expected #{@last_response.body} with code #{@last_response.status} 21 | to not contain an error #{@error_message} with code #{@status_code}" 22 | end 23 | 24 | def matches?(subject) 25 | @last_response.status == @status_code && 26 | @last_response.body == @error_message 27 | end 28 | end 29 | end 30 | end 31 | 32 | -------------------------------------------------------------------------------- /lib/lib/icalendar.rb: -------------------------------------------------------------------------------- 1 | require 'icalendar' 2 | 3 | class IcalendarEvents 4 | include Icalendar 5 | 6 | def initialize(events) 7 | @events = events 8 | @calendar = Calendar.new 9 | @events.each do |event| 10 | @calendar.add_event(IcalendarEvent.new(event).to_event) 11 | end 12 | end 13 | 14 | def any? 15 | @events.to_a.any? 16 | end 17 | 18 | def to_a 19 | @events.to_a 20 | end 21 | 22 | def to_ical 23 | @calendar.to_ical 24 | end 25 | 26 | def to_json 27 | @events.to_a.to_json 28 | end 29 | end 30 | 31 | class IcalendarEvent 32 | include Icalendar 33 | 34 | def initialize(record) 35 | @event = Event.new 36 | @event.start = parse_date(record.start) 37 | @event.end = parse_date(record.end) 38 | @event.description = record.description 39 | @event.summary = record.title 40 | end 41 | 42 | def to_ical 43 | @event.to_ical 44 | end 45 | 46 | def to_event 47 | @event 48 | end 49 | 50 | private 51 | 52 | def parse_date(date) 53 | DateTime.parse(Time.at(date).to_s) 54 | end 55 | end 56 | 57 | -------------------------------------------------------------------------------- /lib/api/application_api.rb: -------------------------------------------------------------------------------- 1 | class CalendarAPI < Grape::API 2 | default_format :json 3 | error_format :json 4 | 5 | content_type :ical, "text/calendar" 6 | 7 | class LengthLt < Grape::Validations::SingleOptionValidator 8 | def validate_param!(attr_name, params) 9 | if params[attr_name].length >= @option 10 | throw :error, :status => 401, :message => { :errors => 11 | { attr_name => ["must be equal or less than #{@option} characters long"] } } 12 | end 13 | end 14 | end 15 | 16 | helpers do 17 | def current_user 18 | @current_user ||= Customer.authorize!(params.api_key) 19 | end 20 | 21 | def authenticate! 22 | error!('401 Unauthorized', 401) unless current_user 23 | end 24 | 25 | def can?(resource) 26 | resource && resource.is_accessible?(current_user, params) 27 | end 28 | 29 | def not_found 30 | error!({ :error => "Not Found" }, 404) 31 | end 32 | 33 | def attributes_error(resource) 34 | error!({ :errors => resource.errors.messages }, 401) 35 | end 36 | end 37 | 38 | before do 39 | authenticate! 40 | end 41 | end 42 | 43 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 3 | 4 | require "mongo_mapper" 5 | require "grape" 6 | require "pry" 7 | 8 | require File.expand_path("lib/api/application_api.rb") 9 | Dir.glob('lib/api/**/*.rb') { |f| require File.expand_path(f) } 10 | 11 | desc "Displays routes" 12 | task 'routes' do 13 | CalendarAPI::routes.each do |route| 14 | options = route.instance_variable_get(:@options) 15 | 16 | meth = options[:method] 17 | path = options[:path] 18 | 19 | puts "#{meth} \t #{path}" 20 | end 21 | end 22 | 23 | desc "seeds data" 24 | task "ical" do 25 | require "ffaker" 26 | MongoMapper.database = "calendar_api" 27 | Dir.glob('lib/lib/**/*.rb') { |f| require File.expand_path(f) } 28 | Dir.glob('lib/models/**/*.rb') { |f| require File.expand_path(f) } 29 | 30 | sen = proc { Faker::Lorem.sentence[0..39] } 31 | 32 | customer = Customer.create 33 | cal = customer.calendars.create(title: sen[]) 34 | cal.events.create(title: sen[], start: 5.days.ago, :end => 1.day.ago) 35 | cal.events.create(title: sen[], start: 9.days.ago, :end => 8.day.ago) 36 | puts "http://localhost:9292/calendars/#{cal.id}?api_key=#{customer.api_key}" 37 | end 38 | 39 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 3 | 4 | require "mongo_mapper" 5 | require "grape" 6 | 7 | require "rack/test" 8 | require "database_cleaner" 9 | require "factory_girl" 10 | 11 | require "pry" 12 | require "pry-nav" 13 | require "pry-remote" 14 | 15 | MongoMapper.database = "calendar_api" 16 | 17 | Dir.glob('spec/support/**/*.rb') { |f| require File.expand_path(f) } 18 | Dir.glob('lib/lib/**/*.rb') { |f| require File.expand_path(f) } 19 | Dir.glob('lib/models/**/*.rb') { |f| require File.expand_path(f) } 20 | require File.expand_path("lib/api/application_api.rb") 21 | Dir.glob('lib/api/**/*.rb') { |f| require File.expand_path(f) } 22 | 23 | require_relative "factories" 24 | 25 | RSpec.configure do |config| 26 | config.include RSpec::CustomMatchers 27 | 28 | config.include Rack::Test::Methods 29 | config.include FactoryGirl::Syntax::Methods 30 | 31 | config.before(:suite) do 32 | DatabaseCleaner[:mongo_mapper].strategy = :truncation 33 | DatabaseCleaner[:mongo_mapper].clean_with(:truncation) 34 | end 35 | 36 | config.before(:each) do 37 | DatabaseCleaner[:mongo_mapper].start 38 | end 39 | 40 | config.after(:each) do 41 | DatabaseCleaner[:mongo_mapper].clean 42 | end 43 | end 44 | 45 | -------------------------------------------------------------------------------- /lib/api/calendar_api.rb: -------------------------------------------------------------------------------- 1 | class CalendarAPI < Grape::API 2 | resource :calendars do 3 | get do 4 | Calendar.for_customer(current_user).all 5 | end 6 | 7 | params do 8 | requires :title, :length_lt => 40, :type => String 9 | optional :description, :length_lt => 1000, :type => String 10 | end 11 | post do 12 | calendar = current_user.calendars.build(params.slice(:title, :description)) 13 | if calendar.save 14 | calendar 15 | else 16 | attributes_error(calendar) 17 | end 18 | end 19 | 20 | get ":id" do 21 | calendar = Calendar.find(params.id) 22 | if can?(calendar) 23 | calendar 24 | else 25 | not_found 26 | end 27 | end 28 | 29 | params do 30 | optional :title, :length_lt => 40, :type => String 31 | optional :description, :length_lt => 1000, :type => String 32 | end 33 | put ":id" do 34 | calendar = Calendar.find(params.id) 35 | if can?(calendar) 36 | if calendar.update_attributes(params.slice(:title, :description)) 37 | calendar 38 | else 39 | attributes_error(calendar) 40 | end 41 | else 42 | not_found 43 | end 44 | end 45 | 46 | delete ":id" do 47 | calendar = Calendar.find(params.id) 48 | if can?(calendar) 49 | calendar.delete 50 | else 51 | not_found 52 | end 53 | end 54 | end 55 | end 56 | 57 | -------------------------------------------------------------------------------- /spec/support/event_matcher.rb: -------------------------------------------------------------------------------- 1 | module RSpec 2 | module CustomMatchers 3 | def contain_events(*events) 4 | if events.empty? 5 | raise ArgumentError, "need at least one argument" 6 | else 7 | EventMatcher.new(events) 8 | end 9 | end 10 | 11 | class EventMatcher 12 | def initialize(events) 13 | @events = events 14 | end 15 | 16 | def failure_message_for_should 17 | "Expected #{@response_body} to contain #{@events}" 18 | end 19 | 20 | def failure_message_for_should_not 21 | "Expected #{@response_body} to not contain #{@events}" 22 | end 23 | 24 | def matches?(response_body) 25 | @response_body = parse_json(response_body) 26 | @events.size == @response_body.size && 27 | @response_body.zip(@events).all? do |actual_event, expected_event| 28 | actual_event.keys.sort == expected_event.keys.sort && 29 | actual_event[:title] == expected_event[:title] && 30 | Time.parse(actual_event[:start]) == Time.at(expected_event[:start]).utc.to_s && 31 | Time.parse(actual_event[:end]) == Time.at(expected_event[:end]).utc.to_s 32 | end 33 | end 34 | 35 | private 36 | 37 | def parse_json(response_body) 38 | json_object = ActiveSupport::JSON.decode(response_body) 39 | case json_object 40 | when Hash 41 | [Hash[json_object.sort].symbolize_keys] 42 | when Array 43 | json_object.map(&:symbolize_keys) 44 | else 45 | json_object 46 | end 47 | end 48 | end 49 | end 50 | end 51 | 52 | -------------------------------------------------------------------------------- /lib/models/event.rb: -------------------------------------------------------------------------------- 1 | class Event 2 | include MongoMapper::Document 3 | 4 | key :calendar_id, ObjectId 5 | key :title, String 6 | key :description, String 7 | 8 | key :start, Time 9 | key :end, Time 10 | 11 | key :color, String 12 | key :customer_id, ObjectId 13 | 14 | belongs_to :calendar 15 | belongs_to :customer 16 | 17 | validates_presence_of :title 18 | 19 | attr_accessible :title, :description, :start, :end, :color 20 | 21 | before_save do 22 | self.customer = calendar.customer 23 | end 24 | 25 | def serializable_hash(options = {}) 26 | super({:only => [:title, :description, :start, :end, :color]}.merge(options)) 27 | end 28 | 29 | def start=(time_in_numbers) 30 | @start = Time.at(time_in_numbers) 31 | end 32 | 33 | def end=(time_in_numbers) 34 | instance_variable_set(:@end, Time.at(time_in_numbers)) 35 | end 36 | 37 | scope :for_customer, lambda { |customer| 38 | where(:customer_id => customer.id.to_s) 39 | } 40 | 41 | scope :calendars, lambda { |ids| 42 | where(:calendar_id.in => ids) 43 | } 44 | 45 | scope :within_time, lambda { |start_at, end_at| 46 | where(:start.gt => Time.at(start_at) - 1.day, :end.lt => Time.at(end_at) + 1.day) 47 | } 48 | 49 | def self.search(params, customer) 50 | events = for_customer(customer) 51 | events = events.calendars(params.calendar_ids.split(",")) 52 | events = events.within_time(*params.values_at("start", "end")) if valid_time_range?(params) 53 | IcalendarEvents.new(events) 54 | end 55 | 56 | def is_accessible?(current_user, params) 57 | current_user.has?(self) && calendar_id.to_s == params.calendar_ids 58 | end 59 | 60 | def to_ical 61 | IcalendarEvent.new(self).to_ical 62 | end 63 | 64 | private 65 | 66 | def self.valid_time_range?(params) 67 | params.start && params.end 68 | end 69 | end 70 | 71 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | activemodel (3.2.8) 5 | activesupport (= 3.2.8) 6 | builder (~> 3.0.0) 7 | activesupport (3.2.8) 8 | i18n (~> 0.6) 9 | multi_json (~> 1.0) 10 | backports (2.6.5) 11 | bson (1.7.0) 12 | bson_ext (1.7.0) 13 | bson (~> 1.7.0) 14 | builder (3.0.4) 15 | coderay (1.0.8) 16 | database_cleaner (0.9.1) 17 | diff-lcs (1.1.3) 18 | factory_girl (4.1.0) 19 | activesupport (>= 3.0.0) 20 | ffaker (1.15.0) 21 | grape (0.2.2) 22 | activesupport 23 | hashie (~> 1.2) 24 | multi_json (>= 1.3.2) 25 | multi_xml 26 | rack 27 | rack-accept 28 | rack-mount 29 | virtus 30 | hashie (1.2.0) 31 | i18n (0.6.1) 32 | icalendar (1.2.0) 33 | method_source (0.8.1) 34 | mongo (1.7.0) 35 | bson (~> 1.7.0) 36 | mongo_mapper (0.12.0) 37 | activemodel (~> 3.0) 38 | activesupport (~> 3.0) 39 | plucky (~> 0.5.2) 40 | multi_json (1.3.6) 41 | multi_xml (0.5.1) 42 | plucky (0.5.2) 43 | mongo (~> 1.5) 44 | pry (0.9.10) 45 | coderay (~> 1.0.5) 46 | method_source (~> 0.8) 47 | slop (~> 3.3.1) 48 | pry-nav (0.2.2) 49 | pry (~> 0.9.10) 50 | pry-remote (0.1.6) 51 | pry (~> 0.9) 52 | slop (~> 3.0) 53 | rack (1.4.1) 54 | rack-accept (0.4.5) 55 | rack (>= 0.4) 56 | rack-mount (0.8.3) 57 | rack (>= 1.0.0) 58 | rack-test (0.6.2) 59 | rack (>= 1.0) 60 | rake (0.9.2.2) 61 | rspec (2.11.0) 62 | rspec-core (~> 2.11.0) 63 | rspec-expectations (~> 2.11.0) 64 | rspec-mocks (~> 2.11.0) 65 | rspec-core (2.11.1) 66 | rspec-expectations (2.11.3) 67 | diff-lcs (~> 1.1.3) 68 | rspec-mocks (2.11.3) 69 | slop (3.3.3) 70 | virtus (0.5.2) 71 | backports (~> 2.6.1) 72 | 73 | PLATFORMS 74 | ruby 75 | 76 | DEPENDENCIES 77 | bson_ext 78 | database_cleaner 79 | factory_girl (~> 4.0) 80 | ffaker 81 | grape 82 | icalendar 83 | mongo_mapper 84 | pry 85 | pry-nav 86 | pry-remote 87 | rack-test 88 | rake 89 | rspec 90 | -------------------------------------------------------------------------------- /lib/api/event_api.rb: -------------------------------------------------------------------------------- 1 | class CalendarAPI < Grape::API 2 | resource :calendars do 3 | namespace "/:calendar_ids" do 4 | resource :events do 5 | params do 6 | optional :start, :type => Integer 7 | optional :end, :type => Integer 8 | end 9 | get do 10 | events = Event.search(params.slice(:calendar_ids, :start, :end), current_user) 11 | if events.any? 12 | events 13 | else 14 | not_found 15 | end 16 | end 17 | 18 | params do 19 | requires :title, :length_lt => 40, :type => String 20 | optional :description, :length_lt => 1000, :type => String 21 | optional :color, :length_lt => 40, :type => String 22 | requires :start, :type => Integer 23 | requires :end, :type => Integer 24 | end 25 | post do 26 | calendar = Calendar.find(params.calendar_ids) 27 | if can?(calendar) 28 | event = calendar.events.build(params.slice(:title, :description, :start, :end, :color)) 29 | if event.save 30 | event 31 | else 32 | attributes_error(event) 33 | end 34 | else 35 | not_found 36 | end 37 | end 38 | 39 | get ":id" do 40 | event = Event.find(params.id) 41 | if can?(event) 42 | event 43 | else 44 | not_found 45 | end 46 | end 47 | 48 | params do 49 | optional :title, :length_lt => 40, :type => String 50 | optional :description, :length_lt => 1000, :type => String 51 | optional :color, :length_lt => 40, :type => String 52 | optional :start, :type => Integer 53 | optional :end, :type => Integer 54 | end 55 | put ":id" do 56 | event = Event.find(params.id) 57 | if can?(event) 58 | if event.update_attributes(params.slice(:title, :description, :start, :end, :color)) 59 | event 60 | else 61 | attributes_error(event) 62 | end 63 | else 64 | not_found 65 | end 66 | end 67 | 68 | delete ":id" do 69 | event = Event.find(params.id) 70 | if can?(event) 71 | event.delete 72 | else 73 | not_found 74 | end 75 | end 76 | end 77 | end 78 | end 79 | end 80 | 81 | -------------------------------------------------------------------------------- /spec/models/event_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper.rb" 2 | 3 | describe Event do 4 | describe ".search" do 5 | let!(:customer) { create(:customer) } 6 | let!(:customer2) { create(:customer) } 7 | 8 | let!(:calendar1) { create(:calendar, :customer => customer) } 9 | let!(:calendar2) { create(:calendar, :customer => customer) } 10 | let!(:calendar3) { create(:calendar, :customer => customer) } 11 | let!(:calendar4) { create(:calendar, :customer => customer2) } 12 | 13 | let!(:event_1) { create(:event, :calendar => calendar1, 14 | :start => 9.days.ago.to_i, :end => 1.day.ago.to_i) } 15 | let!(:event_2) { create(:event, :calendar => calendar1, 16 | :start => 5.days.ago.to_i, :end => 4.day.ago.to_i )} 17 | let!(:event_3) { create(:event, :calendar => calendar2, 18 | :start => 8.days.ago.to_i, :end => 7.day.ago.to_i )} 19 | let!(:event_4) { create(:event, :calendar => calendar3, 20 | :start => 5.days.ago.to_i, :end => 1.day.ago.to_i)} 21 | 22 | let!(:event_5) { create(:event, :calendar => calendar4, 23 | :start => 5.days.ago.to_i, :end => 1.day.ago.to_i)} 24 | 25 | let!(:event_6) { create(:event, :calendar => calendar4, 26 | :start => 9.days.ago.to_i, :end => 1.day.ago.to_i)} 27 | 28 | context "when many users are involved" do 29 | it "uses calendar's customer association to assign customer_id" do 30 | event = calendar4.events.build(attributes_for(:event)) 31 | event.save 32 | event.customer.should == calendar4.customer 33 | event.customer.should == customer2 34 | end 35 | 36 | it "ignores the data which doesn't belong to the current user" do 37 | params = Hashie::Mash[:calendar_ids => calendar2.id.to_s] 38 | Event.search(params, customer2).to_a.should == [] 39 | 40 | params = Hashie::Mash[:calendar_ids => "#{calendar1.id},#{calendar2.id},#{calendar3.id}"] 41 | Event.search(params, customer2).to_a.should == [] 42 | 43 | params = Hashie::Mash[:calendar_ids => "#{calendar1.id},#{calendar2.id},#{calendar3.id},#{calendar4.id}"] 44 | Event.search(params, customer2).to_a.should == [ event_5, event_6 ] 45 | 46 | params = Hashie::Mash[:calendar_ids => calendar4.id.to_s] 47 | Event.search(params, customer).to_a.should == [] 48 | 49 | params = Hashie::Mash[:calendar_ids => calendar4.id.to_s] 50 | Event.search(params, customer2).to_a.should == [ event_5, event_6 ] 51 | end 52 | end 53 | 54 | context "without the time filter" do 55 | it "returns matched events for multiple calendars" do 56 | params = Hashie::Mash[:calendar_ids => "#{calendar1.id},#{calendar3.id}"] 57 | Event.search(params, customer).to_a.should == [ event_1, event_2, event_4 ] 58 | end 59 | 60 | it "returns matched events for a signle calendar" do 61 | params = Hashie::Mash[:calendar_ids => calendar2.id.to_s] 62 | Event.search(params, customer).to_a.should == [ event_3 ] 63 | end 64 | 65 | it "returns an empty array if nothing has been found" do 66 | params = Hashie::Mash[:calendar_ids => "123123123131"] 67 | Event.search(params, customer).to_a.should == [] 68 | end 69 | end 70 | 71 | context "with the time filter" do 72 | it "returns matched events" do 73 | params = Hashie::Mash[:calendar_ids => "#{calendar1.id},#{calendar2.id}", 74 | :start => 6.days.ago.to_i, :end => 4.days.ago.to_i] 75 | Event.search(params, customer).to_a.should == [event_2] 76 | 77 | params = Hashie::Mash[:calendar_ids => "#{calendar1.id},#{calendar2.id},#{calendar3.id}", 78 | :start => 5.days.ago.to_i, :end => 1.days.ago.to_i] 79 | Event.search(params, customer).to_a.should == [event_2, event_4] 80 | 81 | params = Hashie::Mash[:calendar_ids => "#{calendar1.id},#{calendar2.id},#{calendar3.id}", 82 | :start => 9.days.ago.to_i, :end => 1.days.ago.to_i] 83 | Event.search(params, customer).to_a.should == [event_1, event_2, event_3, event_4] 84 | 85 | params = Hashie::Mash[:calendar_ids => "#{calendar2.id},#{calendar3.id}", 86 | :start => 8.days.ago.to_i, :end => 6.days.ago.to_i] 87 | Event.search(params, customer).to_a.should == [event_3] 88 | end 89 | end 90 | end 91 | end 92 | 93 | -------------------------------------------------------------------------------- /spec/api/calendar_api_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper.rb" 2 | require "icalendar" 3 | 4 | describe CalendarAPI do 5 | include Rack::Test::Methods 6 | 7 | def app 8 | CalendarAPI 9 | end 10 | 11 | describe CalendarAPI do 12 | let(:customer) { create(:customer) } 13 | let(:api_key) { { :api_key => customer.api_key }.to_query } 14 | 15 | describe "GET /calendars" do 16 | let(:params1) { attributes_for(:calendar) } 17 | let!(:calendar1) { create(:calendar, params1.merge(:customer => customer)) } 18 | 19 | it "returns all the calendars" do 20 | get "/calendars?#{api_key}" 21 | last_response.status.should == 200 22 | last_response.body.should == [params1.slice(:title)].to_json 23 | end 24 | end 25 | 26 | describe "POST /calendars" do 27 | let(:params) { attributes_for(:calendar, :id => 123, :customer_id => 124) } 28 | 29 | context "when parameters are valid" do 30 | it "creates a new calendar" do 31 | post "/calendars?#{api_key}", params 32 | last_response.status.should == 201 33 | last_response.body.should == params.slice(:title).to_json 34 | Calendar.count.should == 1 35 | Calendar.last.customer_id.should_not == params[:customer_id] 36 | Calendar.last.id.should_not == params[:id] 37 | Calendar.last.customer_id.should == customer.id 38 | end 39 | end 40 | 41 | context "when parameters are invalid" do 42 | it "returns an error if 'title' is not provided" do 43 | post "calendars?#{api_key}", params.merge(:title => "") 44 | last_response.status.should == 401 45 | last_response.body.should == { :errors => { "title" => ["can't be blank"] } }.to_json 46 | Calendar.count.should == 0 47 | end 48 | 49 | it "returns an error if 'title' is too long" do 50 | post "calendars?#{api_key}", params.merge(:title => "a" * 41) 51 | last_response.status.should == 401 52 | last_response.body.should == { :errors => 53 | { "title" => ["must be equal or less than 40 characters long"] } }.to_json 54 | Calendar.count.should == 0 55 | end 56 | 57 | it "returns an error if 'describe' is too long" do 58 | post "calendars?#{api_key}", params.merge(:description => "a" * 1000) 59 | last_response.status.should == 401 60 | last_response.body.should == { :errors => 61 | { "description" => ["must be equal or less than 1000 characters long"] } }.to_json 62 | Calendar.count.should == 0 63 | end 64 | end 65 | end 66 | 67 | describe "GET /calendars/:id" do 68 | let(:params) { attributes_for(:calendar, :description => "q") } 69 | let(:calendar) { create(:calendar, params.merge(:customer => customer)) } 70 | 71 | it "returns the calendar" do 72 | get "/calendars/#{calendar.id}?#{api_key}" 73 | last_response.status.should == 200 74 | last_response.body.should == Hash[params.slice(:title, :description).sort].to_json 75 | end 76 | 77 | it "returns an error if 'id' is invalid" do 78 | get "/calendars/1234124234?#{api_key}" 79 | should response_with_error(404, "Not Found") 80 | end 81 | 82 | describe "GET /calendars/:id.ical" do 83 | let!(:event_1) { create(:event, :calendar => calendar, :start => 9.days.ago, :end => 5.days.ago) } 84 | let!(:event_2) { create(:event, :calendar => calendar, :start => 6.days.ago, :end => 3.days.ago) } 85 | 86 | it "returns the calendar in .ical format" do 87 | get "/calendars/#{calendar.id}.ical?#{api_key}" 88 | last_response.status.should == 200 89 | last_response.header["Content-Type"].should == "text/calendar" 90 | Icalendar.parse(last_response.body).first.events.size.should == calendar.events.size 91 | end 92 | end 93 | end 94 | 95 | describe "PUT /calendars/:id" do 96 | let(:origin_params) { attributes_for(:calendar) } 97 | let(:new_params) { attributes_for(:calendar, :description => "q") } 98 | let(:calendar) { create(:calendar, origin_params.merge(:customer => customer)) } 99 | 100 | context "when parameters are valid" do 101 | it "updates the object" do 102 | put "/calendars/#{calendar.id}?#{api_key}", new_params 103 | last_response.status.should == 200 104 | last_response.body.should == new_params.slice(:description, :title).to_json 105 | 106 | calendar.reload 107 | calendar.title.should == new_params[:title] 108 | calendar.description.should == new_params[:description] 109 | end 110 | end 111 | 112 | context "when parameters are invalid" do 113 | it "returns an error if 'id' if invalid" do 114 | put "/calendars/123213123?#{api_key}", new_params 115 | should response_with_error(404, "Not Found") 116 | end 117 | 118 | it "returns an error if 'title' is blank" do 119 | put "/calendars/#{calendar.id}?#{api_key}", new_params.merge(:title => "") 120 | last_response.status.should == 401 121 | last_response.body.should == { :errors => { "title" => ["can't be blank"] } }.to_json 122 | 123 | calendar.reload 124 | calendar.title.should == origin_params[:title] 125 | calendar.description.should == origin_params[:description] 126 | end 127 | 128 | it "returns an error if 'title' is too long" do 129 | put "/calendars/#{calendar.id}?#{api_key}", new_params.merge("title" => "1"*41) 130 | last_response.status.should == 401 131 | last_response.body.should == { :errors => 132 | { "title" => ["must be equal or less than 40 characters long"] } }.to_json 133 | calendar.reload 134 | calendar.title.should == origin_params[:title] 135 | calendar.description.should == origin_params[:description] 136 | end 137 | 138 | it "returns an error if 'description' is too long" do 139 | put "/calendars/#{calendar.id}?#{api_key}", new_params.merge("description" => "1"*1000) 140 | last_response.status.should == 401 141 | last_response.body.should == { :errors => 142 | { "description" => ["must be equal or less than 1000 characters long"] } }.to_json 143 | calendar.reload 144 | calendar.title.should == origin_params[:title] 145 | calendar.description.should == origin_params[:description] 146 | end 147 | end 148 | end 149 | 150 | describe "DELETE /calendars/:id" do 151 | let!(:calendar) { create(:calendar, :customer => customer) } 152 | 153 | it "removes the calendar" do 154 | delete "/calendars/#{calendar.id}?#{api_key}" 155 | last_response.status.should == 200 156 | Calendar.find(calendar.id).should == nil 157 | end 158 | 159 | it "returns an error if 'id' is invalid" do 160 | delete "/calendars/21312312312?#{api_key}" 161 | should response_with_error(404, "Not Found") 162 | end 163 | end 164 | end 165 | end 166 | 167 | -------------------------------------------------------------------------------- /spec/api/application_api_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper.rb" 2 | 3 | describe CalendarAPI do 4 | include Rack::Test::Methods 5 | 6 | def app 7 | CalendarAPI 8 | end 9 | 10 | describe CalendarAPI do 11 | context "api keys" do 12 | let!(:customer1) { create(:customer) } 13 | let!(:customer2) { create(:customer) } 14 | let!(:api_key1) { { :api_key => customer1.api_key }.to_query } 15 | let!(:api_key2) { { :api_key => customer2.api_key }.to_query } 16 | let!(:attrs1) { attributes_for(:calendar) } 17 | let!(:attrs2) { attributes_for(:calendar) } 18 | let!(:attrs3) { attributes_for(:calendar) } 19 | let!(:calendar1) { create(:calendar, attrs1.merge(:customer => customer1)) } 20 | let!(:calendar2) { create(:calendar, attrs2.merge(:customer => customer2)) } 21 | let!(:calendar3) { create(:calendar, attrs3.merge(:customer => customer2)) } 22 | 23 | it "requires api key" do 24 | get "/calendars" 25 | should response_with_error(401, "401 Unauthorized") 26 | 27 | api_key = { :api_key => "value" }.to_query 28 | get "/calendars?#{api_key}" 29 | should response_with_error(401, "401 Unauthorized") 30 | end 31 | 32 | context "calendar" do 33 | it "doesn't allow the user to read other user's calendars in index" do 34 | api_key = { :api_key => customer1.api_key }.to_query 35 | get "/calendars?#{api_key}" 36 | last_response.status.should == 200 37 | last_response.body.should == [attrs1].to_json 38 | 39 | api_key = { :api_key => customer2.api_key }.to_query 40 | get "/calendars?#{api_key}" 41 | last_response.status.should == 200 42 | last_response.body.should == [attrs2, attrs3].to_json 43 | end 44 | 45 | it "doesn't allow the user to read other user's calendar" do 46 | api_key = { :api_key => customer1.api_key }.to_query 47 | get "/calendars/#{calendar2.id}?#{api_key}" 48 | should response_with_error(404, "Not Found") 49 | 50 | api_key = { :api_key => customer2.api_key }.to_query 51 | get "/calendars/#{calendar2.id}?#{api_key}" 52 | last_response.status.should == 200 53 | last_response.body.should == attrs2.to_json 54 | 55 | api_key = { :api_key => customer2.api_key }.to_query 56 | get "/calendars/#{calendar1.id}?#{api_key}" 57 | should response_with_error(404, "Not Found") 58 | 59 | api_key = { :api_key => customer1.api_key }.to_query 60 | get "/calendars/#{calendar1.id}?#{api_key}" 61 | last_response.status.should == 200 62 | last_response.body.should == attrs1.to_json 63 | end 64 | 65 | it "doesn't allow the user to edit other user's calendar" do 66 | api_key = { :api_key => customer1.api_key }.to_query 67 | put "/calendars/#{calendar2.id}?#{api_key}", attributes_for(:calendar).slice(:title) 68 | should response_with_error(404, "Not Found") 69 | calendar2.reload 70 | calendar2.title.should == attrs2[:title] 71 | 72 | new_attrs = attributes_for(:calendar) 73 | api_key = { :api_key => customer2.api_key }.to_query 74 | put "/calendars/#{calendar2.id}?#{api_key}", new_attrs.slice(:title) 75 | last_response.status.should == 200 76 | last_response.body.should == new_attrs.to_json 77 | calendar2.reload 78 | calendar2.title.should == new_attrs[:title] 79 | 80 | api_key = { :api_key => customer2.api_key }.to_query 81 | put "/calendars/#{calendar1.id}?#{api_key}", attributes_for(:calendar).slice(:title) 82 | should response_with_error(404, "Not Found") 83 | calendar1.reload 84 | calendar1.title.should == attrs1[:title] 85 | 86 | new_attrs = attributes_for(:calendar) 87 | api_key = { :api_key => customer1.api_key }.to_query 88 | put "/calendars/#{calendar1.id}?#{api_key}", new_attrs.slice(:title) 89 | last_response.status.should == 200 90 | last_response.body.should == new_attrs.to_json 91 | calendar1.reload 92 | calendar1.title.should == new_attrs[:title] 93 | end 94 | 95 | it "doesn't allow the user to destroy other user's calendar" do 96 | api_key = { :api_key => customer1.api_key }.to_query 97 | delete "/calendars/#{calendar2.id}?#{api_key}" 98 | should response_with_error(404, "Not Found") 99 | 100 | api_key = { :api_key => customer2.api_key }.to_query 101 | delete "/calendars/#{calendar2.id}?#{api_key}" 102 | last_response.status.should == 200 103 | Calendar.find(calendar2.to_s).should == nil 104 | 105 | api_key = { :api_key => customer2.api_key }.to_query 106 | delete "/calendars/#{calendar1.id}?#{api_key}" 107 | should response_with_error(404, "Not Found") 108 | 109 | api_key = { :api_key => customer1.api_key }.to_query 110 | delete "/calendars/#{calendar1.id}?#{api_key}" 111 | last_response.status.should == 200 112 | Calendar.find(calendar1.to_s).should == nil 113 | end 114 | end 115 | 116 | context "events" do 117 | let(:event_attrs1) { attributes_for(:event, start: 9.days.ago.to_i, end: 5.days.ago.to_i) } 118 | let(:event_attrs2) { attributes_for(:event, start: 6.days.ago.to_i, end: 4.days.ago.to_i) } 119 | let(:event_attrs3) { attributes_for(:event, start: 5.days.ago.to_i, end: 1.days.ago.to_i) } 120 | let(:event_attrs4) { attributes_for(:event, start: 7.days.ago.to_i, end: 3.days.ago.to_i) } 121 | 122 | let!(:event1) { create(:event, event_attrs1.merge(:calendar => calendar1)) } 123 | let!(:event2) { create(:event, event_attrs2.merge(:calendar => calendar1)) } 124 | let!(:event3) { create(:event, event_attrs3.merge(:calendar => calendar2)) } 125 | let!(:event4) { create(:event, event_attrs4.merge(:calendar => calendar3)) } 126 | 127 | it "doesn't allow the user to search for events in other user's calendar(s)" do 128 | get "/calendars/#{calendar2.id}/events?#{api_key1}" 129 | should response_with_error(404, "Not Found") 130 | 131 | get "/calendars/#{calendar1.id}/events?#{api_key2}" 132 | should response_with_error(404, "Not Found") 133 | 134 | get "/calendars/#{calendar2.id},#{calendar1.id}/events?#{api_key1}" 135 | last_response.status.should == 200 136 | last_response.body.should contain_events(event_attrs1, event_attrs2) 137 | 138 | get "/calendars/#{calendar2.id},#{calendar1.id}/events?#{api_key2}" 139 | last_response.status.should == 200 140 | last_response.body.should contain_events(event_attrs3) 141 | end 142 | 143 | it "doesn't allow the user to create events in other user's calendars" do 144 | post "/calendars/#{calendar1.id}/events?#{api_key2}", attributes_for(:event) 145 | should response_with_error(404, "Not Found") 146 | 147 | post "/calendars/#{calendar2.id}/events?#{api_key1}", attributes_for(:event) 148 | should response_with_error(404, "Not Found") 149 | 150 | attrs = attributes_for(:event) 151 | post "/calendars/#{calendar2.id}/events?#{api_key2}", attrs 152 | last_response.status.should == 201 153 | last_response.body.should contain_events(attrs) 154 | 155 | attrs = attributes_for(:event) 156 | post "/calendars/#{calendar1.id}/events?#{api_key1}", attrs 157 | last_response.status.should == 201 158 | last_response.body.should contain_events(attrs) 159 | end 160 | 161 | it "doesn't allow the user to read event in other user's calendar" do 162 | get "/calendars/#{calendar1.id}/events/#{event1.id}?#{api_key2}" 163 | should response_with_error(404, "Not Found") 164 | 165 | get "/calendars/#{calendar1.id}/events/#{event2.id}?#{api_key2}" 166 | should response_with_error(404, "Not Found") 167 | 168 | get "/calendars/#{calendar1.id}/events/#{event2.id}?#{api_key1}" 169 | last_response.status.should == 200 170 | last_response.body.should contain_events(event_attrs2) 171 | 172 | get "/calendars/#{calendar2.id}/events/#{event3.id}?#{api_key1}" 173 | should response_with_error(404, "Not Found") 174 | 175 | get "/calendars/#{calendar2.id}/events/#{event3.id}?#{api_key2}" 176 | last_response.status.should == 200 177 | last_response.body.should contain_events(event_attrs3) 178 | end 179 | 180 | it "doesn't allow the user to edit event in other user's calendar" do 181 | new_attrs = attributes_for(:event) 182 | put "/calendars/#{calendar1.id}/events/#{event1.id}?#{api_key2}", new_attrs 183 | should response_with_error(404, "Not Found") 184 | 185 | new_attrs = attributes_for(:event) 186 | put "/calendars/#{calendar1.id}/events/#{event1.id}?#{api_key1}", new_attrs 187 | last_response.status.should == 200 188 | last_response.body.should contain_events(event_attrs1.merge(new_attrs)) 189 | end 190 | 191 | it "doesn't allow the user to destroy event in other user's calendar" do 192 | delete "/calendars/#{calendar1.id}/events/#{event1.id}?#{api_key2}" 193 | should response_with_error(404, "Not Found") 194 | 195 | delete "/calendars/#{calendar2.id}/events/#{event2.id}?#{api_key1}" 196 | should response_with_error(404, "Not Found") 197 | 198 | expect do 199 | delete "/calendars/#{calendar1.id}/events/#{event1.id}?#{api_key1}" 200 | last_response.status.should == 200 201 | end.to change(Event, :count).by(-1) 202 | 203 | expect do 204 | delete "/calendars/#{calendar2.id}/events/#{event3.id}?#{api_key2}" 205 | last_response.status.should == 200 206 | end.to change(Event, :count).by(-1) 207 | end 208 | end 209 | end 210 | end 211 | end 212 | 213 | -------------------------------------------------------------------------------- /spec/api/event_api_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper.rb" 2 | 3 | describe CalendarAPI do 4 | include Rack::Test::Methods 5 | 6 | def app 7 | CalendarAPI 8 | end 9 | 10 | let(:customer) { create(:customer) } 11 | let(:api_key) { { :api_key => customer.api_key }.to_query } 12 | 13 | describe "GET /calendars/:ids/events" do 14 | let(:calendar1) { create(:calendar, :customer => customer) } 15 | let(:calendar2) { create(:calendar, :customer => customer) } 16 | let(:calendar3) { create(:calendar, :customer => customer) } 17 | 18 | let(:event_attrs1) { attributes_for(:event, start: 9.days.ago.to_i, end: 5.days.ago.to_i) } 19 | let(:event_attrs2) { attributes_for(:event, start: 6.days.ago.to_i, end: 4.days.ago.to_i) } 20 | let(:event_attrs3) { attributes_for(:event, start: 5.days.ago.to_i, end: 1.days.ago.to_i) } 21 | let(:event_attrs4) { attributes_for(:event, start: 7.days.ago.to_i, end: 3.days.ago.to_i) } 22 | 23 | let!(:event1) { create(:event, event_attrs1.merge(:calendar => calendar1)) } 24 | let!(:event2) { create(:event, event_attrs2.merge(:calendar => calendar1)) } 25 | let!(:event3) { create(:event, event_attrs3.merge(:calendar => calendar2)) } 26 | let!(:event4) { create(:event, event_attrs4.merge(:calendar => calendar3)) } 27 | 28 | context "GET /calendars/:ids/events" do 29 | it "returns the events in .ical format" do 30 | get "/calendars/#{calendar1.id},#{calendar3.id}/events.ical?#{api_key}" 31 | last_response.status.should == 200 32 | last_response.header["Content-Type"].should == "text/calendar" 33 | Icalendar.parse(last_response.body).first.events.size.should == 3 34 | end 35 | end 36 | 37 | context "without the time filter" do 38 | it "returns an error message if 'id' is invalid" do 39 | get "/calendars/1231231232423,1231231232432?#{api_key}" 40 | should response_with_error(404, "Not Found") 41 | end 42 | 43 | it "returns all the events for a single calendar" do 44 | get "/calendars/#{calendar1.id}/events?#{api_key}" 45 | last_response.status.should == 200 46 | last_response.body.should contain_events(event_attrs1, event_attrs2) 47 | end 48 | 49 | it "returns all the events for multiple calendars" do 50 | get "/calendars/#{calendar1.id},#{calendar3.id}/events?#{api_key}" 51 | last_response.status.should == 200 52 | last_response.body.should contain_events(event_attrs1, event_attrs2, event_attrs4) 53 | end 54 | end 55 | 56 | context "with the time filter" do 57 | it "returns an error if 'start' is not a number" do 58 | get "/calendars/#{calendar1.id}/events?#{{:start => "abc"}.to_query}&#{api_key}" 59 | last_response.status.should == 400 60 | last_response.body.should == { :error => "invalid parameter: start" }.to_json 61 | end 62 | 63 | it "returns an error if 'start' is more than 'end'" do 64 | time_scope = {:start => 3.days.ago.to_i, :end => 7.days.ago.to_i}.to_query 65 | get "calendars/#{calendar1.id},#{calendar3.id}/events?#{time_scope}&#{api_key}" 66 | should response_with_error(404, "Not Found") 67 | end 68 | 69 | it "returns an error if 'end' is not a number" do 70 | get "/calendars/#{calendar1.id}/events?#{{:end => "abc"}.to_query}&#{api_key}" 71 | last_response.status.should == 400 72 | last_response.body.should == { :error => "invalid parameter: end" }.to_json 73 | end 74 | 75 | it "returns all the events for a single calendar" do 76 | time_scope = {:start => 6.days.ago.to_i, :end => 3.days.ago.to_i}.to_query 77 | get "calendars/#{calendar1.id}/events?#{time_scope}&#{api_key}" 78 | last_response.status.should == 200 79 | last_response.body.should contain_events(event_attrs2) 80 | 81 | time_scope = {:start => 7.days.ago.to_i, :end => 3.days.ago.to_i}.to_query 82 | get "calendars/#{calendar3.id}/events?#{time_scope}&#{api_key}" 83 | last_response.status.should == 200 84 | last_response.body.should contain_events(event_attrs4) 85 | 86 | time_scope = {:start => 7.days.ago.to_i, :end => 3.days.ago.to_i}.to_query 87 | get "calendars/#{calendar3.id}/events?#{time_scope}&#{api_key}" 88 | last_response.status.should == 200 89 | last_response.body.should contain_events(event_attrs4) 90 | end 91 | 92 | it "returns all the events for multiple calendars" do 93 | time_scope = {:start => 7.days.ago.to_i, :end => 3.days.ago.to_i}.to_query 94 | get "calendars/#{calendar1.id},#{calendar3.id}/events?#{time_scope}&#{api_key}" 95 | last_response.status.should == 200 96 | last_response.body.should contain_events(event_attrs2, event_attrs4) 97 | 98 | time_scope = {:start => 5.days.ago.to_i, :end => 1.days.ago.to_i}.to_query 99 | get "calendars/#{calendar1.id},#{calendar2.id},#{calendar3.id}/events?#{time_scope}&#{api_key}" 100 | last_response.status.should == 200 101 | last_response.body.should contain_events(event_attrs3) 102 | 103 | time_scope = {:start => 9.days.ago.to_i, :end => 4.days.ago.to_i}.to_query 104 | get "calendars/#{calendar1.id},#{calendar2.id},#{calendar3.id}/events?#{time_scope}&#{api_key}" 105 | last_response.status.should == 200 106 | last_response.body.should contain_events(event_attrs1, event_attrs2) 107 | 108 | time_scope = {:start => 9.days.ago.to_i, :end => 1.days.ago.to_i}.to_query 109 | get "calendars/#{calendar1.id},#{calendar2.id},#{calendar3.id}/events?#{time_scope}&#{api_key}" 110 | last_response.status.should == 200 111 | last_response.body.should contain_events(event_attrs1, event_attrs2, event_attrs3, event_attrs4) 112 | end 113 | end 114 | end 115 | 116 | describe "POST /calendars/:calendar_id/events" do 117 | let!(:calendar) { create(:calendar, :customer => customer) } 118 | let(:event_attrs) { attributes_for(:event).slice(:title, :start, :end, :color) } 119 | 120 | it "returns an error if calendar is not found" do 121 | post "/calendars/1232131/events?#{api_key}", event_attrs 122 | should response_with_error(404, "Not Found") 123 | Event.count.should == 0 124 | end 125 | 126 | it "returns an error if 'title' is not provided" do 127 | post "/calendars/#{calendar.id}/events?#{api_key}", event_attrs.slice(:start, :end, :color) 128 | last_response.status.should == 400 129 | last_response.body.should == { :error => "missing parameter: title" }.to_json 130 | Event.count.should == 0 131 | end 132 | 133 | it "returns an error if 'start' is not provided" do 134 | post "/calendars/#{calendar.id}/events?#{api_key}", event_attrs.slice(:title, :end, :color) 135 | last_response.status.should == 400 136 | last_response.body.should == { :error => "missing parameter: start" }.to_json 137 | Event.count.should == 0 138 | end 139 | 140 | it "returns an error if 'end' is not provided" do 141 | post "/calendars/#{calendar.id}/events?#{api_key}", event_attrs.slice(:title, :start, :color) 142 | last_response.status.should == 400 143 | last_response.body.should == { :error => "missing parameter: end" }.to_json 144 | Event.count.should == 0 145 | end 146 | 147 | it "returns an error if 'title' is too long" do 148 | post "/calendars/#{calendar.id}/events?#{api_key}", event_attrs.merge(:title => 'a' * 41) 149 | last_response.status.should == 401 150 | last_response.body.should == { :errors => 151 | { "title" => ["must be equal or less than 40 characters long"] } }.to_json 152 | Event.count.should == 0 153 | end 154 | 155 | it "returns an error if 'description' is too long" do 156 | post "/calendars/#{calendar.id}/events?#{api_key}", event_attrs.merge(:description => 'a' * 1001) 157 | last_response.status.should == 401 158 | last_response.body.should == { :errors => 159 | { "description" => ["must be equal or less than 1000 characters long"] } }.to_json 160 | Event.count.should == 0 161 | end 162 | 163 | it "returns an error if 'color' is too long" do 164 | post "/calendars/#{calendar.id}/events?#{api_key}", event_attrs.merge(:color => 'a' * 40) 165 | last_response.status.should == 401 166 | last_response.body.should == { :errors => 167 | { "color" => ["must be equal or less than 40 characters long"] } }.to_json 168 | Event.count.should == 0 169 | end 170 | 171 | it "creates a new event for given calenar" do 172 | post "/calendars/#{calendar.id}/events?#{api_key}", event_attrs 173 | last_response.status.should == 201 174 | last_response.body.should contain_events(event_attrs) 175 | Event.last.title.should == event_attrs[:title] 176 | end 177 | end 178 | 179 | describe "GET /calendars/:calendar_id/events/:id" do 180 | let!(:calendar) { create(:calendar, :customer => customer) } 181 | let!(:event_attrs) { attributes_for(:event) } 182 | let!(:event) { create(:event, event_attrs.merge(:calendar => calendar)) } 183 | 184 | describe "GET /calendars/:calendar_id/events/:id.ical" do 185 | it "returns the event in .ical format" do 186 | get "/calendars/#{calendar.id}/events/#{event.id}.ical?#{api_key}" 187 | last_response.status.should == 200 188 | last_response.header["Content-Type"].should == "text/calendar" 189 | last_response.body[/^DTEND\:(.+)?$/] 190 | DateTime.parse($1.strip).to_i.should == event.end.to_i 191 | last_response.body[/^DTSTART\:(.+)?$/] 192 | DateTime.parse($1.strip).to_i.should == event.start.to_i 193 | end 194 | end 195 | 196 | it "returns an error if 'calendar_id is not valid'" do 197 | get "/calendars/12312312/events/#{event.id}?#{api_key}" 198 | should response_with_error(404, "Not Found") 199 | end 200 | 201 | it "returns an error if 'id' is not valid" do 202 | get "/calendars/#{calendar.id}/events/123123123?#{api_key}" 203 | should response_with_error(404, "Not Found") 204 | end 205 | 206 | it "returns an event" do 207 | get "/calendars/#{calendar.id}/events/#{event.id}?#{api_key}" 208 | last_response.status.should == 200 209 | last_response.body.should contain_events(event_attrs) 210 | end 211 | end 212 | 213 | describe "PUT /calendars/:calendar_id/events/:id" do 214 | let!(:calendar) { create(:calendar, :customer => customer) } 215 | let!(:origin_attrs) { attributes_for(:event) } 216 | let!(:new_attrs) { attributes_for(:event) } 217 | let!(:event) { create(:event, origin_attrs.merge(:calendar => calendar)) } 218 | 219 | it "returns an error if 'calendar_id' is not valid" do 220 | put "/calendars/12312312/events/#{event.id}?#{api_key}" 221 | should response_with_error(404, "Not Found") 222 | end 223 | 224 | it "returns an error if 'id' is not valid" do 225 | put "/calendars/#{calendar.id}/events/#{"a"*24}?#{api_key}" 226 | should response_with_error(404, "Not Found") 227 | end 228 | 229 | it "returns an error if 'title' is blank" do 230 | put "/calendars/#{calendar.id}/events/#{event.id}?#{api_key}", origin_attrs.merge(:title => "") 231 | last_response.status.should == 401 232 | last_response.body.should == { :errors => { "title" => ["can't be blank"] } }.to_json 233 | event.reload 234 | event.title.should == origin_attrs[:title] 235 | event.description.should == origin_attrs[:description] 236 | end 237 | 238 | it "returns an error if 'title' is too long" do 239 | put "/calendars/#{calendar.id}/events/#{event.id}?#{api_key}", origin_attrs.merge(:title => "a"*41) 240 | last_response.status.should == 401 241 | last_response.body.should == { :errors => 242 | { "title" => ["must be equal or less than 40 characters long"] } }.to_json 243 | event.reload 244 | event.title.should == origin_attrs[:title] 245 | event.description.should == origin_attrs[:description] 246 | end 247 | 248 | it "updates the event" do 249 | put "/calendars/#{calendar.id}/events/#{event.id}?#{api_key}", new_attrs 250 | last_response.status.should == 200 251 | last_response.body.should contain_events(new_attrs) 252 | event.reload 253 | event.title.should == new_attrs[:title] 254 | event.description.should == new_attrs[:description] 255 | end 256 | 257 | it "doesn't allow to update 'calendar_id'" do 258 | put "/calendars/#{calendar.id}/events/#{event.id}?#{api_key}", new_attrs.merge!(:calendar_id => "123") 259 | last_response.status.should == 200 260 | event.reload 261 | event.calendar_id.should_not == new_attrs[:calendar_id] 262 | end 263 | 264 | it "doesn't allow to update 'id'" do 265 | put "/calendars/#{calendar.id}/events/#{event.id}?#{api_key}", new_attrs.merge!(:id => "123") 266 | last_response.status.should == 200 267 | event.reload 268 | event.calendar_id.should_not == new_attrs[:id] 269 | end 270 | end 271 | 272 | describe "DELETE /calendars/:calendar_id/events/:id" do 273 | let!(:calendar) { create(:calendar, :customer => customer) } 274 | let!(:event) { create(:event, :calendar => calendar) } 275 | 276 | it "returns an error if 'calendar_id' is not valid" do 277 | delete "/calendars/12312312/events/#{event.id}?#{api_key}" 278 | should response_with_error(404, "Not Found") 279 | Event.count.should == 1 280 | end 281 | 282 | it "returns an error if 'id' is not valid" do 283 | delete "/calendars/#{calendar.id}/events/#{"a"*24}?#{api_key}" 284 | should response_with_error(404, "Not Found") 285 | Event.count.should == 1 286 | end 287 | 288 | it "destroys the event" do 289 | delete "/calendars/#{calendar.id}/events/#{event.id}?#{api_key}" 290 | last_response.status.should == 200 291 | Event.count.should == 0 292 | end 293 | end 294 | end 295 | 296 | --------------------------------------------------------------------------------