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

354 lines
10 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 nico tag already exists in tags" do
  135. before do
  136. Tag.find_or_create_by!(tag_name: TagName.find_or_create_by!(name: 'nico:nico_tag'),
  137. category: 'nico')
  138. end
  139. it 'return 400' do
  140. sign_in_as(member)
  141. post '/posts', params: {
  142. title: 'new post',
  143. url: 'https://example.com/nico_tag',
  144. tags: 'nico:nico_tag',
  145. thumbnail: dummy_upload }
  146. expect(response).to have_http_status(:bad_request)
  147. end
  148. end
  149. context 'when url is blank' do
  150. it 'returns 422' do
  151. sign_in_as(member)
  152. post '/posts', params: {
  153. title: 'new post',
  154. url: ' ',
  155. tags: 'spec_tag', # 既存タグ名を投げる
  156. thumbnail: dummy_upload }
  157. expect(response).to have_http_status(:unprocessable_entity)
  158. end
  159. end
  160. context 'when url is invalid' do
  161. it 'returns 422' do
  162. sign_in_as(member)
  163. post '/posts', params: {
  164. title: 'new post',
  165. url: 'ぼざクリタグ広場',
  166. tags: 'spec_tag', # 既存タグ名を投げる
  167. thumbnail: dummy_upload
  168. }
  169. expect(response).to have_http_status(:unprocessable_entity)
  170. end
  171. end
  172. end
  173. describe 'PUT /posts/:id' do
  174. let(:member) { create(:user, :member) }
  175. it '401 when not logged in' do
  176. sign_out
  177. put "/posts/#{post_record.id}", params: { title: 'updated', tags: 'spec_tag' }
  178. expect(response).to have_http_status(:unauthorized)
  179. end
  180. it '403 when not member' do
  181. sign_in_as(create(:user, role: 'guest'))
  182. put "/posts/#{post_record.id}", params: { title: 'updated', tags: 'spec_tag' }
  183. expect(response).to have_http_status(:forbidden)
  184. end
  185. it '200 and updates title + resync tags when member' do
  186. sign_in_as(member)
  187. # 追加で別タグも作って、更新時に入れ替わることを見る
  188. tn2 = TagName.create!(name: 'spec_tag_2')
  189. Tag.create!(tag_name: tn2, category: 'general')
  190. put "/posts/#{post_record.id}", params: {
  191. title: 'updated title',
  192. tags: 'spec_tag_2'
  193. }
  194. expect(response).to have_http_status(:ok)
  195. expect(json).to have_key('tags')
  196. expect(json['tags']).to be_an(Array)
  197. # show と同様、update 後レスポンスもツリー形式
  198. names = json['tags'].map { |n| n['name'] }
  199. expect(names).to include('spec_tag_2')
  200. end
  201. context "when nico tag already exists in tags" do
  202. before do
  203. Tag.find_or_create_by!(tag_name: TagName.find_or_create_by!(name: 'nico:nico_tag'),
  204. category: 'nico')
  205. end
  206. it 'return 400' do
  207. sign_in_as(member)
  208. put "/posts/#{ post_record.id }", params: {
  209. title: 'updated title',
  210. tags: 'nico:nico_tag' }
  211. expect(response).to have_http_status(:bad_request)
  212. end
  213. end
  214. end
  215. describe 'GET /posts/random' do
  216. it '404 when no posts' do
  217. PostTag.delete_all
  218. Post.delete_all
  219. get '/posts/random'
  220. expect(response).to have_http_status(:not_found)
  221. end
  222. it '200 and returns viewed boolean' do
  223. get '/posts/random'
  224. expect(response).to have_http_status(:ok)
  225. expect(json).to have_key('viewed')
  226. expect([true, false]).to include(json['viewed'])
  227. end
  228. end
  229. describe 'GET /posts/changes' do
  230. let(:member) { create(:user, :member) }
  231. it 'returns add/remove events (history) for a post' do
  232. # add
  233. tn2 = TagName.create!(name: 'spec_tag2')
  234. tag2 = Tag.create!(tag_name: tn2, category: 'general')
  235. pt = PostTag.create!(post: post_record, tag: tag2, created_user: member)
  236. # remove (discard)
  237. pt.discard_by!(member)
  238. get '/posts/changes', params: { id: post_record.id }
  239. expect(response).to have_http_status(:ok)
  240. expect(json).to include('changes', 'count')
  241. expect(json['changes']).to be_an(Array)
  242. expect(json['count']).to be >= 2
  243. types = json['changes'].map { |e| e['change_type'] }.uniq
  244. expect(types).to include('add')
  245. expect(types).to include('remove')
  246. end
  247. end
  248. describe 'POST /posts/:id/viewed' do
  249. let(:user) { create(:user) }
  250. it '401 when not logged in' do
  251. sign_out
  252. post "/posts/#{ post_record.id }/viewed"
  253. expect(response).to have_http_status(:unauthorized)
  254. end
  255. it '204 and marks viewed when logged in' do
  256. sign_in_as(user)
  257. post "/posts/#{ post_record.id }/viewed"
  258. expect(response).to have_http_status(:no_content)
  259. expect(user.reload.viewed?(post_record)).to be(true)
  260. end
  261. end
  262. describe 'DELETE /posts/:id/viewed' do
  263. let(:user) { create(:user) }
  264. it '401 when not logged in' do
  265. sign_out
  266. delete "/posts/#{ post_record.id }/viewed"
  267. expect(response).to have_http_status(:unauthorized)
  268. end
  269. it '204 and unmarks viewed when logged in' do
  270. sign_in_as(user)
  271. # 先に viewed 付けてから外す
  272. user.viewed_posts << post_record
  273. delete "/posts/#{ post_record.id }/viewed"
  274. expect(response).to have_http_status(:no_content)
  275. expect(user.reload.viewed?(post_record)).to be(false)
  276. end
  277. end
  278. end