ぼざクリタグ広場 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.
 
 
 
 
 
 

319 lines
9.5 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', 'has_wiki')
  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. posts = json.fetch('posts')
  60. ids = posts.map { |p| p['id'] }
  61. expect(ids).to include(hit_post.id)
  62. expect(ids).not_to include(miss_post.id)
  63. expect(json['count']).to be_an(Integer)
  64. posts.each do |p|
  65. expect(p['tags']).to be_an(Array)
  66. p['tags'].each do |t|
  67. expect(t).to include('name', 'category', 'has_wiki')
  68. end
  69. end
  70. end
  71. it "returns empty posts when nothing matches" do
  72. get "/posts", params: { tags: "no_such_keyword_12345" }
  73. expect(response).to have_http_status(:ok)
  74. expect(json.fetch("posts")).to eq([])
  75. expect(json.fetch('count')).to eq(0)
  76. end
  77. end
  78. end
  79. describe 'GET /posts/:id' do
  80. subject(:request) { get "/posts/#{post_id}" }
  81. context 'when post exists' do
  82. let(:post_id) { post_record.id }
  83. it 'returns post with tag tree + related + viewed' do
  84. request
  85. expect(response).to have_http_status(:ok)
  86. expect(json).to include('id' => post_record.id)
  87. expect(json).to have_key('tags')
  88. expect(json['tags']).to be_an(Array)
  89. # show は build_tag_tree_for を使うので、tags はツリー形式(children 付き)
  90. node = json['tags'][0]
  91. expect(node).to include('id', 'name', 'category', 'post_count', 'children', 'has_wiki')
  92. expect(node['name']).to eq('spec_tag')
  93. expect(json).to have_key('related')
  94. expect(json['related']).to be_an(Array)
  95. expect(json).to have_key('viewed')
  96. expect([true, false]).to include(json['viewed'])
  97. end
  98. end
  99. context 'when post does not exist' do
  100. let(:post_id) { 999_999_999 }
  101. it 'returns 404' do
  102. request
  103. expect(response).to have_http_status(:not_found)
  104. end
  105. end
  106. end
  107. describe 'POST /posts' do
  108. let(:member) { create(:user, :member) }
  109. it '401 when not logged in' do
  110. sign_out
  111. post '/posts', params: { title: 't', url: 'https://example.com/x', tags: 'a', thumbnail: dummy_upload }
  112. expect(response).to have_http_status(:unauthorized)
  113. end
  114. it '403 when not member' do
  115. sign_in_as(create(:user, role: 'guest'))
  116. post '/posts', params: { title: 't', url: 'https://example.com/x', tags: 'a', thumbnail: dummy_upload }
  117. expect(response).to have_http_status(:forbidden)
  118. end
  119. it '201 and creates post + tags when member' do
  120. sign_in_as(member)
  121. post '/posts', params: {
  122. title: 'new post',
  123. url: 'https://example.com/new',
  124. tags: 'spec_tag', # 既存タグ名を投げる
  125. thumbnail: dummy_upload
  126. }
  127. expect(response).to have_http_status(:created)
  128. expect(json).to include('id', 'title', 'url')
  129. # tags が name を含むこと(API 側の serialization が正しいこと)
  130. expect(json).to have_key('tags')
  131. expect(json['tags']).to be_an(Array)
  132. expect(json['tags'][0]).to have_key('name')
  133. end
  134. context 'when url is blank' do
  135. it 'returns 422' do
  136. sign_in_as(member)
  137. post '/posts', params: {
  138. title: 'new post',
  139. url: ' ',
  140. tags: 'spec_tag', # 既存タグ名を投げる
  141. thumbnail: dummy_upload
  142. }
  143. expect(response).to have_http_status(:unprocessable_entity)
  144. end
  145. end
  146. context 'when url is invalid' do
  147. it 'returns 422' do
  148. sign_in_as(member)
  149. post '/posts', params: {
  150. title: 'new post',
  151. url: 'ぼざクリタグ広場',
  152. tags: 'spec_tag', # 既存タグ名を投げる
  153. thumbnail: dummy_upload
  154. }
  155. expect(response).to have_http_status(:unprocessable_entity)
  156. end
  157. end
  158. end
  159. describe 'PUT /posts/:id' do
  160. let(:member) { create(:user, :member) }
  161. it '401 when not logged in' do
  162. sign_out
  163. put "/posts/#{post_record.id}", params: { title: 'updated', tags: 'spec_tag' }
  164. expect(response).to have_http_status(:unauthorized)
  165. end
  166. it '403 when not member' do
  167. sign_in_as(create(:user, role: 'guest'))
  168. put "/posts/#{post_record.id}", params: { title: 'updated', tags: 'spec_tag' }
  169. expect(response).to have_http_status(:forbidden)
  170. end
  171. it '200 and updates title + resync tags when member' do
  172. sign_in_as(member)
  173. # 追加で別タグも作って、更新時に入れ替わることを見る
  174. tn2 = TagName.create!(name: 'spec_tag_2')
  175. Tag.create!(tag_name: tn2, category: 'general')
  176. put "/posts/#{post_record.id}", params: {
  177. title: 'updated title',
  178. tags: 'spec_tag_2'
  179. }
  180. expect(response).to have_http_status(:ok)
  181. expect(json).to have_key('tags')
  182. expect(json['tags']).to be_an(Array)
  183. # show と同様、update 後レスポンスもツリー形式
  184. names = json['tags'].map { |n| n['name'] }
  185. expect(names).to include('spec_tag_2')
  186. end
  187. end
  188. describe 'GET /posts/random' do
  189. it '404 when no posts' do
  190. PostTag.delete_all
  191. Post.delete_all
  192. get '/posts/random'
  193. expect(response).to have_http_status(:not_found)
  194. end
  195. it '200 and returns viewed boolean' do
  196. get '/posts/random'
  197. expect(response).to have_http_status(:ok)
  198. expect(json).to have_key('viewed')
  199. expect([true, false]).to include(json['viewed'])
  200. end
  201. end
  202. describe 'GET /posts/changes' do
  203. let(:member) { create(:user, :member) }
  204. it 'returns add/remove events (history) for a post' do
  205. # add
  206. tn2 = TagName.create!(name: 'spec_tag2')
  207. tag2 = Tag.create!(tag_name: tn2, category: 'general')
  208. pt = PostTag.create!(post: post_record, tag: tag2, created_user: member)
  209. # remove (discard)
  210. pt.discard_by!(member)
  211. get '/posts/changes', params: { id: post_record.id }
  212. expect(response).to have_http_status(:ok)
  213. expect(json).to include('changes', 'count')
  214. expect(json['changes']).to be_an(Array)
  215. expect(json['count']).to be >= 2
  216. types = json['changes'].map { |e| e['change_type'] }.uniq
  217. expect(types).to include('add')
  218. expect(types).to include('remove')
  219. end
  220. end
  221. describe 'POST /posts/:id/viewed' do
  222. let(:user) { create(:user) }
  223. it '401 when not logged in' do
  224. sign_out
  225. post "/posts/#{ post_record.id }/viewed"
  226. expect(response).to have_http_status(:unauthorized)
  227. end
  228. it '204 and marks viewed when logged in' do
  229. sign_in_as(user)
  230. post "/posts/#{ post_record.id }/viewed"
  231. expect(response).to have_http_status(:no_content)
  232. expect(user.reload.viewed?(post_record)).to be(true)
  233. end
  234. end
  235. describe 'DELETE /posts/:id/viewed' do
  236. let(:user) { create(:user) }
  237. it '401 when not logged in' do
  238. sign_out
  239. delete "/posts/#{ post_record.id }/viewed"
  240. expect(response).to have_http_status(:unauthorized)
  241. end
  242. it '204 and unmarks viewed when logged in' do
  243. sign_in_as(user)
  244. # 先に viewed 付けてから外す
  245. user.viewed_posts << post_record
  246. delete "/posts/#{ post_record.id }/viewed"
  247. expect(response).to have_http_status(:no_content)
  248. expect(user.reload.viewed?(post_record)).to be(false)
  249. end
  250. end
  251. end