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

511 lines
16 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. context 'when tags contain not:' do
  95. let!(:foo_tag_name) { TagName.create!(name: 'not_spec_foo') }
  96. let!(:foo_tag) { Tag.create!(tag_name: foo_tag_name, category: :general) }
  97. let!(:bar_tag_name) { TagName.create!(name: 'not_spec_bar') }
  98. let!(:bar_tag) { Tag.create!(tag_name: bar_tag_name, category: :general) }
  99. let!(:baz_tag_name) { TagName.create!(name: 'not_spec_baz') }
  100. let!(:baz_tag) { Tag.create!(tag_name: baz_tag_name, category: :general) }
  101. let!(:foo_alias_tag_name) do
  102. TagName.create!(name: 'not_spec_foo_alias', canonical: foo_tag_name)
  103. end
  104. let!(:foo_only_post) do
  105. Post.create!(uploaded_user: user, title: 'foo only',
  106. url: 'https://example.com/not-spec-foo').tap do |p|
  107. PostTag.create!(post: p, tag: foo_tag)
  108. end
  109. end
  110. let!(:bar_only_post) do
  111. Post.create!(uploaded_user: user, title: 'bar only',
  112. url: 'https://example.com/not-spec-bar').tap do |p|
  113. PostTag.create!(post: p, tag: bar_tag)
  114. end
  115. end
  116. let!(:baz_only_post) do
  117. Post.create!(uploaded_user: user, title: 'baz only',
  118. url: 'https://example.com/not-spec-baz').tap do |p|
  119. PostTag.create!(post: p, tag: baz_tag)
  120. end
  121. end
  122. let!(:foo_bar_post) do
  123. Post.create!(uploaded_user: user, title: 'foo bar',
  124. url: 'https://example.com/not-spec-foo-bar').tap do |p|
  125. PostTag.create!(post: p, tag: foo_tag)
  126. PostTag.create!(post: p, tag: bar_tag)
  127. end
  128. end
  129. let!(:foo_baz_post) do
  130. Post.create!(uploaded_user: user, title: 'foo baz',
  131. url: 'https://example.com/not-spec-foo-baz').tap do |p|
  132. PostTag.create!(post: p, tag: foo_tag)
  133. PostTag.create!(post: p, tag: baz_tag)
  134. end
  135. end
  136. let(:controlled_ids) do
  137. [foo_only_post.id, bar_only_post.id, baz_only_post.id,
  138. foo_bar_post.id, foo_baz_post.id]
  139. end
  140. it 'supports not search' do
  141. get '/posts', params: { tags: 'not:not_spec_foo' }
  142. expect(response).to have_http_status(:ok)
  143. expect(json.fetch('posts').map { |p| p['id'] } & controlled_ids).to match_array(
  144. [bar_only_post.id, baz_only_post.id]
  145. )
  146. end
  147. it 'supports alias in not search' do
  148. get '/posts', params: { tags: 'not:not_spec_foo_alias' }
  149. expect(response).to have_http_status(:ok)
  150. expect(json.fetch('posts').map { |p| p['id'] } & controlled_ids).to match_array(
  151. [bar_only_post.id, baz_only_post.id]
  152. )
  153. end
  154. it 'treats multiple not terms as AND when match is omitted' do
  155. get '/posts', params: { tags: 'not:not_spec_foo not:not_spec_bar' }
  156. expect(response).to have_http_status(:ok)
  157. expect(json.fetch('posts').map { |p| p['id'] } & controlled_ids).to match_array(
  158. [baz_only_post.id]
  159. )
  160. end
  161. it 'treats multiple not terms as OR when match=any' do
  162. get '/posts', params: { tags: 'not:not_spec_foo not:not_spec_bar', match: 'any' }
  163. expect(response).to have_http_status(:ok)
  164. expect(json.fetch('posts').map { |p| p['id'] } & controlled_ids).to match_array(
  165. [foo_only_post.id, bar_only_post.id, baz_only_post.id, foo_baz_post.id]
  166. )
  167. end
  168. it 'supports mixed positive and negative search with AND' do
  169. get '/posts', params: { tags: 'not_spec_foo not:not_spec_bar' }
  170. expect(response).to have_http_status(:ok)
  171. expect(json.fetch('posts').map { |p| p['id'] } & controlled_ids).to match_array(
  172. [foo_only_post.id, foo_baz_post.id]
  173. )
  174. end
  175. it 'supports mixed positive and negative search with OR when match=any' do
  176. get '/posts', params: { tags: 'not_spec_foo not:not_spec_bar', match: 'any' }
  177. expect(response).to have_http_status(:ok)
  178. expect(json.fetch('posts').map { |p| p['id'] } & controlled_ids).to match_array(
  179. [foo_only_post.id, baz_only_post.id, foo_bar_post.id, foo_baz_post.id]
  180. )
  181. end
  182. end
  183. end
  184. describe 'GET /posts/:id' do
  185. subject(:request) { get "/posts/#{post_id}" }
  186. context 'when post exists' do
  187. let(:post_id) { post_record.id }
  188. it 'returns post with tag tree + related + viewed' do
  189. request
  190. expect(response).to have_http_status(:ok)
  191. expect(json).to include('id' => post_record.id)
  192. expect(json).to have_key('tags')
  193. expect(json['tags']).to be_an(Array)
  194. # show は build_tag_tree_for を使うので、tags はツリー形式(children 付き)
  195. node = json['tags'][0]
  196. expect(node).to include('id', 'name', 'category', 'post_count', 'children', 'has_wiki')
  197. expect(node['name']).to eq('spec_tag')
  198. expect(json).to have_key('related')
  199. expect(json['related']).to be_an(Array)
  200. expect(json).to have_key('viewed')
  201. expect([true, false]).to include(json['viewed'])
  202. end
  203. end
  204. context 'when post does not exist' do
  205. let(:post_id) { 999_999_999 }
  206. it 'returns 404' do
  207. request
  208. expect(response).to have_http_status(:not_found)
  209. end
  210. end
  211. end
  212. describe 'POST /posts' do
  213. let(:member) { create(:user, :member) }
  214. let!(:alias_tag_name) { TagName.create!(name: 'manko', canonical: tag_name) }
  215. it '401 when not logged in' do
  216. sign_out
  217. post '/posts', params: { title: 't', url: 'https://example.com/x', tags: 'a', thumbnail: dummy_upload }
  218. expect(response).to have_http_status(:unauthorized)
  219. end
  220. it '403 when not member' do
  221. sign_in_as(create(:user, role: 'guest'))
  222. post '/posts', params: { title: 't', url: 'https://example.com/x', tags: 'a', thumbnail: dummy_upload }
  223. expect(response).to have_http_status(:forbidden)
  224. end
  225. it '201 and creates post + tags when member' do
  226. sign_in_as(member)
  227. post '/posts', params: {
  228. title: 'new post',
  229. url: 'https://example.com/new',
  230. tags: 'spec_tag', # 既存タグ名を投げる
  231. thumbnail: dummy_upload
  232. }
  233. expect(response).to have_http_status(:created)
  234. expect(json).to include('id', 'title', 'url')
  235. # tags が name を含むこと(API 側の serialization が正しいこと)
  236. expect(json).to have_key('tags')
  237. expect(json['tags']).to be_an(Array)
  238. expect(json['tags'][0]).to have_key('name')
  239. end
  240. it '201 and creates post + tags when member and tags have aliases' do
  241. sign_in_as(member)
  242. post '/posts', params: {
  243. title: 'new post',
  244. url: 'https://example.com/new',
  245. tags: 'manko', # 既存タグ名を投げる
  246. thumbnail: dummy_upload
  247. }
  248. expect(response).to have_http_status(:created)
  249. expect(json).to include('id', 'title', 'url')
  250. # tags が name を含むこと(API 側の serialization が正しいこと)
  251. names = json.fetch('tags').map { |t| t['name'] }
  252. expect(names).to include('spec_tag')
  253. expect(names).not_to include('manko')
  254. end
  255. context "when nico tag already exists in tags" do
  256. before do
  257. Tag.find_or_create_by!(tag_name: TagName.find_or_create_by!(name: 'nico:nico_tag'),
  258. category: :nico)
  259. end
  260. it 'return 400' do
  261. sign_in_as(member)
  262. post '/posts', params: {
  263. title: 'new post',
  264. url: 'https://example.com/nico_tag',
  265. tags: 'nico:nico_tag',
  266. thumbnail: dummy_upload }
  267. expect(response).to have_http_status(:bad_request)
  268. end
  269. end
  270. context 'when url is blank' do
  271. it 'returns 422' do
  272. sign_in_as(member)
  273. post '/posts', params: {
  274. title: 'new post',
  275. url: ' ',
  276. tags: 'spec_tag', # 既存タグ名を投げる
  277. thumbnail: dummy_upload }
  278. expect(response).to have_http_status(:unprocessable_entity)
  279. end
  280. end
  281. context 'when url is invalid' do
  282. it 'returns 422' do
  283. sign_in_as(member)
  284. post '/posts', params: {
  285. title: 'new post',
  286. url: 'ぼざクリタグ広場',
  287. tags: 'spec_tag', # 既存タグ名を投げる
  288. thumbnail: dummy_upload
  289. }
  290. expect(response).to have_http_status(:unprocessable_entity)
  291. end
  292. end
  293. end
  294. describe 'PUT /posts/:id' do
  295. let(:member) { create(:user, :member) }
  296. it '401 when not logged in' do
  297. sign_out
  298. put "/posts/#{post_record.id}", params: { title: 'updated', tags: 'spec_tag' }
  299. expect(response).to have_http_status(:unauthorized)
  300. end
  301. it '403 when not member' do
  302. sign_in_as(create(:user, role: 'guest'))
  303. put "/posts/#{post_record.id}", params: { title: 'updated', tags: 'spec_tag' }
  304. expect(response).to have_http_status(:forbidden)
  305. end
  306. it '200 and updates title + resync tags when member' do
  307. sign_in_as(member)
  308. # 追加で別タグも作って、更新時に入れ替わることを見る
  309. tn2 = TagName.create!(name: 'spec_tag_2')
  310. Tag.create!(tag_name: tn2, category: :general)
  311. put "/posts/#{post_record.id}", params: {
  312. title: 'updated title',
  313. tags: 'spec_tag_2'
  314. }
  315. expect(response).to have_http_status(:ok)
  316. expect(json).to have_key('tags')
  317. expect(json['tags']).to be_an(Array)
  318. # show と同様、update 後レスポンスもツリー形式
  319. names = json['tags'].map { |n| n['name'] }
  320. expect(names).to include('spec_tag_2')
  321. end
  322. context "when nico tag already exists in tags" do
  323. before do
  324. Tag.find_or_create_by!(tag_name: TagName.find_or_create_by!(name: 'nico:nico_tag'),
  325. category: :nico)
  326. end
  327. it 'return 400' do
  328. sign_in_as(member)
  329. put "/posts/#{ post_record.id }", params: {
  330. title: 'updated title',
  331. tags: 'nico:nico_tag' }
  332. expect(response).to have_http_status(:bad_request)
  333. end
  334. end
  335. end
  336. describe 'GET /posts/random' do
  337. it '404 when no posts' do
  338. PostTag.delete_all
  339. Post.delete_all
  340. get '/posts/random'
  341. expect(response).to have_http_status(:not_found)
  342. end
  343. it '200 and returns viewed boolean' do
  344. get '/posts/random'
  345. expect(response).to have_http_status(:ok)
  346. expect(json).to have_key('viewed')
  347. expect([true, false]).to include(json['viewed'])
  348. end
  349. end
  350. describe 'GET /posts/changes' do
  351. let(:member) { create(:user, :member) }
  352. it 'returns add/remove events (history) for a post' do
  353. # add
  354. tn2 = TagName.create!(name: 'spec_tag2')
  355. tag2 = Tag.create!(tag_name: tn2, category: :general)
  356. pt = PostTag.create!(post: post_record, tag: tag2, created_user: member)
  357. # remove (discard)
  358. pt.discard_by!(member)
  359. get '/posts/changes', params: { id: post_record.id }
  360. expect(response).to have_http_status(:ok)
  361. expect(json).to include('changes', 'count')
  362. expect(json['changes']).to be_an(Array)
  363. expect(json['count']).to be >= 2
  364. types = json['changes'].map { |e| e['change_type'] }.uniq
  365. expect(types).to include('add')
  366. expect(types).to include('remove')
  367. end
  368. end
  369. describe 'POST /posts/:id/viewed' do
  370. let(:user) { create(:user) }
  371. it '401 when not logged in' do
  372. sign_out
  373. post "/posts/#{ post_record.id }/viewed"
  374. expect(response).to have_http_status(:unauthorized)
  375. end
  376. it '204 and marks viewed when logged in' do
  377. sign_in_as(user)
  378. post "/posts/#{ post_record.id }/viewed"
  379. expect(response).to have_http_status(:no_content)
  380. expect(user.reload.viewed?(post_record)).to be(true)
  381. end
  382. end
  383. describe 'DELETE /posts/:id/viewed' do
  384. let(:user) { create(:user) }
  385. it '401 when not logged in' do
  386. sign_out
  387. delete "/posts/#{ post_record.id }/viewed"
  388. expect(response).to have_http_status(:unauthorized)
  389. end
  390. it '204 and unmarks viewed when logged in' do
  391. sign_in_as(user)
  392. # 先に viewed 付けてから外す
  393. user.viewed_posts << post_record
  394. delete "/posts/#{ post_record.id }/viewed"
  395. expect(response).to have_http_status(:no_content)
  396. expect(user.reload.viewed?(post_record)).to be(false)
  397. end
  398. end
  399. end