ぼざクリタグ広場 https://hub.nizika.monster
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

311 lines
9.2 KiB

  1. require 'rails_helper'
  2. require 'set'
  3. RSpec.describe 'Posts API', type: :request do
  4. # create / update で thumbnail.attach は走るが、
  5. # resized_thumbnail! が MiniMagick 依存でコケやすいので request spec ではスタブしとくのが無難。
  6. before do
  7. allow_any_instance_of(Post).to receive(:resized_thumbnail!).and_return(true)
  8. end
  9. def dummy_upload
  10. # 中身は何でもいい(加工処理はスタブしてる)
  11. Rack::Test::UploadedFile.new(StringIO.new('dummy'), 'image/jpeg', original_filename: 'dummy.jpg')
  12. end
  13. let!(:tag_name) { TagName.create!(name: 'spec_tag') }
  14. let!(:tag) { Tag.create!(tag_name: tag_name, category: 'general') }
  15. let!(:post_record) do
  16. Post.create!(title: 'spec post', url: 'https://example.com/spec').tap do |p|
  17. PostTag.create!(post: p, tag: tag)
  18. end
  19. end
  20. describe "GET /posts" do
  21. let!(:user) { create_member_user! }
  22. let!(:tag_name) { TagName.create!(name: "spec_tag") }
  23. let!(:tag) { Tag.create!(tag_name:, category: "general") }
  24. let!(:tag_name2) { TagName.create!(name: 'unko') }
  25. let!(:tag2) { Tag.create!(tag_name: tag_name2, category: 'deerjikist') }
  26. let!(:hit_post) do
  27. Post.create!(uploaded_user: user, title: "hello spec world",
  28. url: 'https://example.com/spec2').tap do |p|
  29. PostTag.create!(post: p, tag:)
  30. end
  31. end
  32. let!(:miss_post) do
  33. Post.create!(uploaded_user: user, title: "unrelated title",
  34. url: 'https://example.com/spec3').tap do |p|
  35. PostTag.create!(post: p, tag: tag2)
  36. end
  37. end
  38. it "returns posts with tag name in JSON" do
  39. get "/posts"
  40. expect(response).to have_http_status(:ok)
  41. posts = json.fetch("posts")
  42. # 全postの全tagが name を含むこと
  43. expect(posts).not_to be_empty
  44. posts.each do |p|
  45. expect(p["tags"]).to be_an(Array)
  46. p["tags"].each do |t|
  47. expect(t).to include("name", "category")
  48. end
  49. end
  50. expect(json['count']).to be_an(Integer)
  51. # spec_tag を含む投稿が存在すること
  52. all_tag_names = posts.flat_map { |p| p["tags"].map { |t| t["name"] } }
  53. expect(all_tag_names).to include("spec_tag")
  54. end
  55. context "when q is provided" do
  56. it "filters posts by q (hit case)" do
  57. get "/posts", params: { tags: "spec_tag" }
  58. expect(response).to have_http_status(:ok)
  59. ids = json.fetch("posts").map { |p| p["id"] }
  60. expect(ids).to include(hit_post.id)
  61. expect(ids).not_to include(miss_post.id)
  62. expect(json['count']).to be_an(Integer)
  63. end
  64. it "returns empty posts when nothing matches" do
  65. get "/posts", params: { tags: "no_such_keyword_12345" }
  66. expect(response).to have_http_status(:ok)
  67. expect(json.fetch("posts")).to eq([])
  68. expect(json.fetch('count')).to eq(0)
  69. end
  70. end
  71. end
  72. describe 'GET /posts/:id' do
  73. subject(:request) { get "/posts/#{post_id}" }
  74. context 'when post exists' do
  75. let(:post_id) { post_record.id }
  76. it 'returns post with tag tree + related + viewed' do
  77. request
  78. expect(response).to have_http_status(:ok)
  79. expect(json).to include('id' => post_record.id)
  80. expect(json).to have_key('tags')
  81. expect(json['tags']).to be_an(Array)
  82. # show は build_tag_tree_for を使うので、tags はツリー形式(children 付き)
  83. node = json['tags'][0]
  84. expect(node).to include('id', 'name', 'category', 'post_count', 'children')
  85. expect(node['name']).to eq('spec_tag')
  86. expect(json).to have_key('related')
  87. expect(json['related']).to be_an(Array)
  88. expect(json).to have_key('viewed')
  89. expect([true, false]).to include(json['viewed'])
  90. end
  91. end
  92. context 'when post does not exist' do
  93. let(:post_id) { 999_999_999 }
  94. it 'returns 404' do
  95. request
  96. expect(response).to have_http_status(:not_found)
  97. end
  98. end
  99. end
  100. describe 'POST /posts' do
  101. let(:member) { create(:user, :member) }
  102. it '401 when not logged in' do
  103. sign_out
  104. post '/posts', params: { title: 't', url: 'https://example.com/x', tags: 'a', thumbnail: dummy_upload }
  105. expect(response).to have_http_status(:unauthorized)
  106. end
  107. it '403 when not member' do
  108. sign_in_as(create(:user, role: 'guest'))
  109. post '/posts', params: { title: 't', url: 'https://example.com/x', tags: 'a', thumbnail: dummy_upload }
  110. expect(response).to have_http_status(:forbidden)
  111. end
  112. it '201 and creates post + tags when member' do
  113. sign_in_as(member)
  114. post '/posts', params: {
  115. title: 'new post',
  116. url: 'https://example.com/new',
  117. tags: 'spec_tag', # 既存タグ名を投げる
  118. thumbnail: dummy_upload
  119. }
  120. expect(response).to have_http_status(:created)
  121. expect(json).to include('id', 'title', 'url')
  122. # tags が name を含むこと(API 側の serialization が正しいこと)
  123. expect(json).to have_key('tags')
  124. expect(json['tags']).to be_an(Array)
  125. expect(json['tags'][0]).to have_key('name')
  126. end
  127. context 'when url is blank' do
  128. it 'returns 422' do
  129. sign_in_as(member)
  130. post '/posts', params: {
  131. title: 'new post',
  132. url: ' ',
  133. tags: 'spec_tag', # 既存タグ名を投げる
  134. thumbnail: dummy_upload
  135. }
  136. expect(response).to have_http_status(:unprocessable_entity)
  137. end
  138. end
  139. context 'when url is invalid' do
  140. it 'returns 422' do
  141. sign_in_as(member)
  142. post '/posts', params: {
  143. title: 'new post',
  144. url: 'ぼざクリタグ広場',
  145. tags: 'spec_tag', # 既存タグ名を投げる
  146. thumbnail: dummy_upload
  147. }
  148. expect(response).to have_http_status(:unprocessable_entity)
  149. end
  150. end
  151. end
  152. describe 'PUT /posts/:id' do
  153. let(:member) { create(:user, :member) }
  154. it '401 when not logged in' do
  155. sign_out
  156. put "/posts/#{post_record.id}", params: { title: 'updated', tags: 'spec_tag' }
  157. expect(response).to have_http_status(:unauthorized)
  158. end
  159. it '403 when not member' do
  160. sign_in_as(create(:user, role: 'guest'))
  161. put "/posts/#{post_record.id}", params: { title: 'updated', tags: 'spec_tag' }
  162. expect(response).to have_http_status(:forbidden)
  163. end
  164. it '200 and updates title + resync tags when member' do
  165. sign_in_as(member)
  166. # 追加で別タグも作って、更新時に入れ替わることを見る
  167. tn2 = TagName.create!(name: 'spec_tag_2')
  168. Tag.create!(tag_name: tn2, category: 'general')
  169. put "/posts/#{post_record.id}", params: {
  170. title: 'updated title',
  171. tags: 'spec_tag_2'
  172. }
  173. expect(response).to have_http_status(:ok)
  174. expect(json).to have_key('tags')
  175. expect(json['tags']).to be_an(Array)
  176. # show と同様、update 後レスポンスもツリー形式
  177. names = json['tags'].map { |n| n['name'] }
  178. expect(names).to include('spec_tag_2')
  179. end
  180. end
  181. describe 'GET /posts/random' do
  182. it '404 when no posts' do
  183. PostTag.delete_all
  184. Post.delete_all
  185. get '/posts/random'
  186. expect(response).to have_http_status(:not_found)
  187. end
  188. it '200 and returns viewed boolean' do
  189. get '/posts/random'
  190. expect(response).to have_http_status(:ok)
  191. expect(json).to have_key('viewed')
  192. expect([true, false]).to include(json['viewed'])
  193. end
  194. end
  195. describe 'GET /posts/changes' do
  196. let(:member) { create(:user, :member) }
  197. it 'returns add/remove events (history) for a post' do
  198. # add
  199. tn2 = TagName.create!(name: 'spec_tag2')
  200. tag2 = Tag.create!(tag_name: tn2, category: 'general')
  201. pt = PostTag.create!(post: post_record, tag: tag2, created_user: member)
  202. # remove (discard)
  203. pt.discard_by!(member)
  204. get '/posts/changes', params: { id: post_record.id }
  205. expect(response).to have_http_status(:ok)
  206. expect(json).to include('changes', 'count')
  207. expect(json['changes']).to be_an(Array)
  208. expect(json['count']).to be >= 2
  209. types = json['changes'].map { |e| e['change_type'] }.uniq
  210. expect(types).to include('add')
  211. expect(types).to include('remove')
  212. end
  213. end
  214. describe 'POST /posts/:id/viewed' do
  215. let(:user) { create(:user) }
  216. it '401 when not logged in' do
  217. sign_out
  218. post "/posts/#{ post_record.id }/viewed"
  219. expect(response).to have_http_status(:unauthorized)
  220. end
  221. it '204 and marks viewed when logged in' do
  222. sign_in_as(user)
  223. post "/posts/#{ post_record.id }/viewed"
  224. expect(response).to have_http_status(:no_content)
  225. expect(user.reload.viewed?(post_record)).to be(true)
  226. end
  227. end
  228. describe 'DELETE /posts/:id/viewed' do
  229. let(:user) { create(:user) }
  230. it '401 when not logged in' do
  231. sign_out
  232. delete "/posts/#{ post_record.id }/viewed"
  233. expect(response).to have_http_status(:unauthorized)
  234. end
  235. it '204 and unmarks viewed when logged in' do
  236. sign_in_as(user)
  237. # 先に viewed 付けてから外す
  238. user.viewed_posts << post_record
  239. delete "/posts/#{ post_record.id }/viewed"
  240. expect(response).to have_http_status(:no_content)
  241. expect(user.reload.viewed?(post_record)).to be(false)
  242. end
  243. end
  244. end