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

1181 lines
38 KiB

  1. require 'rails_helper'
  2. require 'set'
  3. include ActiveSupport::Testing::TimeHelpers
  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 tags contain not:' do
  96. let!(:foo_tag_name) { TagName.create!(name: 'not_spec_foo') }
  97. let!(:foo_tag) { Tag.create!(tag_name: foo_tag_name, category: :general) }
  98. let!(:bar_tag_name) { TagName.create!(name: 'not_spec_bar') }
  99. let!(:bar_tag) { Tag.create!(tag_name: bar_tag_name, category: :general) }
  100. let!(:baz_tag_name) { TagName.create!(name: 'not_spec_baz') }
  101. let!(:baz_tag) { Tag.create!(tag_name: baz_tag_name, category: :general) }
  102. let!(:foo_alias_tag_name) do
  103. TagName.create!(name: 'not_spec_foo_alias', canonical: foo_tag_name)
  104. end
  105. let!(:foo_only_post) do
  106. Post.create!(uploaded_user: user, title: 'foo only',
  107. url: 'https://example.com/not-spec-foo').tap do |p|
  108. PostTag.create!(post: p, tag: foo_tag)
  109. end
  110. end
  111. let!(:bar_only_post) do
  112. Post.create!(uploaded_user: user, title: 'bar only',
  113. url: 'https://example.com/not-spec-bar').tap do |p|
  114. PostTag.create!(post: p, tag: bar_tag)
  115. end
  116. end
  117. let!(:baz_only_post) do
  118. Post.create!(uploaded_user: user, title: 'baz only',
  119. url: 'https://example.com/not-spec-baz').tap do |p|
  120. PostTag.create!(post: p, tag: baz_tag)
  121. end
  122. end
  123. let!(:foo_bar_post) do
  124. Post.create!(uploaded_user: user, title: 'foo bar',
  125. url: 'https://example.com/not-spec-foo-bar').tap do |p|
  126. PostTag.create!(post: p, tag: foo_tag)
  127. PostTag.create!(post: p, tag: bar_tag)
  128. end
  129. end
  130. let!(:foo_baz_post) do
  131. Post.create!(uploaded_user: user, title: 'foo baz',
  132. url: 'https://example.com/not-spec-foo-baz').tap do |p|
  133. PostTag.create!(post: p, tag: foo_tag)
  134. PostTag.create!(post: p, tag: baz_tag)
  135. end
  136. end
  137. let(:controlled_ids) do
  138. [foo_only_post.id, bar_only_post.id, baz_only_post.id,
  139. foo_bar_post.id, foo_baz_post.id]
  140. end
  141. it 'supports not search' do
  142. get '/posts', params: { tags: 'not:not_spec_foo' }
  143. expect(response).to have_http_status(:ok)
  144. expect(json.fetch('posts').map { |p| p['id'] } & controlled_ids).to match_array(
  145. [bar_only_post.id, baz_only_post.id]
  146. )
  147. end
  148. it 'supports alias in not search' do
  149. get '/posts', params: { tags: 'not:not_spec_foo_alias' }
  150. expect(response).to have_http_status(:ok)
  151. expect(json.fetch('posts').map { |p| p['id'] } & controlled_ids).to match_array(
  152. [bar_only_post.id, baz_only_post.id]
  153. )
  154. end
  155. it 'treats multiple not terms as AND when match is omitted' do
  156. get '/posts', params: { tags: 'not:not_spec_foo not:not_spec_bar' }
  157. expect(response).to have_http_status(:ok)
  158. expect(json.fetch('posts').map { |p| p['id'] } & controlled_ids).to match_array(
  159. [baz_only_post.id]
  160. )
  161. end
  162. it 'treats multiple not terms as OR when match=any' do
  163. get '/posts', params: { tags: 'not:not_spec_foo not:not_spec_bar', match: 'any' }
  164. expect(response).to have_http_status(:ok)
  165. expect(json.fetch('posts').map { |p| p['id'] } & controlled_ids).to match_array(
  166. [foo_only_post.id, bar_only_post.id, baz_only_post.id, foo_baz_post.id]
  167. )
  168. end
  169. it 'supports mixed positive and negative search with AND' do
  170. get '/posts', params: { tags: 'not_spec_foo not:not_spec_bar' }
  171. expect(response).to have_http_status(:ok)
  172. expect(json.fetch('posts').map { |p| p['id'] } & controlled_ids).to match_array(
  173. [foo_only_post.id, foo_baz_post.id]
  174. )
  175. end
  176. it 'supports mixed positive and negative search with OR when match=any' do
  177. get '/posts', params: { tags: 'not_spec_foo not:not_spec_bar', match: 'any' }
  178. expect(response).to have_http_status(:ok)
  179. expect(json.fetch('posts').map { |p| p['id'] } & controlled_ids).to match_array(
  180. [foo_only_post.id, baz_only_post.id, foo_bar_post.id, foo_baz_post.id]
  181. )
  182. end
  183. end
  184. context 'when url is provided' do
  185. let!(:url_hit_post) do
  186. Post.create!(uploaded_user: user, title: 'url hit',
  187. url: 'https://example.com/needle-url-xyz').tap do |p|
  188. PostTag.create!(post: p, tag:)
  189. end
  190. end
  191. let!(:url_miss_post) do
  192. Post.create!(uploaded_user: user, title: 'url miss',
  193. url: 'https://example.com/other-url').tap do |p|
  194. PostTag.create!(post: p, tag:)
  195. end
  196. end
  197. it 'filters posts by url substring' do
  198. get '/posts', params: { url: 'needle-url-xyz' }
  199. expect(response).to have_http_status(:ok)
  200. ids = json.fetch('posts').map { |p| p['id'] }
  201. expect(ids).to include(url_hit_post.id)
  202. expect(ids).not_to include(url_miss_post.id)
  203. expect(json.fetch('count')).to eq(1)
  204. end
  205. end
  206. context 'when title is provided' do
  207. let!(:title_hit_post) do
  208. Post.create!(uploaded_user: user, title: 'needle-title-xyz',
  209. url: 'https://example.com/title-hit').tap do |p|
  210. PostTag.create!(post: p, tag:)
  211. end
  212. end
  213. let!(:title_miss_post) do
  214. Post.create!(uploaded_user: user, title: 'other title',
  215. url: 'https://example.com/title-miss').tap do |p|
  216. PostTag.create!(post: p, tag:)
  217. end
  218. end
  219. it 'filters posts by title substring' do
  220. get '/posts', params: { title: 'needle-title-xyz' }
  221. expect(response).to have_http_status(:ok)
  222. ids = json.fetch('posts').map { |p| p['id'] }
  223. expect(ids).to include(title_hit_post.id)
  224. expect(ids).not_to include(title_miss_post.id)
  225. expect(json.fetch('count')).to eq(1)
  226. end
  227. end
  228. context 'when created_from/created_to are provided' do
  229. let(:t_created_hit) { Time.zone.local(2010, 1, 5, 12, 0, 0) }
  230. let(:t_created_miss) { Time.zone.local(2012, 1, 5, 12, 0, 0) }
  231. let!(:created_hit_post) do
  232. travel_to(t_created_hit) do
  233. Post.create!(uploaded_user: user, title: 'created hit',
  234. url: 'https://example.com/created-hit').tap do |p|
  235. PostTag.create!(post: p, tag:)
  236. end
  237. end
  238. end
  239. let!(:created_miss_post) do
  240. travel_to(t_created_miss) do
  241. Post.create!(uploaded_user: user, title: 'created miss',
  242. url: 'https://example.com/created-miss').tap do |p|
  243. PostTag.create!(post: p, tag:)
  244. end
  245. end
  246. end
  247. it 'filters posts by created_at range' do
  248. get '/posts', params: {
  249. created_from: Time.zone.local(2010, 1, 1, 0, 0, 0).iso8601,
  250. created_to: Time.zone.local(2010, 12, 31, 23, 59, 59).iso8601
  251. }
  252. expect(response).to have_http_status(:ok)
  253. ids = json.fetch('posts').map { |p| p['id'] }
  254. expect(ids).to include(created_hit_post.id)
  255. expect(ids).not_to include(created_miss_post.id)
  256. expect(json.fetch('count')).to eq(1)
  257. end
  258. end
  259. context 'when updated_from/updated_to are provided' do
  260. let(:t0) { Time.zone.local(2011, 2, 1, 12, 0, 0) }
  261. let(:t1) { Time.zone.local(2011, 2, 10, 12, 0, 0) }
  262. let!(:updated_hit_post) do
  263. p = nil
  264. travel_to(t0) do
  265. p = Post.create!(uploaded_user: user, title: 'updated hit',
  266. url: 'https://example.com/updated-hit').tap do |pp|
  267. PostTag.create!(post: pp, tag:)
  268. end
  269. end
  270. travel_to(t1) do
  271. p.update!(title: 'updated hit v2')
  272. end
  273. p
  274. end
  275. let!(:updated_miss_post) do
  276. travel_to(Time.zone.local(2013, 1, 1, 12, 0, 0)) do
  277. Post.create!(uploaded_user: user, title: 'updated miss',
  278. url: 'https://example.com/updated-miss').tap do |p|
  279. PostTag.create!(post: p, tag:)
  280. end
  281. end
  282. end
  283. it 'filters posts by updated_at range' do
  284. get '/posts', params: {
  285. updated_from: Time.zone.local(2011, 2, 5, 0, 0, 0).iso8601,
  286. updated_to: Time.zone.local(2011, 2, 20, 23, 59, 59).iso8601
  287. }
  288. expect(response).to have_http_status(:ok)
  289. ids = json.fetch('posts').map { |p| p['id'] }
  290. expect(ids).to include(updated_hit_post.id)
  291. expect(ids).not_to include(updated_miss_post.id)
  292. expect(json.fetch('count')).to eq(1)
  293. end
  294. end
  295. context 'when original_created_from/original_created_to are provided' do
  296. # 注意: controller の現状ロジックに合わせてる
  297. # original_created_from は `original_created_before > ?`
  298. # original_created_to は `original_created_from <= ?`
  299. let!(:oc_hit_post) do
  300. Post.create!(uploaded_user: user, title: 'oc hit',
  301. url: 'https://example.com/oc-hit',
  302. original_created_from: Time.zone.local(2015, 1, 1, 0, 0, 0),
  303. original_created_before: Time.zone.local(2015, 1, 10, 0, 0, 0)).tap do |p|
  304. PostTag.create!(post: p, tag:)
  305. end
  306. end
  307. # original_created_from の条件は「original_created_before > param」なので、
  308. # before が param 以下になるようにする(ただし before >= from は守る)
  309. let!(:oc_miss_post_for_from) do
  310. Post.create!(
  311. uploaded_user: user,
  312. title: 'oc miss for from',
  313. url: 'https://example.com/oc-miss-from',
  314. original_created_from: Time.zone.local(2014, 12, 1, 0, 0, 0),
  315. original_created_before: Time.zone.local(2015, 1, 1, 0, 0, 0)
  316. ).tap { |p| PostTag.create!(post: p, tag:) }
  317. end
  318. # original_created_to の条件は「original_created_from <= param」なので、
  319. # from が param より後になるようにする(before >= from は守る)
  320. let!(:oc_miss_post_for_to) do
  321. Post.create!(
  322. uploaded_user: user,
  323. title: 'oc miss for to',
  324. url: 'https://example.com/oc-miss-to',
  325. original_created_from: Time.zone.local(2015, 2, 1, 0, 0, 0),
  326. original_created_before: Time.zone.local(2015, 2, 10, 0, 0, 0)
  327. ).tap { |p| PostTag.create!(post: p, tag:) }
  328. end
  329. it 'filters posts by original_created_from (current controller behavior)' do
  330. get '/posts', params: {
  331. original_created_from: Time.zone.local(2015, 1, 5, 0, 0, 0).iso8601
  332. }
  333. expect(response).to have_http_status(:ok)
  334. ids = json.fetch('posts').map { |p| p['id'] }
  335. expect(ids).to include(oc_hit_post.id)
  336. expect(ids).not_to include(oc_miss_post_for_from.id)
  337. expect(json.fetch('count')).to eq(2)
  338. end
  339. it 'filters posts by original_created_to (current controller behavior)' do
  340. get '/posts', params: {
  341. original_created_to: Time.zone.local(2015, 1, 15, 0, 0, 0).iso8601
  342. }
  343. expect(response).to have_http_status(:ok)
  344. ids = json.fetch('posts').map { |p| p['id'] }
  345. expect(ids).to include(oc_hit_post.id)
  346. expect(ids).not_to include(oc_miss_post_for_to.id)
  347. expect(json.fetch('count')).to eq(2)
  348. end
  349. end
  350. end
  351. describe 'GET /posts/:id' do
  352. subject(:request) { get "/posts/#{post_id}" }
  353. context 'when post exists' do
  354. let(:post_id) { post_record.id }
  355. it 'returns post with tag tree + related + viewed' do
  356. request
  357. expect(response).to have_http_status(:ok)
  358. expect(json).to include('id' => post_record.id)
  359. expect(json).to have_key('tags')
  360. expect(json['tags']).to be_an(Array)
  361. # show は build_tag_tree_for を使うので、tags はツリー形式(children 付き)
  362. node = json['tags'][0]
  363. expect(node).to include('id', 'name', 'category', 'post_count', 'children', 'has_wiki')
  364. expect(node['name']).to eq('spec_tag')
  365. expect(json).to have_key('related')
  366. expect(json['related']).to be_an(Array)
  367. expect(json).to have_key('viewed')
  368. expect([true, false]).to include(json['viewed'])
  369. end
  370. end
  371. context 'when post does not exist' do
  372. let(:post_id) { 999_999_999 }
  373. it 'returns 404' do
  374. request
  375. expect(response).to have_http_status(:not_found)
  376. end
  377. end
  378. end
  379. describe 'POST /posts' do
  380. let(:member) { create(:user, :member) }
  381. let!(:alias_tag_name) { TagName.create!(name: 'manko', canonical: tag_name) }
  382. it '401 when not logged in' do
  383. sign_out
  384. post '/posts', params: { title: 't', url: 'https://example.com/x', tags: 'a', thumbnail: dummy_upload }
  385. expect(response).to have_http_status(:unauthorized)
  386. end
  387. it '403 when not member' do
  388. sign_in_as(create(:user, role: 'guest'))
  389. post '/posts', params: { title: 't', url: 'https://example.com/x', tags: 'a', thumbnail: dummy_upload }
  390. expect(response).to have_http_status(:forbidden)
  391. end
  392. it '201 and creates post + tags when member' do
  393. sign_in_as(member)
  394. post '/posts', params: {
  395. title: 'new post',
  396. url: 'https://example.com/new',
  397. tags: 'spec_tag', # 既存タグ名を投げる
  398. thumbnail: dummy_upload
  399. }
  400. expect(response).to have_http_status(:created)
  401. expect(json).to include('id', 'title', 'url')
  402. # tags が name を含むこと(API 側の serialization が正しいこと)
  403. expect(json).to have_key('tags')
  404. expect(json['tags']).to be_an(Array)
  405. expect(json['tags'][0]).to have_key('name')
  406. end
  407. it '201 and creates post + tags when member and tags have aliases' do
  408. sign_in_as(member)
  409. post '/posts', params: {
  410. title: 'new post',
  411. url: 'https://example.com/new',
  412. tags: 'manko', # 既存タグ名を投げる
  413. thumbnail: dummy_upload
  414. }
  415. expect(response).to have_http_status(:created)
  416. expect(json).to include('id', 'title', 'url')
  417. # tags が name を含むこと(API 側の serialization が正しいこと)
  418. names = json.fetch('tags').map { |t| t['name'] }
  419. expect(names).to include('spec_tag')
  420. expect(names).not_to include('manko')
  421. end
  422. context "when nico tag already exists in tags" do
  423. before do
  424. Tag.find_undiscard_or_create_by!(
  425. tag_name: TagName.find_undiscard_or_create_by!(name: 'nico:nico_tag'),
  426. category: :nico)
  427. end
  428. it 'return 400' do
  429. sign_in_as(member)
  430. post '/posts', params: {
  431. title: 'new post',
  432. url: 'https://example.com/nico_tag',
  433. tags: 'nico:nico_tag',
  434. thumbnail: dummy_upload }
  435. expect(response).to have_http_status(:bad_request)
  436. end
  437. end
  438. context 'when url is blank' do
  439. it 'returns 422' do
  440. sign_in_as(member)
  441. post '/posts', params: {
  442. title: 'new post',
  443. url: ' ',
  444. tags: 'spec_tag', # 既存タグ名を投げる
  445. thumbnail: dummy_upload }
  446. expect(response).to have_http_status(:unprocessable_entity)
  447. end
  448. end
  449. context 'when url is invalid' do
  450. it 'returns 422' do
  451. sign_in_as(member)
  452. post '/posts', params: {
  453. title: 'new post',
  454. url: 'ぼざクリタグ広場',
  455. tags: 'spec_tag', # 既存タグ名を投げる
  456. thumbnail: dummy_upload
  457. }
  458. expect(response).to have_http_status(:unprocessable_entity)
  459. end
  460. end
  461. end
  462. describe 'PUT /posts/:id' do
  463. let(:member) { create(:user, :member) }
  464. it '401 when not logged in' do
  465. sign_out
  466. put "/posts/#{post_record.id}", params: { title: 'updated', tags: 'spec_tag' }
  467. expect(response).to have_http_status(:unauthorized)
  468. end
  469. it '403 when not member' do
  470. sign_in_as(create(:user, role: 'guest'))
  471. put "/posts/#{post_record.id}", params: { title: 'updated', tags: 'spec_tag' }
  472. expect(response).to have_http_status(:forbidden)
  473. end
  474. it '200 and updates title + resync tags when member' do
  475. sign_in_as(member)
  476. # 追加で別タグも作って、更新時に入れ替わることを見る
  477. tn2 = TagName.create!(name: 'spec_tag_2')
  478. Tag.create!(tag_name: tn2, category: :general)
  479. put "/posts/#{post_record.id}", params: {
  480. title: 'updated title',
  481. tags: 'spec_tag_2'
  482. }
  483. expect(response).to have_http_status(:ok)
  484. expect(json).to have_key('tags')
  485. expect(json['tags']).to be_an(Array)
  486. # show と同様、update 後レスポンスもツリー形式
  487. names = json['tags'].map { |n| n['name'] }
  488. expect(names).to include('spec_tag_2')
  489. end
  490. context "when nico tag already exists in tags" do
  491. before do
  492. Tag.find_undiscard_or_create_by!(
  493. tag_name: TagName.find_undiscard_or_create_by!(name: 'nico:nico_tag'),
  494. category: :nico)
  495. end
  496. it 'return 400' do
  497. sign_in_as(member)
  498. put "/posts/#{ post_record.id }", params: {
  499. title: 'updated title',
  500. tags: 'nico:nico_tag' }
  501. expect(response).to have_http_status(:bad_request)
  502. end
  503. end
  504. end
  505. describe 'GET /posts/random' do
  506. it '404 when no posts' do
  507. PostTag.delete_all
  508. Post.delete_all
  509. get '/posts/random'
  510. expect(response).to have_http_status(:not_found)
  511. end
  512. it '200 and returns viewed boolean' do
  513. get '/posts/random'
  514. expect(response).to have_http_status(:ok)
  515. expect(json).to have_key('viewed')
  516. expect([true, false]).to include(json['viewed'])
  517. end
  518. end
  519. describe 'GET /posts/changes' do
  520. let(:member) { create(:user, :member) }
  521. it 'returns add/remove events (history) for a post' do
  522. # add
  523. tn2 = TagName.create!(name: 'spec_tag2')
  524. tag2 = Tag.create!(tag_name: tn2, category: :general)
  525. pt = PostTag.create!(post: post_record, tag: tag2, created_user: member)
  526. # remove (discard)
  527. pt.discard_by!(member)
  528. get '/posts/changes', params: { id: post_record.id }
  529. expect(response).to have_http_status(:ok)
  530. expect(json).to include('changes', 'count')
  531. expect(json['changes']).to be_an(Array)
  532. expect(json['count']).to be >= 2
  533. types = json['changes'].map { |e| e['change_type'] }.uniq
  534. expect(types).to include('add')
  535. expect(types).to include('remove')
  536. end
  537. it 'filters history by tag' do
  538. tn2 = TagName.create!(name: 'history_tag_hit')
  539. tag2 = Tag.create!(tag_name: tn2, category: :general)
  540. tn3 = TagName.create!(name: 'history_tag_miss')
  541. tag3 = Tag.create!(tag_name: tn3, category: :general)
  542. other_post = Post.create!(
  543. title: 'other post',
  544. url: 'https://example.com/history-other'
  545. )
  546. # hit: add
  547. PostTag.create!(post: post_record, tag: tag2, created_user: member)
  548. # hit: add + remove
  549. pt2 = PostTag.create!(post: other_post, tag: tag2, created_user: member)
  550. pt2.discard_by!(member)
  551. # miss: add + remove
  552. pt3 = PostTag.create!(post: post_record, tag: tag3, created_user: member)
  553. pt3.discard_by!(member)
  554. get '/posts/changes', params: { tag: tag2.id }
  555. expect(response).to have_http_status(:ok)
  556. expect(json).to include('changes', 'count')
  557. expect(json['count']).to eq(3)
  558. changes = json.fetch('changes')
  559. expect(changes.map { |e| e.dig('tag', 'id') }.uniq).to eq([tag2.id])
  560. expect(changes.map { |e| e['change_type'] }).to match_array(%w[add add remove])
  561. expect(changes.map { |e| e.dig('post', 'id') }).to match_array([
  562. post_record.id,
  563. other_post.id,
  564. other_post.id
  565. ])
  566. end
  567. it 'filters history by post and tag together' do
  568. tn2 = TagName.create!(name: 'history_tag_combo_hit')
  569. tag2 = Tag.create!(tag_name: tn2, category: :general)
  570. tn3 = TagName.create!(name: 'history_tag_combo_miss')
  571. tag3 = Tag.create!(tag_name: tn3, category: :general)
  572. other_post = Post.create!(
  573. title: 'other combo post',
  574. url: 'https://example.com/history-combo-other'
  575. )
  576. # hit
  577. PostTag.create!(post: post_record, tag: tag2, created_user: member)
  578. # miss by post
  579. pt2 = PostTag.create!(post: other_post, tag: tag2, created_user: member)
  580. pt2.discard_by!(member)
  581. # miss by tag
  582. pt3 = PostTag.create!(post: post_record, tag: tag3, created_user: member)
  583. pt3.discard_by!(member)
  584. get '/posts/changes', params: { id: post_record.id, tag: tag2.id }
  585. expect(response).to have_http_status(:ok)
  586. expect(json).to include('changes', 'count')
  587. expect(json['count']).to eq(1)
  588. changes = json.fetch('changes')
  589. expect(changes.size).to eq(1)
  590. expect(changes[0]['change_type']).to eq('add')
  591. expect(changes[0].dig('post', 'id')).to eq(post_record.id)
  592. expect(changes[0].dig('tag', 'id')).to eq(tag2.id)
  593. end
  594. it 'returns empty history when tag does not match' do
  595. tn2 = TagName.create!(name: 'history_tag_no_hit')
  596. tag2 = Tag.create!(tag_name: tn2, category: :general)
  597. get '/posts/changes', params: { tag: tag2.id }
  598. expect(response).to have_http_status(:ok)
  599. expect(json.fetch('changes')).to eq([])
  600. expect(json.fetch('count')).to eq(0)
  601. end
  602. end
  603. describe 'GET /posts/versions' do
  604. let(:member) { create(:user, :member, name: 'version member') }
  605. let(:t_v1) { Time.zone.local(2020, 1, 1, 12, 0, 0) }
  606. let(:t_v2) { Time.zone.local(2020, 1, 2, 12, 0, 0) }
  607. let(:t_other) { Time.zone.local(2020, 1, 3, 12, 0, 0) }
  608. let(:oc_from) { Time.zone.local(2019, 12, 31, 0, 0, 0) }
  609. let(:oc_before) { Time.zone.local(2020, 1, 1, 0, 0, 0) }
  610. let!(:tag_name2) { TagName.create!(name: 'spec_tag_2') }
  611. let!(:tag2) { Tag.create!(tag_name: tag_name2, category: :general) }
  612. def snapshot_tags(post)
  613. post.snapshot_tag_names.join(' ')
  614. end
  615. def create_post_version!(post, version_no:, event_type:, created_by_user:, created_at:)
  616. PostVersion.create!(
  617. post: post,
  618. version_no: version_no,
  619. event_type: event_type,
  620. title: post.title,
  621. url: post.url,
  622. thumbnail_base: post.thumbnail_base,
  623. tags: snapshot_tags(post),
  624. original_created_from: post.original_created_from,
  625. original_created_before: post.original_created_before,
  626. created_at: created_at,
  627. created_by_user: created_by_user
  628. )
  629. end
  630. let!(:v1) do
  631. travel_to(t_v1) do
  632. create_post_version!(
  633. post_record,
  634. version_no: 1,
  635. event_type: 'create',
  636. created_by_user: member,
  637. created_at: t_v1
  638. )
  639. end
  640. end
  641. let!(:v2) do
  642. post_record.post_tags.kept.find_by!(tag: tag).discard_by!(member)
  643. PostTag.create!(post: post_record, tag: tag2, created_user: member)
  644. post_record.update!(
  645. title: 'updated spec post',
  646. original_created_from: oc_from,
  647. original_created_before: oc_before
  648. )
  649. travel_to(t_v2) do
  650. create_post_version!(
  651. post_record.reload,
  652. version_no: 2,
  653. event_type: 'update',
  654. created_by_user: member,
  655. created_at: t_v2
  656. )
  657. end
  658. end
  659. let!(:other_post_version) do
  660. other_post = Post.create!(
  661. title: 'other versioned post',
  662. url: 'https://example.com/other-versioned'
  663. )
  664. PostTag.create!(post: other_post, tag: tag)
  665. travel_to(t_other) do
  666. create_post_version!(
  667. other_post,
  668. version_no: 1,
  669. event_type: 'create',
  670. created_by_user: member,
  671. created_at: t_other
  672. )
  673. end
  674. end
  675. it 'returns versions for the specified post in reverse chronological order' do
  676. get '/posts/versions', params: { post: post_record.id }
  677. expect(response).to have_http_status(:ok)
  678. expect(json).to include('versions', 'count')
  679. expect(json.fetch('count')).to eq(2)
  680. versions = json.fetch('versions')
  681. expect(versions.map { |v| v['post_id'] }.uniq).to eq([post_record.id])
  682. expect(versions.map { |v| v['version_no'] }).to eq([2, 1])
  683. latest = versions.first
  684. expect(latest).to include(
  685. 'post_id' => post_record.id,
  686. 'version_no' => 2,
  687. 'event_type' => 'update',
  688. 'created_by_user' => {
  689. 'id' => member.id,
  690. 'name' => member.name
  691. }
  692. )
  693. expect(latest.fetch('title')).to eq(
  694. 'current' => 'updated spec post',
  695. 'prev' => 'spec post'
  696. )
  697. expect(latest.fetch('url')).to eq(
  698. 'current' => 'https://example.com/spec',
  699. 'prev' => 'https://example.com/spec'
  700. )
  701. expect(latest.fetch('thumbnail')).to eq(
  702. 'current' => nil,
  703. 'prev' => nil
  704. )
  705. expect(latest.fetch('thumbnail_base')).to eq(
  706. 'current' => nil,
  707. 'prev' => nil
  708. )
  709. expect(latest.fetch('tags')).to include(
  710. { 'name' => 'spec_tag_2', 'type' => 'added' },
  711. { 'name' => 'spec_tag', 'type' => 'removed' }
  712. )
  713. expect(latest.fetch('original_created_from')).to eq(
  714. 'current' => oc_from.iso8601,
  715. 'prev' => nil
  716. )
  717. expect(latest.fetch('original_created_before')).to eq(
  718. 'current' => oc_before.iso8601,
  719. 'prev' => nil
  720. )
  721. expect(latest.fetch('created_at')).to eq(t_v2.iso8601)
  722. first = versions.second
  723. expect(first).to include(
  724. 'post_id' => post_record.id,
  725. 'version_no' => 1,
  726. 'event_type' => 'create',
  727. 'created_by_user' => {
  728. 'id' => member.id,
  729. 'name' => member.name
  730. }
  731. )
  732. expect(first.fetch('title')).to eq(
  733. 'current' => 'spec post',
  734. 'prev' => nil
  735. )
  736. expect(first.fetch('tags')).to include(
  737. { 'name' => 'spec_tag', 'type' => 'added' }
  738. )
  739. expect(first.fetch('created_at')).to eq(t_v1.iso8601)
  740. end
  741. it 'filters versions by tag when the current snapshot includes the tag' do
  742. get '/posts/versions', params: { post: post_record.id, tag: tag2.id }
  743. expect(response).to have_http_status(:ok)
  744. expect(json.fetch('count')).to eq(1)
  745. versions = json.fetch('versions')
  746. expect(versions.size).to eq(1)
  747. expect(versions[0]['post_id']).to eq(post_record.id)
  748. expect(versions[0]['version_no']).to eq(2)
  749. expect(versions[0]['tags']).to include(
  750. { 'name' => 'spec_tag_2', 'type' => 'added' }
  751. )
  752. end
  753. it 'filters versions by tag when the tag exists in either current or previous snapshot' do
  754. get '/posts/versions', params: { post: post_record.id, tag: tag.id }
  755. expect(response).to have_http_status(:ok)
  756. expect(json.fetch('count')).to eq(2)
  757. versions = json.fetch('versions')
  758. expect(versions.map { |v| v['post_id'] }).to all(eq(post_record.id))
  759. expect(versions.map { |v| v['version_no'] }).to eq([2, 1])
  760. latest = versions[0]
  761. first = versions[1]
  762. expect(latest['tags']).to include(
  763. { 'name' => 'spec_tag', 'type' => 'removed' }
  764. )
  765. expect(first['tags']).to include(
  766. { 'name' => 'spec_tag', 'type' => 'added' }
  767. )
  768. end
  769. it 'returns empty when tag does not exist' do
  770. get '/posts/versions', params: { tag: 999_999_999 }
  771. expect(response).to have_http_status(:ok)
  772. expect(json.fetch('versions')).to eq([])
  773. expect(json.fetch('count')).to eq(0)
  774. end
  775. it 'clamps page and limit to at least 1' do
  776. get '/posts/versions', params: { post: post_record.id, page: 0, limit: 0 }
  777. expect(response).to have_http_status(:ok)
  778. expect(json.fetch('count')).to eq(2)
  779. versions = json.fetch('versions')
  780. expect(versions.size).to eq(1)
  781. expect(versions[0]['version_no']).to eq(2)
  782. end
  783. end
  784. describe 'POST /posts/:id/viewed' do
  785. let(:user) { create(:user) }
  786. it '401 when not logged in' do
  787. sign_out
  788. post "/posts/#{ post_record.id }/viewed"
  789. expect(response).to have_http_status(:unauthorized)
  790. end
  791. it '204 and marks viewed when logged in' do
  792. sign_in_as(user)
  793. post "/posts/#{ post_record.id }/viewed"
  794. expect(response).to have_http_status(:no_content)
  795. expect(user.reload.viewed?(post_record)).to be(true)
  796. end
  797. end
  798. describe 'DELETE /posts/:id/viewed' do
  799. let(:user) { create(:user) }
  800. it '401 when not logged in' do
  801. sign_out
  802. delete "/posts/#{ post_record.id }/viewed"
  803. expect(response).to have_http_status(:unauthorized)
  804. end
  805. it '204 and unmarks viewed when logged in' do
  806. sign_in_as(user)
  807. # 先に viewed 付けてから外す
  808. user.viewed_posts << post_record
  809. delete "/posts/#{ post_record.id }/viewed"
  810. expect(response).to have_http_status(:no_content)
  811. expect(user.reload.viewed?(post_record)).to be(false)
  812. end
  813. end
  814. describe 'post versioning' do
  815. let(:member) { create(:user, :member) }
  816. def snapshot_tags(post)
  817. post.snapshot_tag_names.join(' ')
  818. end
  819. def create_post_version_for!(post)
  820. PostVersion.create!(
  821. post: post,
  822. version_no: 1,
  823. event_type: 'create',
  824. title: post.title,
  825. url: post.url,
  826. thumbnail_base: post.thumbnail_base,
  827. tags: snapshot_tags(post),
  828. original_created_from: post.original_created_from,
  829. original_created_before: post.original_created_before,
  830. created_at: post.created_at,
  831. created_by_user: post.uploaded_user
  832. )
  833. end
  834. it 'creates version 1 on POST /posts' do
  835. sign_in_as(member)
  836. expect do
  837. post '/posts', params: {
  838. title: 'versioned post',
  839. url: 'https://example.com/versioned-post',
  840. tags: 'spec_tag',
  841. thumbnail: dummy_upload
  842. }
  843. end.to change(PostVersion, :count).by(1)
  844. expect(response).to have_http_status(:created)
  845. created_post = Post.find(json.fetch('id'))
  846. version = PostVersion.find_by!(post: created_post, version_no: 1)
  847. expect(version.event_type).to eq('create')
  848. expect(version.title).to eq('versioned post')
  849. expect(version.url).to eq('https://example.com/versioned-post')
  850. expect(version.created_by_user_id).to eq(member.id)
  851. expect(version.tags).to eq(snapshot_tags(created_post))
  852. end
  853. it 'creates next version on PUT /posts/:id when snapshot changes' do
  854. sign_in_as(member)
  855. create_post_version_for!(post_record)
  856. tag_name2 = TagName.create!(name: 'spec_tag_2')
  857. Tag.create!(tag_name: tag_name2, category: :general)
  858. expect do
  859. put "/posts/#{post_record.id}", params: {
  860. title: 'updated title',
  861. tags: 'spec_tag_2'
  862. }
  863. end.to change(PostVersion, :count).by(1)
  864. expect(response).to have_http_status(:ok)
  865. version = post_record.reload.post_versions.order(:version_no).last
  866. expect(version.version_no).to eq(2)
  867. expect(version.event_type).to eq('update')
  868. expect(version.title).to eq('updated title')
  869. expect(version.created_by_user_id).to eq(member.id)
  870. expect(version.tags).to eq(snapshot_tags(post_record.reload))
  871. end
  872. it 'does not create a new version on PUT /posts/:id when snapshot is unchanged' do
  873. sign_in_as(member)
  874. PostTag.create!(post: post_record, tag: Tag.no_deerjikist)
  875. create_post_version_for!(post_record.reload)
  876. expect {
  877. put "/posts/#{post_record.id}", params: {
  878. title: post_record.title,
  879. tags: 'spec_tag'
  880. }
  881. }.not_to change(PostVersion, :count)
  882. expect(response).to have_http_status(:ok)
  883. version = post_record.reload.post_versions.order(:version_no).last
  884. expect(version.version_no).to eq(1)
  885. expect(version.event_type).to eq('create')
  886. expect(version.tags).to eq(snapshot_tags(post_record))
  887. end
  888. it 'does not create a version when POST /posts is invalid' do
  889. sign_in_as(member)
  890. expect do
  891. post '/posts', params: {
  892. title: 'invalid post',
  893. url: 'ぼざクリタグ広場',
  894. tags: 'spec_tag',
  895. thumbnail: dummy_upload
  896. }
  897. end.not_to change(PostVersion, :count)
  898. expect(response).to have_http_status(:unprocessable_entity)
  899. end
  900. it 'does not create a version when PUT /posts/:id is invalid' do
  901. sign_in_as(member)
  902. create_post_version_for!(post_record)
  903. expect do
  904. put "/posts/#{post_record.id}", params: {
  905. title: 'updated title',
  906. tags: 'spec_tag',
  907. original_created_from: Time.zone.local(2020, 1, 2, 0, 0, 0).iso8601,
  908. original_created_before: Time.zone.local(2020, 1, 1, 0, 0, 0).iso8601
  909. }
  910. end.not_to change(PostVersion, :count)
  911. expect(response).to have_http_status(:unprocessable_entity)
  912. end
  913. end
  914. describe 'tag versioning from post write actions' do
  915. let(:member) { create(:user, :member) }
  916. it 'creates tag snapshot for normalised tags on POST /posts' do
  917. sign_in_as(member)
  918. expect {
  919. post '/posts', params: {
  920. title: 'tag versioned post',
  921. url: 'https://example.com/tag-versioned-post',
  922. tags: 'spec_tag',
  923. thumbnail: dummy_upload
  924. }
  925. }.to change { tag.reload.tag_versions.count }.by(1)
  926. expect(response).to have_http_status(:created)
  927. version = tag.reload.tag_versions.order(:version_no).last
  928. expect(version.version_no).to eq(1)
  929. expect(version.event_type).to eq('create')
  930. expect(version.name).to eq('spec_tag')
  931. expect(version.category).to eq('general')
  932. expect(version.created_by_user_id).to eq(member.id)
  933. end
  934. it 'creates tag snapshot for normalised tags on PUT /posts/:id' do
  935. sign_in_as(member)
  936. tag_name2 = TagName.create!(name: 'spec_tag_2')
  937. tag2 = Tag.create!(tag_name: tag_name2, category: :general)
  938. expect {
  939. put "/posts/#{post_record.id}", params: {
  940. title: 'updated title',
  941. tags: 'spec_tag_2'
  942. }
  943. }.to change { tag2.reload.tag_versions.count }.by(1)
  944. expect(response).to have_http_status(:ok)
  945. version = tag2.reload.tag_versions.order(:version_no).last
  946. expect(version.version_no).to eq(1)
  947. expect(version.event_type).to eq('create')
  948. expect(version.name).to eq('spec_tag_2')
  949. expect(version.created_by_user_id).to eq(member.id)
  950. end
  951. end
  952. end