From b5de2c17e94052246eee4aab194fca2d7583aa21 Mon Sep 17 00:00:00 2001 From: Daniel M Brasil Date: Thu, 8 Jul 2021 20:17:16 -0300 Subject: [PATCH 1/2] update API controllers and models with polymorphic relations for Likes and Comments --- app/controllers/api/v1/comments_controller.rb | 50 ++++++++-------- app/controllers/api/v1/likes_controller.rb | 36 +++++++---- app/controllers/api/v1/posts_controller.rb | 10 ++-- app/models/bot.rb | 4 +- app/models/comment.rb | 6 +- app/models/like.rb | 4 +- app/models/post.rb | 4 +- config/routes.rb | 14 ++--- db/schema.rb | 59 +++++++++++-------- 9 files changed, 104 insertions(+), 83 deletions(-) diff --git a/app/controllers/api/v1/comments_controller.rb b/app/controllers/api/v1/comments_controller.rb index 44b6a1b..543d740 100644 --- a/app/controllers/api/v1/comments_controller.rb +++ b/app/controllers/api/v1/comments_controller.rb @@ -3,13 +3,13 @@ module Api module V1 class CommentsController < ApplicationController - before_action :find_post, only: :create - before_action :new_comment, only: :create - before_action :find_comment, only: %i[show destroy] - before_action :find_bot, only: :destroy + before_action :find_commentable, only: :create + before_action :create_comment, only: :create + before_action :find_comment, only: :destroy + before_action :find_commenter, only: :destroy before_action :require_authorization!, only: :destroy - # POST /:username/posts/:post_id/resource + # POST /api/v1/:username/posts/:post_id/comment def create if @comment.save render json: { status: 'success', message: 'Comment created', data: @comment }, status: :created @@ -18,7 +18,7 @@ def create end end - # DELETE /:username/posts/:post_id/resource/:id + # DELETE /api/v1/:username/posts/:post_id/comment/:id def destroy if @comment.destroy render json: { status: 'success', message: 'Comment deleted', data: @comment }, status: :accepted @@ -27,21 +27,16 @@ def destroy end end - # GET /:username/posts/:post_id/resource/:id - def show - render json: { status: 'success', message: 'Comment loaded', data: @comment }, status: :ok - end - private - # Find post by post_id - def find_post - @post = Post.find(params[:post_id]) + # Find commentable. If :post_id exists, commentable is a post + def find_commentable + @commentable = Post.find(params[:post_id]) if params[:post_id] end # Define what params are permitted def comment_params - params.permit(:body) + params.require(:comment).permit(:id, :body, :post_id) end # Find comment by id @@ -49,22 +44,27 @@ def find_comment @comment = Comment.find(params[:id]) end - # Define new comment content - def new_comment - @comment = Comment.new(comment_params) - @comment.bot = @current_bot - @comment.post = @post + # Create new comment with body passed as param + # Bot as commenter + # Post as commentable + def create_comment + @comment = @commentable.comments.create(commenter_id: @current_bot.id, commenter_type: 'Bot', + body: params[:comment][:body]) end - # Find comment's bot - def find_bot - @bot = @comment.bot + # Find comment's commenter + def find_commenter + @commenter = @comment.commenter end - # Verify if bot owns comment (is authorized) + # Return if current_bot is equal to commenter (comment's owner) + # + # Render unauthorized otherwise def require_authorization! + return if @current_bot == @commenter + render json: { status: 'error', message: 'Resource does not belong to you' }, - status: :unauthorized unless @current_bot == @bot + status: :unauthorized end end end diff --git a/app/controllers/api/v1/likes_controller.rb b/app/controllers/api/v1/likes_controller.rb index 9d9d0b3..c93a521 100644 --- a/app/controllers/api/v1/likes_controller.rb +++ b/app/controllers/api/v1/likes_controller.rb @@ -3,23 +3,29 @@ module Api module V1 class LikesController < ApplicationController - before_action :find_post + before_action :find_likeable before_action :find_like, only: :destroy - # POST /:username/posts/:post_id/like + # POST /:username/posts/:post_id/like if like belongs to a post + # + # POST /:username/posts/:post_id/comments/:comment_id/like if like belongs to a comment def create if already_liked? render json: { status: 'error', message: 'Already liked', data: nil }, status: :unprocessable_entity else - @post.likes.create(bot_id: @current_bot.id) - render json: { status: 'success', message: 'Post liked', data: @post }, status: :created if @post.save + @likeable.likes.create(liker_id: @current_bot.id, liker_type: 'Bot') + render json: { status: 'success', message: "#{@likeable.class.name} liked", data: @likeable }, status: :created if @likeable.save end end - # DELETE /:username/posts/:post_id/like + # DELETE /:username/posts/:post_id/like if like belongs to a post + # + # DELETE /:username/posts/:post_id/comments/:comment_id/like if like belongs to a comment def destroy if already_liked? - render json: { status: 'success', message: 'Post unliked', data: @post }, status: :accepted if @like.destroy + if @like.destroy + render json: { status: 'success', message: "#{@likeable.class.name} unliked", data: @likeable }, status: :accepted + end else render json: { status: 'error', message: 'Cannot unlike', data: nil }, status: :unprocessable_entity end @@ -27,19 +33,25 @@ def destroy private - # Find post by post_id - def find_post - @post = Post.find(params[:post_id]) + # Find likeable + # Likeable is a Post if post_id is present and comment_id is not present + # This happens because comments belong to posts, therefore post_id is always going to be present + def find_likeable + if params[:post_id] && !params[:comment_id] + @likeable = Post.find(params[:post_id]) + elsif params[:comment_id] + @likeable = Comment.find(params[:comment_id]) + end end # Find current bot's like by current_bot.id def find_like - @like = @post.likes.find_by(bot_id: @current_bot.id) + @like = @likeable.likes.where(liker_id: @current_bot.id, liker_type: 'Bot').first end - # Check if current bot already liked post + # Check if current bot has already liked current resource def already_liked? - Like.where(bot_id: @current_bot.id, post_id: params[:post_id]).exists? + @likeable.likes.where(liker_id: @current_bot.id, liker_type: 'Bot').exists? end end end diff --git a/app/controllers/api/v1/posts_controller.rb b/app/controllers/api/v1/posts_controller.rb index 953305f..a9ba2ab 100644 --- a/app/controllers/api/v1/posts_controller.rb +++ b/app/controllers/api/v1/posts_controller.rb @@ -35,7 +35,7 @@ def destroy # Define permitted params def post_params - params.permit(:body, :media) + params.permit(:body, :media, :username) end # Define new post content @@ -47,7 +47,7 @@ def new_post # Set post and its content (comment and likes) def set_post @post = Post.find(params[:id]) - @comments = Comment.where(post_id: @post.id) + @comments = @post.comments.where(commentable_id: @post.id, commentable_type: 'Post').first @likes = @post.likes.count end @@ -61,9 +61,9 @@ def set_response end def require_authorization! - unless @current_bot == @post.bot - render json: { status: 'error', message: 'Resource does not belong to you' }, status: :unauthorized - end + return if @current_bot == @post.bot + + render json: { status: 'error', message: 'Resource does not belong to you' }, status: :unauthorized end end end diff --git a/app/models/bot.rb b/app/models/bot.rb index fbe6c33..ee697dd 100644 --- a/app/models/bot.rb +++ b/app/models/bot.rb @@ -2,8 +2,8 @@ class Bot < ApplicationRecord has_many :posts, dependent: :destroy - has_many :comments, dependent: :destroy - has_many :likes, dependent: :destroy + has_many :comments, as: :commenter, dependent: :destroy + has_many :likes, as: :liker, dependent: :destroy mount_uploader :avatar, AvatarUploader diff --git a/app/models/comment.rb b/app/models/comment.rb index 22e4713..e916a14 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true class Comment < ApplicationRecord - belongs_to :post - belongs_to :bot + belongs_to :commentable, polymorphic: true + belongs_to :commenter, polymorphic: true + + has_many :likes, as: :likeable, dependent: :destroy validates_presence_of :body validates_length_of :body, maximum: 512 # validates length of a comment diff --git a/app/models/like.rb b/app/models/like.rb index 2ec436b..cc0974b 100644 --- a/app/models/like.rb +++ b/app/models/like.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true class Like < ApplicationRecord - belongs_to :post - belongs_to :bot, optional: true + belongs_to :likeable, polymorphic: true + belongs_to :liker, polymorphic: true end diff --git a/app/models/post.rb b/app/models/post.rb index ac9fce0..39a9746 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -3,8 +3,8 @@ class Post < ApplicationRecord belongs_to :bot - has_many :comments, dependent: :destroy - has_many :likes, dependent: :destroy + has_many :comments, as: :commentable, dependent: :destroy + has_many :likes, as: :likeable, dependent: :destroy mount_uploader :media, MediaUploader diff --git a/config/routes.rb b/config/routes.rb index 71698fa..8c59b06 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,29 +1,29 @@ # frozen_string_literal: true Rails.application.routes.draw do - namespace 'api' do namespace 'v1' do # posts - resources :posts, only: %i[create] + resources :posts, only: :create get '/:username/posts/:id', to: 'posts#show' delete '/:username/posts/:id', to: 'posts#destroy' - # posts/comments - get '/:username/posts/:post_id/comment/:id', to: 'comments#show' + # Comments that belongs to posts post '/:username/posts/:post_id/comment', to: 'comments#create' delete '/:username/posts/:post_id/comment/:id', to: 'comments#destroy' - # posts/likes + # Likes that belongs to posts post '/:username/posts/:post_id/like', to: 'likes#create' delete '/:username/posts/:post_id/like', to: 'likes#destroy' + # Likes that belongs to comments + post '/:username/posts/:post_id/comments/:comment_id/like', to: 'likes#create' + delete '/:username/posts/:post_id/comments/:comment_id/like', to: 'likes#destroy' + # bots resources :bots, param: :username - resources :comments - # likes resources :likes, param: :post_id end diff --git a/db/schema.rb b/db/schema.rb index 332cca6..2e09fc6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_03_19_161503) do +ActiveRecord::Schema.define(version: 2021_07_05_212932) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -27,18 +27,27 @@ t.boolean "verified" t.string "slug" t.string "avatar" + t.string "cover" + t.string "repository" + t.index ["api_key"], name: "index_bots_on_api_key", unique: true + t.index ["api_secret"], name: "index_bots_on_api_secret", unique: true t.index ["developer_id"], name: "index_bots_on_developer_id" t.index ["slug"], name: "index_bots_on_slug", unique: true t.index ["username"], name: "index_bots_on_username", unique: true end create_table "comments", force: :cascade do |t| - t.integer "post_id" - t.bigint "bot_id", null: false + t.string "commentable_type" + t.bigint "commentable_id" + t.string "commenter_type" + t.bigint "commenter_id" + t.text "body" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false - t.text "body" - t.index ["bot_id"], name: "index_comments_on_bot_id" + t.index ["commentable_id", "commentable_type"], name: "index_comments_on_commentable_id_and_commentable_type" + t.index ["commentable_type", "commentable_id"], name: "index_comments_on_commentable" + t.index ["commenter_id", "commenter_type"], name: "index_comments_on_commenter_id_and_commenter_type" + t.index ["commenter_type", "commenter_id"], name: "index_comments_on_commenter" end create_table "developers", force: :cascade do |t| @@ -53,6 +62,17 @@ t.string "name", default: "", null: false t.string "slug" t.string "avatar" + t.string "bio" + t.boolean "verified" + t.string "cover" + t.string "confirmation_token" + t.datetime "confirmed_at" + t.datetime "confirmation_sent_at" + t.string "unconfirmed_email" + t.integer "failed_attempts", default: 0, null: false + t.string "unlock_token" + t.datetime "locked_at" + t.string "locale" t.index ["email"], name: "index_developers_on_email", unique: true t.index ["reset_password_token"], name: "index_developers_on_reset_password_token", unique: true t.index ["slug"], name: "index_developers_on_slug", unique: true @@ -84,27 +104,17 @@ t.index ["sluggable_type", "sluggable_id"], name: "index_friendly_id_slugs_on_sluggable_type_and_sluggable_id" end - create_table "guests", force: :cascade do |t| - t.string "email", default: "", null: false - t.string "encrypted_password", default: "", null: false - t.string "reset_password_token" - t.datetime "reset_password_sent_at" - t.datetime "remember_created_at" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.string "username" - t.index ["email"], name: "index_guests_on_email", unique: true - t.index ["reset_password_token"], name: "index_guests_on_reset_password_token", unique: true - t.index ["username"], name: "index_guests_on_username", unique: true - end - create_table "likes", force: :cascade do |t| - t.bigint "post_id", null: false - t.bigint "bot_id", null: false + t.string "likeable_type" + t.bigint "likeable_id" + t.string "liker_type" + t.bigint "liker_id" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false - t.index ["bot_id"], name: "index_likes_on_bot_id" - t.index ["post_id"], name: "index_likes_on_post_id" + t.index ["likeable_id", "likeable_type"], name: "index_likes_on_likeable_id_and_likeable_type" + t.index ["likeable_type", "likeable_id"], name: "index_votes_on_likeable" + t.index ["liker_id", "liker_type"], name: "index_likes_on_liker_id_and_liker_type" + t.index ["liker_type", "liker_id"], name: "index_votes_on_liker" end create_table "posts", force: :cascade do |t| @@ -144,9 +154,6 @@ end add_foreign_key "bots", "developers" - add_foreign_key "comments", "bots" - add_foreign_key "likes", "bots" - add_foreign_key "likes", "posts" add_foreign_key "posts", "bots" add_foreign_key "taggings", "tags" end From 7500beacdf0bb2cbf8162cca4ee6bffc5d573eeb Mon Sep 17 00:00:00 2001 From: Daniel M Brasil Date: Thu, 8 Jul 2021 20:17:49 -0300 Subject: [PATCH 2/2] update rspec tests adding polymorphic relations to comments and likes --- .../api/v1/comments_controller_spec.rb | 28 +++--------- .../api/v1/likes_controller_spec.rb | 43 +++++++++++++++++++ spec/factories/bot.rb | 4 +- spec/factories/developer.rb | 2 +- spec/factories/post.rb | 2 +- spec/support/bot_helpers.rb | 2 +- spec/support/comment_helpers.rb | 6 ++- 7 files changed, 59 insertions(+), 28 deletions(-) diff --git a/spec/controllers/api/v1/comments_controller_spec.rb b/spec/controllers/api/v1/comments_controller_spec.rb index 5d375de..2d5aa04 100644 --- a/spec/controllers/api/v1/comments_controller_spec.rb +++ b/spec/controllers/api/v1/comments_controller_spec.rb @@ -13,7 +13,9 @@ before do post "/api/v1/username/posts/#{post_with_no_media.id}/comment", params: { - body: body + comment: { + body: body + } }, headers: { Authorization: "Token api_key=#{bot.api_key} api_secret=#{bot.api_secret}" } @@ -33,7 +35,9 @@ context 'When creating a comment with no body' do before do post "/api/v1/username/posts/#{post_with_no_media.id}/comment", params: { - body: '' + comment: { + body: '' + } }, headers: { Authorization: "Token api_key=#{bot.api_key} api_secret=#{bot.api_secret}" } @@ -49,27 +53,9 @@ end end - context 'When fetching a comment' do - before do - get "/api/v1/username/posts/#{comment.post_id}/comment/#{comment.id}", headers: { - Authorization: "Token api_key=#{bot.api_key} api_secret=#{bot.api_secret}" - } - end - - it 'returns 200' do - expect(response.status).to eq(200) - end - - it 'returns success message and comment data' do - expect(json['status']).to eq('success') - expect(json['message']).to eq('Comment loaded') - expect(json['data']['body']).to eq(comment.body) - end - end - context 'When deleting another bot comment' do before do - delete "/api/v1/username/posts/#{comment.post_id}/comment/#{comment.id}", headers: { + delete "/api/v1/username/posts/#{comment.commentable_id}/comment/#{comment.id}", headers: { Authorization: "Token api_key=#{bot.api_key} api_secret=#{bot.api_secret}" } end diff --git a/spec/controllers/api/v1/likes_controller_spec.rb b/spec/controllers/api/v1/likes_controller_spec.rb index 15876a2..1f36f27 100644 --- a/spec/controllers/api/v1/likes_controller_spec.rb +++ b/spec/controllers/api/v1/likes_controller_spec.rb @@ -79,4 +79,47 @@ expect(json['message']).to eq('Cannot unlike') end end + + let(:comment) { create_comment } + + context 'When liking a comment' do + before do + post "/api/v1/username/posts/#{post_with_no_media.id}/comment", params: { + comment: { + body: 'Comment' + } + }, headers: { + Authorization: "Token api_key=#{bot.api_key} api_secret=#{bot.api_secret}" + } + post "/api/v1/username/posts/#{post_with_no_media.id}/comments/#{comment.id}/like", headers: { + Authorization: "Token api_key=#{bot.api_key} api_secret=#{bot.api_secret}" + } + end + + it 'returns 201' do + expect(response.status).to eq(201) + end + end + + context 'When unliking a comment' do + before do + post "/api/v1/username/posts/#{post_with_no_media.id}/comment", params: { + comment: { + body: 'Comment' + } + }, headers: { + Authorization: "Token api_key=#{bot.api_key} api_secret=#{bot.api_secret}" + } + post "/api/v1/username/posts/#{post_with_no_media.id}/comments/#{comment.id}/like", headers: { + Authorization: "Token api_key=#{bot.api_key} api_secret=#{bot.api_secret}" + } + delete "/api/v1/username/posts/#{post_with_no_media.id}/comments/#{comment.id}/like", headers: { + Authorization: "Token api_key=#{bot.api_key} api_secret=#{bot.api_secret}" + } + end + + it 'returns 202' do + expect(response.status).to eq(202) + end + end end diff --git a/spec/factories/bot.rb b/spec/factories/bot.rb index 6f217b0..126751d 100644 --- a/spec/factories/bot.rb +++ b/spec/factories/bot.rb @@ -2,8 +2,8 @@ FactoryBot.define do factory :bot do - name { Faker::Name.name[4..32] } - username { SecureRandom.hex(10) } + name { Faker::Name.name[0..32] } + username { SecureRandom.hex(10) } bio { Faker::Book.title[1..512] } developer_id { FactoryBot.create(:developer).id } api_key { SecureRandom.hex(16) } diff --git a/spec/factories/developer.rb b/spec/factories/developer.rb index cde15cb..ea5df5d 100644 --- a/spec/factories/developer.rb +++ b/spec/factories/developer.rb @@ -2,7 +2,7 @@ FactoryBot.define do factory :developer do - name { Faker::Name.name[4..32] } + name { Faker::Name.name[0..32] } username { SecureRandom.hex(10) } email { Faker::Internet.unique.email[0..32] } end diff --git a/spec/factories/post.rb b/spec/factories/post.rb index 4ae8bf1..c98544c 100644 --- a/spec/factories/post.rb +++ b/spec/factories/post.rb @@ -3,6 +3,6 @@ FactoryBot.define do factory :post do body { Faker::Games::Minecraft.achievement[0..32] } - bot_id { FactoryBot.create(:bot).id[0..32] } + bot_id { FactoryBot.create(:bot).id } end end diff --git a/spec/support/bot_helpers.rb b/spec/support/bot_helpers.rb index 94dd79c..500c3b6 100644 --- a/spec/support/bot_helpers.rb +++ b/spec/support/bot_helpers.rb @@ -6,7 +6,7 @@ module BotHelpers def create_bot FactoryBot.create(:bot, - name: Faker::Name.name[4..32], + name: Faker::Name.name[0..32], username: SecureRandom.hex(10), api_key: SecureRandom.hex(16), api_secret: SecureRandom.hex(16), diff --git a/spec/support/comment_helpers.rb b/spec/support/comment_helpers.rb index 14e8f16..e62b6d9 100644 --- a/spec/support/comment_helpers.rb +++ b/spec/support/comment_helpers.rb @@ -7,7 +7,9 @@ module CommentHelpers def create_comment FactoryBot.create(:comment, body: Faker::Games::Fallout.quote, - post_id: FactoryBot.create(:post).id, - bot_id: FactoryBot.create(:bot).id) + commentable_id: FactoryBot.create(:post).id, + commentable_type: 'Post', + commenter_id: FactoryBot.create(:bot).id, + commenter_type: 'Bot') end end