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

593 lines
19 KiB

  1. include ActiveSupport::Testing::TimeHelpers
  2. require 'rails_helper'
  3. require 'set'
  4. RSpec.describe 'Posts API', type: :request do
  5. # create / update で thumbnail.attach は走るが、
  6. # resized_thumbnail! が MiniMagick 依存でコケやすいので request spec ではスタブしとくのが無難。
  7. before do
  8. allow_any_instance_of(Post).to receive(:resized_thumbnail!).and_return(true)
  9. end
  10. def dummy_upload
  11. # 中身は何でもいい(加工処理はスタブしてる)
  12. Rack::Test::UploadedFile.new(StringIO.new('dummy'), 'image/jpeg', original_filename: 'dummy.jpg')
  13. end
  14. let!(:tag_name) { TagName.create!(name: 'spec_tag') }
  15. let!(:tag) { Tag.create!(tag_name: tag_name, category: 'general') }
  16. let!(:post_record) do
  17. Post.create!(title: 'spec post', url: 'https://example.com/spec').tap do |p|
  18. PostTag.create!(post: p, tag: tag)
  19. end
  20. end
  21. describe "GET /posts" do
  22. let!(:user) { create_member_user! }
  23. let!(:tag_name) { TagName.create!(name: "spec_tag") }
  24. let!(:tag) { Tag.create!(tag_name:, category: "general") }
  25. let!(:tag_name2) { TagName.create!(name: 'unko') }
  26. let!(:tag2) { Tag.create!(tag_name: tag_name2, category: 'deerjikist') }
  27. let!(:alias_tag_name) { TagName.create!(name: 'manko', canonical: tag_name) }
  28. let!(:hit_post) do
  29. Post.create!(uploaded_user: user, title: "hello spec world",
  30. url: 'https://example.com/spec2').tap do |p|
  31. PostTag.create!(post: p, tag:)
  32. end
  33. end
  34. let!(:miss_post) do
  35. Post.create!(uploaded_user: user, title: "unrelated title",
  36. url: 'https://example.com/spec3').tap do |p|
  37. PostTag.create!(post: p, tag: tag2)
  38. end
  39. end
  40. it "returns posts with tag name in JSON" do
  41. get "/posts"
  42. expect(response).to have_http_status(:ok)
  43. posts = json.fetch("posts")
  44. # 全postの全tagが name を含むこと
  45. expect(posts).not_to be_empty
  46. posts.each do |p|
  47. expect(p['tags']).to be_an(Array)
  48. p['tags'].each do |t|
  49. expect(t).to include('name', 'category', 'has_wiki')
  50. end
  51. end
  52. expect(json['count']).to be_an(Integer)
  53. # spec_tag を含む投稿が存在すること
  54. all_tag_names = posts.flat_map { |p| p["tags"].map { |t| t["name"] } }
  55. expect(all_tag_names).to include("spec_tag")
  56. end
  57. context "when q is provided" do
  58. it "filters posts by q (hit case)" do
  59. get "/posts", params: { tags: "spec_tag" }
  60. expect(response).to have_http_status(:ok)
  61. posts = json.fetch('posts')
  62. ids = posts.map { |p| p['id'] }
  63. expect(ids).to include(hit_post.id)
  64. expect(ids).not_to include(miss_post.id)
  65. expect(json['count']).to be_an(Integer)
  66. posts.each do |p|
  67. expect(p['tags']).to be_an(Array)
  68. p['tags'].each do |t|
  69. expect(t).to include('name', 'category', 'has_wiki')
  70. end
  71. end
  72. end
  73. it "filters posts by q (hit case by alias)" do
  74. get "/posts", params: { tags: "manko" }
  75. expect(response).to have_http_status(:ok)
  76. posts = json.fetch('posts')
  77. ids = posts.map { |p| p['id'] }
  78. expect(ids).to include(hit_post.id)
  79. expect(ids).not_to include(miss_post.id)
  80. expect(json['count']).to be_an(Integer)
  81. posts.each do |p|
  82. expect(p['tags']).to be_an(Array)
  83. p['tags'].each do |t|
  84. expect(t).to include('name', 'category', 'has_wiki')
  85. end
  86. end
  87. end
  88. it "returns empty posts when nothing matches" do
  89. get "/posts", params: { tags: "no_such_keyword_12345" }
  90. expect(response).to have_http_status(:ok)
  91. expect(json.fetch("posts")).to eq([])
  92. expect(json.fetch('count')).to eq(0)
  93. end
  94. end
  95. context 'when url is provided' do
  96. let!(:url_hit_post) do
  97. Post.create!(uploaded_user: user, title: 'url hit',
  98. url: 'https://example.com/needle-url-xyz').tap do |p|
  99. PostTag.create!(post: p, tag:)
  100. end
  101. end
  102. let!(:url_miss_post) do
  103. Post.create!(uploaded_user: user, title: 'url miss',
  104. url: 'https://example.com/other-url').tap do |p|
  105. PostTag.create!(post: p, tag:)
  106. end
  107. end
  108. it 'filters posts by url substring' do
  109. get '/posts', params: { url: 'needle-url-xyz' }
  110. expect(response).to have_http_status(:ok)
  111. ids = json.fetch('posts').map { |p| p['id'] }
  112. expect(ids).to include(url_hit_post.id)
  113. expect(ids).not_to include(url_miss_post.id)
  114. expect(json.fetch('count')).to eq(1)
  115. end
  116. end
  117. context 'when title is provided' do
  118. let!(:title_hit_post) do
  119. Post.create!(uploaded_user: user, title: 'needle-title-xyz',
  120. url: 'https://example.com/title-hit').tap do |p|
  121. PostTag.create!(post: p, tag:)
  122. end
  123. end
  124. let!(:title_miss_post) do
  125. Post.create!(uploaded_user: user, title: 'other title',
  126. url: 'https://example.com/title-miss').tap do |p|
  127. PostTag.create!(post: p, tag:)
  128. end
  129. end
  130. it 'filters posts by title substring' do
  131. get '/posts', params: { title: 'needle-title-xyz' }
  132. expect(response).to have_http_status(:ok)
  133. ids = json.fetch('posts').map { |p| p['id'] }
  134. expect(ids).to include(title_hit_post.id)
  135. expect(ids).not_to include(title_miss_post.id)
  136. expect(json.fetch('count')).to eq(1)
  137. end
  138. end
  139. context 'when created_from/created_to are provided' do
  140. let(:t_created_hit) { Time.zone.local(2010, 1, 5, 12, 0, 0) }
  141. let(:t_created_miss) { Time.zone.local(2012, 1, 5, 12, 0, 0) }
  142. let!(:created_hit_post) do
  143. travel_to(t_created_hit) do
  144. Post.create!(uploaded_user: user, title: 'created hit',
  145. url: 'https://example.com/created-hit').tap do |p|
  146. PostTag.create!(post: p, tag:)
  147. end
  148. end
  149. end
  150. let!(:created_miss_post) do
  151. travel_to(t_created_miss) do
  152. Post.create!(uploaded_user: user, title: 'created miss',
  153. url: 'https://example.com/created-miss').tap do |p|
  154. PostTag.create!(post: p, tag:)
  155. end
  156. end
  157. end
  158. it 'filters posts by created_at range' do
  159. get '/posts', params: {
  160. created_from: Time.zone.local(2010, 1, 1, 0, 0, 0).iso8601,
  161. created_to: Time.zone.local(2010, 12, 31, 23, 59, 59).iso8601
  162. }
  163. expect(response).to have_http_status(:ok)
  164. ids = json.fetch('posts').map { |p| p['id'] }
  165. expect(ids).to include(created_hit_post.id)
  166. expect(ids).not_to include(created_miss_post.id)
  167. expect(json.fetch('count')).to eq(1)
  168. end
  169. end
  170. context 'when updated_from/updated_to are provided' do
  171. let(:t0) { Time.zone.local(2011, 2, 1, 12, 0, 0) }
  172. let(:t1) { Time.zone.local(2011, 2, 10, 12, 0, 0) }
  173. let!(:updated_hit_post) do
  174. p = nil
  175. travel_to(t0) do
  176. p = Post.create!(uploaded_user: user, title: 'updated hit',
  177. url: 'https://example.com/updated-hit').tap do |pp|
  178. PostTag.create!(post: pp, tag:)
  179. end
  180. end
  181. travel_to(t1) do
  182. p.update!(title: 'updated hit v2')
  183. end
  184. p
  185. end
  186. let!(:updated_miss_post) do
  187. travel_to(Time.zone.local(2013, 1, 1, 12, 0, 0)) do
  188. Post.create!(uploaded_user: user, title: 'updated miss',
  189. url: 'https://example.com/updated-miss').tap do |p|
  190. PostTag.create!(post: p, tag:)
  191. end
  192. end
  193. end
  194. it 'filters posts by updated_at range' do
  195. get '/posts', params: {
  196. updated_from: Time.zone.local(2011, 2, 5, 0, 0, 0).iso8601,
  197. updated_to: Time.zone.local(2011, 2, 20, 23, 59, 59).iso8601
  198. }
  199. expect(response).to have_http_status(:ok)
  200. ids = json.fetch('posts').map { |p| p['id'] }
  201. expect(ids).to include(updated_hit_post.id)
  202. expect(ids).not_to include(updated_miss_post.id)
  203. expect(json.fetch('count')).to eq(1)
  204. end
  205. end
  206. context 'when original_created_from/original_created_to are provided' do
  207. # 注意: controller の現状ロジックに合わせてる
  208. # original_created_from は `original_created_before > ?`
  209. # original_created_to は `original_created_from <= ?`
  210. let!(:oc_hit_post) do
  211. Post.create!(uploaded_user: user, title: 'oc hit',
  212. url: 'https://example.com/oc-hit',
  213. original_created_from: Time.zone.local(2015, 1, 1, 0, 0, 0),
  214. original_created_before: Time.zone.local(2015, 1, 10, 0, 0, 0)).tap do |p|
  215. PostTag.create!(post: p, tag:)
  216. end
  217. end
  218. # original_created_from の条件は「original_created_before > param」なので、
  219. # before が param 以下になるようにする(ただし before >= from は守る)
  220. let!(:oc_miss_post_for_from) do
  221. Post.create!(
  222. uploaded_user: user,
  223. title: 'oc miss for from',
  224. url: 'https://example.com/oc-miss-from',
  225. original_created_from: Time.zone.local(2014, 12, 1, 0, 0, 0),
  226. original_created_before: Time.zone.local(2015, 1, 1, 0, 0, 0)
  227. ).tap { |p| PostTag.create!(post: p, tag:) }
  228. end
  229. # original_created_to の条件は「original_created_from <= param」なので、
  230. # from が param より後になるようにする(before >= from は守る)
  231. let!(:oc_miss_post_for_to) do
  232. Post.create!(
  233. uploaded_user: user,
  234. title: 'oc miss for to',
  235. url: 'https://example.com/oc-miss-to',
  236. original_created_from: Time.zone.local(2015, 2, 1, 0, 0, 0),
  237. original_created_before: Time.zone.local(2015, 2, 10, 0, 0, 0)
  238. ).tap { |p| PostTag.create!(post: p, tag:) }
  239. end
  240. it 'filters posts by original_created_from (current controller behavior)' do
  241. get '/posts', params: {
  242. original_created_from: Time.zone.local(2015, 1, 5, 0, 0, 0).iso8601
  243. }
  244. expect(response).to have_http_status(:ok)
  245. ids = json.fetch('posts').map { |p| p['id'] }
  246. expect(ids).to include(oc_hit_post.id)
  247. expect(ids).not_to include(oc_miss_post_for_from.id)
  248. expect(json.fetch('count')).to eq(2)
  249. end
  250. it 'filters posts by original_created_to (current controller behavior)' do
  251. get '/posts', params: {
  252. original_created_to: Time.zone.local(2015, 1, 15, 0, 0, 0).iso8601
  253. }
  254. expect(response).to have_http_status(:ok)
  255. ids = json.fetch('posts').map { |p| p['id'] }
  256. expect(ids).to include(oc_hit_post.id)
  257. expect(ids).not_to include(oc_miss_post_for_to.id)
  258. expect(json.fetch('count')).to eq(2)
  259. end
  260. end
  261. end
  262. describe 'GET /posts/:id' do
  263. subject(:request) { get "/posts/#{post_id}" }
  264. context 'when post exists' do
  265. let(:post_id) { post_record.id }
  266. it 'returns post with tag tree + related + viewed' do
  267. request
  268. expect(response).to have_http_status(:ok)
  269. expect(json).to include('id' => post_record.id)
  270. expect(json).to have_key('tags')
  271. expect(json['tags']).to be_an(Array)
  272. # show は build_tag_tree_for を使うので、tags はツリー形式(children 付き)
  273. node = json['tags'][0]
  274. expect(node).to include('id', 'name', 'category', 'post_count', 'children', 'has_wiki')
  275. expect(node['name']).to eq('spec_tag')
  276. expect(json).to have_key('related')
  277. expect(json['related']).to be_an(Array)
  278. expect(json).to have_key('viewed')
  279. expect([true, false]).to include(json['viewed'])
  280. end
  281. end
  282. context 'when post does not exist' do
  283. let(:post_id) { 999_999_999 }
  284. it 'returns 404' do
  285. request
  286. expect(response).to have_http_status(:not_found)
  287. end
  288. end
  289. end
  290. describe 'POST /posts' do
  291. let(:member) { create(:user, :member) }
  292. let!(:alias_tag_name) { TagName.create!(name: 'manko', canonical: tag_name) }
  293. it '401 when not logged in' do
  294. sign_out
  295. post '/posts', params: { title: 't', url: 'https://example.com/x', tags: 'a', thumbnail: dummy_upload }
  296. expect(response).to have_http_status(:unauthorized)
  297. end
  298. it '403 when not member' do
  299. sign_in_as(create(:user, role: 'guest'))
  300. post '/posts', params: { title: 't', url: 'https://example.com/x', tags: 'a', thumbnail: dummy_upload }
  301. expect(response).to have_http_status(:forbidden)
  302. end
  303. it '201 and creates post + tags when member' do
  304. sign_in_as(member)
  305. post '/posts', params: {
  306. title: 'new post',
  307. url: 'https://example.com/new',
  308. tags: 'spec_tag', # 既存タグ名を投げる
  309. thumbnail: dummy_upload
  310. }
  311. expect(response).to have_http_status(:created)
  312. expect(json).to include('id', 'title', 'url')
  313. # tags が name を含むこと(API 側の serialization が正しいこと)
  314. expect(json).to have_key('tags')
  315. expect(json['tags']).to be_an(Array)
  316. expect(json['tags'][0]).to have_key('name')
  317. end
  318. it '201 and creates post + tags when member and tags have aliases' do
  319. sign_in_as(member)
  320. post '/posts', params: {
  321. title: 'new post',
  322. url: 'https://example.com/new',
  323. tags: 'manko', # 既存タグ名を投げる
  324. thumbnail: dummy_upload
  325. }
  326. expect(response).to have_http_status(:created)
  327. expect(json).to include('id', 'title', 'url')
  328. # tags が name を含むこと(API 側の serialization が正しいこと)
  329. names = json.fetch('tags').map { |t| t['name'] }
  330. expect(names).to include('spec_tag')
  331. expect(names).not_to include('manko')
  332. end
  333. context "when nico tag already exists in tags" do
  334. before do
  335. Tag.find_or_create_by!(tag_name: TagName.find_or_create_by!(name: 'nico:nico_tag'),
  336. category: 'nico')
  337. end
  338. it 'return 400' do
  339. sign_in_as(member)
  340. post '/posts', params: {
  341. title: 'new post',
  342. url: 'https://example.com/nico_tag',
  343. tags: 'nico:nico_tag',
  344. thumbnail: dummy_upload }
  345. expect(response).to have_http_status(:bad_request)
  346. end
  347. end
  348. context 'when url is blank' do
  349. it 'returns 422' do
  350. sign_in_as(member)
  351. post '/posts', params: {
  352. title: 'new post',
  353. url: ' ',
  354. tags: 'spec_tag', # 既存タグ名を投げる
  355. thumbnail: dummy_upload }
  356. expect(response).to have_http_status(:unprocessable_entity)
  357. end
  358. end
  359. context 'when url is invalid' do
  360. it 'returns 422' do
  361. sign_in_as(member)
  362. post '/posts', params: {
  363. title: 'new post',
  364. url: 'ぼざクリタグ広場',
  365. tags: 'spec_tag', # 既存タグ名を投げる
  366. thumbnail: dummy_upload
  367. }
  368. expect(response).to have_http_status(:unprocessable_entity)
  369. end
  370. end
  371. end
  372. describe 'PUT /posts/:id' do
  373. let(:member) { create(:user, :member) }
  374. it '401 when not logged in' do
  375. sign_out
  376. put "/posts/#{post_record.id}", params: { title: 'updated', tags: 'spec_tag' }
  377. expect(response).to have_http_status(:unauthorized)
  378. end
  379. it '403 when not member' do
  380. sign_in_as(create(:user, role: 'guest'))
  381. put "/posts/#{post_record.id}", params: { title: 'updated', tags: 'spec_tag' }
  382. expect(response).to have_http_status(:forbidden)
  383. end
  384. it '200 and updates title + resync tags when member' do
  385. sign_in_as(member)
  386. # 追加で別タグも作って、更新時に入れ替わることを見る
  387. tn2 = TagName.create!(name: 'spec_tag_2')
  388. Tag.create!(tag_name: tn2, category: 'general')
  389. put "/posts/#{post_record.id}", params: {
  390. title: 'updated title',
  391. tags: 'spec_tag_2'
  392. }
  393. expect(response).to have_http_status(:ok)
  394. expect(json).to have_key('tags')
  395. expect(json['tags']).to be_an(Array)
  396. # show と同様、update 後レスポンスもツリー形式
  397. names = json['tags'].map { |n| n['name'] }
  398. expect(names).to include('spec_tag_2')
  399. end
  400. context "when nico tag already exists in tags" do
  401. before do
  402. Tag.find_or_create_by!(tag_name: TagName.find_or_create_by!(name: 'nico:nico_tag'),
  403. category: 'nico')
  404. end
  405. it 'return 400' do
  406. sign_in_as(member)
  407. put "/posts/#{ post_record.id }", params: {
  408. title: 'updated title',
  409. tags: 'nico:nico_tag' }
  410. expect(response).to have_http_status(:bad_request)
  411. end
  412. end
  413. end
  414. describe 'GET /posts/random' do
  415. it '404 when no posts' do
  416. PostTag.delete_all
  417. Post.delete_all
  418. get '/posts/random'
  419. expect(response).to have_http_status(:not_found)
  420. end
  421. it '200 and returns viewed boolean' do
  422. get '/posts/random'
  423. expect(response).to have_http_status(:ok)
  424. expect(json).to have_key('viewed')
  425. expect([true, false]).to include(json['viewed'])
  426. end
  427. end
  428. describe 'GET /posts/changes' do
  429. let(:member) { create(:user, :member) }
  430. it 'returns add/remove events (history) for a post' do
  431. # add
  432. tn2 = TagName.create!(name: 'spec_tag2')
  433. tag2 = Tag.create!(tag_name: tn2, category: 'general')
  434. pt = PostTag.create!(post: post_record, tag: tag2, created_user: member)
  435. # remove (discard)
  436. pt.discard_by!(member)
  437. get '/posts/changes', params: { id: post_record.id }
  438. expect(response).to have_http_status(:ok)
  439. expect(json).to include('changes', 'count')
  440. expect(json['changes']).to be_an(Array)
  441. expect(json['count']).to be >= 2
  442. types = json['changes'].map { |e| e['change_type'] }.uniq
  443. expect(types).to include('add')
  444. expect(types).to include('remove')
  445. end
  446. end
  447. describe 'POST /posts/:id/viewed' do
  448. let(:user) { create(:user) }
  449. it '401 when not logged in' do
  450. sign_out
  451. post "/posts/#{ post_record.id }/viewed"
  452. expect(response).to have_http_status(:unauthorized)
  453. end
  454. it '204 and marks viewed when logged in' do
  455. sign_in_as(user)
  456. post "/posts/#{ post_record.id }/viewed"
  457. expect(response).to have_http_status(:no_content)
  458. expect(user.reload.viewed?(post_record)).to be(true)
  459. end
  460. end
  461. describe 'DELETE /posts/:id/viewed' do
  462. let(:user) { create(:user) }
  463. it '401 when not logged in' do
  464. sign_out
  465. delete "/posts/#{ post_record.id }/viewed"
  466. expect(response).to have_http_status(:unauthorized)
  467. end
  468. it '204 and unmarks viewed when logged in' do
  469. sign_in_as(user)
  470. # 先に viewed 付けてから外す
  471. user.viewed_posts << post_record
  472. delete "/posts/#{ post_record.id }/viewed"
  473. expect(response).to have_http_status(:no_content)
  474. expect(user.reload.viewed?(post_record)).to be(false)
  475. end
  476. end
  477. end