require 'rails_helper' require 'set' RSpec.describe 'Posts API', type: :request do # create / update で thumbnail.attach は走るが、 # resized_thumbnail! が MiniMagick 依存でコケやすいので request spec ではスタブしとくのが無難。 before do allow_any_instance_of(Post).to receive(:resized_thumbnail!).and_return(true) end def dummy_upload # 中身は何でもいい(加工処理はスタブしてる) Rack::Test::UploadedFile.new(StringIO.new('dummy'), 'image/jpeg', original_filename: 'dummy.jpg') end let!(:tag_name) { TagName.create!(name: 'spec_tag') } let!(:tag) { Tag.create!(tag_name: tag_name, category: 'general') } let!(:post_record) do Post.create!(title: 'spec post', url: 'https://example.com/spec').tap do |p| PostTag.create!(post: p, tag: tag) end end describe "GET /posts" do let!(:user) { create_member_user! } let!(:tag_name) { TagName.create!(name: "spec_tag") } let!(:tag) { Tag.create!(tag_name:, category: "general") } let!(:tag_name2) { TagName.create!(name: 'unko') } let!(:tag2) { Tag.create!(tag_name: tag_name2, category: 'deerjikist') } let!(:hit_post) do Post.create!(uploaded_user: user, title: "hello spec world", url: 'https://example.com/spec2').tap do |p| PostTag.create!(post: p, tag:) end end let!(:miss_post) do Post.create!(uploaded_user: user, title: "unrelated title", url: 'https://example.com/spec3').tap do |p| PostTag.create!(post: p, tag: tag2) end end it "returns posts with tag name in JSON" do get "/posts" expect(response).to have_http_status(:ok) posts = json.fetch("posts") # 全postの全tagが name を含むこと expect(posts).not_to be_empty posts.each do |p| expect(p["tags"]).to be_an(Array) p["tags"].each do |t| expect(t).to include("name", "category") end end expect(json['count']).to be_an(Integer) # spec_tag を含む投稿が存在すること all_tag_names = posts.flat_map { |p| p["tags"].map { |t| t["name"] } } expect(all_tag_names).to include("spec_tag") end context "when q is provided" do it "filters posts by q (hit case)" do get "/posts", params: { tags: "spec_tag" } expect(response).to have_http_status(:ok) ids = json.fetch("posts").map { |p| p["id"] } expect(ids).to include(hit_post.id) expect(ids).not_to include(miss_post.id) expect(json['count']).to be_an(Integer) end it "returns empty posts when nothing matches" do get "/posts", params: { tags: "no_such_keyword_12345" } expect(response).to have_http_status(:ok) expect(json.fetch("posts")).to eq([]) expect(json.fetch('count')).to eq(0) end end end describe 'GET /posts/:id' do subject(:request) { get "/posts/#{post_id}" } context 'when post exists' do let(:post_id) { post_record.id } it 'returns post with tag tree + related + viewed' do request expect(response).to have_http_status(:ok) expect(json).to include('id' => post_record.id) expect(json).to have_key('tags') expect(json['tags']).to be_an(Array) # show は build_tag_tree_for を使うので、tags はツリー形式(children 付き) node = json['tags'][0] expect(node).to include('id', 'name', 'category', 'post_count', 'children') expect(node['name']).to eq('spec_tag') expect(json).to have_key('related') expect(json['related']).to be_an(Array) expect(json).to have_key('viewed') expect([true, false]).to include(json['viewed']) end end context 'when post does not exist' do let(:post_id) { 999_999_999 } it 'returns 404' do request expect(response).to have_http_status(:not_found) end end end describe 'POST /posts' do let(:member) { create(:user, :member) } it '401 when not logged in' do sign_out post '/posts', params: { title: 't', url: 'https://example.com/x', tags: 'a', thumbnail: dummy_upload } expect(response).to have_http_status(:unauthorized) end it '403 when not member' do sign_in_as(create(:user, role: 'guest')) post '/posts', params: { title: 't', url: 'https://example.com/x', tags: 'a', thumbnail: dummy_upload } expect(response).to have_http_status(:forbidden) end it '201 and creates post + tags when member' do sign_in_as(member) post '/posts', params: { title: 'new post', url: 'https://example.com/new', tags: 'spec_tag', # 既存タグ名を投げる thumbnail: dummy_upload } expect(response).to have_http_status(:created) expect(json).to include('id', 'title', 'url') # tags が name を含むこと(API 側の serialization が正しいこと) expect(json).to have_key('tags') expect(json['tags']).to be_an(Array) expect(json['tags'][0]).to have_key('name') end context 'when url is blank' do it 'returns 422' do sign_in_as(member) post '/posts', params: { title: 'new post', url: ' ', tags: 'spec_tag', # 既存タグ名を投げる thumbnail: dummy_upload } expect(response).to have_http_status(:unprocessable_entity) end end context 'when url is invalid' do it 'returns 422' do sign_in_as(member) post '/posts', params: { title: 'new post', url: 'ぼざクリタグ広場', tags: 'spec_tag', # 既存タグ名を投げる thumbnail: dummy_upload } expect(response).to have_http_status(:unprocessable_entity) end end end describe 'PUT /posts/:id' do let(:member) { create(:user, :member) } it '401 when not logged in' do sign_out put "/posts/#{post_record.id}", params: { title: 'updated', tags: 'spec_tag' } expect(response).to have_http_status(:unauthorized) end it '403 when not member' do sign_in_as(create(:user, role: 'guest')) put "/posts/#{post_record.id}", params: { title: 'updated', tags: 'spec_tag' } expect(response).to have_http_status(:forbidden) end it '200 and updates title + resync tags when member' do sign_in_as(member) # 追加で別タグも作って、更新時に入れ替わることを見る tn2 = TagName.create!(name: 'spec_tag_2') Tag.create!(tag_name: tn2, category: 'general') put "/posts/#{post_record.id}", params: { title: 'updated title', tags: 'spec_tag_2' } expect(response).to have_http_status(:ok) expect(json).to have_key('tags') expect(json['tags']).to be_an(Array) # show と同様、update 後レスポンスもツリー形式 names = json['tags'].map { |n| n['name'] } expect(names).to include('spec_tag_2') end end describe 'GET /posts/random' do it '404 when no posts' do PostTag.delete_all Post.delete_all get '/posts/random' expect(response).to have_http_status(:not_found) end it '200 and returns viewed boolean' do get '/posts/random' expect(response).to have_http_status(:ok) expect(json).to have_key('viewed') expect([true, false]).to include(json['viewed']) end end describe 'GET /posts/changes' do let(:member) { create(:user, :member) } it 'returns add/remove events (history) for a post' do # add tn2 = TagName.create!(name: 'spec_tag2') tag2 = Tag.create!(tag_name: tn2, category: 'general') pt = PostTag.create!(post: post_record, tag: tag2, created_user: member) # remove (discard) pt.discard_by!(member) get '/posts/changes', params: { id: post_record.id } expect(response).to have_http_status(:ok) expect(json).to include('changes', 'count') expect(json['changes']).to be_an(Array) expect(json['count']).to be >= 2 types = json['changes'].map { |e| e['change_type'] }.uniq expect(types).to include('add') expect(types).to include('remove') end end describe 'POST /posts/:id/viewed' do let(:user) { create(:user) } it '401 when not logged in' do sign_out post "/posts/#{ post_record.id }/viewed" expect(response).to have_http_status(:unauthorized) end it '204 and marks viewed when logged in' do sign_in_as(user) post "/posts/#{ post_record.id }/viewed" expect(response).to have_http_status(:no_content) expect(user.reload.viewed?(post_record)).to be(true) end end describe 'DELETE /posts/:id/viewed' do let(:user) { create(:user) } it '401 when not logged in' do sign_out delete "/posts/#{ post_record.id }/viewed" expect(response).to have_http_status(:unauthorized) end it '204 and unmarks viewed when logged in' do sign_in_as(user) # 先に viewed 付けてから外す user.viewed_posts << post_record delete "/posts/#{ post_record.id }/viewed" expect(response).to have_http_status(:no_content) expect(user.reload.viewed?(post_record)).to be(false) end end end