From 7ae36de2cf85189666b880b0d615b9dbebba8524 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Thu, 8 Nov 2018 09:33:30 -0500 Subject: [PATCH] Tags API Endpoint complete with full tests --- .../api/v1/tag_followings_controller.rb | 39 ++++++ app/controllers/tag_followings_controller.rb | 28 ++-- app/services/tag_following_service.rb | 33 +++++ config/locales/diaspora/en.yml | 2 + config/routes.rb | 1 + .../tag_followings_controller_spec.rb | 63 +++++++++ .../api/tag_followings_controller_spec.rb | 120 ++++++++++++++++++ spec/services/tag_following_service_spec.rb | 78 ++++++++++++ 8 files changed, 348 insertions(+), 16 deletions(-) create mode 100755 app/controllers/api/v1/tag_followings_controller.rb create mode 100644 app/services/tag_following_service.rb create mode 100755 spec/integration/api/tag_followings_controller_spec.rb create mode 100644 spec/services/tag_following_service_spec.rb diff --git a/app/controllers/api/v1/tag_followings_controller.rb b/app/controllers/api/v1/tag_followings_controller.rb new file mode 100755 index 000000000..a8ed8ebfa --- /dev/null +++ b/app/controllers/api/v1/tag_followings_controller.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Api + module V1 + class TagFollowingsController < Api::V1::BaseController + before_action only: %i[index] do + require_access_token %w[read] + end + + before_action only: %i[create destroy] do + require_access_token %w[read write] + end + + rescue_from StandardError do + render json: I18n.t("api.endpoint_errors.tags.cant_process"), status: :bad_request + end + + def index + render json: tag_followings_service.index.map(&:name) + end + + def create + tag_followings_service.create(params.require(:name)) + head :no_content + end + + def destroy + tag_followings_service.destroy_by_name(params.require(:id)) + head :no_content + end + + private + + def tag_followings_service + TagFollowingService.new(current_user) + end + end + end +end diff --git a/app/controllers/tag_followings_controller.rb b/app/controllers/tag_followings_controller.rb index a6bedd113..d4c73e19f 100644 --- a/app/controllers/tag_followings_controller.rb +++ b/app/controllers/tag_followings_controller.rb @@ -14,28 +14,18 @@ class TagFollowingsController < ApplicationController # POST /tag_followings # POST /tag_followings.xml def create - name_normalized = ActsAsTaggableOn::Tag.normalize(params['name']) - - if name_normalized.nil? || name_normalized.empty? - head :forbidden - else - @tag = ActsAsTaggableOn::Tag.find_or_create_by(name: name_normalized) - @tag_following = current_user.tag_followings.new(:tag_id => @tag.id) - - if @tag_following.save - render :json => @tag.to_json, :status => 201 - else - head :forbidden - end - end + tag = tag_followings_service.create(params["name"]) + render json: tag.to_json, status: :created + rescue StandardError + head :forbidden end # DELETE /tag_followings/1 # DELETE /tag_followings/1.xml def destroy - tag_following = current_user.tag_followings.find_by_tag_id( params['id'] ) + success = tag_followings_service.destroy(params["id"]) - if tag_following && tag_following.destroy + if success respond_to do |format| format.any(:js, :json) { head :no_content } end @@ -56,4 +46,10 @@ class TagFollowingsController < ApplicationController redirect_to followed_tags_stream_path unless request.format == :mobile gon.preloads[:tagFollowings] = tags end + + private + + def tag_followings_service + TagFollowingService.new(current_user) + end end diff --git a/app/services/tag_following_service.rb b/app/services/tag_following_service.rb new file mode 100644 index 000000000..eb38dd92a --- /dev/null +++ b/app/services/tag_following_service.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class TagFollowingService + def initialize(user=nil) + @user = user + end + + def create(name) + name_normalized = ActsAsTaggableOn::Tag.normalize(name) + raise ArgumentError, "Name field null or empty" if name_normalized.blank? + + tag = ActsAsTaggableOn::Tag.find_or_create_by(name: name_normalized) + tag_following = @user.tag_followings.new(tag_id: tag.id) + + raise "Can't process tag entity" unless tag_following.save + tag + end + + def destroy(id) + tag_following = @user.tag_followings.find_by(tag_id: id) + tag_following&.destroy + end + + def destroy_by_name(name) + name_normalized = ActsAsTaggableOn::Tag.normalize(name) + followed_tag = @user.followed_tags.find_by(name: name_normalized) + destroy(followed_tag.id) if followed_tag + end + + def index + @user.followed_tags + end +end diff --git a/config/locales/diaspora/en.yml b/config/locales/diaspora/en.yml index b266c96b0..751712dbc 100644 --- a/config/locales/diaspora/en.yml +++ b/config/locales/diaspora/en.yml @@ -972,6 +972,8 @@ en: post_not_found: "Post with provided guid could not be found" failed_create: "Failed to create the post" failed_delete: "Not allowed to delete the post" + tags: + cant_process: "Failed to process the tag followings request" error: not_found: "No record found for given id." diff --git a/config/routes.rb b/config/routes.rb index d90f5bf4d..8f98bd050 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -234,6 +234,7 @@ Rails.application.routes.draw do resources :messages, only: %i[index create] end + resources :tag_followings, only: %i[index create destroy] get "streams/activity" => "streams#activity", :as => "activity_stream" get "streams/main" => "streams#multi", :as => "stream" get "streams/tags" => "streams#followed_tags", :as => "followed_tags_stream" diff --git a/spec/controllers/tag_followings_controller_spec.rb b/spec/controllers/tag_followings_controller_spec.rb index c5c6dd8eb..041d13c11 100644 --- a/spec/controllers/tag_followings_controller_spec.rb +++ b/spec/controllers/tag_followings_controller_spec.rb @@ -29,4 +29,67 @@ describe TagFollowingsController, type: :controller do end end end + + describe "#create" do + before do + sign_in alice, scope: :user + end + + it "Creates new tag with valid name" do + name = SecureRandom.uuid + post :create, params: {name: name} + expect(response.status).to be(201) + tag_data = JSON.parse(response.body) + expect(tag_data["name"]).to eq(name) + expect(tag_data.has_key?("id")).to be_truthy + expect(tag_data["taggings_count"]).to eq(0) + end + + it "Fails with missing name field" do + post :create, params: {} + expect(response.status).to eq(403) + end + end + + describe "#destroy" do + before do + sign_in alice, scope: :user + @tag_name = SecureRandom.uuid + post :create, params: {name: @tag_name} + @tag_id_to_delete = JSON.parse(response.body)["id"] + end + + it "Deletes tag with valid id" do + delete :destroy, params: {id: @tag_id_to_delete}, format: :json + expect(response.status).to eq(204) + expect(alice.followed_tags.find_by(name: @tag_name)).to be_nil + end + + it "Fails with missing name field" do + delete :create, params: {}, format: :json + expect(response.status).to eq(403) + end + + it "Fails with bad Tag ID" do + delete :create, params: {id: -1}, format: :json + expect(response.status).to eq(403) + end + end + + describe "#index" do + before do + sign_in alice, scope: :user + post :create, params: {name: "tag1"} + post :create, params: {name: "tag2"} + end + + it "Gets Tags" do + get :index, format: :json + expect(response.status).to eq(200) + tag_followings = JSON.parse(response.body) + expect(tag_followings.length).to eq(2) + expect(tag_followings.find(name: "tag1")).to_not be_nil + expect(tag_followings.find(name: "tag2")).to_not be_nil + end + end end diff --git a/spec/integration/api/tag_followings_controller_spec.rb b/spec/integration/api/tag_followings_controller_spec.rb new file mode 100755 index 000000000..3e6485858 --- /dev/null +++ b/spec/integration/api/tag_followings_controller_spec.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Api::V1::TagFollowingsController do + let(:auth) { FactoryGirl.create(:auth_with_read_and_write) } + let!(:access_token) { auth.create_access_token.to_s } + + before do + @expected_tags = %w[tag1 tag2 tag3] + @expected_tags.each {|tag| add_tag(tag, auth.user) } + @initial_count = @expected_tags.length + end + + describe "#create" do + context "valid tag ID" do + it "succeeds in adding a tag" do + post( + api_v1_tag_followings_path, + params: {name: "tag4", access_token: access_token} + ) + expect(response.status).to eq(204) + + get( + api_v1_tag_followings_path, + params: {access_token: access_token} + ) + expect(response.status).to eq(200) + items = JSON.parse(response.body) + expect(items.length).to eq(@initial_count + 1) + end + end + + context "missing name parameter" do + it "fails to add" do + post( + api_v1_tag_followings_path, + params: {access_token: access_token} + ) + + expect(response.status).to eq(400) + expect(response.body).to eq(I18n.t("api.endpoint_errors.tags.cant_process")) + end + end + + context "duplicate tag" do + it "fails to add" do + post( + api_v1_tag_followings_path, + params: {name: "tag3", access_token: access_token} + ) + + expect(response.status).to eq(400) + expect(response.body).to eq(I18n.t("api.endpoint_errors.tags.cant_process")) + end + end + end + + describe "#index" do + context "list all followed tags" do + it "succeeds" do + get( + api_v1_tag_followings_path, + params: {access_token: access_token} + ) + expect(response.status).to eq(200) + items = JSON.parse(response.body) + expect(items.length).to eq(@expected_tags.length) + @expected_tags.each {|tag| expect(items.find(tag)).to be_truthy } + end + end + end + + describe "#delete" do + context "valid tag" do + it "succeeds in deleting tag" do + delete( + api_v1_tag_following_path("tag1"), + params: {access_token: access_token} + ) + expect(response.status).to eq(204) + + get( + api_v1_tag_followings_path, + params: {access_token: access_token} + ) + expect(response.status).to eq(200) + items = JSON.parse(response.body) + expect(items.length).to eq(@initial_count - 1) + end + end + + context "tag that's not followed" do + it "does nothing" do + delete( + api_v1_tag_following_path(SecureRandom.uuid.to_s), + params: {access_token: access_token} + ) + expect(response.status).to eq(204) + + get( + api_v1_tag_followings_path, + params: {access_token: access_token} + ) + expect(response.status).to eq(200) + items = JSON.parse(response.body) + expect(items.length).to eq(@initial_count) + end + end + end + + private + + def add_tag(name, user) + tag = ActsAsTaggableOn::Tag.find_or_create_by(name: name) + tag_following = user.tag_followings.new(tag_id: tag.id) + tag_following.save + tag_following + end +end diff --git a/spec/services/tag_following_service_spec.rb b/spec/services/tag_following_service_spec.rb new file mode 100644 index 000000000..47a846df5 --- /dev/null +++ b/spec/services/tag_following_service_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +describe TagFollowingService do + before do + add_tag("tag1", alice) + add_tag("tag2", alice) + end + + describe "#create" do + it "Creates new tag with valid name" do + name = SecureRandom.uuid + expect(alice.followed_tags.find_by(name: name)).to be_nil + tag_data = tag_following_service(alice).create(name) + expect(alice.followed_tags.find_by(name: name).name).to eq(name) + expect(tag_data["name"]).to eq(name) + expect(tag_data["id"]).to be_truthy + expect(tag_data["taggings_count"]).to eq(0) + end + + it "Throws error with empty tag" do + expect { tag_following_service(alice).create(nil) }.to raise_error(ArgumentError) + expect { tag_following_service(alice).create("") }.to raise_error(ArgumentError) + expect { tag_following_service(alice).create("#") }.to raise_error(ArgumentError) + expect { tag_following_service(alice).create(" ") }.to raise_error(ArgumentError) + end + end + + describe "#destroy" do + it "Deletes tag with valid name" do + name = SecureRandom.uuid + add_tag(name, alice) + expect(alice.followed_tags.find_by(name: name).name).to eq(name) + expect(tag_following_service(alice).destroy_by_name(name)).to be_truthy + expect(alice.followed_tags.find_by(name: name)).to be_nil + end + + it "Deletes tag with id" do + name = SecureRandom.uuid + new_tag = add_tag(name, alice) + expect(alice.followed_tags.find_by(name: name).name).to eq(name) + expect(tag_following_service(alice).destroy(new_tag.tag_id)).to be_truthy + expect(alice.followed_tags.find_by(name: name)).to be_nil + end + + it "Does nothing with tag that isn't already followed" do + original_length = alice.followed_tags.length + tag_following_service(alice).destroy_by_name(SecureRandom.uuid) + tag_following_service(alice).destroy(-1) + expect(alice.followed_tags.length).to eq(original_length) + end + + it "Does nothing with empty tag name" do + original_length = alice.followed_tags.length + tag_following_service(alice).destroy_by_name("") + expect(alice.followed_tags.length).to eq(original_length) + end + end + + describe "#index" do + it "Returns user's list of tags" do + tags = tag_following_service(alice).index + expect(tags.length).to eq(alice.followed_tags.length) + end + end + + private + + def tag_following_service(user=alice) + TagFollowingService.new(user) + end + + def add_tag(name, user) + tag = ActsAsTaggableOn::Tag.find_or_create_by(name: name) + tag_following = user.tag_followings.new(tag_id: tag.id) + tag_following.save + tag_following + end +end