├── 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 |
| Password | 56 |Administrator | 57 ||
|---|---|---|
| administrator@lofocats.com | 62 |administrator | 63 |Yes | 64 |
| user@lofocats.com | 67 |user123456 | 68 |No | 69 |
| another_user@lofocats.com | 72 |user123456 | 73 |No | 74 |
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 |
--------------------------------------------------------------------------------