├── db ├── seeds.rb ├── migrate │ ├── 20150920101142_create_users.rb │ ├── 20150920182059_create_authentication_tokens.rb │ └── 20150920192224_create_cat_entries.rb └── schema.rb ├── log └── .keep ├── .ruby-version ├── .ruby-gemset ├── config ├── locales │ └── en.yml ├── initializers │ ├── cookies_serializer.rb │ ├── session_store.rb │ ├── wrap_parameters.rb │ ├── active_model_serializers.rb │ └── filter_parameter_logging.rb ├── boot.rb ├── environment.rb ├── database.yml ├── secrets.yml ├── routes.rb ├── application.rb └── environments │ ├── development.rb │ ├── test.rb │ └── production.rb ├── public └── robots.txt ├── Rakefile ├── spec ├── support │ ├── factory_girl.rb │ └── request_helpers.rb ├── factories │ ├── authentication_tokens.rb │ ├── users.rb │ └── cat_entries.rb ├── serializers │ ├── user_serializer_spec.rb │ ├── authentication_token_serializer_spec.rb │ ├── user_session_serializer_spec.rb │ └── cat_entry_serializer_spec.rb ├── routing │ └── api │ │ └── v1 │ │ ├── sessions_routing_spec.rb │ │ ├── users_routing_spec.rb │ │ └── cat_entries_routing_spec.rb ├── lib │ └── api_constraints_spec.rb ├── models │ ├── authentication_token_spec.rb │ ├── user_spec.rb │ ├── cat_entry_spec.rb │ └── ability_spec.rb ├── controllers │ ├── application_controller_spec.rb │ └── api │ │ └── v1 │ │ ├── sessions_controller_spec.rb │ │ ├── cat_entries_controller_spec.rb │ │ └── users_controller_spec.rb └── spec_helper.rb ├── bin ├── bundle ├── rake ├── rails └── setup ├── app ├── serializers │ ├── user_serializer.rb │ ├── authentication_token_serializer.rb │ ├── cat_entry_serializer.rb │ └── user_session_serializer.rb ├── models │ ├── ability.rb │ ├── user.rb │ ├── authentication_token.rb │ └── cat_entry.rb └── controllers │ ├── application_controller.rb │ └── api │ └── v1 │ ├── sessions_controller.rb │ ├── users_controller.rb │ └── cat_entries_controller.rb ├── config.ru ├── .gitignore ├── lib ├── validators │ └── email_validator.rb ├── api_constraints.rb └── tasks │ └── db.rake ├── Gemfile ├── LICENSE.md ├── README.md └── Gemfile.lock /db/seeds.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.4.3 2 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | lofocats-api 2 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require File.expand_path('../config/application', __FILE__) 2 | 3 | Rails.application.load_tasks 4 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.action_dispatch.cookies_serializer = :json 2 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.session_store :cookie_store, key: '_lofocats_api_session' 2 | -------------------------------------------------------------------------------- /spec/support/factory_girl.rb: -------------------------------------------------------------------------------- 1 | # RSpec 2 | RSpec.configure do |config| 3 | config.include FactoryGirl::Syntax::Methods 4 | end -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | ActiveSupport.on_load(:action_controller) do 2 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 3 | end -------------------------------------------------------------------------------- /app/serializers/user_serializer.rb: -------------------------------------------------------------------------------- 1 | class UserSerializer < ActiveModel::Serializer 2 | # Serialized attributes for the User 3 | attributes :id, :email, :admin 4 | end 5 | -------------------------------------------------------------------------------- /config/initializers/active_model_serializers.rb: -------------------------------------------------------------------------------- 1 | # Disable root key when serializing 2 | ActiveModel::Serializer.root = false 3 | ActiveModel::ArraySerializer.root = false -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Include the authentication token in the filtered parameters 2 | Rails.application.config.filter_parameters += [:password, :authentication_token] 3 | -------------------------------------------------------------------------------- /spec/factories/authentication_tokens.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :authentication_token do 3 | user { FactoryGirl.build(:user) } 4 | token { 'ABCD-EFAAA-ABC982374' } 5 | end 6 | end -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path("../spring", __FILE__) 4 | rescue LoadError 5 | end 6 | require_relative '../config/boot' 7 | require 'rake' 8 | Rake.application.run 9 | -------------------------------------------------------------------------------- /spec/support/request_helpers.rb: -------------------------------------------------------------------------------- 1 | module Request 2 | module JsonHelpers 3 | def json_response 4 | @json_response ||= JSON.parse(response.body, symbolize_names: true) 5 | end 6 | end 7 | end -------------------------------------------------------------------------------- /app/serializers/authentication_token_serializer.rb: -------------------------------------------------------------------------------- 1 | class AuthenticationTokenSerializer < ActiveModel::Serializer 2 | # Serialized attributes for the authentication token 3 | attributes :token, :expires_at 4 | end 5 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path("../spring", __FILE__) 4 | rescue LoadError 5 | end 6 | APP_PATH = File.expand_path('../../config/application', __FILE__) 7 | require_relative '../config/boot' 8 | require 'rails/commands' 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore bundler config. 2 | /.bundle 3 | 4 | # Ignore the default SQLite database. 5 | /db/*.sqlite3 6 | /db/*.sqlite3-journal 7 | 8 | # Ignore all logfiles and tempfiles. 9 | /log/* 10 | !/log/.keep 11 | /tmp 12 | 13 | .rspec 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: sqlite3 3 | pool: 5 4 | timeout: 5000 5 | 6 | development: 7 | <<: *default 8 | database: db/development.sqlite3 9 | 10 | test: 11 | <<: *default 12 | database: db/test.sqlite3 13 | 14 | production: 15 | <<: *default 16 | database: db/production.sqlite3 17 | -------------------------------------------------------------------------------- /app/serializers/cat_entry_serializer.rb: -------------------------------------------------------------------------------- 1 | class CatEntrySerializer < ActiveModel::Serializer 2 | # Serialized attirbutes for the Cat entry 3 | attributes :id, :breed, :color, :longitude, :latitude, :contact_phone, :contact_email, :event_date, :entry_type, :resolved, :chip, :photo_url 4 | 5 | # Include user as well 6 | has_one :user 7 | end 8 | -------------------------------------------------------------------------------- /spec/serializers/user_serializer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe UserSerializer do 4 | let(:user) { FactoryGirl.create(:user) } 5 | 6 | subject { UserSerializer.new(user).to_json } 7 | 8 | it 'should serialize the correct attributes' do 9 | expect(subject).to eq '{"id":1,"email":"test@lofocat.dev","admin":null}' 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/factories/users.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :user do 3 | email 'test@lofocat.dev' 4 | password '12345678' 5 | password_confirmation '12345678' 6 | end 7 | 8 | factory :admin_user, :class => User do 9 | email 'admin@lofocat.dev' 10 | password '12345678' 11 | password_confirmation '12345678' 12 | admin true 13 | end 14 | end -------------------------------------------------------------------------------- /lib/validators/email_validator.rb: -------------------------------------------------------------------------------- 1 | # A simple email validator (regex is not mine :), retrieved by Rails documentation) 2 | class EmailValidator < ActiveModel::EachValidator 3 | def validate_each(record, attribute, value) 4 | unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i 5 | record.errors[attribute] << (options[:message] || "is not an email") 6 | end 7 | end 8 | end -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | development: 2 | secret_key_base: c32dde6c69b9e6475b3fe0708222e679bbf421de2f0fe0e2d0c66a59a4b4daa7b816ed3c0f21f4a612fab8317a19e1ee7e3d25b7b84cce7fe0fef7697fb4ab11 3 | 4 | test: 5 | secret_key_base: 8efc70f750b2ee204b98dd6fa28bbc22962362b392aa866ca41f5531d1d80d80cf7da99dca2e3bfb678f5b72b82f199048881d8f08ed401eaeb64a95f8b08c5b 6 | 7 | production: 8 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 9 | -------------------------------------------------------------------------------- /db/migrate/20150920101142_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 2 | def change 3 | create_table(:users) do |t| 4 | t.string :email, :null => false, default: "" 5 | t.string :password_digest, :null => false, default: "" 6 | t.boolean :admin 7 | 8 | t.timestamps 9 | end 10 | 11 | add_index :users, :email, unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/api_constraints.rb: -------------------------------------------------------------------------------- 1 | # Simple constraint to match the desired API version in the routes 2 | class ApiConstraints 3 | def initialize(options) 4 | @version = options[:version] 5 | @default = options[:default] 6 | end 7 | 8 | # Resolve if the given request matches this version or reached the default version 9 | def matches?(req) 10 | @default || req.headers['Accept'].include?("application/vnd.lofocats.v#{@version}") 11 | end 12 | end -------------------------------------------------------------------------------- /spec/serializers/authentication_token_serializer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe AuthenticationTokenSerializer do 4 | let(:authentication_token) { FactoryGirl.build(:authentication_token) } 5 | 6 | subject { AuthenticationTokenSerializer.new(authentication_token).to_json } 7 | 8 | it 'should serialize the correct attributes' do 9 | expect(subject).to eq '{"token":"ABCD-EFAAA-ABC982374","expires_at":null}' 10 | end 11 | end -------------------------------------------------------------------------------- /spec/routing/api/v1/sessions_routing_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'routes for Sessions' do 4 | it 'routes post /api/sessions to sessions#create' do 5 | expect({:post => '/api/sessions'}).to route_to(:controller => 'api/v1/sessions', :action => 'create', :format => :json) 6 | end 7 | 8 | it 'routes delete /api/sessions to sessions#destroy' do 9 | expect({:delete => '/api/sessions'}).to route_to(:controller => 'api/v1/sessions', :action => 'destroy', :format => :json) 10 | end 11 | end -------------------------------------------------------------------------------- /spec/factories/cat_entries.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :cat_entry do 3 | user { FactoryGirl.build(:user) } 4 | breed 'Persian' 5 | color 'Grey' 6 | longitude 13.222444 7 | latitude 34.234123 8 | contact_phone '0031123456789' 9 | contact_email 'found_a_cat@lofocats.com' 10 | event_date { Date.new(2015,9,26) } 11 | entry_type 'lost' 12 | photo_url 'http://lofocats.com/an_image.png' 13 | 14 | factory :new_cat_entry, :class => CatEntry do 15 | user nil 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /db/migrate/20150920182059_create_authentication_tokens.rb: -------------------------------------------------------------------------------- 1 | class CreateAuthenticationTokens < ActiveRecord::Migration 2 | def change 3 | create_table :authentication_tokens do |t| 4 | t.integer :user_id, :null => false 5 | t.string :token, :null => false 6 | t.datetime :expires_at, :null => false 7 | 8 | t.timestamps null: false 9 | end 10 | 11 | add_index :authentication_tokens, :token, :unique => true 12 | add_index :authentication_tokens, :user_id 13 | add_index :authentication_tokens, :expires_at 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | namespace :api, :defaults => { :format => :json } do 3 | # Serve and default to v1 version of the API 4 | scope :module => :v1, :constraints => ApiConstraints.new(version: 1, default: true) do 5 | # Users 6 | resources :users, :only => [:index, :show, :create, :update, :destroy] 7 | 8 | # Sessions 9 | resource :sessions, :only => [:create, :destroy] 10 | 11 | # Cat entries 12 | resources :cat_entries, :only => [:index, :show, :create, :update, :destroy] 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/serializers/user_session_serializer.rb: -------------------------------------------------------------------------------- 1 | # Serialized attributes for a Session consisting of the user & authentication token 2 | class UserSessionSerializer < ActiveModel::Serializer 3 | 4 | # The signed in user 5 | has_one :user 6 | 7 | # The created authentication token 8 | has_one :authentication_token 9 | 10 | # Extracts the user from the given object (hash) 11 | def user 12 | object[:user] 13 | end 14 | 15 | # Extracts the authentication token from the given object (hash) 16 | def authentication_token 17 | object[:authentication_token] 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require "rails" 4 | 5 | require "active_model/railtie" 6 | #require "active_job/railtie" 7 | require "active_record/railtie" 8 | require "action_controller/railtie" 9 | #require "action_mailer/railtie" 10 | #require "action_view/railtie" 11 | 12 | Bundler.require(*Rails.groups) 13 | 14 | module LofoCatsApi 15 | class Application < Rails::Application 16 | config.active_record.raise_in_transactional_callbacks = true 17 | 18 | config.autoload_paths << Rails.root.join('lib') << Rails.root.join('lib/validators') 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Rails 4 | gem 'rails', '~>4.2' 5 | # Database 6 | gem 'sqlite3' 7 | 8 | # Required to use 'has_secure_password' in ActiveRecord 9 | gem 'bcrypt' 10 | 11 | # Serializers for the JSON responses 12 | gem 'active_model_serializers' 13 | 14 | # Used for API authorization 15 | gem 'cancan' 16 | 17 | group :test do 18 | # Rspec 19 | gem 'rspec-rails', '~> 3.0' 20 | # FactoryGirl factories 21 | gem "factory_girl_rails" 22 | # Use should matchers 23 | gem 'shoulda-matchers', require: false 24 | # To better handle time sensitive tests 25 | gem 'timecop' 26 | end 27 | -------------------------------------------------------------------------------- /spec/serializers/user_session_serializer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe UserSessionSerializer do 4 | let(:user) { FactoryGirl.build(:user, :id => 1) } 5 | let(:authentication_token) { FactoryGirl.build(:authentication_token, :user => user) } 6 | let(:session_information) { {:user => user, :authentication_token => authentication_token} } 7 | 8 | subject { UserSessionSerializer.new(session_information).to_json } 9 | 10 | it 'should serialize the correct attributes' do 11 | expect(subject).to eq '{"user":{"id":1,"email":"test@lofocat.dev","admin":null},"authentication_token":{"token":"ABCD-EFAAA-ABC982374","expires_at":null}}' 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/ability.rb: -------------------------------------------------------------------------------- 1 | class Ability 2 | include CanCan::Ability 3 | 4 | def initialize(user) 5 | user ||= User.new 6 | 7 | if user.admin? 8 | # Admin 9 | can :manage, :all 10 | elsif user.id.present? 11 | # Normal user 12 | can :destroy, :session 13 | 14 | can :read, CatEntry 15 | can :create, CatEntry 16 | can :update, CatEntry, :user_id => user.id 17 | can :destroy, CatEntry, :user_id => user.id 18 | 19 | can :read, User, :id => user.id 20 | can :update, User, :id => user.id 21 | else 22 | # Guest 23 | can :create, :session 24 | 25 | can :read, CatEntry 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/serializers/cat_entry_serializer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CatEntrySerializer do 4 | let(:cat_entry) { FactoryGirl.create(:cat_entry, :user => FactoryGirl.create(:user)) } 5 | 6 | subject { CatEntrySerializer.new(cat_entry).to_json } 7 | 8 | it 'should serialize the correct attributes' do 9 | expect(subject).to eq '{"id":1,"breed":"Persian","color":"Grey","longitude":"13.222444","latitude":"34.234123","contact_phone":"0031123456789","contact_email":"found_a_cat@lofocats.com","event_date":"2015-09-26","entry_type":"lost","resolved":null,"chip":null,"photo_url":"http://lofocats.com/an_image.png","user":{"id":1,"email":"test@lofocat.dev","admin":null}}' 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/lib/api_constraints_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ApiConstraints do 4 | let(:api_constraints_v1) { ApiConstraints.new(version: 1) } 5 | let(:api_constraints_v2) { ApiConstraints.new(version: 2, default: true) } 6 | 7 | describe "matches?" do 8 | 9 | it "returns true when the version matches the 'Accept' header" do 10 | request = double(host: 'lofocats', 11 | headers: {'Accept' => 'application/vnd.lofocats.v1'}) 12 | 13 | expect(api_constraints_v1.matches?(request)).to be_truthy 14 | end 15 | 16 | it "returns the default version when 'default' option is specified" do 17 | request = double(host: 'lofocats') 18 | expect(api_constraints_v2.matches?(request)).to be_truthy 19 | end 20 | end 21 | end -------------------------------------------------------------------------------- /spec/models/authentication_token_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe AuthenticationToken do 4 | subject { FactoryGirl.create(:authentication_token) } 5 | 6 | ### Attributes ### 7 | 8 | it { should respond_to :user, :user= } 9 | it { should respond_to :token, :token= } 10 | it { should respond_to :expires_at, :expires_at= } 11 | 12 | ### Validations ### 13 | 14 | it { should be_valid } 15 | it { should validate_presence_of :user } 16 | it { should validate_presence_of :token } 17 | it { should validate_uniqueness_of :token } 18 | 19 | ### Hooks ### 20 | it 'should auto-set expiration' do 21 | Timecop.freeze(Time.local(2015)) do 22 | expect(FactoryGirl.create(:authentication_token).expires_at).to eq (DateTime.now + 3.days) 23 | end 24 | end 25 | end -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | ### Relations ### 3 | 4 | # Enable Rails' functionality for password management 5 | has_secure_password 6 | 7 | has_many :authentication_tokens, :inverse_of => :user, :dependent => :destroy 8 | has_many :cat_entries, :inverse_of => :user, :dependent => :destroy 9 | 10 | ### Validations ### 11 | validates :email, :presence => true, :uniqueness => true, :email => true 12 | validates :password, :length => { :minimum => 6, :if => 'password.present?' } 13 | validates :password_confirmation, :presence => { :if => 'password.present?'} 14 | 15 | # Generates a new token for the user. Explicit user save has to be executed to be saved. 16 | def generate_new_token! 17 | authentication_tokens.build(:token => SecureRandom.uuid) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/routing/api/v1/users_routing_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'routes for Users' do 4 | it 'routes /api/users/:id to users#show' do 5 | expect({:get => '/api/users/1'}).to route_to(:controller => 'api/v1/users', :action => 'show', :id => '1', :format => :json) 6 | end 7 | 8 | it 'routes post /api/users to users#create' do 9 | expect({:post => '/api/users'}).to route_to(:controller => 'api/v1/users', :action => 'create', :format => :json) 10 | end 11 | 12 | it 'routes put /api/users/:id to users#update' do 13 | expect({:put => '/api/users/1'}).to route_to(:controller => 'api/v1/users', :action => 'update', :id => '1', :format => :json) 14 | end 15 | 16 | it 'routes delete /api/users/:id to users#destroy' do 17 | expect({:delete => '/api/users/1'}).to route_to(:controller => 'api/v1/users', :action => 'destroy', :id => '1', :format => :json) 18 | end 19 | end -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # CSRF handling for APIS (instead of raising exceptions) 3 | protect_from_forgery :with => :null_session 4 | 5 | # Whenever an AccessDenied is raised based on the current ability, respond with 401 6 | rescue_from CanCan::AccessDenied do |exception| 7 | head 401 8 | end 9 | 10 | # Retrieve current user 11 | def current_user 12 | @current_user ||= resolve_user_by_token 13 | end 14 | 15 | # Retrieve current authentication token from the request's respective header 16 | def current_authentication_token 17 | request.headers['Authorization'] 18 | end 19 | 20 | private 21 | 22 | # Retrieve user from the current authentication token 23 | def resolve_user_by_token 24 | token = AuthenticationToken.find_by_token(current_authentication_token) 25 | token.try(:user) 26 | end 27 | end -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | # path to your application root. 5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 6 | 7 | Dir.chdir APP_ROOT do 8 | # This script is a starting point to setup your application. 9 | # Add necessary setup steps to this file: 10 | 11 | puts "== Installing dependencies ==" 12 | system "gem install bundler --conservative" 13 | system "bundle check || bundle install" 14 | 15 | # puts "\n== Copying sample files ==" 16 | # unless File.exist?("config/database.yml") 17 | # system "cp config/database.yml.sample config/database.yml" 18 | # end 19 | 20 | puts "\n== Preparing database ==" 21 | system "bin/rake db:setup" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system "rm -f log/*" 25 | system "rm -rf tmp/cache" 26 | 27 | puts "\n== Restarting application server ==" 28 | system "touch tmp/restart.txt" 29 | end 30 | -------------------------------------------------------------------------------- /app/models/authentication_token.rb: -------------------------------------------------------------------------------- 1 | class AuthenticationToken < ActiveRecord::Base 2 | ### Relations ### 3 | 4 | # The user for this authentication token 5 | belongs_to :user, :inverse_of => :authentication_tokens 6 | 7 | ### Hooks ### 8 | 9 | # Set expiration upon save 10 | before_save :set_expiration 11 | 12 | ### Validations ### 13 | validates :user, :presence => true 14 | validates :token, :presence => true, :uniqueness => true 15 | 16 | ### Scopes ### 17 | 18 | # Default scope excludes expired tokens 19 | default_scope -> { where('expires_at > :now', :now => DateTime.now) } 20 | scope :expired, -> { unscoped.where('expires_at <= :now', :now => DateTime.now) } 21 | 22 | private 23 | 24 | # Sets the default expiration of the token 25 | def set_expiration 26 | # TODO: load the expiration period (now fixed to 3) from a configuration file 27 | self.expires_at ||= DateTime.now + 3.days 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /db/migrate/20150920192224_create_cat_entries.rb: -------------------------------------------------------------------------------- 1 | class CreateCatEntries < ActiveRecord::Migration 2 | def change 3 | create_table :cat_entries do |t| 4 | t.integer :user_id, :null => false 5 | t.string :breed, :null => false 6 | t.string :color, :null => false 7 | t.string :longitude, :null => false 8 | t.string :latitude, :null => false 9 | t.string :contact_phone, :null => false 10 | t.string :contact_email, :null => false 11 | t.date :event_date, :null => false 12 | t.string :entry_type, :null => false 13 | t.boolean :resolved 14 | t.string :chip 15 | t.text :photo_url, :null => false 16 | 17 | t.timestamps null: false 18 | end 19 | 20 | add_index :cat_entries, :user_id 21 | add_index :cat_entries, :event_date 22 | add_index :cat_entries, :entry_type 23 | add_index :cat_entries, :resolved 24 | add_index :cat_entries, :chip 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe User do 4 | # Attributes 5 | 6 | it { should respond_to :email, :email= } 7 | it { should respond_to :password, :password= } 8 | it { should respond_to :password_confirmation, :password_confirmation= } 9 | it { should respond_to :password_digest, :password_digest= } 10 | it { should respond_to :admin, :admin=, :admin? } 11 | 12 | it { should respond_to :cat_entries, :cat_entries= } 13 | it { should respond_to :authentication_tokens, :authentication_tokens= } 14 | 15 | # Validations 16 | it { should validate_presence_of(:email) } 17 | it { should validate_uniqueness_of(:email) } 18 | it { should allow_value('example@domain.com').for(:email) } 19 | 20 | it { should validate_presence_of(:password) } 21 | it { should validate_confirmation_of(:password) } 22 | it { should validate_length_of(:password).is_at_least(6) } 23 | 24 | it 'validates presence of password confirmation if password is set' do 25 | user = User.new(:password => '123') 26 | expect(user).to validate_presence_of(:password_confirmation) 27 | end 28 | end -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Lazarus Lazaridis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/models/cat_entry.rb: -------------------------------------------------------------------------------- 1 | class CatEntry < ActiveRecord::Base 2 | ### Relations ### 3 | belongs_to :user, :inverse_of => :cat_entries 4 | 5 | ### Validations ### 6 | validates :user, :presence => true 7 | validates :breed, :presence => true, :length => { :maximum => 128 } 8 | validates :color, :presence => true, :length => { :maximum => 48 } 9 | validates :longitude, :presence => true, :length => { :maximum => 16 } 10 | validates :latitude, :presence => true, :length => { :maximum => 16 } 11 | validates :contact_phone, :presence => true, :length => { :maximum => 16 } 12 | validates :contact_email, :presence => true, :email => true 13 | validates :event_date, :presence => true 14 | validates :entry_type, :inclusion => { :in => %w(lost found) } 15 | validates :photo_url, :format => { :with => URI::regexp(%w(http https)) } 16 | 17 | ### Scopes ### 18 | 19 | # Default scope excludes resolved entries 20 | default_scope -> { where(:resolved => [false, nil]) } 21 | 22 | scope :found, -> { where(:entry_type => :found) } 23 | scope :lost, -> { where(:entry_type => :lost) } 24 | scope :resolved, -> { unscoped.where(:resolved => true) } 25 | end 26 | -------------------------------------------------------------------------------- /spec/routing/api/v1/cat_entries_routing_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'routes for Cat entries' do 4 | it 'routes /api/cat_entries to cat_entries#index' do 5 | expect({:get => '/api/cat_entries'}).to route_to(:controller => 'api/v1/cat_entries', :action => 'index', :format => :json) 6 | end 7 | 8 | it 'routes /api/cat_entries/:id to cat_entries#show' do 9 | expect({:get => '/api/cat_entries/1'}).to route_to(:controller => 'api/v1/cat_entries', :action => 'show', :id => '1', :format => :json) 10 | end 11 | 12 | it 'routes post /api/cat_entries to cat_entries#create' do 13 | expect({:post => '/api/cat_entries'}).to route_to(:controller => 'api/v1/cat_entries', :action => 'create', :format => :json) 14 | end 15 | 16 | it 'routes put /api/cat_entries/:id to cat_entries#update' do 17 | expect({:put => '/api/cat_entries/1'}).to route_to(:controller => 'api/v1/cat_entries', :action => 'update', :id => '1', :format => :json) 18 | end 19 | 20 | it 'routes delete /api/cat_entries/:id to cat_entries#destroy' do 21 | expect({:delete => '/api/cat_entries/1'}).to route_to(:controller => 'api/v1/cat_entries', :action => 'destroy', :id => '1', :format => :json) 22 | end 23 | end -------------------------------------------------------------------------------- /app/controllers/api/v1/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::SessionsController < ApplicationController 2 | 3 | # POST '/api/sessions' 4 | def create 5 | authorize! :create, :session 6 | 7 | # Fail with missing params 8 | handle_authentication_failure and return unless params[:session].present? 9 | 10 | email = params[:session][:email] 11 | password = params[:session][:password] 12 | user = User.find_by_email(email) 13 | 14 | if user.present? && user.authenticate(password) 15 | # Generate a new authentication token 16 | token = user.generate_new_token! 17 | user.save 18 | 19 | render :json => { :user => user, :authentication_token => token }, 20 | :serializer => UserSessionSerializer, 21 | :status => 201 22 | else 23 | handle_authentication_failure 24 | end 25 | end 26 | 27 | # DELETE '/api/sessions' 28 | def destroy 29 | authorize! :destroy, :session 30 | 31 | token = AuthenticationToken.find_by_token(current_authentication_token) 32 | 33 | if token.present? 34 | token.destroy 35 | head 204 36 | else 37 | head 404 38 | end 39 | end 40 | 41 | private 42 | 43 | # Heads 401 44 | def handle_authentication_failure 45 | head 401 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Print deprecation notices to the Rails logger. 17 | config.active_support.deprecation = :log 18 | 19 | # Raise an error on page load if there are pending migrations. 20 | config.active_record.migration_error = :page_load 21 | 22 | # Debug mode disables concatenation and preprocessing of assets. 23 | # This option may cause significant delays in view rendering with a large 24 | # number of complex assets. 25 | config.assets.debug = true 26 | 27 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 28 | # yet still be able to expire them through the digest params. 29 | config.assets.digest = true 30 | 31 | # Adds additional error checking when serving assets at runtime. 32 | # Checks for improperly declared sprockets dependencies. 33 | # Raises helpful error messages. 34 | config.assets.raise_runtime_errors = true 35 | 36 | # Raises error for missing translations 37 | # config.action_view.raise_on_missing_translations = true 38 | end 39 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static file server for tests with Cache-Control for performance. 16 | config.serve_static_files = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Randomize the order test cases are executed. 30 | config.active_support.test_order = :random 31 | 32 | # Print deprecation notices to the stderr. 33 | config.active_support.deprecation = :stderr 34 | 35 | # Raises error for missing translations 36 | # config.action_view.raise_on_missing_translations = true 37 | end 38 | -------------------------------------------------------------------------------- /app/controllers/api/v1/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::UsersController < ApplicationController 2 | 3 | # Load the user based on the id parameter 4 | before_filter :load_user, :only => [:show, :update, :destroy] 5 | 6 | # GET '/api/users' 7 | def index 8 | authorize! :read_all, User 9 | 10 | render :json => User.all 11 | end 12 | 13 | # GET '/api/users/:id' 14 | def show 15 | authorize! :read, User 16 | 17 | render :json => @user 18 | end 19 | 20 | # POST '/api/users' 21 | def create 22 | authorize! :create, User 23 | 24 | user = User.new user_params 25 | 26 | if user.save 27 | render :json => user, :status => 201 28 | else 29 | render :json => { :errors => user.errors }, :status => 422 30 | end 31 | end 32 | 33 | # PUT/PATCH '/api/users/:id' 34 | def update 35 | authorize! :update, @user 36 | 37 | if @user.update(user_params) 38 | render json: @user, status: 200 39 | else 40 | render json: { errors: @user.errors }, status: 422 41 | end 42 | end 43 | 44 | # DELETE '/api/users/:id' 45 | def destroy 46 | authorize! :destroy, @user 47 | 48 | if current_user != @user 49 | @user.destroy 50 | 51 | head 204 52 | else 53 | head 422 54 | end 55 | end 56 | 57 | private 58 | 59 | # Retrieves the user with the parameter id 60 | def load_user 61 | @user = User.find_by_id(params[:id]) 62 | head 404 and return unless @user.present? 63 | end 64 | 65 | # Define allowed parameters 66 | def user_params 67 | params.require(:user).permit(:email, 68 | :admin, 69 | :password, 70 | :password_confirmation) 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/controllers/application_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ApplicationController do 4 | controller do 5 | def index 6 | raise CanCan::AccessDenied 7 | end 8 | end 9 | 10 | describe 'Handling of CanCan Access denied exceptions' do 11 | before do 12 | get :index 13 | end 14 | 15 | it { should respond_with 401 } 16 | end 17 | 18 | describe '#current_user' do 19 | let(:user) { FactoryGirl.build(:user) } 20 | 21 | it 'should keep the user previously resolved by the authentication token' do 22 | expect(controller).to receive(:resolve_user_by_token).once.and_return(user) 23 | 24 | expect(controller.current_user).to eq user 25 | expect(controller.current_user).to eq user 26 | end 27 | end 28 | 29 | describe '#current_authentication_token' do 30 | let(:token) { 'ABCD' } 31 | it 'should return the request authorization header' do 32 | request.headers['Authorization'] = token 33 | 34 | expect(controller.current_authentication_token).to eq token 35 | end 36 | end 37 | 38 | describe '#resolve_user_by_token' do 39 | context 'without token' do 40 | it 'should return nil' do 41 | expect(AuthenticationToken).to receive(:find_by_token).and_return(nil) 42 | 43 | expect(controller.send(:resolve_user_by_token)).to be_nil 44 | end 45 | end 46 | 47 | context 'with token' do 48 | it 'should return the token user' do 49 | user = FactoryGirl.create(:user) 50 | authentication_token = FactoryGirl.create(:authentication_token, :user => user) 51 | 52 | expect(AuthenticationToken).to receive(:find_by_token).and_return(authentication_token) 53 | 54 | expect(controller.send(:resolve_user_by_token)).to eq user 55 | end 56 | end 57 | end 58 | end -------------------------------------------------------------------------------- /spec/controllers/api/v1/sessions_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Api::V1::SessionsController do 4 | describe "POST '/api/sessions'" do 5 | let(:user) { FactoryGirl.create :user } 6 | 7 | before do 8 | expect(controller).to receive(:authorize!).with(:create, :session) 9 | 10 | post :create, { :session => credentials } 11 | end 12 | 13 | context 'without session parameters' do 14 | let(:credentials) { nil } 15 | it { should respond_with 401 } 16 | end 17 | 18 | context 'with invalid credentials' do 19 | let(:credentials) { 20 | { 21 | :email => user.email, 22 | :password => 'foofoo' 23 | } 24 | } 25 | 26 | it { should respond_with 401 } 27 | end 28 | 29 | context 'with valid credentials' do 30 | let(:credentials) { 31 | { 32 | :email => user.email, 33 | :password => '12345678' 34 | } 35 | } 36 | 37 | it { should respond_with 201 } 38 | 39 | it 'should respond with user and authentication token information' do 40 | expect(json_response.to_json).to eq UserSessionSerializer.new({:user => user, :authentication_token => user.authentication_tokens.first}).to_json 41 | end 42 | end 43 | end 44 | 45 | describe "DELETE '/api/sessions'" do 46 | let(:user) { FactoryGirl.create(:user) } 47 | let!(:authentication_token) { FactoryGirl.create(:authentication_token, :user => user, :token => 'ABC') } 48 | let(:current_authentication_token) { nil } 49 | 50 | before do 51 | expect(controller).to receive(:authorize!).with(:destroy, :session) 52 | allow(controller).to receive(:current_authentication_token).and_return(current_authentication_token) 53 | 54 | delete :destroy 55 | end 56 | 57 | context 'for non existent token' do 58 | let(:current_authentication_token) { 'DEF' } 59 | 60 | it { should respond_with 404 } 61 | end 62 | 63 | context 'for existent token' do 64 | let(:current_authentication_token) { 'ABC' } 65 | 66 | it { should respond_with 204 } 67 | 68 | it 'should destroy the token' do 69 | expect(AuthenticationToken.count).to eq 0 70 | end 71 | end 72 | end 73 | end -------------------------------------------------------------------------------- /app/controllers/api/v1/cat_entries_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::CatEntriesController < ApplicationController 2 | # Load the cat entry with the given id 3 | before_filter :load_cat_entry, :only => [:show, :update, :destroy] 4 | 5 | # GET '/api/cat_entries' 6 | def index 7 | authorize! :read, CatEntry 8 | 9 | @cat_entries = CatEntry.all 10 | 11 | render :json => @cat_entries 12 | end 13 | 14 | # GET '/api/cat_entries/:id' 15 | def show 16 | authorize! :read, CatEntry 17 | 18 | render :json => @cat_entry 19 | end 20 | 21 | # POST '/api/cat_entries' 22 | def create 23 | authorize! :create, CatEntry 24 | 25 | @cat_entry = current_user.cat_entries.build cat_entry_params 26 | 27 | if @cat_entry.save 28 | render :json => @cat_entry, :status => 201 29 | else 30 | respond_with_invalid_cat_entry_errors 31 | end 32 | end 33 | 34 | # PUT/PATCH '/api/cat_entries/:id' 35 | def update 36 | authorize! :update, @cat_entry 37 | 38 | if @cat_entry.update(cat_entry_params) 39 | render json: @cat_entry, status: 200 40 | else 41 | respond_with_invalid_cat_entry_errors 42 | end 43 | end 44 | 45 | # DELETE '/api/cat_entries' 46 | def destroy 47 | authorize! :destroy, @cat_entry 48 | @cat_entry.destroy 49 | 50 | head 204 51 | end 52 | 53 | protected 54 | 55 | # Renders validation errors of a cat entry with status 422 56 | def respond_with_invalid_cat_entry_errors 57 | render json: { errors: @cat_entry.errors }, status: 422 58 | end 59 | 60 | # Loads the cat entry with the given id to the @cat_entry instance variable 61 | def load_cat_entry 62 | @cat_entry = CatEntry.find(params[:id]) if params[:id].present? 63 | end 64 | 65 | # Defined valid params 66 | def cat_entry_params 67 | params.require(:cat_entry).permit(:breed, 68 | :color, 69 | :longitude, 70 | :latitude, 71 | :contact_phone, 72 | :contact_email, 73 | :event_date, 74 | :entry_type, 75 | :resolved, 76 | :chip, 77 | :photo_url) 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/models/cat_entry_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CatEntry do 4 | subject { FactoryGirl.build(:cat_entry) } 5 | 6 | it { should respond_to :user, :user= } 7 | it { should respond_to :breed, :breed= } 8 | it { should respond_to :color, :color= } 9 | it { should respond_to :longitude, :longitude= } 10 | it { should respond_to :latitude, :latitude= } 11 | it { should respond_to :contact_phone, :contact_phone= } 12 | it { should respond_to :contact_email, :contact_email= } 13 | it { should respond_to :event_date, :event_date= } 14 | it { should respond_to :entry_type, :entry_type=} 15 | it { should respond_to :resolved, :resolved=, :resolved? } 16 | it { should respond_to :chip, :chip= } 17 | it { should respond_to :photo_url, :photo_url= } 18 | 19 | it { should validate_presence_of :user } 20 | it { should validate_presence_of :breed } 21 | it { should validate_length_of(:breed).is_at_most(128) } 22 | it { should validate_presence_of :color } 23 | it { should validate_length_of(:color).is_at_most(48) } 24 | it { should validate_presence_of :longitude } 25 | it { should validate_length_of(:longitude).is_at_most(16) } 26 | it { should validate_presence_of :latitude } 27 | it { should validate_length_of(:latitude).is_at_most(16) } 28 | it { should validate_presence_of :contact_phone } 29 | it { should validate_length_of(:contact_phone).is_at_most(16) } 30 | it { should validate_presence_of :contact_email } 31 | it { should validate_presence_of :event_date } 32 | it { should validate_inclusion_of(:entry_type).in_array(%w(lost found)) } 33 | 34 | context 'email validation' do 35 | it 'should not allow invalid contact email' do 36 | subject.contact_email = 'foo' 37 | 38 | expect(subject.valid?).to be_falsey 39 | expect(subject.errors[:contact_email]).to include('is not an email') 40 | end 41 | 42 | it 'should allow valid contact emails' do 43 | subject.contact_email = 'lazarus@lofocats.com' 44 | 45 | expect(subject).to be_valid 46 | end 47 | end 48 | 49 | context 'photo url validation' do 50 | it 'should not allow invalid urls' do 51 | subject.photo_url = 'invalid.url' 52 | 53 | expect(subject.valid?).to be_falsey 54 | expect(subject.errors[:photo_url]).to include('is invalid') 55 | end 56 | 57 | it 'should allow valid urls' do 58 | subject.photo_url = 'http://lofocats.com/foo.png' 59 | 60 | expect(subject.valid?).to be_truthy 61 | end 62 | end 63 | end -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 20150920192224) do 15 | 16 | create_table "authentication_tokens", force: :cascade do |t| 17 | t.integer "user_id", null: false 18 | t.string "token", null: false 19 | t.datetime "expires_at", null: false 20 | t.datetime "created_at", null: false 21 | t.datetime "updated_at", null: false 22 | end 23 | 24 | add_index "authentication_tokens", ["expires_at"], name: "index_authentication_tokens_on_expires_at" 25 | add_index "authentication_tokens", ["token"], name: "index_authentication_tokens_on_token", unique: true 26 | add_index "authentication_tokens", ["user_id"], name: "index_authentication_tokens_on_user_id" 27 | 28 | create_table "cat_entries", force: :cascade do |t| 29 | t.integer "user_id", null: false 30 | t.string "breed", null: false 31 | t.string "color", null: false 32 | t.string "longitude", null: false 33 | t.string "latitude", null: false 34 | t.string "contact_phone", null: false 35 | t.string "contact_email", null: false 36 | t.date "event_date", null: false 37 | t.string "entry_type", null: false 38 | t.boolean "resolved" 39 | t.string "chip" 40 | t.text "photo_url", null: false 41 | t.datetime "created_at", null: false 42 | t.datetime "updated_at", null: false 43 | end 44 | 45 | create_table "users", force: :cascade do |t| 46 | t.string "email", default: "", null: false 47 | t.string "password_digest", default: "", null: false 48 | t.boolean "admin" 49 | t.datetime "created_at" 50 | t.datetime "updated_at" 51 | end 52 | 53 | add_index "users", ["email"], name: "index_users_on_email", unique: true 54 | 55 | end 56 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | ENV["RAILS_ENV"] ||= 'test' 3 | require File.expand_path("../../config/environment", __FILE__) 4 | require 'rspec/rails' 5 | require 'shoulda/matchers' 6 | 7 | # Requires supporting ruby files with custom matchers and macros, etc, in 8 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 9 | # run as spec files by default. This means that files in spec/support that end 10 | # in _spec.rb will both be required and run as specs, causing the specs to be 11 | # run twice. It is recommended that you do not name files matching this glob to 12 | # end with _spec.rb. You can configure this pattern with with the --pattern 13 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 14 | Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } 15 | 16 | # Checks for pending migrations before tests are run. 17 | # If you are not using ActiveRecord, you can remove this line. 18 | ActiveRecord::Migration.maintain_test_schema! 19 | 20 | RSpec.configure do |config| 21 | # ## Mock Framework 22 | # 23 | # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line: 24 | # 25 | # config.mock_with :mocha 26 | # config.mock_with :flexmock 27 | # config.mock_with :rr 28 | 29 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 30 | # examples within a transaction, remove the following line or assign false 31 | # instead of true. 32 | config.use_transactional_fixtures = true 33 | 34 | # If true, the base class of anonymous controllers will be inferred 35 | # automatically. This will be the default behavior in future versions of 36 | # rspec-rails. 37 | config.infer_base_class_for_anonymous_controllers = false 38 | 39 | # Run specs in random order to surface order dependencies. If you find an 40 | # order dependency and want to debug it, you can fix the order by providing 41 | # the seed, which is printed after each run. 42 | # --seed 1234 43 | config.order = "random" 44 | 45 | config.include Request::JsonHelpers, :type => :controller 46 | 47 | # RSpec Rails can automatically mix in different behaviours to your tests 48 | # based on their file location, for example enabling you to call `get` and 49 | # `post` in specs under `spec/controllers`. 50 | # 51 | # You can disable this behaviour by removing the line below, and instead 52 | # explictly tag your specs with their type, e.g.: 53 | # 54 | # describe UsersController, :type => :controller do 55 | # # ... 56 | # end 57 | # 58 | # The different available types are documented in the features, such as in 59 | # https://relishapp.com/rspec/rspec-rails/v/3-0/docs 60 | config.infer_spec_type_from_file_location! 61 | end 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LofoCats API 2 | LofoCats API is a simple API built with Ruby on Rails created for demo purposes. (There's also a simple application consuming it, built with Rails: [LofoCats UI](https://github.com/iridakos/lofocats_ui)). 3 | 4 | # Functionality 5 | The API provides endpoints for interacting with a registry of lost and found cats. 6 | 7 | # Endpoints 8 | ### Users 9 | GET /api/users Retrieves all users. Requires administrator priviledges. 10 | 11 | GET /api/users/:id Retrieves a user. Requires administrator priviledges. 12 | 13 | POST /api/users Creates a new user. Requires administrator priviledges. 14 | 15 | PUT/PATCH /api/users/:id Updates a user. Requires administrator priviledges. 16 | 17 | DELETE /api/users/:id Deletes a user. Requires administrator priviledges. 18 | 19 | ### Session 20 | 21 | POST /api/sessions Creates an authentication token to be used for subsequent requests for authorization. 22 | 23 | DELETE /api/sessions Deletes the previously authentication token. Requires signed in user. 24 | 25 | ### Cat entries 26 | 27 | GET /api/cat_entries Retrieves cat entries. Available for all users. 28 | 29 | GET /api/cat_entries/:id Retrieves a cat entry. Available for all users. 30 | 31 | POST /api/cat_entries Creates a new cat entry. Only for signed in users. 32 | 33 | UPDATE /api/cat_entries/:id Updates a cat entry. Administrators can update all entries, signed in users can update only their own entries. Guests can't update any entry. 34 | 35 | DELETE /api/cat_entries/:id Deletes a cat entry. Administrators can delete all entries, signed in users can delete only their own entries. Guests can't delete any entry. 36 | 37 | # Authentication & Authorization 38 | 39 | In order to consume endpoints that require a signed in user (administrator or not) you must first obtain an authentication token by posting to the respective sessions endpoint described above. You have to use this token as the Authorization header of your requests to the desired endpoints. 40 | 41 | # Setting up the application 42 | 43 | * Clone the repository. 44 | * Execute bundle install to install the required gems. 45 | * Execute rake db:setup to setup the database. 46 | * Execute rake db:load\_demo\_data to load some demo data to the application. 47 | * Execute rails server to start the application on the default port. 48 | 49 | If you loaded the demo data, the following users are available: 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
EmailPasswordAdministrator
administrator@lofocats.comadministratorYes
user@lofocats.comuser123456No
another_user@lofocats.comuser123456No
77 | 78 | # Testing 79 | 80 | The application contains RSpec specs. To run the tests: 81 | 82 | * Execute rake db:test:prepare 83 | * Execute rspec 84 | 85 | # TODO 86 | 87 | * Document request parameters & responses 88 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like 20 | # NGINX, varnish or squid. 21 | # config.action_dispatch.rack_cache = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? 26 | 27 | # Compress JavaScripts and CSS. 28 | config.assets.js_compressor = :uglifier 29 | # config.assets.css_compressor = :sass 30 | 31 | # Do not fallback to assets pipeline if a precompiled asset is missed. 32 | config.assets.compile = false 33 | 34 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 35 | # yet still be able to expire them through the digest params. 36 | config.assets.digest = true 37 | 38 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 39 | 40 | # Specifies the header that your server uses for sending files. 41 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 42 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 43 | 44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 45 | # config.force_ssl = true 46 | 47 | # Use the lowest log level to ensure availability of diagnostic information 48 | # when problems arise. 49 | config.log_level = :debug 50 | 51 | # Prepend all log lines with the following tags. 52 | # config.log_tags = [ :subdomain, :uuid ] 53 | 54 | # Use a different logger for distributed setups. 55 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 56 | 57 | # Use a different cache store in production. 58 | # config.cache_store = :mem_cache_store 59 | 60 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 61 | # config.action_controller.asset_host = 'http://assets.example.com' 62 | 63 | # Ignore bad email addresses and do not raise email delivery errors. 64 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 65 | # config.action_mailer.raise_delivery_errors = false 66 | 67 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 68 | # the I18n.default_locale when a translation cannot be found). 69 | config.i18n.fallbacks = true 70 | 71 | # Send deprecation notices to registered listeners. 72 | config.active_support.deprecation = :notify 73 | 74 | # Use default logging formatter so that PID and timestamp are not suppressed. 75 | config.log_formatter = ::Logger::Formatter.new 76 | 77 | # Do not dump schema after migrations. 78 | config.active_record.dump_schema_after_migration = false 79 | end 80 | -------------------------------------------------------------------------------- /lib/tasks/db.rake: -------------------------------------------------------------------------------- 1 | namespace :db do 2 | desc 'Loads demo data to the db' 3 | task :load_demo_data => :environment do 4 | administrator = User.create(:email => 'administrator@lofocats.com', :password => 'administrator', :password_confirmation => 'administrator', :admin => true) 5 | user = User.create(:email => 'user@lofocats.com', :password => 'user123456', :password_confirmation => 'user123456') 6 | another_user = User.create(:email => 'another_user@lofocats.com', :password => 'user123456', :password_confirmation => 'user123456') 7 | 8 | CatEntry.create(:entry_type => 'lost', :user => administrator, :breed => 'European', :color => 'White', :longitude => '4.299676', :latitude => '52.084631', :contact_email => administrator.email, :contact_phone => '0031070000001', :photo_url => 'https://2.bp.blogspot.com/-1hEkKFP_YPo/VgG8bdvUe-I/AAAAAAAABao/Z9O4nwstPew/s1600/2.jpg', :event_date => (Date.today - 2.days)) 9 | CatEntry.create(:entry_type => 'found', :user => user, :breed => 'Persian', :color => 'Black', :longitude => '4.299676', :latitude => '52.084631', :contact_email => user.email, :contact_phone => '0031070000002', :photo_url => 'https://2.bp.blogspot.com/-f043Y09ptp8/VgG8byVdbPI/AAAAAAAABa4/Ii8ke1xh4V8/s1600/4.jpg', :event_date => (Date.today - 3.days)) 10 | CatEntry.create(:entry_type => 'found', :user => user, :breed => 'Unknown', :color => 'Red', :longitude => '4.299676', :latitude => '52.084631', :contact_email => user.email, :contact_phone => '0031070000003', :photo_url => 'https://1.bp.blogspot.com/-EpJhlSYc2zw/VgG8cCSx0JI/AAAAAAAABa0/XireyVETZPw/s1600/5.jpg', :event_date => (Date.today - 4.days)) 11 | CatEntry.create(:entry_type => 'lost', :user => user, :breed => 'European', :color => 'Grey', :longitude => '4.299676', :latitude => '52.084631', :contact_email => user.email, :contact_phone => '0031070000004', :photo_url => 'https://3.bp.blogspot.com/-t6yGS64LrmY/VgG8cja1XhI/AAAAAAAABa8/njDEOAprrzM/s1600/6.jpg', :event_date => (Date.today - 1.days)) 12 | CatEntry.create(:entry_type => 'lost', :user => user, :breed => 'European', :color => 'Red', :longitude => '4.299676', :latitude => '52.084631', :contact_email => user.email, :contact_phone => '0031070000005', :photo_url => 'https://4.bp.blogspot.com/-1uV5vDoELd0/VgG8dImm3YI/AAAAAAAABbA/OfBzmnPf29k/s1600/7.jpg', :event_date => (Date.today - 6.days)) 13 | CatEntry.create(:entry_type => 'found', :user => another_user, :breed => 'Persian', :color => 'Black', :longitude => '4.299676', :latitude => '52.084631', :contact_email => another_user.email, :contact_phone => '0031070000006', :photo_url => 'https://2.bp.blogspot.com/-AdZalA4UBKo/VgG8dcegzBI/AAAAAAAABbE/myp_XxMNbus/s1600/8.jpg', :event_date => (Date.today - 20.days)) 14 | CatEntry.create(:entry_type => 'lost', :user => another_user, :breed => 'European', :color => 'Red', :longitude => '4.299676', :latitude => '52.084631', :contact_email => another_user.email, :contact_phone => '0031070000007', :photo_url => 'https://2.bp.blogspot.com/-BRpzEuWN3YQ/VgG8dkEObhI/AAAAAAAABbI/aT5CilepuRI/s1600/9.jpg', :event_date => (Date.today - 12.days)) 15 | CatEntry.create(:entry_type => 'found', :user => another_user, :breed => 'Siamese', :color => 'Black', :longitude => '4.299676', :latitude => '52.084631', :contact_email => another_user.email, :contact_phone => '0031070000008', :photo_url => 'https://3.bp.blogspot.com/-yAjFpcS8OpI/VgG8a6YaeAI/AAAAAAAABbw/aDu7DeV_LRo/s1600/10.jpg', :event_date => (Date.today - 8.days)) 16 | CatEntry.create(:entry_type => 'lost', :user => administrator, :breed => 'Unknown', :color => 'White', :longitude => '4.299676', :latitude => '52.084631', :contact_email => administrator.email, :contact_phone => '0031070000009', :photo_url => 'https://4.bp.blogspot.com/-rWz5MuC9KeI/VgG8a98acXI/AAAAAAAABak/Aans_mxSqyg/s1600/11.jpg', :event_date => (Date.today - 9.days)) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/models/ability_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'cancan/matchers' 3 | 4 | describe Ability do 5 | let(:user) { nil } 6 | 7 | subject { Ability.new(user) } 8 | 9 | context 'administrator' do 10 | let(:user) { FactoryGirl.build(:admin_user) } 11 | 12 | it 'should be able to manage the whole universe' do 13 | expect(subject.can?(:manage, :all)).to be true 14 | end 15 | end 16 | 17 | context 'normal user' do 18 | let(:user) { FactoryGirl.build(:user, :id => 1) } 19 | 20 | it 'should be able to sign out' do 21 | expect(subject.can?(:destroy, :session)).to be true 22 | end 23 | 24 | it 'should be able to read cat entries' do 25 | expect(subject.can?(:read, CatEntry)).to be true 26 | end 27 | 28 | it 'should be able to create cat entries' do 29 | expect(subject.can?(:create, CatEntry)).to be true 30 | end 31 | 32 | it 'should be able to update own cat entries' do 33 | expect(subject.can?(:update, FactoryGirl.build(:cat_entry, :user => user))).to be true 34 | end 35 | 36 | it 'should not be able to update other user cat entries' do 37 | other_user = FactoryGirl.build(:user, :id => 2) 38 | expect(subject.can?(:update, FactoryGirl.build(:cat_entry, :user => other_user))).to be false 39 | end 40 | 41 | it 'should be able to destroy own cat entries' do 42 | expect(subject.can?(:destroy, FactoryGirl.build(:cat_entry, :user => user))).to be true 43 | end 44 | 45 | it 'should not be able to destroy other user cat entries' do 46 | other_user = FactoryGirl.build(:user, :id => 2) 47 | expect(subject.can?(:destroy, FactoryGirl.build(:cat_entry, :user => other_user))).to be false 48 | end 49 | 50 | it 'should be able to show own profile' do 51 | expect(subject.can?(:read, subject)).to be false 52 | end 53 | 54 | it 'should not be able to show other users' do 55 | expect(subject.can?(:read, User.new(:id => 2))).to be false 56 | end 57 | 58 | it 'should not be able to show all users' do 59 | expect(subject.can?(:read_all, User)).to be false 60 | end 61 | 62 | it 'should not be able to create users' do 63 | expect(subject.can?(:create, User)).to be false 64 | end 65 | 66 | it 'should be able to update own profile' do 67 | expect(subject.can?(:update, subject)).to be false 68 | end 69 | 70 | it 'should not be able to update other users' do 71 | expect(subject.can?(:update, User.new(:id => 3))).to be false 72 | end 73 | 74 | it 'should not be able to destroy users' do 75 | expect(subject.can?(:destroy, User)).to be false 76 | end 77 | end 78 | 79 | context 'guest' do 80 | it 'should be able to create a session' do 81 | expect(subject.can?(:create, :session)).to be true 82 | end 83 | 84 | it 'should be able to read cat entries' do 85 | expect(subject.can?(:read, CatEntry)).to be true 86 | end 87 | 88 | it 'should not be able to create cat entries' do 89 | expect(subject.can?(:create, FactoryGirl.build(:cat_entry))).to be false 90 | end 91 | 92 | it 'should not be able to update cat entries' do 93 | expect(subject.can?(:update, FactoryGirl.build(:cat_entry))).to be false 94 | end 95 | 96 | it 'should not be able to delete cat entries' do 97 | expect(subject.can?(:delete, FactoryGirl.build(:cat_entry))).to be false 98 | end 99 | 100 | it 'should not be able to show all users' do 101 | expect(subject.can?(:read_all, User)).to be false 102 | end 103 | 104 | it 'should not be able to show users' do 105 | expect(subject.can?(:read, User)).to be false 106 | end 107 | 108 | it 'should not be able to create users' do 109 | expect(subject.can?(:create, User)).to be false 110 | end 111 | 112 | it 'should not be able to update users' do 113 | expect(subject.can?(:update, User)).to be false 114 | end 115 | 116 | it 'should not be able to destroy users' do 117 | expect(subject.can?(:destroy, User)).to be false 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionmailer (4.2.10) 5 | actionpack (= 4.2.10) 6 | actionview (= 4.2.10) 7 | activejob (= 4.2.10) 8 | mail (~> 2.5, >= 2.5.4) 9 | rails-dom-testing (~> 1.0, >= 1.0.5) 10 | actionpack (4.2.10) 11 | actionview (= 4.2.10) 12 | activesupport (= 4.2.10) 13 | rack (~> 1.6) 14 | rack-test (~> 0.6.2) 15 | rails-dom-testing (~> 1.0, >= 1.0.5) 16 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 17 | actionview (4.2.10) 18 | activesupport (= 4.2.10) 19 | builder (~> 3.1) 20 | erubis (~> 2.7.0) 21 | rails-dom-testing (~> 1.0, >= 1.0.5) 22 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 23 | active_model_serializers (0.9.3) 24 | activemodel (>= 3.2) 25 | activejob (4.2.10) 26 | activesupport (= 4.2.10) 27 | globalid (>= 0.3.0) 28 | activemodel (4.2.10) 29 | activesupport (= 4.2.10) 30 | builder (~> 3.1) 31 | activerecord (4.2.10) 32 | activemodel (= 4.2.10) 33 | activesupport (= 4.2.10) 34 | arel (~> 6.0) 35 | activesupport (4.2.10) 36 | i18n (~> 0.7) 37 | minitest (~> 5.1) 38 | thread_safe (~> 0.3, >= 0.3.4) 39 | tzinfo (~> 1.1) 40 | arel (6.0.4) 41 | bcrypt (3.1.10) 42 | builder (3.2.3) 43 | cancan (1.6.10) 44 | concurrent-ruby (1.0.5) 45 | crass (1.0.4) 46 | diff-lcs (1.2.5) 47 | erubis (2.7.0) 48 | factory_girl (4.5.0) 49 | activesupport (>= 3.0.0) 50 | factory_girl_rails (4.5.0) 51 | factory_girl (~> 4.5.0) 52 | railties (>= 3.0.0) 53 | globalid (0.4.1) 54 | activesupport (>= 4.2.0) 55 | i18n (0.9.5) 56 | concurrent-ruby (~> 1.0) 57 | loofah (2.2.2) 58 | crass (~> 1.0.2) 59 | nokogiri (>= 1.5.9) 60 | mail (2.7.0) 61 | mini_mime (>= 0.1.1) 62 | mini_mime (1.0.0) 63 | mini_portile2 (2.3.0) 64 | minitest (5.11.3) 65 | nokogiri (1.8.3) 66 | mini_portile2 (~> 2.3.0) 67 | rack (1.6.10) 68 | rack-test (0.6.3) 69 | rack (>= 1.0) 70 | rails (4.2.10) 71 | actionmailer (= 4.2.10) 72 | actionpack (= 4.2.10) 73 | actionview (= 4.2.10) 74 | activejob (= 4.2.10) 75 | activemodel (= 4.2.10) 76 | activerecord (= 4.2.10) 77 | activesupport (= 4.2.10) 78 | bundler (>= 1.3.0, < 2.0) 79 | railties (= 4.2.10) 80 | sprockets-rails 81 | rails-deprecated_sanitizer (1.0.3) 82 | activesupport (>= 4.2.0.alpha) 83 | rails-dom-testing (1.0.9) 84 | activesupport (>= 4.2.0, < 5.0) 85 | nokogiri (~> 1.6) 86 | rails-deprecated_sanitizer (>= 1.0.1) 87 | rails-html-sanitizer (1.0.4) 88 | loofah (~> 2.2, >= 2.2.2) 89 | railties (4.2.10) 90 | actionpack (= 4.2.10) 91 | activesupport (= 4.2.10) 92 | rake (>= 0.8.7) 93 | thor (>= 0.18.1, < 2.0) 94 | rake (12.3.1) 95 | rspec-core (3.3.2) 96 | rspec-support (~> 3.3.0) 97 | rspec-expectations (3.3.1) 98 | diff-lcs (>= 1.2.0, < 2.0) 99 | rspec-support (~> 3.3.0) 100 | rspec-mocks (3.3.2) 101 | diff-lcs (>= 1.2.0, < 2.0) 102 | rspec-support (~> 3.3.0) 103 | rspec-rails (3.3.3) 104 | actionpack (>= 3.0, < 4.3) 105 | activesupport (>= 3.0, < 4.3) 106 | railties (>= 3.0, < 4.3) 107 | rspec-core (~> 3.3.0) 108 | rspec-expectations (~> 3.3.0) 109 | rspec-mocks (~> 3.3.0) 110 | rspec-support (~> 3.3.0) 111 | rspec-support (3.3.0) 112 | shoulda-matchers (2.8.0) 113 | activesupport (>= 3.0.0) 114 | sprockets (3.7.1) 115 | concurrent-ruby (~> 1.0) 116 | rack (> 1, < 3) 117 | sprockets-rails (3.2.1) 118 | actionpack (>= 4.0) 119 | activesupport (>= 4.0) 120 | sprockets (>= 3.0.0) 121 | sqlite3 (1.3.10) 122 | thor (0.20.0) 123 | thread_safe (0.3.6) 124 | timecop (0.8.0) 125 | tzinfo (1.2.5) 126 | thread_safe (~> 0.1) 127 | 128 | PLATFORMS 129 | ruby 130 | 131 | DEPENDENCIES 132 | active_model_serializers 133 | bcrypt 134 | cancan 135 | factory_girl_rails 136 | rails (~> 4.2) 137 | rspec-rails (~> 3.0) 138 | shoulda-matchers 139 | sqlite3 140 | timecop 141 | 142 | BUNDLED WITH 143 | 1.16.1 144 | -------------------------------------------------------------------------------- /spec/controllers/api/v1/cat_entries_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Api::V1::CatEntriesController do 4 | describe "GET #index" do 5 | before do 6 | 3.times do |i| 7 | FactoryGirl.create(:cat_entry, :breed => "Breed #{i}", :user => FactoryGirl.create(:user, 8 | :email => "user#{i}@lofocats.com")) 9 | end 10 | 11 | 2.times do |i| 12 | FactoryGirl.create(:cat_entry, :breed => "Breed #{3 + i}", :resolved => true, :user => FactoryGirl.create(:user, 13 | :email => "user#{3 + i}@lofocats.com")) 14 | end 15 | 16 | expect(controller).to receive(:authorize!).with(:read, CatEntry).once 17 | 18 | get :index 19 | end 20 | 21 | it { should respond_with 200 } 22 | 23 | describe '@assigns' do 24 | subject { assigns(:cat_entries) } 25 | 26 | it "should have 3 items" do 27 | expect(subject.size).to eq 3 28 | end 29 | end 30 | end 31 | 32 | describe "GET '/api/cat_entries/:id'" do 33 | let(:cat_entry) { FactoryGirl.create(:cat_entry) } 34 | 35 | before do 36 | expect(controller).to receive(:authorize!).with(:read, CatEntry).once.and_call_original 37 | 38 | get :show, :id => cat_entry.id 39 | end 40 | 41 | it { should respond_with 200 } 42 | 43 | it 'should respond with the serialized cat entry' do 44 | expect(json_response.to_json).to eq CatEntrySerializer.new(cat_entry).to_json 45 | end 46 | end 47 | 48 | describe "POST #create" do 49 | let(:user) { FactoryGirl.create(:user) } 50 | let(:cat_entry_params) { nil } 51 | 52 | before do 53 | expect(controller).to receive(:authorize!).with(:create, CatEntry) 54 | allow(controller).to receive(:current_user).and_return(user) 55 | 56 | post :create, :cat_entry => cat_entry_params 57 | end 58 | 59 | context 'with invalid parameters' do 60 | let(:cat_entry_params) { FactoryGirl.attributes_for :new_cat_entry, :breed => nil } 61 | 62 | it { should respond_with 422 } 63 | 64 | it 'should respond with errors' do 65 | invalid_cat_entry = CatEntry.new(cat_entry_params.merge(:user => user)) 66 | 67 | expect(invalid_cat_entry).to be_invalid 68 | expect(json_response[:errors]).to match(invalid_cat_entry.errors.as_json) 69 | end 70 | end 71 | 72 | context 'with valid parameters' do 73 | let(:cat_entry_params) { FactoryGirl.attributes_for :cat_entry, :user => nil } 74 | 75 | it { should respond_with 201 } 76 | 77 | it 'should respond with the created cat entry' do 78 | expect(CatEntry.count).to eq 1 79 | 80 | expect(json_response.to_json).to eq CatEntrySerializer.new(CatEntry.first).to_json 81 | end 82 | end 83 | end 84 | 85 | describe "PUT/PATCH #update" do 86 | let(:cat_entry_params) { nil } 87 | let(:user) { FactoryGirl.create(:user) } 88 | let(:cat_entry) { FactoryGirl.create :cat_entry, :user => user } 89 | 90 | before do 91 | expect(controller).to receive(:authorize!).with(:update, cat_entry).once.and_call_original 92 | allow(controller).to receive(:current_user).and_return(user) 93 | 94 | put :update, :id => cat_entry.id, :cat_entry => cat_entry_params 95 | end 96 | 97 | context 'with invalid parameters' do 98 | let(:cat_entry_params) { FactoryGirl.attributes_for :new_cat_entry, :breed => nil } 99 | 100 | it { should respond_with 422 } 101 | 102 | it 'should respond with errors' do 103 | expect(json_response[:errors][:breed]).to eq ["can't be blank"] 104 | end 105 | end 106 | 107 | context 'with valid parameters' do 108 | let(:cat_entry_params) { 109 | FactoryGirl.attributes_for :new_cat_entry, :breed => 'European' 110 | } 111 | 112 | it { should respond_with 200 } 113 | 114 | it 'should respond with the created cat entry' do 115 | cat_entry.reload 116 | expect(json_response.to_json).to eq CatEntrySerializer.new(cat_entry).to_json 117 | end 118 | end 119 | end 120 | 121 | describe "DELETE #destroy" do 122 | let(:user) { FactoryGirl.create(:user) } 123 | let(:cat_entry) { FactoryGirl.create(:cat_entry, :user => user) } 124 | 125 | before do 126 | expect(controller).to receive(:authorize!).with(:destroy, cat_entry) 127 | allow(controller).to receive(:current_user).and_return(user) 128 | 129 | delete :destroy, :id => cat_entry.id 130 | end 131 | 132 | it { should respond_with 204 } 133 | 134 | it 'should delete the entry' do 135 | expect(CatEntry.count).to eq 0 136 | end 137 | end 138 | end -------------------------------------------------------------------------------- /spec/controllers/api/v1/users_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Api::V1::UsersController do 4 | describe "GET #index" do 5 | before do 6 | FactoryGirl.create(:user) 7 | FactoryGirl.create(:admin_user) 8 | 9 | expect(controller).to receive(:authorize!).with(:read_all, User) 10 | 11 | get :index 12 | end 13 | 14 | it { should respond_with 200 } 15 | 16 | it 'should respond with users information' do 17 | expect(json_response.to_json).to eq User.all.map{|user| UserSerializer.new(user)}.to_json 18 | end 19 | end 20 | 21 | describe "GET #show" do 22 | let(:user_id) { nil } 23 | 24 | context 'for existent user' do 25 | let(:user) { FactoryGirl.create :user } 26 | let(:user_id) { user.id } 27 | 28 | before do 29 | expect(controller).to receive(:authorize!).with(:read, User).once 30 | 31 | get :show, :id => user_id 32 | end 33 | 34 | it { should respond_with 200 } 35 | 36 | it 'should respond with user information' do 37 | expect(json_response.to_json).to eq UserSerializer.new(user).to_json 38 | end 39 | end 40 | 41 | context 'for non existent user' do 42 | let(:user_id) { 1 } 43 | 44 | before do 45 | expect(controller).to receive(:authorize!).with(:read, User).never 46 | 47 | get :show, :id => user_id 48 | end 49 | 50 | it { should respond_with 404 } 51 | end 52 | end 53 | 54 | describe "POST #create" do 55 | context "with valid parameters" do 56 | let(:user_attributes) { FactoryGirl.attributes_for :user } 57 | 58 | before(:each) do 59 | allow(controller).to receive(:authorize!).with(:create, User).once 60 | 61 | post :create, { user: user_attributes } 62 | end 63 | 64 | it { should respond_with 201 } 65 | 66 | it "renders with user information" do 67 | expect(json_response.to_json).to eql UserSerializer.new(User.first).to_json 68 | end 69 | end 70 | 71 | context "with invalid parameters" do 72 | let(:invalid_user_attributes) { 73 | { 74 | password: "1", 75 | password_confirmation: "10" 76 | } 77 | } 78 | 79 | before(:each) do 80 | expect(controller).to receive(:authorize!).with(:create, User).once 81 | 82 | post :create, { user: invalid_user_attributes } 83 | end 84 | 85 | it { should respond_with 422 } 86 | 87 | it "renders the json errors" do 88 | user = User.new(invalid_user_attributes) 89 | expect(user).to be_invalid 90 | expect(json_response[:errors].to_json).to eq user.errors.to_json 91 | end 92 | end 93 | end 94 | 95 | describe "PUT/PATCH #update" do 96 | context 'for existent user' do 97 | let(:user) { FactoryGirl.create :user } 98 | 99 | context "with valid parameters" do 100 | before do 101 | expect(controller).to receive(:authorize!).with(:update, user).once 102 | 103 | patch :update, id: user.id, user: { email: "test@lofocats.com" } 104 | end 105 | 106 | it { should respond_with 200 } 107 | 108 | it "responds with user information" do 109 | expect(json_response.to_json).to eql UserSerializer.new(User.first).to_json 110 | end 111 | end 112 | 113 | context "with invalid parameters" do 114 | before do 115 | expect(controller).to receive(:authorize!).with(:update, user).once 116 | 117 | patch :update, id: user.id, user: { email: "waaat.com" } 118 | end 119 | 120 | it { should respond_with 422 } 121 | 122 | it "renders the json errors" do 123 | expect(json_response[:errors][:email]).to include "is not an email" 124 | end 125 | end 126 | end 127 | 128 | context 'for non-existent user' do 129 | before do 130 | expect(controller).not_to receive(:authorize!) 131 | 132 | put :update, :id => 2, :user => { :email => 'test@lofocats.com' } 133 | end 134 | 135 | it { should respond_with 404 } 136 | end 137 | end 138 | 139 | describe "DELETE #destroy" do 140 | context 'for existent user' do 141 | let(:user) { FactoryGirl.create :user } 142 | 143 | before(:each) do 144 | expect(controller).to receive(:authorize!).with(:destroy, user) 145 | 146 | delete :destroy, id: user.id 147 | end 148 | 149 | it { should respond_with 204 } 150 | end 151 | 152 | context 'when user tries to delete his account' do 153 | let(:user) { FactoryGirl.create :user } 154 | 155 | before(:each) do 156 | expect(controller).to receive(:authorize!).with(:destroy, user) 157 | allow(controller).to receive(:current_user).and_return(user) 158 | 159 | delete :destroy, id: user.id 160 | end 161 | 162 | it { should respond_with 422 } 163 | end 164 | 165 | context 'for non-existent user' do 166 | before(:each) do 167 | expect(controller).not_to receive(:authorize!) 168 | 169 | delete :destroy, id: 1 170 | end 171 | 172 | it { should respond_with 404 } 173 | end 174 | end 175 | end 176 | --------------------------------------------------------------------------------