| @@ -49,13 +49,19 @@ class TagsController < ApplicationController | |||
| return head :unauthorized unless current_user | |||
| return head :forbidden unless current_user.member? | |||
| name = params[:name].presence | |||
| category = params[:category].presence | |||
| tag = Tag.find(params[:id]) | |||
| attrs = { name: params[:name].presence, | |||
| category: params[:category].presence }.compact | |||
| if name.present? | |||
| tag.tag_name.update!(name:) | |||
| end | |||
| tag.update!(attrs) if attrs.present? | |||
| if category.present? | |||
| tag.update!(category:) | |||
| end | |||
| render json: tag | |||
| render json: tag.as_json(methods: [:name]) | |||
| end | |||
| end | |||
| @@ -7,5 +7,9 @@ FactoryBot.define do | |||
| trait :member do | |||
| role { "member" } | |||
| end | |||
| trait :admin do | |||
| role { 'admin' } | |||
| end | |||
| end | |||
| end | |||
| @@ -0,0 +1,134 @@ | |||
| # spec/requests/tag_children_spec.rb | |||
| require "rails_helper" | |||
| RSpec.describe "TagChildren", type: :request do | |||
| let!(:parent) { create(:tag) } | |||
| let!(:child) { create(:tag) } | |||
| # ここは君のUser factoryに合わせて調整 | |||
| let(:user) { create_member_user! } | |||
| let(:admin) { create_admin_user! } | |||
| # current_user を ApplicationController でスタブ | |||
| def stub_current_user(user_or_nil) | |||
| allow_any_instance_of(ApplicationController) | |||
| .to receive(:current_user) | |||
| .and_return(user_or_nil) | |||
| end | |||
| describe "POST /tag_children" do | |||
| subject(:do_request) do | |||
| post "/tags/#{ parent_id }/children/#{ child_id }" | |||
| end | |||
| context "when not logged in" do | |||
| let(:parent_id) { parent.id } | |||
| let(:child_id) { child.id } | |||
| it "returns 401" do | |||
| stub_current_user(nil) | |||
| do_request | |||
| expect(response).to have_http_status(:unauthorized) | |||
| end | |||
| end | |||
| context "when logged in but not admin" do | |||
| let(:parent_id) { parent.id } | |||
| let(:child_id) { child.id } | |||
| it "returns 403" do | |||
| stub_current_user(user) | |||
| do_request | |||
| expect(response).to have_http_status(:forbidden) | |||
| end | |||
| end | |||
| context "when admin and params are present" do | |||
| before { stub_current_user(admin) } | |||
| let(:parent_id) { parent.id } | |||
| let(:child_id) { child.id } | |||
| it "returns 204 and adds child to parent.children" do | |||
| expect(parent.children).not_to include(child) | |||
| expect { do_request } | |||
| .to change { parent.reload.children.ids.include?(child.id) } | |||
| .from(false).to(true) | |||
| expect(response).to have_http_status(:no_content) | |||
| end | |||
| end | |||
| context "when Tag.find raises (invalid ids) it still returns 204" do | |||
| before { stub_current_user(admin) } | |||
| let(:parent_id) { -1 } | |||
| let(:child_id) { -1 } | |||
| it "returns 204 (rescue nil)" do | |||
| do_request | |||
| expect(response).to have_http_status(:no_content) | |||
| end | |||
| end | |||
| end | |||
| describe "DELETE /tag_children" do | |||
| subject(:do_request) do | |||
| delete "/tags/#{ parent_id }/children/#{ child_id }" | |||
| end | |||
| context "when not logged in" do | |||
| let(:parent_id) { parent.id } | |||
| let(:child_id) { child.id } | |||
| it "returns 401" do | |||
| stub_current_user(nil) | |||
| do_request | |||
| expect(response).to have_http_status(:unauthorized) | |||
| end | |||
| end | |||
| context "when logged in but not admin" do | |||
| let(:parent_id) { parent.id } | |||
| let(:child_id) { child.id } | |||
| it "returns 403" do | |||
| stub_current_user(user) | |||
| do_request | |||
| expect(response).to have_http_status(:forbidden) | |||
| end | |||
| end | |||
| context "when admin and params are present" do | |||
| before do | |||
| stub_current_user(admin) | |||
| parent.children << child | |||
| end | |||
| let(:parent_id) { parent.id } | |||
| let(:child_id) { child.id } | |||
| it "returns 204 and removes child from parent.children" do | |||
| expect(parent.reload.children).to include(child) | |||
| expect { do_request } | |||
| .to change { parent.reload.children.ids.include?(child.id) } | |||
| .from(true).to(false) | |||
| expect(response).to have_http_status(:no_content) | |||
| end | |||
| end | |||
| context "when Tag.find raises (invalid ids) it still returns 204" do | |||
| before { stub_current_user(admin) } | |||
| let(:parent_id) { -1 } | |||
| let(:child_id) { -1 } | |||
| it "returns 204 (rescue nil)" do | |||
| do_request | |||
| expect(response).to have_http_status(:no_content) | |||
| end | |||
| end | |||
| end | |||
| end | |||
| @@ -76,4 +76,103 @@ RSpec.describe 'Tags API', type: :request do | |||
| expect(response).to have_http_status(:not_found) | |||
| end | |||
| end | |||
| # member? を持つ user を想定(Factory 側で trait 作ってもOK) | |||
| let(:member_user) { create(:user) } | |||
| let(:non_member_user) { create(:user) } | |||
| def stub_current_user(user) | |||
| allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user) | |||
| end | |||
| before do | |||
| allow(member_user).to receive(:member?).and_return(true) | |||
| allow(non_member_user).to receive(:member?).and_return(false) | |||
| end | |||
| describe "PATCH /tags/:id" do | |||
| context "未ログイン" do | |||
| before { stub_current_user(nil) } | |||
| it "401 を返す" do | |||
| patch "/tags/#{tag.id}", params: { name: "new" } | |||
| expect(response).to have_http_status(:unauthorized) | |||
| end | |||
| end | |||
| context "ログインしてゐるが member でない" do | |||
| before { stub_current_user(non_member_user) } | |||
| it "403 を返す" do | |||
| patch "/tags/#{tag.id}", params: { name: "new" } | |||
| expect(response).to have_http_status(:forbidden) | |||
| end | |||
| end | |||
| context "member" do | |||
| before { stub_current_user(member_user) } | |||
| it "name だけ更新できる" do | |||
| patch "/tags/#{tag.id}", params: { name: "new" } | |||
| expect(response).to have_http_status(:ok) | |||
| tag.reload | |||
| expect(tag.name).to eq("new") | |||
| expect(tag.category).to eq("general") | |||
| json = JSON.parse(response.body) | |||
| expect(json["id"]).to eq(tag.id) | |||
| expect(json["name"]).to eq("new") | |||
| expect(json["category"]).to eq("general") | |||
| end | |||
| it "category だけ更新できる" do | |||
| patch "/tags/#{tag.id}", params: { category: "meme" } | |||
| expect(response).to have_http_status(:ok) | |||
| tag.reload | |||
| expect(tag.name).to eq("spec_tag") | |||
| expect(tag.category).to eq("meme") | |||
| end | |||
| it "空文字は presence により無視され、更新は走らない(値が変わらない)" do | |||
| patch "/tags/#{tag.id}", params: { name: "", category: " " } | |||
| expect(response).to have_http_status(:ok) | |||
| tag.reload | |||
| expect(tag.name).to eq("spec_tag") | |||
| expect(tag.category).to eq("general") | |||
| end | |||
| it "両方更新できる" do | |||
| patch "/tags/#{tag.id}", params: { name: "n", category: "meta" } | |||
| expect(response).to have_http_status(:ok) | |||
| tag.reload | |||
| expect(tag.name).to eq("n") | |||
| expect(tag.category).to eq("meta") | |||
| end | |||
| it "存在しない id だと RecordNotFound になる(通常は 404)" do | |||
| # Rails 設定次第で例外がそのまま上がる/404になる | |||
| # APIなら rescue_from で 404 にしてることが多いので、その場合は 404 を期待。 | |||
| patch "/tags/999999999", params: { name: "x" } | |||
| expect(response.status).to be_in([404, 500]) | |||
| end | |||
| it "バリデーションで update! が失敗したら(通常は 422 か 500)" do | |||
| patch "/tags/#{tag.id}", params: { name: 'new', category: 'nico' } | |||
| # rescue_from の実装次第で変はる: | |||
| # - RecordInvalid を 422 にしてるなら 422 | |||
| # - 未処理なら 500 | |||
| expect(response.status).to be_in([422, 500]) | |||
| end | |||
| end | |||
| end | |||
| end | |||
| @@ -5,4 +5,11 @@ module TestRecords | |||
| role: 'member', | |||
| banned: false) | |||
| end | |||
| def create_admin_user! | |||
| User.create!(name: 'spec admin', | |||
| inheritance_code: SecureRandom.hex(16), | |||
| role: 'admin', | |||
| banned: false) | |||
| end | |||
| end | |||
| @@ -0,0 +1,34 @@ | |||
| require 'rails_helper' | |||
| RSpec.describe 'post_similarity:calc' do | |||
| include RakeTaskHelper | |||
| it 'calls Similarity::Calc with Post and :tags' do | |||
| # 必要最低限のデータ | |||
| t1 = Tag.create!(name: "t1") | |||
| t2 = Tag.create!(name: "t2") | |||
| t3 = Tag.create!(name: "t3") | |||
| p1 = Post.create!(url: "https://example.com/1") | |||
| p2 = Post.create!(url: "https://example.com/2") | |||
| p3 = Post.create!(url: "https://example.com/3") | |||
| # kept スコープが絡むなら、PostTag がデフォで kept になる前提 | |||
| PostTag.create!(post: p1, tag: t1) | |||
| PostTag.create!(post: p1, tag: t2) | |||
| PostTag.create!(post: p2, tag: t1) | |||
| PostTag.create!(post: p2, tag: t3) | |||
| PostTag.create!(post: p3, tag: t3) | |||
| expect { run_rake_task("post_similarity:calc") } | |||
| .to change { PostSimilarity.count }.from(0) | |||
| ps = PostSimilarity.find_by!(post_id: p1.id, target_post_id: p2.id) | |||
| ps_rev = PostSimilarity.find_by!(post_id: p2.id, target_post_id: p1.id) | |||
| expect(ps_rev.cos).to eq(ps.cos) | |||
| end | |||
| end | |||
| @@ -0,0 +1,34 @@ | |||
| require 'rails_helper' | |||
| RSpec.describe 'tag_similarity:calc' do | |||
| include RakeTaskHelper | |||
| it 'calls Similarity::Calc with Tag and :posts' do | |||
| # 必要最低限のデータ | |||
| t1 = Tag.create!(name: "t1") | |||
| t2 = Tag.create!(name: "t2") | |||
| t3 = Tag.create!(name: "t3") | |||
| p1 = Post.create!(url: "https://example.com/1") | |||
| p2 = Post.create!(url: "https://example.com/2") | |||
| p3 = Post.create!(url: "https://example.com/3") | |||
| # kept スコープが絡むなら、PostTag がデフォで kept になる前提 | |||
| PostTag.create!(post: p1, tag: t1) | |||
| PostTag.create!(post: p1, tag: t2) | |||
| PostTag.create!(post: p2, tag: t1) | |||
| PostTag.create!(post: p2, tag: t3) | |||
| PostTag.create!(post: p3, tag: t3) | |||
| expect { run_rake_task("tag_similarity:calc") } | |||
| .to change { TagSimilarity.count }.from(0) | |||
| ps = TagSimilarity.find_by!(tag_id: t1.id, target_tag_id: t2.id) | |||
| ps_rev = TagSimilarity.find_by!(tag_id: t2.id, target_tag_id: t1.id) | |||
| expect(ps_rev.cos).to eq(ps.cos) | |||
| end | |||
| end | |||