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

394 lines
12 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!(:alias_tag_name) { TagName.create!(name: 'manko', canonical: tag_name) }
  27. let!(:hit_post) do
  28. Post.create!(uploaded_user: user, title: "hello spec world",
  29. url: 'https://example.com/spec2').tap do |p|
  30. PostTag.create!(post: p, tag:)
  31. end
  32. end
  33. let!(:miss_post) do
  34. Post.create!(uploaded_user: user, title: "unrelated title",
  35. url: 'https://example.com/spec3').tap do |p|
  36. PostTag.create!(post: p, tag: tag2)
  37. end
  38. end
  39. it "returns posts with tag name in JSON" do
  40. get "/posts"
  41. expect(response).to have_http_status(:ok)
  42. posts = json.fetch("posts")
  43. # 全postの全tagが name を含むこと
  44. expect(posts).not_to be_empty
  45. posts.each do |p|
  46. expect(p['tags']).to be_an(Array)
  47. p['tags'].each do |t|
  48. expect(t).to include('name', 'category', 'has_wiki')
  49. end
  50. end
  51. expect(json['count']).to be_an(Integer)
  52. # spec_tag を含む投稿が存在すること
  53. all_tag_names = posts.flat_map { |p| p["tags"].map { |t| t["name"] } }
  54. expect(all_tag_names).to include("spec_tag")
  55. end
  56. context "when q is provided" do
  57. it "filters posts by q (hit case)" do
  58. get "/posts", params: { tags: "spec_tag" }
  59. expect(response).to have_http_status(:ok)
  60. posts = json.fetch('posts')
  61. ids = posts.map { |p| p['id'] }
  62. expect(ids).to include(hit_post.id)
  63. expect(ids).not_to include(miss_post.id)
  64. expect(json['count']).to be_an(Integer)
  65. posts.each do |p|
  66. expect(p['tags']).to be_an(Array)
  67. p['tags'].each do |t|
  68. expect(t).to include('name', 'category', 'has_wiki')
  69. end
  70. end
  71. end
  72. it "filters posts by q (hit case by alias)" do
  73. get "/posts", params: { tags: "manko" }
  74. expect(response).to have_http_status(:ok)
  75. posts = json.fetch('posts')
  76. ids = posts.map { |p| p['id'] }
  77. expect(ids).to include(hit_post.id)
  78. expect(ids).not_to include(miss_post.id)
  79. expect(json['count']).to be_an(Integer)
  80. posts.each do |p|
  81. expect(p['tags']).to be_an(Array)
  82. p['tags'].each do |t|
  83. expect(t).to include('name', 'category', 'has_wiki')
  84. end
  85. end
  86. end
  87. it "returns empty posts when nothing matches" do
  88. get "/posts", params: { tags: "no_such_keyword_12345" }
  89. expect(response).to have_http_status(:ok)
  90. expect(json.fetch("posts")).to eq([])
  91. expect(json.fetch('count')).to eq(0)
  92. end
  93. end
  94. end
  95. describe 'GET /posts/:id' do
  96. subject(:request) { get "/posts/#{post_id}" }
  97. context 'when post exists' do
  98. let(:post_id) { post_record.id }
  99. it 'returns post with tag tree + related + viewed' do
  100. request
  101. expect(response).to have_http_status(:ok)
  102. expect(json).to include('id' => post_record.id)
  103. expect(json).to have_key('tags')
  104. expect(json['tags']).to be_an(Array)
  105. # show は build_tag_tree_for を使うので、tags はツリー形式(children 付き)
  106. node = json['tags'][0]
  107. expect(node).to include('id', 'name', 'category', 'post_count', 'children', 'has_wiki')
  108. expect(node['name']).to eq('spec_tag')
  109. expect(json).to have_key('related')
  110. expect(json['related']).to be_an(Array)
  111. expect(json).to have_key('viewed')
  112. expect([true, false]).to include(json['viewed'])
  113. end
  114. end
  115. context 'when post does not exist' do
  116. let(:post_id) { 999_999_999 }
  117. it 'returns 404' do
  118. request
  119. expect(response).to have_http_status(:not_found)
  120. end
  121. end
  122. end
  123. describe 'POST /posts' do
  124. let(:member) { create(:user, :member) }
  125. it '401 when not logged in' do
  126. sign_out
  127. post '/posts', params: { title: 't', url: 'https://example.com/x', tags: 'a', thumbnail: dummy_upload }
  128. expect(response).to have_http_status(:unauthorized)
  129. end
  130. it '403 when not member' do
  131. sign_in_as(create(:user, role: 'guest'))
  132. post '/posts', params: { title: 't', url: 'https://example.com/x', tags: 'a', thumbnail: dummy_upload }
  133. expect(response).to have_http_status(:forbidden)
  134. end
  135. it '201 and creates post + tags when member' do
  136. sign_in_as(member)
  137. post '/posts', params: {
  138. title: 'new post',
  139. url: 'https://example.com/new',
  140. tags: 'spec_tag', # 既存タグ名を投げる
  141. thumbnail: dummy_upload
  142. }
  143. expect(response).to have_http_status(:created)
  144. expect(json).to include('id', 'title', 'url')
  145. # tags が name を含むこと(API 側の serialization が正しいこと)
  146. expect(json).to have_key('tags')
  147. expect(json['tags']).to be_an(Array)
  148. expect(json['tags'][0]).to have_key('name')
  149. end
  150. it '201 and creates post + tags when member and tags have aliases' do
  151. sign_in_as(member)
  152. post '/posts', params: {
  153. title: 'new post',
  154. url: 'https://example.com/new',
  155. tags: 'manko', # 既存タグ名を投げる
  156. thumbnail: dummy_upload
  157. }
  158. expect(response).to have_http_status(:created)
  159. expect(json).to include('id', 'title', 'url')
  160. # tags が name を含むこと(API 側の serialization が正しいこと)
  161. expect(json).to have_key('tags')
  162. expect(json['tags']).to be_an(Array)
  163. expect(json['tags'][0]).to have_key('name')
  164. expect(json['tags'][0]['name']).to eq('spec_tag')
  165. end
  166. context "when nico tag already exists in tags" do
  167. before do
  168. Tag.find_or_create_by!(tag_name: TagName.find_or_create_by!(name: 'nico:nico_tag'),
  169. category: 'nico')
  170. end
  171. it 'return 400' do
  172. sign_in_as(member)
  173. post '/posts', params: {
  174. title: 'new post',
  175. url: 'https://example.com/nico_tag',
  176. tags: 'nico:nico_tag',
  177. thumbnail: dummy_upload }
  178. expect(response).to have_http_status(:bad_request)
  179. end
  180. end
  181. context 'when url is blank' do
  182. it 'returns 422' do
  183. sign_in_as(member)
  184. post '/posts', params: {
  185. title: 'new post',
  186. url: ' ',
  187. tags: 'spec_tag', # 既存タグ名を投げる
  188. thumbnail: dummy_upload }
  189. expect(response).to have_http_status(:unprocessable_entity)
  190. end
  191. end
  192. context 'when url is invalid' do
  193. it 'returns 422' do
  194. sign_in_as(member)
  195. post '/posts', params: {
  196. title: 'new post',
  197. url: 'ぼざクリタグ広場',
  198. tags: 'spec_tag', # 既存タグ名を投げる
  199. thumbnail: dummy_upload
  200. }
  201. expect(response).to have_http_status(:unprocessable_entity)
  202. end
  203. end
  204. end
  205. describe 'PUT /posts/:id' do
  206. let(:member) { create(:user, :member) }
  207. it '401 when not logged in' do
  208. sign_out
  209. put "/posts/#{post_record.id}", params: { title: 'updated', tags: 'spec_tag' }
  210. expect(response).to have_http_status(:unauthorized)
  211. end
  212. it '403 when not member' do
  213. sign_in_as(create(:user, role: 'guest'))
  214. put "/posts/#{post_record.id}", params: { title: 'updated', tags: 'spec_tag' }
  215. expect(response).to have_http_status(:forbidden)
  216. end
  217. it '200 and updates title + resync tags when member' do
  218. sign_in_as(member)
  219. # 追加で別タグも作って、更新時に入れ替わることを見る
  220. tn2 = TagName.create!(name: 'spec_tag_2')
  221. Tag.create!(tag_name: tn2, category: 'general')
  222. put "/posts/#{post_record.id}", params: {
  223. title: 'updated title',
  224. tags: 'spec_tag_2'
  225. }
  226. expect(response).to have_http_status(:ok)
  227. expect(json).to have_key('tags')
  228. expect(json['tags']).to be_an(Array)
  229. # show と同様、update 後レスポンスもツリー形式
  230. names = json['tags'].map { |n| n['name'] }
  231. expect(names).to include('spec_tag_2')
  232. end
  233. context "when nico tag already exists in tags" do
  234. before do
  235. Tag.find_or_create_by!(tag_name: TagName.find_or_create_by!(name: 'nico:nico_tag'),
  236. category: 'nico')
  237. end
  238. it 'return 400' do
  239. sign_in_as(member)
  240. put "/posts/#{ post_record.id }", params: {
  241. title: 'updated title',
  242. tags: 'nico:nico_tag' }
  243. expect(response).to have_http_status(:bad_request)
  244. end
  245. end
  246. end
  247. describe 'GET /posts/random' do
  248. it '404 when no posts' do
  249. PostTag.delete_all
  250. Post.delete_all
  251. get '/posts/random'
  252. expect(response).to have_http_status(:not_found)
  253. end
  254. it '200 and returns viewed boolean' do
  255. get '/posts/random'
  256. expect(response).to have_http_status(:ok)
  257. expect(json).to have_key('viewed')
  258. expect([true, false]).to include(json['viewed'])
  259. end
  260. end
  261. describe 'GET /posts/changes' do
  262. let(:member) { create(:user, :member) }
  263. it 'returns add/remove events (history) for a post' do
  264. # add
  265. tn2 = TagName.create!(name: 'spec_tag2')
  266. tag2 = Tag.create!(tag_name: tn2, category: 'general')
  267. pt = PostTag.create!(post: post_record, tag: tag2, created_user: member)
  268. # remove (discard)
  269. pt.discard_by!(member)
  270. get '/posts/changes', params: { id: post_record.id }
  271. expect(response).to have_http_status(:ok)
  272. expect(json).to include('changes', 'count')
  273. expect(json['changes']).to be_an(Array)
  274. expect(json['count']).to be >= 2
  275. types = json['changes'].map { |e| e['change_type'] }.uniq
  276. expect(types).to include('add')
  277. expect(types).to include('remove')
  278. end
  279. end
  280. describe 'POST /posts/:id/viewed' do
  281. let(:user) { create(:user) }
  282. it '401 when not logged in' do
  283. sign_out
  284. post "/posts/#{ post_record.id }/viewed"
  285. expect(response).to have_http_status(:unauthorized)
  286. end
  287. it '204 and marks viewed when logged in' do
  288. sign_in_as(user)
  289. post "/posts/#{ post_record.id }/viewed"
  290. expect(response).to have_http_status(:no_content)
  291. expect(user.reload.viewed?(post_record)).to be(true)
  292. end
  293. end
  294. describe 'DELETE /posts/:id/viewed' do
  295. let(:user) { create(:user) }
  296. it '401 when not logged in' do
  297. sign_out
  298. delete "/posts/#{ post_record.id }/viewed"
  299. expect(response).to have_http_status(:unauthorized)
  300. end
  301. it '204 and unmarks viewed when logged in' do
  302. sign_in_as(user)
  303. # 先に viewed 付けてから外す
  304. user.viewed_posts << post_record
  305. delete "/posts/#{ post_record.id }/viewed"
  306. expect(response).to have_http_status(:no_content)
  307. expect(user.reload.viewed?(post_record)).to be(false)
  308. end
  309. end
  310. end