From 63580737b1b3b6fb7cee1397050f84825c315020 Mon Sep 17 00:00:00 2001 From: Israel De La Hoz Date: Tue, 14 Feb 2017 00:50:44 -0500 Subject: [PATCH 1/9] add basic test for user creation --- application/spec/api/users/post_spec.rb | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 application/spec/api/users/post_spec.rb diff --git a/application/spec/api/users/post_spec.rb b/application/spec/api/users/post_spec.rb new file mode 100644 index 0000000..9250006 --- /dev/null +++ b/application/spec/api/users/post_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +describe 'POST /api/users' do + it 'should create a new user' do + expect { post "api/v1.0/users", user: { + first_name: 'juancho', last_name: 'polo', email: 'juancho@test.com', password: 'password', born_on: '1990-02-14'} + }.to change{ Api::Models::User.count }.by(1) + end +end From 190ee30df9b6706025e4820fd454bbe0c59c67ad Mon Sep 17 00:00:00 2001 From: Israel De La Hoz Date: Wed, 15 Feb 2017 09:39:47 -0500 Subject: [PATCH 2/9] API entities and post user feature --- application/api/users.rb | 15 +++++++++++++++ application/api_entities/user.rb | 10 ++++++++++ application/spec/api/users/post_spec.rb | 14 +++++++++++--- 3 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 application/api_entities/user.rb diff --git a/application/api/users.rb b/application/api/users.rb index 40de636..8df481e 100644 --- a/application/api/users.rb +++ b/application/api/users.rb @@ -9,5 +9,20 @@ class Api data: users } end + + params do + group :user, type: Hash do + requires(:email, type: String, desc: "Users email address") + requires(:first_name, type: String, desc: "Users first name") + requires(:last_name, type: String, desc: "Users last name") + optional(:password, type: String, desc: "Users password") + optional(:born_on, type: DateTime, desc: "Users birth date (YYYY-MM-DD)") + end + end + + post do + user = Api::Models::User.create(params[:user]) + present :user, user, with: API::Entities::User + end end end diff --git a/application/api_entities/user.rb b/application/api_entities/user.rb new file mode 100644 index 0000000..bcfbe1e --- /dev/null +++ b/application/api_entities/user.rb @@ -0,0 +1,10 @@ +module API + module Entities + class User < Grape::Entity + expose :first_name + expose :last_name + expose :full_name + expose :born_on + end + end +end \ No newline at end of file diff --git a/application/spec/api/users/post_spec.rb b/application/spec/api/users/post_spec.rb index 9250006..c35d1ed 100644 --- a/application/spec/api/users/post_spec.rb +++ b/application/spec/api/users/post_spec.rb @@ -1,9 +1,17 @@ require 'spec_helper' describe 'POST /api/users' do + let(:user_attributes){ attributes_for(:user) } + it 'should create a new user' do - expect { post "api/v1.0/users", user: { - first_name: 'juancho', last_name: 'polo', email: 'juancho@test.com', password: 'password', born_on: '1990-02-14'} - }.to change{ Api::Models::User.count }.by(1) + expect { post "api/v1.0/users", user: user_attributes }.to change{ Api::Models::User.count }.by(1) + end + + it "should return the created user" do + post "api/v1.0/users", user: user_attributes + user = response_body[:user] + expect(user[:first_name]).to eq(user_attributes[:first_name]) + expect(user[:last_name]).to eq(user_attributes[:last_name]) + expect(user[:full_name]).to eq("#{user_attributes[:first_name]} #{user_attributes[:last_name]}") end end From b78f68e132e8b2ff6123d3ecc55940e476ab4b64 Mon Sep 17 00:00:00 2001 From: Israel De La Hoz Date: Wed, 15 Feb 2017 17:59:39 -0500 Subject: [PATCH 3/9] working validations --- application/api.rb | 1 + application/api/users.rb | 11 +++++++++-- application/config/hanami.rb | 3 ++- application/validators/user_validator.rb | 9 +++++++++ 4 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 application/validators/user_validator.rb diff --git a/application/api.rb b/application/api.rb index 7ddc2d9..d143368 100644 --- a/application/api.rb +++ b/application/api.rb @@ -59,6 +59,7 @@ class Api < Grape::API end Dir['./application/api_entities/**/*.rb'].each { |rb| require rb } + Dir['./application/validators/**/*.rb'].each { |rb| require rb } Dir['./application/api/**/*.rb'].each { |rb| require rb } add_swagger_documentation \ diff --git a/application/api/users.rb b/application/api/users.rb index 8df481e..ef02d10 100644 --- a/application/api/users.rb +++ b/application/api/users.rb @@ -21,8 +21,15 @@ class Api end post do - user = Api::Models::User.create(params[:user]) - present :user, user, with: API::Entities::User + user_validator = UserValidator.new(params[:user]) + + if user_validator.validate.success? + user = Api::Models::User.create(params[:user]) + + present :user, user, with: API::Entities::User + else + + end end end end diff --git a/application/config/hanami.rb b/application/config/hanami.rb index 6d2aaf0..1611fd6 100644 --- a/application/config/hanami.rb +++ b/application/config/hanami.rb @@ -18,6 +18,7 @@ module FormPredicates predicate(:datetime_str?) do |current| # Format: YYYY-MM-DD HH:MM:SS TZ - ex: 2016-01-01 02:03:04 -0800 # Timezone is optional - current.match(/^\d{4}-\d{2}-\d{2} \d{1,2}\:\d{1,2}\:\d{1,2}( \-\d{4})?$/) + return true if current.kind_of? DateTime + current.to_s.match(/^\d{4}-\d{2}-\d{2} \d{1,2}\:\d{1,2}\:\d{1,2}( \-\d{4})?$/) end end diff --git a/application/validators/user_validator.rb b/application/validators/user_validator.rb new file mode 100644 index 0000000..04ae38b --- /dev/null +++ b/application/validators/user_validator.rb @@ -0,0 +1,9 @@ +class UserValidator + include Hanami::Validations + predicates FormPredicates + + validations do + required("email"){ str? } + optional("born_on"){ datetime_str? } + end +end \ No newline at end of file From e097fa8b4fb189815a170cd6b6103cb7b94c7b25 Mon Sep 17 00:00:00 2001 From: Israel De La Hoz Date: Wed, 15 Feb 2017 18:01:32 -0500 Subject: [PATCH 4/9] add missing validations --- application/validators/user_validator.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/application/validators/user_validator.rb b/application/validators/user_validator.rb index 04ae38b..7dc693b 100644 --- a/application/validators/user_validator.rb +++ b/application/validators/user_validator.rb @@ -3,7 +3,10 @@ class UserValidator predicates FormPredicates validations do - required("email"){ str? } + required("first_name"){ str? } + required("first_name"){ str? } + required("password"){ str? } + required("email"){ email? } optional("born_on"){ datetime_str? } end end \ No newline at end of file From 455e51ff726a3a92693b6ecacda6d4764b4b6bab Mon Sep 17 00:00:00 2001 From: Israel De La Hoz Date: Thu, 16 Feb 2017 16:23:42 -0500 Subject: [PATCH 5/9] sidekiq test, put action --- Gemfile | 1 + Gemfile.lock | 12 +++++++++- application/api.rb | 2 ++ application/api/users.rb | 11 +++++----- application/config/mail.rb | 5 +++++ application/config/sidekiq.rb | 9 ++++++++ application/spec/api/users/post_spec.rb | 19 ++++++++++++++++ application/spec/api/users/put_spec.rb | 12 ++++++++++ application/spec/spec_helper.rb | 5 +++++ .../spec/workers/welcome_email_worker_spec.rb | 18 +++++++++++++++ application/workers/welcome_email_worker.rb | 22 +++++++++++++++++++ 11 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 application/config/mail.rb create mode 100644 application/config/sidekiq.rb create mode 100644 application/spec/api/users/put_spec.rb create mode 100644 application/spec/workers/welcome_email_worker_spec.rb create mode 100644 application/workers/welcome_email_worker.rb diff --git a/Gemfile b/Gemfile index b654c5a..93a7d35 100644 --- a/Gemfile +++ b/Gemfile @@ -20,6 +20,7 @@ gem 'hanami-validations', '0.6.0' # form validation gem 'dry-validation', '0.10.4' # validation methods for reform gem 'ability_list', '0.0.4' gem 'activesupport', '5.0.0' +gem 'sidekiq', '4.2.9' group :development, :test do gem 'awesome_print', '1.7.0' diff --git a/Gemfile.lock b/Gemfile.lock index ad129a5..5ffd997 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -19,6 +19,7 @@ GEM coercible (1.0.0) descendants_tracker (~> 0.0.1) concurrent-ruby (1.0.4) + connection_pool (2.2.1) crack (0.4.3) safe_yaml (~> 1.0.0) daemons (1.2.4) @@ -118,12 +119,15 @@ GEM rack (>= 0.4) rack-indifferent (1.1.0) rack (>= 1.5) + rack-protection (1.5.3) + rack rack-test (0.6.3) rack (>= 1.0) rake (11.2.2) rb-fsevent (0.9.8) rb-inotify (0.9.7) ffi (>= 0.5.0) + redis (3.3.3) rerun (0.11.0) listen (~> 3.0) rspec (3.5.0) @@ -142,6 +146,11 @@ GEM ruby_dep (1.5.0) safe_yaml (1.0.4) sequel (4.40.0) + sidekiq (4.2.9) + concurrent-ruby (~> 1.0) + connection_pool (~> 2.2, >= 2.2.0) + rack-protection (>= 1.5.0) + redis (~> 3.2, >= 3.2.1) slop (3.6.0) thin (1.7.0) daemons (~> 1.0, >= 1.0.9) @@ -190,6 +199,7 @@ DEPENDENCIES rerun (= 0.11.0) rspec (= 3.5.0) sequel (= 4.40.0) + sidekiq (= 4.2.9) thin (= 1.7.0) uuidtools (= 2.1.5) vcr (= 3.0.3) @@ -199,4 +209,4 @@ RUBY VERSION ruby 2.3.3p222 BUNDLED WITH - 1.13.6 + 1.14.3 diff --git a/application/api.rb b/application/api.rb index d143368..5c0def1 100644 --- a/application/api.rb +++ b/application/api.rb @@ -22,6 +22,7 @@ class Api < Grape::API; end require 'config/sequel' require 'config/hanami' require 'config/grape' +require 'config/sidekiq' # require some global libs require 'lib/core_ext' @@ -60,6 +61,7 @@ class Api < Grape::API Dir['./application/api_entities/**/*.rb'].each { |rb| require rb } Dir['./application/validators/**/*.rb'].each { |rb| require rb } + Dir['./application/workers/**/*.rb'].each { |rb| require rb } Dir['./application/api/**/*.rb'].each { |rb| require rb } add_swagger_documentation \ diff --git a/application/api/users.rb b/application/api/users.rb index ef02d10..1ad2b10 100644 --- a/application/api/users.rb +++ b/application/api/users.rb @@ -13,22 +13,23 @@ class Api params do group :user, type: Hash do requires(:email, type: String, desc: "Users email address") - requires(:first_name, type: String, desc: "Users first name") requires(:last_name, type: String, desc: "Users last name") - optional(:password, type: String, desc: "Users password") + requires(:first_name, type: String, desc: "Users first name") + requires(:password, type: String, desc: "Users password") optional(:born_on, type: DateTime, desc: "Users birth date (YYYY-MM-DD)") end end post do user_validator = UserValidator.new(params[:user]) - - if user_validator.validate.success? + validation_result = user_validator.validate + if validation_result.success? user = Api::Models::User.create(params[:user]) + WelcomeEmailWorker.perform_async(user.email) present :user, user, with: API::Entities::User else - + error!({ errors: validation_result.messages }, 400) end end end diff --git a/application/config/mail.rb b/application/config/mail.rb new file mode 100644 index 0000000..f37fa61 --- /dev/null +++ b/application/config/mail.rb @@ -0,0 +1,5 @@ +require 'mail' + +Mail.defaults do + delivery_method :smtp, address: "localhost", port: 1025 +end \ No newline at end of file diff --git a/application/config/sidekiq.rb b/application/config/sidekiq.rb new file mode 100644 index 0000000..e711339 --- /dev/null +++ b/application/config/sidekiq.rb @@ -0,0 +1,9 @@ +require 'sidekiq' + +Sidekiq.configure_server do |config| + config.redis = { db: 1 } +end + +Sidekiq.configure_client do |config| + config.redis = { db: 1 } +end \ No newline at end of file diff --git a/application/spec/api/users/post_spec.rb b/application/spec/api/users/post_spec.rb index c35d1ed..fd56554 100644 --- a/application/spec/api/users/post_spec.rb +++ b/application/spec/api/users/post_spec.rb @@ -14,4 +14,23 @@ expect(user[:last_name]).to eq(user_attributes[:last_name]) expect(user[:full_name]).to eq("#{user_attributes[:first_name]} #{user_attributes[:last_name]}") end + + it "should return validation errors" do + user_attributes[:email] = "juanchopolo@" + post "api/v1.0/users", user: user_attributes + + errors = response_body[:errors] + expect(errors).not_to be_empty + expect(errors[:email].first).to eq "must be an email" + end + + it "should add a job to send welcome email" do + expect{ post "api/v1.0/users", user: user_attributes }.to change{ WelcomeEmailWorker.jobs.size }.by(1) + end + + it "should queue a job to send an email to the user's email" do + post "api/v1.0/users", user: user_attributes + + expect(WelcomeEmailWorker.jobs.last["args"]).to eq [user_attributes[:email]] + end end diff --git a/application/spec/api/users/put_spec.rb b/application/spec/api/users/put_spec.rb new file mode 100644 index 0000000..75bf54e --- /dev/null +++ b/application/spec/api/users/put_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe 'PUT /api/users' do + before do + @user = create(:user, email: 'test_user@test.com', password: 'password') + end + + it "should update a user" do + put "api/v1.0/users/:id", { user: {first_name: "Lionel", last_name: "Messi"} }, {'X-Auth-Token' => @user.token } + + end +end diff --git a/application/spec/spec_helper.rb b/application/spec/spec_helper.rb index 4b54421..92945f2 100644 --- a/application/spec/spec_helper.rb +++ b/application/spec/spec_helper.rb @@ -4,6 +4,7 @@ require './application/api' require 'faker' require 'factory_girl' +require 'sidekiq/testing' # Load up all application files that we'll be testing in the suites Dir['./application/models/**/*.rb'].sort.each { |rb| require rb } @@ -82,4 +83,8 @@ def current_user example.run end end + + config.before(:each) do + Sidekiq::Testing.fake! + end end diff --git a/application/spec/workers/welcome_email_worker_spec.rb b/application/spec/workers/welcome_email_worker_spec.rb new file mode 100644 index 0000000..c2ad257 --- /dev/null +++ b/application/spec/workers/welcome_email_worker_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe WelcomeEmailWorker do + before do + Mail.defaults do + delivery_method :test + end + Sidekiq::Testing.inline! + end + + it "should send an email" do + WelcomeEmailWorker.perform_async("juancho@test.com") + mail = Mail::TestMailer.deliveries.last + expect(mail.to).to eq ["juancho@test.com"] + expect(mail.subject).to eq "Welcome to our app" + expect(mail.body.to_s).to eq "Welcome to this app" + end +end \ No newline at end of file diff --git a/application/workers/welcome_email_worker.rb b/application/workers/welcome_email_worker.rb new file mode 100644 index 0000000..657843d --- /dev/null +++ b/application/workers/welcome_email_worker.rb @@ -0,0 +1,22 @@ +require 'sidekiq' +require_relative '../config/mail.rb' + +Sidekiq.configure_server do |config| + config.redis = { db: 1 } +end + +Sidekiq.configure_client do |config| + config.redis = { db: 1 } +end + +class WelcomeEmailWorker + include Sidekiq::Worker + def perform(email) + Mail.deliver do + from 'no-reply@testapp.com' + to email + subject 'Welcome to our app' + body "Welcome to this app" + end + end +end \ No newline at end of file From 9bbf583b3a3d0f2876d2bfaeee2df21b3e387563 Mon Sep 17 00:00:00 2001 From: Israel De La Hoz Date: Fri, 17 Feb 2017 08:18:19 -0500 Subject: [PATCH 6/9] working authentication --- application/api.rb | 4 ---- application/api/users.rb | 20 +++++++++++++++++++ application/api_helpers/auth.rb | 10 +++++++++- .../migrate/1487335421_add_token_to_users.rb | 5 +++++ application/models/user.rb | 5 +++++ application/spec/api/users/put_spec.rb | 10 ++++++++-- 6 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 application/migrate/1487335421_add_token_to_users.rb diff --git a/application/api.rb b/application/api.rb index 5c0def1..c463d91 100644 --- a/application/api.rb +++ b/application/api.rb @@ -55,10 +55,6 @@ class Api < Grape::API helpers ApiResponse include Auth - before do - authenticate! - end - Dir['./application/api_entities/**/*.rb'].each { |rb| require rb } Dir['./application/validators/**/*.rb'].each { |rb| require rb } Dir['./application/workers/**/*.rb'].each { |rb| require rb } diff --git a/application/api/users.rb b/application/api/users.rb index 1ad2b10..4852581 100644 --- a/application/api/users.rb +++ b/application/api/users.rb @@ -32,5 +32,25 @@ class Api error!({ errors: validation_result.messages }, 400) end end + + params do + group :user, type: Hash do + optional(:email, type: String, desc: "Users email address") + optional(:last_name, type: String, desc: "Users last name") + optional(:first_name, type: String, desc: "Users first name") + optional(:born_on, type: DateTime, desc: "Users birth date (YYYY-MM-DD)") + end + end + + desc "updates a user" do + headers XAuthToken: { + description: 'Valdates your identity', + required: true + } + end + + put ':id' do + authenticate! + end end end diff --git a/application/api_helpers/auth.rb b/application/api_helpers/auth.rb index b46a936..dd43a6e 100644 --- a/application/api_helpers/auth.rb +++ b/application/api_helpers/auth.rb @@ -8,12 +8,20 @@ module Auth module HelperMethods def authenticate! - # Library to authenticate user can go here + token = env["X-Auth-Token"] + unauthorize! unless token.present? + + @current_user = Api::Models::User.find(token: '123') + unauthorized! unless @current_user.present? end def current_user @current_user end + + def unauthorized! + error!({ error: "Unauthorized request" }, 401) + end end end end diff --git a/application/migrate/1487335421_add_token_to_users.rb b/application/migrate/1487335421_add_token_to_users.rb new file mode 100644 index 0000000..30ef7c2 --- /dev/null +++ b/application/migrate/1487335421_add_token_to_users.rb @@ -0,0 +1,5 @@ +Sequel.migration do + change do + add_column :users, :token, String + end +end diff --git a/application/models/user.rb b/application/models/user.rb index 802a14b..7bca1ec 100644 --- a/application/models/user.rb +++ b/application/models/user.rb @@ -5,6 +5,11 @@ module Models class User < Sequel::Model(:users) include AbilityList::Helpers + def before_create + self.token = SecureRandom.hex(18) + super + end + def abilities @abilities ||= Abilities.new(self) end diff --git a/application/spec/api/users/put_spec.rb b/application/spec/api/users/put_spec.rb index 75bf54e..e79102f 100644 --- a/application/spec/api/users/put_spec.rb +++ b/application/spec/api/users/put_spec.rb @@ -5,8 +5,14 @@ @user = create(:user, email: 'test_user@test.com', password: 'password') end - it "should update a user" do - put "api/v1.0/users/:id", { user: {first_name: "Lionel", last_name: "Messi"} }, {'X-Auth-Token' => @user.token } + xit "should update a user" do + put "api/v1.0/users/#{@user.id}", { user: {first_name: "Lionel", last_name: "Messi"} }, {'X-Auth-Token' => @user.token } + expect(response_body[:user][:ful_name]).to eq "Lionel Messi" + end + it "should reject a request with the wrong token" do + put "api/v1.0/users/#{@user.id}", { user: {first_name: "Lionel", last_name: "Messi"} }, {'X-Auth-Token' => '123345677' } + expect(last_response.status).to eq 401 + expect(response_body[:error]).to eq "Unauthorized request" end end From 15a5ed5e79a635241097f0fade32383c0207496a Mon Sep 17 00:00:00 2001 From: Israel De La Hoz Date: Fri, 17 Feb 2017 08:54:17 -0500 Subject: [PATCH 7/9] update user --- application/api/users.rb | 15 +++++++++++++- application/api_helpers/auth.rb | 2 +- application/spec/api/users/put_spec.rb | 20 +++++++++++++++++-- application/spec/spec_helper.rb | 12 ----------- ...alidator.rb => user_creation_validator.rb} | 2 +- .../validators/user_update_validator.rb | 11 ++++++++++ 6 files changed, 45 insertions(+), 17 deletions(-) rename application/validators/{user_validator.rb => user_creation_validator.rb} (90%) create mode 100644 application/validators/user_update_validator.rb diff --git a/application/api/users.rb b/application/api/users.rb index 4852581..42e9c19 100644 --- a/application/api/users.rb +++ b/application/api/users.rb @@ -21,7 +21,7 @@ class Api end post do - user_validator = UserValidator.new(params[:user]) + user_validator = UserCreationValidator.new(params[:user]) validation_result = user_validator.validate if validation_result.success? user = Api::Models::User.create(params[:user]) @@ -51,6 +51,19 @@ class Api put ':id' do authenticate! + user = Api::Models::User.find(id: params[:id]) + unauthorized! if user.id != current_user.id + + user_validator = UserCreationValidator.new(params[:user]) + validation_result = user_validator.validate + + if validation_result.success? + user.update(params[:user]) + + present :user, user, with: API::Entities::User + else + error!({ errors: validation_result.messages }, 400) + end end end end diff --git a/application/api_helpers/auth.rb b/application/api_helpers/auth.rb index dd43a6e..62e9984 100644 --- a/application/api_helpers/auth.rb +++ b/application/api_helpers/auth.rb @@ -11,7 +11,7 @@ def authenticate! token = env["X-Auth-Token"] unauthorize! unless token.present? - @current_user = Api::Models::User.find(token: '123') + @current_user = Api::Models::User.find(token: token) unauthorized! unless @current_user.present? end diff --git a/application/spec/api/users/put_spec.rb b/application/spec/api/users/put_spec.rb index e79102f..315cfcd 100644 --- a/application/spec/api/users/put_spec.rb +++ b/application/spec/api/users/put_spec.rb @@ -5,9 +5,18 @@ @user = create(:user, email: 'test_user@test.com', password: 'password') end - xit "should update a user" do + it "should update a user" do put "api/v1.0/users/#{@user.id}", { user: {first_name: "Lionel", last_name: "Messi"} }, {'X-Auth-Token' => @user.token } - expect(response_body[:user][:ful_name]).to eq "Lionel Messi" + expect(response_body[:user][:full_name]).to eq "Lionel Messi" + expect(Api::Models::User.find(id: @user.id).full_name).to eq "Lionel Messi" + end + + it "should return validation errors" do + put "api/v1.0/users/#{@user.id}", { user: {email: "wrong_email"} }, {'X-Auth-Token' => @user.token } + + errors = response_body[:errors] + expect(errors).not_to be_empty + expect(errors[:email].first).to eq "must be an email" end it "should reject a request with the wrong token" do @@ -15,4 +24,11 @@ expect(last_response.status).to eq 401 expect(response_body[:error]).to eq "Unauthorized request" end + + it "should reject a request with another user's token" do + another_user = create(:user) + put "api/v1.0/users/#{@user.id}", { user: {first_name: "Lionel", last_name: "Messi"} }, {'X-Auth-Token' => another_user.token } + expect(last_response.status).to eq 401 + expect(response_body[:error]).to eq "Unauthorized request" + end end diff --git a/application/spec/spec_helper.rb b/application/spec/spec_helper.rb index 92945f2..87df03c 100644 --- a/application/spec/spec_helper.rb +++ b/application/spec/spec_helper.rb @@ -42,18 +42,6 @@ def get_scope opts = {} end end -class Api - helpers do - def current_user - begin - @current_user = Api.class_variable_get(:@@current_user) - rescue - nil - end - end - end -end - SEQUEL_DB = Api::SEQUEL_DB # Clear old test data SEQUEL_DB.tables.each do |t| diff --git a/application/validators/user_validator.rb b/application/validators/user_creation_validator.rb similarity index 90% rename from application/validators/user_validator.rb rename to application/validators/user_creation_validator.rb index 7dc693b..963eb53 100644 --- a/application/validators/user_validator.rb +++ b/application/validators/user_creation_validator.rb @@ -1,4 +1,4 @@ -class UserValidator +class UserCreationValidator include Hanami::Validations predicates FormPredicates diff --git a/application/validators/user_update_validator.rb b/application/validators/user_update_validator.rb new file mode 100644 index 0000000..9d3f14f --- /dev/null +++ b/application/validators/user_update_validator.rb @@ -0,0 +1,11 @@ +class UserCreationValidator + include Hanami::Validations + predicates FormPredicates + + validations do + optional("first_name"){ str? } + optional("last_name"){ str? } + optional("born_on"){ datetime_str? } + optional("email"){ email? } + end +end \ No newline at end of file From 269def23faf3f5e23b2e1a2a66f22ead748af0d1 Mon Sep 17 00:00:00 2001 From: Israel De La Hoz Date: Fri, 17 Feb 2017 14:22:25 -0500 Subject: [PATCH 8/9] use abilities to check user permision to edit itself --- application/api/users.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/api/users.rb b/application/api/users.rb index 42e9c19..fb6e8a9 100644 --- a/application/api/users.rb +++ b/application/api/users.rb @@ -52,7 +52,7 @@ class Api put ':id' do authenticate! user = Api::Models::User.find(id: params[:id]) - unauthorized! if user.id != current_user.id + unauthorized! unless current_user.can? :edit, user user_validator = UserCreationValidator.new(params[:user]) validation_result = user_validator.validate From 4303e51904c00346b63d26aa26f5c78cce560615 Mon Sep 17 00:00:00 2001 From: Israel De La Hoz Date: Fri, 17 Feb 2017 17:23:14 -0500 Subject: [PATCH 9/9] change password feature --- application/api/users.rb | 33 +++++++++++++- application/lib/abilities.rb | 6 ++- application/spec/api/users/put_spec.rb | 2 +- .../spec/api/users/reset_password_spec.rb | 43 +++++++++++++++++++ .../password_changed_email_worker_spec.rb | 18 ++++++++ .../validators/change_password_validator.rb | 8 ++++ .../validators/user_update_validator.rb | 2 +- .../workers/password_changed_email_worker.rb | 22 ++++++++++ 8 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 application/spec/api/users/reset_password_spec.rb create mode 100644 application/spec/workers/password_changed_email_worker_spec.rb create mode 100644 application/validators/change_password_validator.rb create mode 100644 application/workers/password_changed_email_worker.rb diff --git a/application/api/users.rb b/application/api/users.rb index fb6e8a9..c499558 100644 --- a/application/api/users.rb +++ b/application/api/users.rb @@ -54,7 +54,7 @@ class Api user = Api::Models::User.find(id: params[:id]) unauthorized! unless current_user.can? :edit, user - user_validator = UserCreationValidator.new(params[:user]) + user_validator = UserUpdateValidator.new(params[:user]) validation_result = user_validator.validate if validation_result.success? @@ -65,5 +65,36 @@ class Api error!({ errors: validation_result.messages }, 400) end end + + params do + optional(:new_password, type: String, desc: "password to set") + optional(:new_password_confirmation, type: String, desc: "confirmation of the new password") + end + + desc "Changes a user password" do + headers XAuthToken: { + description: 'Valdates your identity', + required: true + } + end + + patch ':id/reset_password' do + authenticate! + user = Api::Models::User.find(id: params[:id]) + unauthorized! unless current_user.can? :change_password, user + + password_validator = ChangePasswordValidator.new(params.symbolize_keys) + validation_result = password_validator.validate + + if validation_result.success? + user.update(password: params[:new_password]) + PasswordChangedEmailWorker.perform_async(user.email) + + present :success, true + else + error!({ errors: validation_result.messages }, 400) + end + end + end end diff --git a/application/lib/abilities.rb b/application/lib/abilities.rb index 2556055..94d4ace 100644 --- a/application/lib/abilities.rb +++ b/application/lib/abilities.rb @@ -12,6 +12,10 @@ def user_permissions(user) can :edit, Models::User do |check_user| next true if user.id == check_user.id end + + can :change_password, Models::User do |check_user| + next true if user.id == check_user.id + end end end -end +end \ No newline at end of file diff --git a/application/spec/api/users/put_spec.rb b/application/spec/api/users/put_spec.rb index 315cfcd..eb853d0 100644 --- a/application/spec/api/users/put_spec.rb +++ b/application/spec/api/users/put_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'PUT /api/users' do +describe 'PUT /api/users/:id' do before do @user = create(:user, email: 'test_user@test.com', password: 'password') end diff --git a/application/spec/api/users/reset_password_spec.rb b/application/spec/api/users/reset_password_spec.rb new file mode 100644 index 0000000..480f483 --- /dev/null +++ b/application/spec/api/users/reset_password_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe 'PATCH /api/users/:id/reset_password' do + before do + @user = create(:user, password: '123456') + end + + it "should update a user" do + patch "api/v1.0/users/#{@user.id}/reset_password", { new_password: 'password', new_password_confirmation: 'password' }, {'X-Auth-Token' => @user.token } + expect(response_body[:success]).to eq true + expect(Api::Models::User.find(id: @user.id).password).to eq "password" + end + + it "should return validation errors" do + patch "api/v1.0/users/#{@user.id}/reset_password", { new_password: 'password', new_password_confirmation: 'different' }, {'X-Auth-Token' => @user.token } + + errors = response_body[:errors] + expect(errors).not_to be_empty + expect(errors[:new_password_confirmation].first).to eq "must be equal to password" + end + + it "should reject a request with the wrong token" do + patch "api/v1.0/users/#{@user.id}/reset_password", { new_password: 'password', new_password_confirmation: 'password' }, {'X-Auth-Token' => '123345677' } + expect(response_body[:error]).to eq "Unauthorized request" + end + + it "should reject a request with another user's token" do + another_user = create(:user) + patch "api/v1.0/users/#{@user.id}/reset_password", { new_password: 'password', new_password_confirmation: 'password' }, {'X-Auth-Token' => another_user.token } + expect(last_response.status).to eq 401 + expect(response_body[:error]).to eq "Unauthorized request" + end + + it "should add a job to send welcome email" do + expect{ patch "api/v1.0/users/#{@user.id}/reset_password", { new_password: 'password', new_password_confirmation: 'password' }, {'X-Auth-Token' => @user.token } }.to change{ PasswordChangedEmailWorker.jobs.size }.by(1) + end + + it "should queue a job to send an email to the user's email" do + patch "api/v1.0/users/#{@user.id}/reset_password", { new_password: 'password', new_password_confirmation: 'password' }, {'X-Auth-Token' => @user.token } + + expect(PasswordChangedEmailWorker.jobs.last["args"]).to eq [@user.email] + end +end diff --git a/application/spec/workers/password_changed_email_worker_spec.rb b/application/spec/workers/password_changed_email_worker_spec.rb new file mode 100644 index 0000000..a5755a7 --- /dev/null +++ b/application/spec/workers/password_changed_email_worker_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe PasswordChangedEmailWorker do + before do + Mail.defaults do + delivery_method :test + end + Sidekiq::Testing.inline! + end + + it "should send an email" do + PasswordChangedEmailWorker.perform_async("juancho@test.com") + mail = Mail::TestMailer.deliveries.last + expect(mail.to).to eq ["juancho@test.com"] + expect(mail.subject).to eq "password changed" + expect(mail.body.to_s).to eq "Your password has been changed" + end +end \ No newline at end of file diff --git a/application/validators/change_password_validator.rb b/application/validators/change_password_validator.rb new file mode 100644 index 0000000..9ee727e --- /dev/null +++ b/application/validators/change_password_validator.rb @@ -0,0 +1,8 @@ +class ChangePasswordValidator + include Hanami::Validations + predicates FormPredicates + + validations do + required(:new_password).filled.confirmation + end +end \ No newline at end of file diff --git a/application/validators/user_update_validator.rb b/application/validators/user_update_validator.rb index 9d3f14f..6f5f2b6 100644 --- a/application/validators/user_update_validator.rb +++ b/application/validators/user_update_validator.rb @@ -1,4 +1,4 @@ -class UserCreationValidator +class UserUpdateValidator include Hanami::Validations predicates FormPredicates diff --git a/application/workers/password_changed_email_worker.rb b/application/workers/password_changed_email_worker.rb new file mode 100644 index 0000000..15f1ffb --- /dev/null +++ b/application/workers/password_changed_email_worker.rb @@ -0,0 +1,22 @@ +require 'sidekiq' +require_relative '../config/mail.rb' + +Sidekiq.configure_server do |config| + config.redis = { db: 1 } +end + +Sidekiq.configure_client do |config| + config.redis = { db: 1 } +end + +class PasswordChangedEmailWorker + include Sidekiq::Worker + def perform(email) + Mail.deliver do + from 'no-reply@testapp.com' + to email + subject 'password changed' + body "Your password has been changed" + end + end +end \ No newline at end of file