From fa6b69bb7e416d8510dfbb0fa4f804e7223aa804 Mon Sep 17 00:00:00 2001 From: Kuldeep Aggarwal Date: Fri, 29 Jan 2016 23:24:11 +0530 Subject: [PATCH] [fixes #5, #2] add Comment CRUD now user has ability to create blog and comments --- app/abilities/blogger_ability.rb | 2 + app/controllers/api/v1/comments_controller.rb | 9 + app/models/blog.rb | 1 + app/models/comment.rb | 8 + app/models/user.rb | 1 + app/serializers/api/v1/blog_serializer.rb | 2 +- app/serializers/api/v1/comment_serializer.rb | 3 + config/routes.rb | 3 + db/migrate/20160129160122_create_comments.rb | 11 + db/schema.rb | 12 +- .../api/v1/comments_controller_spec.rb | 276 ++++++++++++++++++ spec/factories/comments.rb | 7 + spec/models/ability_spec.rb | 6 + spec/models/blog_spec.rb | 1 + spec/models/comment_spec.rb | 12 + spec/models/user_spec.rb | 1 + .../api/v1/comment_serializer_spec.rb | 16 + 17 files changed, 369 insertions(+), 2 deletions(-) create mode 100644 app/controllers/api/v1/comments_controller.rb create mode 100644 app/models/comment.rb create mode 100644 app/serializers/api/v1/comment_serializer.rb create mode 100644 db/migrate/20160129160122_create_comments.rb create mode 100644 spec/controllers/api/v1/comments_controller_spec.rb create mode 100644 spec/factories/comments.rb create mode 100644 spec/models/comment_spec.rb create mode 100644 spec/serializers/api/v1/comment_serializer_spec.rb diff --git a/app/abilities/blogger_ability.rb b/app/abilities/blogger_ability.rb index 5ad4c55..d756499 100644 --- a/app/abilities/blogger_ability.rb +++ b/app/abilities/blogger_ability.rb @@ -2,6 +2,8 @@ class BloggerAbility < Ability def initialize(user) can [:show, :destroy, :update], User, { id: user.id } can [:create, :destroy, :update], Blog, { user_id: user.id } + can [:index, :create], Comment + can [:update, :destroy], Comment, { creator_id: user.id } super end end diff --git a/app/controllers/api/v1/comments_controller.rb b/app/controllers/api/v1/comments_controller.rb new file mode 100644 index 0000000..c631c61 --- /dev/null +++ b/app/controllers/api/v1/comments_controller.rb @@ -0,0 +1,9 @@ +class Api::V1::CommentsController < Api::V1::ResourceController + load_and_authorize_resource :blog + load_and_authorize_resource :comment, through: :blog + + private + def resource_params + params.require(:comment).permit(:text).merge(creator_id: current_user.id) + end +end diff --git a/app/models/blog.rb b/app/models/blog.rb index 68e2039..ced6480 100644 --- a/app/models/blog.rb +++ b/app/models/blog.rb @@ -1,6 +1,7 @@ class Blog < ApplicationRecord # Associations belongs_to :author, foreign_key: :user_id, class_name: :User + has_many :comments, dependent: :destroy # Validations validates :title, :description, presence: true diff --git a/app/models/comment.rb b/app/models/comment.rb new file mode 100644 index 0000000..28b97d6 --- /dev/null +++ b/app/models/comment.rb @@ -0,0 +1,8 @@ +class Comment < ApplicationRecord + # Validations + belongs_to :blog + belongs_to :creator, class_name: :User + + # Validations + validates :text, presence: true +end diff --git a/app/models/user.rb b/app/models/user.rb index b718581..9918e2d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -8,6 +8,7 @@ class User < ApplicationRecord # Associations has_many :blogs, dependent: :destroy + has_many :comments, dependent: :destroy, foreign_key: :creator_id # Validations validates :email, :first_name, presence: true diff --git a/app/serializers/api/v1/blog_serializer.rb b/app/serializers/api/v1/blog_serializer.rb index 4999d31..55eff5b 100644 --- a/app/serializers/api/v1/blog_serializer.rb +++ b/app/serializers/api/v1/blog_serializer.rb @@ -1,5 +1,5 @@ class Api::V1::BlogSerializer < ActiveModel::Serializer attributes :id, :title, :description - has_one :author, serializer: UserSerializer + has_one :author, serializer: Api::V1::UserSerializer end diff --git a/app/serializers/api/v1/comment_serializer.rb b/app/serializers/api/v1/comment_serializer.rb new file mode 100644 index 0000000..7569529 --- /dev/null +++ b/app/serializers/api/v1/comment_serializer.rb @@ -0,0 +1,3 @@ +class Api::V1::CommentSerializer < ActiveModel::Serializer + attributes :id, :text, :blog_id, :creator_id +end diff --git a/config/routes.rb b/config/routes.rb index 0ad1ca6..081ac1a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -9,6 +9,9 @@ resources :users, only: [:index, :create, :show, :update, :destroy] do resources :blogs, only: [:create, :show, :update, :destroy], shallow: true end + resources :blogs, only: [] do + resources :comments, only: [:create, :update, :destroy, :index] + end resources :blogs, only: [:index] end end diff --git a/db/migrate/20160129160122_create_comments.rb b/db/migrate/20160129160122_create_comments.rb new file mode 100644 index 0000000..43f068b --- /dev/null +++ b/db/migrate/20160129160122_create_comments.rb @@ -0,0 +1,11 @@ +class CreateComments < ActiveRecord::Migration[5.0] + def change + create_table :comments do |t| + t.references :blog, index: true, foreign_key: true + t.references :creator, index: true, foreign_key: true + t.text :text + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 599205f..cb4917c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160128192002) do +ActiveRecord::Schema.define(version: 20160129160122) do create_table "blogs", force: :cascade do |t| t.integer "user_id" @@ -22,6 +22,16 @@ t.index ["user_id"], name: "index_blogs_on_user_id" end + create_table "comments", force: :cascade do |t| + t.integer "blog_id" + t.integer "creator_id" + t.text "text" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["blog_id"], name: "index_comments_on_blog_id" + t.index ["creator_id"], name: "index_comments_on_creator_id" + end + create_table "users", force: :cascade do |t| t.string "email" t.string "password_digest" diff --git a/spec/controllers/api/v1/comments_controller_spec.rb b/spec/controllers/api/v1/comments_controller_spec.rb new file mode 100644 index 0000000..15a231a --- /dev/null +++ b/spec/controllers/api/v1/comments_controller_spec.rb @@ -0,0 +1,276 @@ +require 'rails_helper' + +RSpec.describe Api::V1::BlogsController, type: :api do + let(:admin) { FactoryGirl.create(:user, :admin) } + let(:blogger) { FactoryGirl.create(:user, :blogger) } + let(:blog) { FactoryGirl.create(:blog) } + + describe '#index' do + context 'when guest' do + before { get api_v1_blog_comments_path(blog) } + + it 'returns unauthorized error' do + expect(last_response.status).to eq(401) + end + end + + shared_examples_for 'logged in user who sees list of comments' do + let!(:comments) { FactoryGirl.create_list(:comment, 2, blog: blog) } + let(:response) do + ActiveModel::ArraySerializer.new( + comments, + each_serializer: Api::V1::CommentSerializer, + root: 'comments', + meta: meta_attributes(comments) + ).to_json + end + + before { get api_v1_blog_comments_path(blog) } + + it 'returns comments for a blog' do + expect(last_response.body).to eq(response) + end + end + + context 'when admin' do + sign_in(:admin) + + it_behaves_like('logged in user who sees list of comments') + end + + context 'when blogger' do + sign_in(:blogger) + + it_behaves_like('logged in user who sees list of comments') + end + end + + describe '#create' do + context 'when guest' do + before { post api_v1_blog_comments_path(blog) } + + it 'returns unauthorized error' do + expect(last_response.status).to eq(401) + end + end + + shared_examples_for 'logged in user who create a comment' do + let!(:comment_attributes) { { text: "New comment" } } + let(:response) { Api::V1::CommentSerializer.new(comment).to_json } + + context 'when valid paramaters' do + let(:comment) { Comment.last } + before do + post api_v1_blog_comments_path(blog, params: { comment: comment_attributes }) + end + + it 'returns 201 status code' do + expect(last_response.status).to eq(201) + end + + it 'returns comment details' do + expect(last_response.body).to eq(response) + end + end + + context 'when invalid paramaters' do + before do + post api_v1_blog_comments_path(blog, params: { comment: { text: '' } }) + end + + it 'returns 422 status code' do + expect(last_response.status).to eq(422) + end + + it 'returns error messages' do + errors = ["Text can't be blank"] + expect(last_response.body).to eq(errors.to_json) + end + end + end + + context 'when admin' do + sign_in(:admin) + + it_behaves_like('logged in user who create a comment') + end + + context 'when blogger' do + sign_in(:blogger) + + it_behaves_like('logged in user who create a comment') + end + end + + describe '#update' do + let(:comment) do + _comment = FactoryGirl.build(:comment).tap do |comment| + comment.creator = user if defined?(user) + end + _comment.save! + _comment + end + let(:comment_attributes) { { text: 'Updated comment' } } + let(:response) { Api::V1::CommentSerializer.new(comment).to_json } + + shared_examples_for 'user_updates_comment' do + context 'when valid paramaters' do + before do + put api_v1_blog_comment_path(comment.blog, comment, params: { comment: comment_attributes }) + end + + it 'returns 200 status code' do + expect(last_response.status).to eq(200) + end + + it 'returns blog details' do + comment.reload + expect(comment.text).to eq('Updated comment') + expect(last_response.body).to eq(response) + end + end + + context 'when invalid paramaters' do + before do + put api_v1_blog_comment_path(comment.blog, comment, params: { comment: { text: '' } }) + end + + it 'returns 422 status code' do + expect(last_response.status).to eq(422) + end + + it 'returns error messages' do + errors = ["Text can't be blank"] + expect(last_response.body).to eq(errors.to_json) + end + end + end + + context 'when guest' do + before do + put api_v1_blog_comment_path(comment.blog, comment, params: { comment: comment_attributes }) + end + + it 'returns authentication error' do + expect(last_response.status).to eq(401) + end + end + + context 'when blogger' do + sign_in(:blogger) + + context 'when updating for self' do + let(:user) { blogger } + + it_behaves_like 'user_updates_comment' + end + + context 'when updating for other' do + let(:user) { admin } + + before do + put api_v1_blog_comment_path(comment.blog, comment, params: { comment: comment_attributes }) + end + + it 'returns unauthorized error' do + expect(last_response.status).to eq(403) + end + end + end + + context 'when admins visits' do + sign_in(:admin) + + context 'when updating for self' do + let(:user) { admin } + + it_behaves_like 'user_updates_comment' + end + + context 'when updating for other' do + let(:user) { blogger } + + it_behaves_like 'user_updates_comment' + end + end + end + + describe '#destroy' do + let(:comment) do + _comment = FactoryGirl.build(:comment).tap do |comment| + comment.creator = user if defined?(user) + end + _comment.save! + _comment + end + let(:response) { { message: 'resource deleted successfully' }.to_json } + + shared_examples_for 'user_deletes_comment' do + context 'when successful' do + before do + delete api_v1_blog_comment_path(comment.blog, comment) + end + + it 'returns 200 status code' do + expect(last_response.status).to eq(200) + end + + it 'returns blog details' do + expect(last_response.body).to eq(response) + end + end + + context 'when unsuccessful' do + pending "not possible" + end + end + + context 'when guest' do + before do + delete api_v1_blog_comment_path(comment.blog, comment) + end + + it 'returns authentication error' do + expect(last_response.status).to eq(401) + end + end + + context 'when blogger' do + sign_in(:blogger) + + context 'when deleting for self' do + let(:user) { blogger } + + it_behaves_like 'user_deletes_comment' + end + + context 'when deleting for other' do + let(:user) { admin } + + before do + delete api_v1_blog_comment_path(comment.blog, comment) + end + + it 'returns unauthorized error' do + expect(last_response.status).to eq(403) + end + end + end + + context 'when admins visits' do + sign_in(:admin) + + context 'when deleting for self' do + let(:user) { admin } + + it_behaves_like 'user_deletes_comment' + end + + context 'when deleting for other' do + let(:user) { blogger } + + it_behaves_like 'user_deletes_comment' + end + end + end +end diff --git a/spec/factories/comments.rb b/spec/factories/comments.rb new file mode 100644 index 0000000..e3285b2 --- /dev/null +++ b/spec/factories/comments.rb @@ -0,0 +1,7 @@ +FactoryGirl.define do + factory :comment do + text { Faker::Lorem.sentences(3).join("\n") } + blog { FactoryGirl.create(:blog) } + creator { blog.author } + end +end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index c29c090..67712b8 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -22,6 +22,7 @@ it { should have_abilities([:show, :index], Blog) } it { should not_have_abilities([:create, :destroy, :update], blogger_blog) } it { should not_have_abilities([:create, :destroy, :update], admin_blog) } + it { should not_have_abilities([:index, :create, :update, :destroy], Comment) } end context 'when admin' do @@ -32,6 +33,8 @@ context 'when blogger' do let(:user) { blogger } + let(:comment) { FactoryGirl.create(:comment, creator: user) } + let(:admin_comment) { FactoryGirl.create(:comment, creator: admin) } it { should_not be_able_to(:index, User) } it { should have_abilities([:show, :destroy, :update], user) } @@ -39,5 +42,8 @@ it { should have_abilities([:show, :index], Blog) } it { should have_abilities([:create, :destroy, :update], blogger_blog) } it { should not_have_abilities([:create, :destroy, :update], admin_blog) } + it { should have_abilities([:index, :create], Comment) } + it { should have_abilities([:update, :destroy], comment) } + it { should not_have_abilities([:update, :destroy], admin_comment) } end end diff --git a/spec/models/blog_spec.rb b/spec/models/blog_spec.rb index bef191d..da33e54 100644 --- a/spec/models/blog_spec.rb +++ b/spec/models/blog_spec.rb @@ -3,6 +3,7 @@ RSpec.describe Blog, type: :model do describe 'Associations' do it { should belong_to(:author).class_name('User') } + it { should have_many(:comments).dependent(:destroy) } end describe 'Validations' do diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb new file mode 100644 index 0000000..29d2de5 --- /dev/null +++ b/spec/models/comment_spec.rb @@ -0,0 +1,12 @@ +require 'rails_helper' + +RSpec.describe Comment, type: :model do + describe 'Validations' do + it { should validate_presence_of(:text) } + end + + describe 'Associations' do + it { should belong_to(:blog) } + it { should belong_to(:creator).class_name(:User) } + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 2dfc248..f139725 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -15,6 +15,7 @@ describe 'Associations' do it { should have_many(:blogs).dependent(:destroy) } + it { should have_many(:comments).dependent(:destroy) } end describe 'Callbacks' do diff --git a/spec/serializers/api/v1/comment_serializer_spec.rb b/spec/serializers/api/v1/comment_serializer_spec.rb new file mode 100644 index 0000000..e07c7ad --- /dev/null +++ b/spec/serializers/api/v1/comment_serializer_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' + +RSpec.describe Api::V1::CommentSerializer, type: :serializer do + let(:resource) { FactoryGirl.create(:comment) } + let(:serializer) { described_class.new(resource) } + let(:serialization) { serializer.as_json } + + it "returns id, text, blog_id & creator_id" do + expect(serialization).to eq({ + id: resource.id, + text: resource.text, + blog_id: resource.blog_id, + creator_id: resource.creator_id + }) + end +end