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

1774 lines
57 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 create_nico_tag!(name)
  11. Tag.find_or_create_by_tag_name!(name, category: :nico)
  12. end
  13. def dummy_upload
  14. # 中身は何でもいい(加工処理はスタブしてる)
  15. Rack::Test::UploadedFile.new(StringIO.new('dummy'), 'image/jpeg', original_filename: 'dummy.jpg')
  16. end
  17. def post_write_params params = { }
  18. { parent_post_ids: '' }.merge(params)
  19. end
  20. def create_parent_post! title:, url:
  21. Post.create!(title:, url:)
  22. end
  23. def create_post_version_for!(post)
  24. version =
  25. PostVersion.create!(
  26. post:,
  27. version_no: 1,
  28. event_type: 'create',
  29. title: post.title,
  30. url: post.url,
  31. thumbnail_base: post.thumbnail_base,
  32. tags: post.snapshot_tag_names.join(' '),
  33. parent_post_ids: post.snapshot_parent_post_ids.join(' '),
  34. original_created_from: post.original_created_from,
  35. original_created_before: post.original_created_before,
  36. created_at: post.created_at,
  37. created_by_user: post.uploaded_user)
  38. post.update_columns(version_no: version.version_no) if post.has_attribute?(:version_no)
  39. post.version_no = version.version_no if post.respond_to?(:version_no=)
  40. version
  41. end
  42. def post_update_params(post, params = { })
  43. base_version =
  44. post.post_versions.order(version_no: :desc).first ||
  45. create_post_version_for!(post.reload)
  46. post_write_params({ base_version_no: base_version.version_no }.merge(params))
  47. end
  48. let!(:tag_name) { TagName.create!(name: 'spec_tag') }
  49. let!(:tag) { Tag.create!(tag_name: tag_name, category: :general) }
  50. let!(:post_record) do
  51. Post.create!(title: 'spec post', url: 'https://example.com/spec').tap do |p|
  52. PostTag.create!(post: p, tag: tag)
  53. end
  54. end
  55. describe "GET /posts" do
  56. let!(:user) { create_member_user! }
  57. let!(:tag_name) { TagName.create!(name: "spec_tag") }
  58. let!(:tag) { Tag.create!(tag_name:, category: :general) }
  59. let!(:tag_name2) { TagName.create!(name: 'unko') }
  60. let!(:tag2) { Tag.create!(tag_name: tag_name2, category: :deerjikist) }
  61. let!(:alias_tag_name) { TagName.create!(name: 'manko', canonical: tag_name) }
  62. let!(:hit_post) do
  63. Post.create!(uploaded_user: user, title: "hello spec world",
  64. url: 'https://example.com/spec2').tap do |p|
  65. PostTag.create!(post: p, tag:)
  66. end
  67. end
  68. let!(:miss_post) do
  69. Post.create!(uploaded_user: user, title: "unrelated title",
  70. url: 'https://example.com/spec3').tap do |p|
  71. PostTag.create!(post: p, tag: tag2)
  72. end
  73. end
  74. it "returns posts with tag name in JSON" do
  75. get "/posts"
  76. expect(response).to have_http_status(:ok)
  77. posts = json.fetch("posts")
  78. # 全postの全tagが name を含むこと
  79. expect(posts).not_to be_empty
  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. expect(json['count']).to be_an(Integer)
  87. # spec_tag を含む投稿が存在すること
  88. all_tag_names = posts.flat_map { |p| p["tags"].map { |t| t["name"] } }
  89. expect(all_tag_names).to include("spec_tag")
  90. end
  91. context "when q is provided" do
  92. it "filters posts by q (hit case)" do
  93. get "/posts", params: { tags: "spec_tag" }
  94. expect(response).to have_http_status(:ok)
  95. posts = json.fetch('posts')
  96. ids = posts.map { |p| p['id'] }
  97. expect(ids).to include(hit_post.id)
  98. expect(ids).not_to include(miss_post.id)
  99. expect(json['count']).to be_an(Integer)
  100. posts.each do |p|
  101. expect(p['tags']).to be_an(Array)
  102. p['tags'].each do |t|
  103. expect(t).to include('name', 'category', 'has_wiki')
  104. end
  105. end
  106. end
  107. it "filters posts by q (hit case by alias)" do
  108. get "/posts", params: { tags: "manko" }
  109. expect(response).to have_http_status(:ok)
  110. posts = json.fetch('posts')
  111. ids = posts.map { |p| p['id'] }
  112. expect(ids).to include(hit_post.id)
  113. expect(ids).not_to include(miss_post.id)
  114. expect(json['count']).to be_an(Integer)
  115. posts.each do |p|
  116. expect(p['tags']).to be_an(Array)
  117. p['tags'].each do |t|
  118. expect(t).to include('name', 'category', 'has_wiki')
  119. end
  120. end
  121. end
  122. it "returns empty posts when nothing matches" do
  123. get "/posts", params: { tags: "no_such_keyword_12345" }
  124. expect(response).to have_http_status(:ok)
  125. expect(json.fetch("posts")).to eq([])
  126. expect(json.fetch('count')).to eq(0)
  127. end
  128. end
  129. context 'when tags contain not:' do
  130. let!(:foo_tag_name) { TagName.create!(name: 'not_spec_foo') }
  131. let!(:foo_tag) { Tag.create!(tag_name: foo_tag_name, category: :general) }
  132. let!(:bar_tag_name) { TagName.create!(name: 'not_spec_bar') }
  133. let!(:bar_tag) { Tag.create!(tag_name: bar_tag_name, category: :general) }
  134. let!(:baz_tag_name) { TagName.create!(name: 'not_spec_baz') }
  135. let!(:baz_tag) { Tag.create!(tag_name: baz_tag_name, category: :general) }
  136. let!(:foo_alias_tag_name) do
  137. TagName.create!(name: 'not_spec_foo_alias', canonical: foo_tag_name)
  138. end
  139. let!(:foo_only_post) do
  140. Post.create!(uploaded_user: user, title: 'foo only',
  141. url: 'https://example.com/not-spec-foo').tap do |p|
  142. PostTag.create!(post: p, tag: foo_tag)
  143. end
  144. end
  145. let!(:bar_only_post) do
  146. Post.create!(uploaded_user: user, title: 'bar only',
  147. url: 'https://example.com/not-spec-bar').tap do |p|
  148. PostTag.create!(post: p, tag: bar_tag)
  149. end
  150. end
  151. let!(:baz_only_post) do
  152. Post.create!(uploaded_user: user, title: 'baz only',
  153. url: 'https://example.com/not-spec-baz').tap do |p|
  154. PostTag.create!(post: p, tag: baz_tag)
  155. end
  156. end
  157. let!(:foo_bar_post) do
  158. Post.create!(uploaded_user: user, title: 'foo bar',
  159. url: 'https://example.com/not-spec-foo-bar').tap do |p|
  160. PostTag.create!(post: p, tag: foo_tag)
  161. PostTag.create!(post: p, tag: bar_tag)
  162. end
  163. end
  164. let!(:foo_baz_post) do
  165. Post.create!(uploaded_user: user, title: 'foo baz',
  166. url: 'https://example.com/not-spec-foo-baz').tap do |p|
  167. PostTag.create!(post: p, tag: foo_tag)
  168. PostTag.create!(post: p, tag: baz_tag)
  169. end
  170. end
  171. let(:controlled_ids) do
  172. [foo_only_post.id, bar_only_post.id, baz_only_post.id,
  173. foo_bar_post.id, foo_baz_post.id]
  174. end
  175. it 'supports not search' do
  176. get '/posts', params: { tags: 'not:not_spec_foo' }
  177. expect(response).to have_http_status(:ok)
  178. expect(json.fetch('posts').map { |p| p['id'] } & controlled_ids).to match_array(
  179. [bar_only_post.id, baz_only_post.id]
  180. )
  181. end
  182. it 'supports alias in not search' do
  183. get '/posts', params: { tags: 'not:not_spec_foo_alias' }
  184. expect(response).to have_http_status(:ok)
  185. expect(json.fetch('posts').map { |p| p['id'] } & controlled_ids).to match_array(
  186. [bar_only_post.id, baz_only_post.id]
  187. )
  188. end
  189. it 'treats multiple not terms as AND when match is omitted' do
  190. get '/posts', params: { tags: 'not:not_spec_foo not:not_spec_bar' }
  191. expect(response).to have_http_status(:ok)
  192. expect(json.fetch('posts').map { |p| p['id'] } & controlled_ids).to match_array(
  193. [baz_only_post.id]
  194. )
  195. end
  196. it 'treats multiple not terms as OR when match=any' do
  197. get '/posts', params: { tags: 'not:not_spec_foo not:not_spec_bar', match: 'any' }
  198. expect(response).to have_http_status(:ok)
  199. expect(json.fetch('posts').map { |p| p['id'] } & controlled_ids).to match_array(
  200. [foo_only_post.id, bar_only_post.id, baz_only_post.id, foo_baz_post.id]
  201. )
  202. end
  203. it 'supports mixed positive and negative search with AND' do
  204. get '/posts', params: { tags: 'not_spec_foo not:not_spec_bar' }
  205. expect(response).to have_http_status(:ok)
  206. expect(json.fetch('posts').map { |p| p['id'] } & controlled_ids).to match_array(
  207. [foo_only_post.id, foo_baz_post.id]
  208. )
  209. end
  210. it 'supports mixed positive and negative search with OR when match=any' do
  211. get '/posts', params: { tags: 'not_spec_foo not:not_spec_bar', match: 'any' }
  212. expect(response).to have_http_status(:ok)
  213. expect(json.fetch('posts').map { |p| p['id'] } & controlled_ids).to match_array(
  214. [foo_only_post.id, baz_only_post.id, foo_bar_post.id, foo_baz_post.id]
  215. )
  216. end
  217. end
  218. context 'when url is provided' do
  219. let!(:url_hit_post) do
  220. Post.create!(uploaded_user: user, title: 'url hit',
  221. url: 'https://example.com/needle-url-xyz').tap do |p|
  222. PostTag.create!(post: p, tag:)
  223. end
  224. end
  225. let!(:url_miss_post) do
  226. Post.create!(uploaded_user: user, title: 'url miss',
  227. url: 'https://example.com/other-url').tap do |p|
  228. PostTag.create!(post: p, tag:)
  229. end
  230. end
  231. it 'filters posts by url substring' do
  232. get '/posts', params: { url: 'needle-url-xyz' }
  233. expect(response).to have_http_status(:ok)
  234. ids = json.fetch('posts').map { |p| p['id'] }
  235. expect(ids).to include(url_hit_post.id)
  236. expect(ids).not_to include(url_miss_post.id)
  237. expect(json.fetch('count')).to eq(1)
  238. end
  239. end
  240. context 'when title is provided' do
  241. let!(:title_hit_post) do
  242. Post.create!(uploaded_user: user, title: 'needle-title-xyz',
  243. url: 'https://example.com/title-hit').tap do |p|
  244. PostTag.create!(post: p, tag:)
  245. end
  246. end
  247. let!(:title_miss_post) do
  248. Post.create!(uploaded_user: user, title: 'other title',
  249. url: 'https://example.com/title-miss').tap do |p|
  250. PostTag.create!(post: p, tag:)
  251. end
  252. end
  253. it 'filters posts by title substring' do
  254. get '/posts', params: { title: 'needle-title-xyz' }
  255. expect(response).to have_http_status(:ok)
  256. ids = json.fetch('posts').map { |p| p['id'] }
  257. expect(ids).to include(title_hit_post.id)
  258. expect(ids).not_to include(title_miss_post.id)
  259. expect(json.fetch('count')).to eq(1)
  260. end
  261. end
  262. context 'when created_from/created_to are provided' do
  263. let(:t_created_hit) { Time.zone.local(2010, 1, 5, 12, 0, 0) }
  264. let(:t_created_miss) { Time.zone.local(2012, 1, 5, 12, 0, 0) }
  265. let!(:created_hit_post) do
  266. travel_to(t_created_hit) do
  267. Post.create!(uploaded_user: user, title: 'created hit',
  268. url: 'https://example.com/created-hit').tap do |p|
  269. PostTag.create!(post: p, tag:)
  270. end
  271. end
  272. end
  273. let!(:created_miss_post) do
  274. travel_to(t_created_miss) do
  275. Post.create!(uploaded_user: user, title: 'created miss',
  276. url: 'https://example.com/created-miss').tap do |p|
  277. PostTag.create!(post: p, tag:)
  278. end
  279. end
  280. end
  281. it 'filters posts by created_at range' do
  282. get '/posts', params: {
  283. created_from: Time.zone.local(2010, 1, 1, 0, 0, 0).iso8601,
  284. created_to: Time.zone.local(2010, 12, 31, 23, 59, 59).iso8601
  285. }
  286. expect(response).to have_http_status(:ok)
  287. ids = json.fetch('posts').map { |p| p['id'] }
  288. expect(ids).to include(created_hit_post.id)
  289. expect(ids).not_to include(created_miss_post.id)
  290. expect(json.fetch('count')).to eq(1)
  291. end
  292. end
  293. context 'when updated_from/updated_to are provided' do
  294. let(:t0) { Time.zone.local(2011, 2, 1, 12, 0, 0) }
  295. let(:t1) { Time.zone.local(2011, 2, 10, 12, 0, 0) }
  296. let!(:updated_hit_post) do
  297. p = nil
  298. travel_to(t0) do
  299. p = Post.create!(uploaded_user: user, title: 'updated hit',
  300. url: 'https://example.com/updated-hit').tap do |pp|
  301. PostTag.create!(post: pp, tag:)
  302. end
  303. end
  304. travel_to(t1) do
  305. p.update!(title: 'updated hit v2')
  306. end
  307. p
  308. end
  309. let!(:updated_miss_post) do
  310. travel_to(Time.zone.local(2013, 1, 1, 12, 0, 0)) do
  311. Post.create!(uploaded_user: user, title: 'updated miss',
  312. url: 'https://example.com/updated-miss').tap do |p|
  313. PostTag.create!(post: p, tag:)
  314. end
  315. end
  316. end
  317. it 'filters posts by updated_at range' do
  318. get '/posts', params: {
  319. updated_from: Time.zone.local(2011, 2, 5, 0, 0, 0).iso8601,
  320. updated_to: Time.zone.local(2011, 2, 20, 23, 59, 59).iso8601
  321. }
  322. expect(response).to have_http_status(:ok)
  323. ids = json.fetch('posts').map { |p| p['id'] }
  324. expect(ids).to include(updated_hit_post.id)
  325. expect(ids).not_to include(updated_miss_post.id)
  326. expect(json.fetch('count')).to eq(1)
  327. end
  328. end
  329. context 'when original_created_from/original_created_to are provided' do
  330. # 注意: controller の現状ロジックに合わせてる
  331. # original_created_from は `original_created_before > ?`
  332. # original_created_to は `original_created_from <= ?`
  333. let!(:oc_hit_post) do
  334. Post.create!(uploaded_user: user, title: 'oc hit',
  335. url: 'https://example.com/oc-hit',
  336. original_created_from: Time.zone.local(2015, 1, 1, 0, 0, 0),
  337. original_created_before: Time.zone.local(2015, 1, 10, 0, 0, 0)).tap do |p|
  338. PostTag.create!(post: p, tag:)
  339. end
  340. end
  341. # original_created_from の条件は「original_created_before > param」なので、
  342. # before が param 以下になるようにする(ただし before >= from は守る)
  343. let!(:oc_miss_post_for_from) do
  344. Post.create!(
  345. uploaded_user: user,
  346. title: 'oc miss for from',
  347. url: 'https://example.com/oc-miss-from',
  348. original_created_from: Time.zone.local(2014, 12, 1, 0, 0, 0),
  349. original_created_before: Time.zone.local(2015, 1, 1, 0, 0, 0)
  350. ).tap { |p| PostTag.create!(post: p, tag:) }
  351. end
  352. # original_created_to の条件は「original_created_from <= param」なので、
  353. # from が param より後になるようにする(before >= from は守る)
  354. let!(:oc_miss_post_for_to) do
  355. Post.create!(
  356. uploaded_user: user,
  357. title: 'oc miss for to',
  358. url: 'https://example.com/oc-miss-to',
  359. original_created_from: Time.zone.local(2015, 2, 1, 0, 0, 0),
  360. original_created_before: Time.zone.local(2015, 2, 10, 0, 0, 0)
  361. ).tap { |p| PostTag.create!(post: p, tag:) }
  362. end
  363. it 'filters posts by original_created_from (current controller behavior)' do
  364. get '/posts', params: {
  365. original_created_from: Time.zone.local(2015, 1, 5, 0, 0, 0).iso8601
  366. }
  367. expect(response).to have_http_status(:ok)
  368. ids = json.fetch('posts').map { |p| p['id'] }
  369. expect(ids).to include(oc_hit_post.id)
  370. expect(ids).not_to include(oc_miss_post_for_from.id)
  371. expect(json.fetch('count')).to eq(2)
  372. end
  373. it 'filters posts by original_created_to (current controller behavior)' do
  374. get '/posts', params: {
  375. original_created_to: Time.zone.local(2015, 1, 15, 0, 0, 0).iso8601
  376. }
  377. expect(response).to have_http_status(:ok)
  378. ids = json.fetch('posts').map { |p| p['id'] }
  379. expect(ids).to include(oc_hit_post.id)
  380. expect(ids).not_to include(oc_miss_post_for_to.id)
  381. expect(json.fetch('count')).to eq(2)
  382. end
  383. end
  384. end
  385. describe 'GET /posts/:id' do
  386. subject(:request) { get "/posts/#{post_id}" }
  387. context 'when post exists' do
  388. let(:post_id) { post_record.id }
  389. it 'returns post with tag tree + related + viewed' do
  390. request
  391. expect(response).to have_http_status(:ok)
  392. expect(json).to include('id' => post_record.id)
  393. expect(json).to have_key('tags')
  394. expect(json['tags']).to be_an(Array)
  395. # show は build_tag_tree_for を使うので、tags はツリー形式(children 付き)
  396. node = json['tags'][0]
  397. expect(node).to include('id', 'name', 'category', 'post_count', 'children', 'has_wiki')
  398. expect(node['name']).to eq('spec_tag')
  399. expect(json).to have_key('related')
  400. expect(json['related']).to be_an(Array)
  401. expect(json).to have_key('viewed')
  402. expect([true, false]).to include(json['viewed'])
  403. end
  404. context 'when post has parent, child, and sibling posts' do
  405. let!(:parent_post) do
  406. create_parent_post!(
  407. title: 'shared parent post',
  408. url: 'https://example.com/shared-parent-post'
  409. )
  410. end
  411. let!(:child_post) do
  412. Post.create!(
  413. title: 'child post',
  414. url: 'https://example.com/show-child-post'
  415. )
  416. end
  417. let!(:sibling_post) do
  418. Post.create!(
  419. title: 'sibling post',
  420. url: 'https://example.com/show-sibling-post'
  421. )
  422. end
  423. before do
  424. PostImplication.create!(
  425. post: post_record,
  426. parent_post:
  427. )
  428. PostImplication.create!(
  429. post: child_post,
  430. parent_post: post_record
  431. )
  432. PostImplication.create!(
  433. post: sibling_post,
  434. parent_post:
  435. )
  436. end
  437. it 'returns parent_posts, child_posts, and sibling_posts' do
  438. get "/posts/#{post_record.id}"
  439. expect(response).to have_http_status(:ok)
  440. parent_ids = json.fetch('parent_posts').map { |p| p.fetch('id') }
  441. child_ids = json.fetch('child_posts').map { |p| p.fetch('id') }
  442. expect(parent_ids).to include(parent_post.id)
  443. expect(child_ids).to include(child_post.id)
  444. sibling_posts_by_parent = json.fetch('sibling_posts')
  445. siblings = sibling_posts_by_parent.fetch(parent_post.id.to_s)
  446. sibling_ids = siblings.map { |p| p.fetch('id') }
  447. expect(sibling_ids).to include(post_record.id)
  448. expect(sibling_ids).to include(sibling_post.id)
  449. end
  450. end
  451. end
  452. context 'when post does not exist' do
  453. let(:post_id) { 999_999_999 }
  454. it 'returns 404' do
  455. request
  456. expect(response).to have_http_status(:not_found)
  457. end
  458. end
  459. end
  460. describe 'POST /posts' do
  461. let(:member) { create(:user, :member) }
  462. let!(:alias_tag_name) { TagName.create!(name: 'manko', canonical: tag_name) }
  463. it '401 when not logged in' do
  464. sign_out
  465. post '/posts', params: post_write_params(title: 't', url: 'https://example.com/x', tags: 'a',
  466. thumbnail: dummy_upload)
  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. post '/posts', params: post_write_params(title: 't', url: 'https://example.com/x', tags: 'a',
  472. thumbnail: dummy_upload)
  473. expect(response).to have_http_status(:forbidden)
  474. end
  475. it '201 and creates post + tags when member' do
  476. sign_in_as(member)
  477. post '/posts', params: post_write_params(
  478. title: 'new post',
  479. url: 'https://example.com/new',
  480. tags: 'spec_tag', # 既存タグ名を投げる
  481. thumbnail: dummy_upload
  482. )
  483. expect(response).to have_http_status(:created)
  484. expect(json).to include('id', 'title', 'url')
  485. # tags が name を含むこと(API 側の serialization が正しいこと)
  486. expect(json).to have_key('tags')
  487. expect(json['tags']).to be_an(Array)
  488. expect(json['tags'][0]).to have_key('name')
  489. end
  490. it '201 and creates post + tags when member and tags have aliases' do
  491. sign_in_as(member)
  492. post '/posts', params: post_write_params(
  493. title: 'new post',
  494. url: 'https://example.com/new',
  495. tags: 'manko', # 既存タグ名を投げる
  496. thumbnail: dummy_upload
  497. )
  498. expect(response).to have_http_status(:created)
  499. expect(json).to include('id', 'title', 'url')
  500. # tags が name を含むこと(API 側の serialization が正しいこと)
  501. names = json.fetch('tags').map { |t| t['name'] }
  502. expect(names).to include('spec_tag')
  503. expect(names).not_to include('manko')
  504. end
  505. context "when nico tag already exists in tags" do
  506. before do
  507. Tag.find_undiscard_or_create_by!(
  508. tag_name: TagName.find_undiscard_or_create_by!(name: 'nico:nico_tag'),
  509. category: :nico)
  510. end
  511. it 'return 400' do
  512. sign_in_as(member)
  513. post '/posts', params: post_write_params(
  514. title: 'new post',
  515. url: 'https://example.com/nico-tag-post',
  516. tags: 'nico:nico_tag',
  517. thumbnail: dummy_upload
  518. )
  519. expect(response).to have_http_status(:bad_request), response.body
  520. end
  521. end
  522. context 'when url is blank' do
  523. it 'returns 422' do
  524. sign_in_as(member)
  525. post '/posts', params: post_write_params(
  526. title: 'new post',
  527. url: ' ',
  528. tags: 'spec_tag', # 既存タグ名を投げる
  529. thumbnail: dummy_upload)
  530. expect(response).to have_http_status(:unprocessable_entity)
  531. end
  532. end
  533. context 'when url is invalid' do
  534. it 'returns 422' do
  535. sign_in_as(member)
  536. post '/posts', params: post_write_params(
  537. title: 'new post',
  538. url: 'ぼざクリタグ広場',
  539. tags: 'spec_tag', # 既存タグ名を投げる
  540. thumbnail: dummy_upload)
  541. expect(response).to have_http_status(:unprocessable_entity)
  542. end
  543. end
  544. context 'when parent_post_ids is provided' do
  545. let!(:parent_post_1) do
  546. create_parent_post!(
  547. title: 'parent post 1',
  548. url: 'https://example.com/parent-post-1'
  549. )
  550. end
  551. let!(:parent_post_2) do
  552. create_parent_post!(
  553. title: 'parent post 2',
  554. url: 'https://example.com/parent-post-2'
  555. )
  556. end
  557. it 'creates post implications for parent posts' do
  558. sign_in_as(member)
  559. expect {
  560. post '/posts', params: {
  561. title: 'child post',
  562. url: 'https://example.com/child-post',
  563. tags: 'spec_tag',
  564. parent_post_ids: "#{parent_post_1.id} #{parent_post_2.id}",
  565. thumbnail: dummy_upload }
  566. }.to change(PostImplication, :count).by(2)
  567. expect(response).to have_http_status(:created)
  568. created_post = Post.find(json.fetch('id'))
  569. expect(created_post.parent_posts.order(:id).pluck(:id)).to eq(
  570. [parent_post_1.id, parent_post_2.id].sort
  571. )
  572. expect(PostImplication.exists?(
  573. post_id: created_post.id,
  574. parent_post_id: parent_post_1.id
  575. )).to be(true)
  576. expect(PostImplication.exists?(
  577. post_id: created_post.id,
  578. parent_post_id: parent_post_2.id
  579. )).to be(true)
  580. end
  581. it 'deduplicates parent_post_ids' do
  582. sign_in_as(member)
  583. expect {
  584. post '/posts', params: post_write_params(
  585. title: 'dedup child post',
  586. url: 'https://example.com/dedup-child-post',
  587. tags: 'spec_tag',
  588. parent_post_ids: "#{parent_post_1.id} #{parent_post_1.id}",
  589. thumbnail: dummy_upload
  590. )
  591. }.to change(PostImplication, :count).by(1)
  592. expect(response).to have_http_status(:created)
  593. created_post = Post.find(json.fetch('id'))
  594. expect(created_post.parent_posts.pluck(:id)).to eq([parent_post_1.id])
  595. end
  596. it 'records parent_post_ids in post version' do
  597. sign_in_as(member)
  598. post '/posts', params: post_write_params(
  599. title: 'versioned child post',
  600. url: 'https://example.com/versioned-child-post',
  601. tags: 'spec_tag',
  602. parent_post_ids: "#{parent_post_1.id} #{parent_post_2.id}",
  603. thumbnail: dummy_upload
  604. )
  605. expect(response).to have_http_status(:created)
  606. created_post = Post.find(json.fetch('id'))
  607. version = PostVersion.find_by!(post: created_post, version_no: 1)
  608. expect(version.parent_post_ids.split.map(&:to_i)).to eq(
  609. [parent_post_1.id, parent_post_2.id].sort
  610. )
  611. end
  612. end
  613. context 'when parent_post_ids is missing' do
  614. it 'returns 422' do
  615. sign_in_as(member)
  616. expect {
  617. post '/posts', params: {
  618. title: 'missing parent_post_ids',
  619. url: 'https://example.com/missing-parent-post-ids',
  620. tags: 'spec_tag',
  621. thumbnail: dummy_upload }
  622. }.not_to change(Post, :count)
  623. expect(response).to have_http_status(:unprocessable_entity)
  624. expect(json.fetch('errors')).to be_present
  625. end
  626. end
  627. context 'when parent_post_ids includes invalid token' do
  628. it 'returns 422 and does not create post' do
  629. sign_in_as(member)
  630. expect {
  631. post '/posts', params: post_write_params(
  632. title: 'invalid parent ids',
  633. url: 'https://example.com/invalid-parent-ids',
  634. tags: 'spec_tag',
  635. parent_post_ids: 'abc',
  636. thumbnail: dummy_upload
  637. )
  638. }.not_to change(Post, :count)
  639. expect(response).to have_http_status(:unprocessable_entity)
  640. expect(json.fetch('errors')).to be_present
  641. end
  642. end
  643. context 'when parent_post_ids includes nonexistent post id' do
  644. it 'returns 422 and does not create post implication' do
  645. sign_in_as(member)
  646. expect {
  647. post '/posts', params: post_write_params(
  648. title: 'missing parent post',
  649. url: 'https://example.com/missing-parent-post',
  650. tags: 'spec_tag',
  651. parent_post_ids: '999999999',
  652. thumbnail: dummy_upload
  653. )
  654. }.not_to change(PostImplication, :count)
  655. expect(response).to have_http_status(:unprocessable_entity)
  656. expect(json.fetch('errors')).to be_present
  657. end
  658. end
  659. end
  660. describe 'PUT /posts/:id' do
  661. let(:member) { create(:user, :member) }
  662. it '401 when not logged in' do
  663. sign_out
  664. put "/posts/#{post_record.id}", params: post_update_params(
  665. post_record, title: 'updated', tags: 'spec_tag')
  666. expect(response).to have_http_status(:unauthorized)
  667. end
  668. it '403 when not member' do
  669. sign_in_as(create(:user, role: 'guest'))
  670. put "/posts/#{post_record.id}", params: post_update_params(
  671. post_record, title: 'updated', tags: 'spec_tag')
  672. expect(response).to have_http_status(:forbidden)
  673. end
  674. it '200 and updates title + resync tags when member' do
  675. sign_in_as(member)
  676. tn2 = TagName.create!(name: 'spec_tag_2')
  677. Tag.create!(tag_name: tn2, category: :general)
  678. put "/posts/#{post_record.id}", params: post_update_params(
  679. post_record,
  680. title: 'updated title',
  681. tags: 'spec_tag_2')
  682. expect(response).to have_http_status(:ok)
  683. expect(json).to have_key('tags')
  684. expect(json['tags']).to be_an(Array)
  685. names = json['tags'].map { |n| n['name'] }
  686. expect(names).to include('spec_tag_2')
  687. end
  688. context "when nico tag already exists in tags" do
  689. before do
  690. Tag.find_undiscard_or_create_by!(
  691. tag_name: TagName.find_undiscard_or_create_by!(name: 'nico:nico_tag'),
  692. category: :nico)
  693. end
  694. it 'return 400' do
  695. sign_in_as(member)
  696. put "/posts/#{post_record.id}", params: post_update_params(
  697. post_record,
  698. title: 'updated title',
  699. tags: 'nico:nico_tag')
  700. expect(response).to have_http_status(:bad_request), response.body
  701. end
  702. end
  703. context 'when parent_post_ids is provided' do
  704. let!(:old_parent_post) do
  705. create_parent_post!(
  706. title: 'old parent post',
  707. url: 'https://example.com/old-parent-post'
  708. )
  709. end
  710. let!(:new_parent_post_1) do
  711. create_parent_post!(
  712. title: 'new parent post 1',
  713. url: 'https://example.com/new-parent-post-1'
  714. )
  715. end
  716. let!(:new_parent_post_2) do
  717. create_parent_post!(
  718. title: 'new parent post 2',
  719. url: 'https://example.com/new-parent-post-2'
  720. )
  721. end
  722. before do
  723. PostImplication.create!(
  724. post: post_record,
  725. parent_post: old_parent_post
  726. )
  727. end
  728. it 'replaces parent posts' do
  729. sign_in_as(member)
  730. put "/posts/#{post_record.id}", params: post_update_params(
  731. post_record,
  732. title: 'updated title',
  733. tags: 'spec_tag',
  734. parent_post_ids: "#{new_parent_post_1.id} #{new_parent_post_2.id}")
  735. expect(response).to have_http_status(:ok)
  736. expect(post_record.reload.parent_posts.order(:id).pluck(:id)).to eq(
  737. [new_parent_post_1.id, new_parent_post_2.id].sort
  738. )
  739. expect(PostImplication.exists?(
  740. post_id: post_record.id,
  741. parent_post_id: old_parent_post.id
  742. )).to be(false)
  743. end
  744. it 'clears parent posts when parent_post_ids is blank' do
  745. sign_in_as(member)
  746. put "/posts/#{post_record.id}", params: post_update_params(
  747. post_record,
  748. title: 'updated title',
  749. tags: 'spec_tag',
  750. parent_post_ids: ''
  751. )
  752. expect(response).to have_http_status(:ok)
  753. expect(post_record.reload.parent_posts).to be_empty
  754. end
  755. it 'records changed parent_post_ids in post version' do
  756. sign_in_as(member)
  757. create_post_version_for!(post_record.reload)
  758. put "/posts/#{post_record.id}", params: post_update_params(
  759. post_record,
  760. title: 'updated title',
  761. tags: 'spec_tag',
  762. parent_post_ids: "#{new_parent_post_1.id} #{new_parent_post_2.id}"
  763. )
  764. expect(response).to have_http_status(:ok)
  765. version = post_record.reload.post_versions.order(:version_no).last
  766. expect(version.version_no).to eq(2)
  767. expect(version.parent_post_ids.split.map(&:to_i)).to eq(
  768. [new_parent_post_1.id, new_parent_post_2.id].sort
  769. )
  770. end
  771. end
  772. context 'when parent_post_ids is missing' do
  773. it 'returns 422' do
  774. sign_in_as(member)
  775. base_version = create_post_version_for!(post_record.reload)
  776. put "/posts/#{post_record.id}", params: {
  777. base_version_no: base_version.version_no,
  778. title: 'updated title',
  779. tags: 'spec_tag' }
  780. expect(response).to have_http_status(:unprocessable_entity)
  781. expect(json.fetch('errors')).to be_present
  782. end
  783. end
  784. context 'when parent_post_ids includes invalid token' do
  785. it 'returns 422 and does not change parent posts' do
  786. sign_in_as(member)
  787. parent_post = create_parent_post!(
  788. title: 'valid parent post',
  789. url: 'https://example.com/valid-parent-post'
  790. )
  791. PostImplication.create!(
  792. post: post_record,
  793. parent_post:
  794. )
  795. put "/posts/#{post_record.id}", params: post_update_params(
  796. post_record,
  797. title: 'updated title',
  798. tags: 'spec_tag',
  799. parent_post_ids: 'abc'
  800. )
  801. expect(response).to have_http_status(:unprocessable_entity)
  802. expect(post_record.reload.parent_posts.pluck(:id)).to eq([parent_post.id])
  803. end
  804. end
  805. context 'when parent_post_ids includes nonexistent post id' do
  806. it 'returns 422 and does not change parent posts' do
  807. sign_in_as(member)
  808. parent_post = create_parent_post!(
  809. title: 'existing parent post',
  810. url: 'https://example.com/existing-parent-post'
  811. )
  812. PostImplication.create!(
  813. post: post_record,
  814. parent_post:
  815. )
  816. put "/posts/#{post_record.id}", params: post_update_params(
  817. post_record,
  818. title: 'updated title',
  819. tags: 'spec_tag',
  820. parent_post_ids: '999999999'
  821. )
  822. expect(response).to have_http_status(:unprocessable_entity)
  823. expect(post_record.reload.parent_posts.pluck(:id)).to eq([parent_post.id])
  824. end
  825. end
  826. context 'when parent_post_ids includes self id' do
  827. it 'returns 422 and does not create self implication' do
  828. sign_in_as(member)
  829. put "/posts/#{post_record.id}", params: post_update_params(
  830. post_record,
  831. title: 'updated title',
  832. tags: 'spec_tag',
  833. parent_post_ids: post_record.id.to_s
  834. )
  835. expect(response).to have_http_status(:unprocessable_entity)
  836. expect(PostImplication.exists?(
  837. post_id: post_record.id,
  838. parent_post_id: post_record.id
  839. )).to be(false)
  840. end
  841. end
  842. context 'with optimistic locking' do
  843. let!(:no_deerjikist_tag) { Tag.no_deerjikist }
  844. before do
  845. PostTag.create!(post: post_record, tag: no_deerjikist_tag)
  846. end
  847. it '400 when base_version_no is missing without force' do
  848. sign_in_as(member)
  849. put "/posts/#{post_record.id}", params: post_write_params(
  850. title: 'updated title',
  851. tags: 'spec_tag')
  852. expect(response).to have_http_status(:bad_request)
  853. end
  854. it '400 when force and merge are both true' do
  855. sign_in_as(member)
  856. put "/posts/#{post_record.id}", params: post_write_params(
  857. title: 'updated title',
  858. tags: 'spec_tag',
  859. force: '1',
  860. merge: '1')
  861. expect(response).to have_http_status(:bad_request)
  862. end
  863. it '409 when scalar fields are changed both by current and incoming updates' do
  864. sign_in_as(member)
  865. base_version = create_post_version_for!(post_record.reload)
  866. post_record.update!(title: 'updated by other user')
  867. PostVersionRecorder.record!(
  868. post: post_record.reload,
  869. event_type: :update,
  870. created_by_user: member)
  871. put "/posts/#{post_record.id}", params: post_write_params(
  872. base_version_no: base_version.version_no,
  873. title: 'updated by me',
  874. tags: "spec_tag #{Tag.no_deerjikist.name}")
  875. expect(response).to have_http_status(:conflict)
  876. expect(json.fetch('error')).to eq('conflict')
  877. expect(json.fetch('base_version_no')).to eq(base_version.version_no)
  878. expect(json.fetch('current_version_no')).to eq(2)
  879. expect(json.fetch('mergeable')).to be(false)
  880. conflict_fields = json.fetch('conflicts').map { |change| change.fetch('field') }
  881. expect(conflict_fields).to include('title')
  882. expect(post_record.reload.title).to eq('updated by other user')
  883. end
  884. it 'returns 409 with mergeable true when stale tag changes do not conflict but merge is not requested' do
  885. sign_in_as(member)
  886. base_version = create_post_version_for!(post_record.reload)
  887. current_tag = Tag.find_or_create_by_tag_name!('current_added_tag', category: :general)
  888. PostTag.create!(post: post_record, tag: current_tag, created_user: member)
  889. PostVersionRecorder.record!(
  890. post: post_record.reload,
  891. event_type: :update,
  892. created_by_user: member)
  893. put "/posts/#{post_record.id}", params: post_write_params(
  894. base_version_no: base_version.version_no,
  895. title: post_record.title,
  896. tags: "spec_tag #{Tag.no_deerjikist.name} incoming_added_tag")
  897. expect(response).to have_http_status(:conflict)
  898. expect(json.fetch('mergeable')).to be(true)
  899. tag_change = json.fetch('changes').find { |change| change.fetch('field') == 'tag_names' }
  900. expect(tag_change).to be_present
  901. expect(tag_change.fetch('conflict')).to be(false)
  902. expect(tag_change.fetch('added_by_current')).to include('current_added_tag')
  903. expect(tag_change.fetch('added_by_me')).to include('incoming_added_tag')
  904. end
  905. it 'merges non-conflicting stale tag changes when merge is true' do
  906. sign_in_as(member)
  907. base_version = create_post_version_for!(post_record.reload)
  908. current_tag = Tag.find_or_create_by_tag_name!('current_merge_tag', category: :general)
  909. PostTag.create!(post: post_record, tag: current_tag, created_user: member)
  910. PostVersionRecorder.record!(
  911. post: post_record.reload,
  912. event_type: :update,
  913. created_by_user: member)
  914. put "/posts/#{post_record.id}", params: post_write_params(
  915. base_version_no: base_version.version_no,
  916. title: post_record.title,
  917. tags: "spec_tag #{Tag.no_deerjikist.name} incoming_merge_tag",
  918. merge: '1')
  919. expect(response).to have_http_status(:ok)
  920. names = post_record.reload.tags.map(&:name)
  921. expect(names).to include('spec_tag')
  922. expect(names).to include(Tag.no_deerjikist.name)
  923. expect(names).to include('current_merge_tag')
  924. expect(names).to include('incoming_merge_tag')
  925. end
  926. it 'does not conflict when only nico tags changed after the base version' do
  927. sign_in_as(member)
  928. base_version = create_post_version_for!(post_record.reload)
  929. nico_tag = create_nico_tag!('nico:optimistic_lock_nico')
  930. PostTag.create!(post: post_record, tag: nico_tag, created_user: member)
  931. PostVersionRecorder.record!(
  932. post: post_record.reload,
  933. event_type: :update,
  934. created_by_user: member)
  935. expect(post_record.reload.version_no).to eq(2)
  936. put "/posts/#{post_record.id}", params: post_write_params(
  937. base_version_no: base_version.version_no,
  938. title: post_record.title,
  939. tags: "spec_tag #{ Tag.no_deerjikist.name }")
  940. expect(response).to have_http_status(:ok)
  941. names = post_record.reload.tags.map(&:name)
  942. expect(names).to include('spec_tag')
  943. expect(names).to include(Tag.no_deerjikist.name)
  944. expect(names).to include(nico_tag.name)
  945. end
  946. it 'keeps nico tags even when they are not included in PUT tags' do
  947. sign_in_as(member)
  948. nico_tag = create_nico_tag!('nico:readonly_update_nico')
  949. PostTag.create!(post: post_record, tag: nico_tag, created_user: member)
  950. base_version = create_post_version_for!(post_record.reload)
  951. put "/posts/#{post_record.id}", params: post_write_params(
  952. base_version_no: base_version.version_no,
  953. title: 'updated title',
  954. tags: "spec_tag #{ Tag.no_deerjikist.name }")
  955. expect(response).to have_http_status(:ok)
  956. names = post_record.reload.tags.map(&:name)
  957. expect(names).to include('spec_tag')
  958. expect(names).to include(Tag.no_deerjikist.name)
  959. expect(names).to include(nico_tag.name)
  960. end
  961. it 'allows non-nico tags linked from nico tags to be removed by normal post update' do
  962. sign_in_as(member)
  963. nico_tag = create_nico_tag!('nico:relation_source')
  964. linked_tag = Tag.find_or_create_by_tag_name!('relation_linked_tag', category: :general)
  965. NicoTagRelation.create!(nico_tag:, tag: linked_tag)
  966. PostTag.create!(post: post_record, tag: nico_tag, created_user: member)
  967. PostTag.create!(post: post_record, tag: linked_tag, created_user: member)
  968. base_version = create_post_version_for!(post_record.reload)
  969. put "/posts/#{post_record.id}", params: post_write_params(
  970. base_version_no: base_version.version_no,
  971. title: post_record.title,
  972. tags: "spec_tag #{ Tag.no_deerjikist.name }")
  973. expect(response).to have_http_status(:ok)
  974. names = post_record.reload.tags.map(&:name)
  975. expect(names).to include(nico_tag.name)
  976. expect(names).to include('spec_tag')
  977. expect(names).to include(Tag.no_deerjikist.name)
  978. expect(names).not_to include(linked_tag.name)
  979. end
  980. it 'force-updates stale posts without base_version_no' do
  981. sign_in_as(member)
  982. create_post_version_for!(post_record.reload)
  983. post_record.update!(title: 'updated by other user')
  984. PostVersionRecorder.record!(
  985. post: post_record.reload,
  986. event_type: :update,
  987. created_by_user: member)
  988. put "/posts/#{post_record.id}", params: post_write_params(
  989. title: 'forced title',
  990. tags: "spec_tag #{Tag.no_deerjikist.name}",
  991. force: '1')
  992. expect(response).to have_http_status(:ok)
  993. expect(post_record.reload.title).to eq('forced title')
  994. end
  995. end
  996. end
  997. describe 'GET /posts/random' do
  998. it '404 when no posts' do
  999. PostTag.delete_all
  1000. Post.delete_all
  1001. get '/posts/random'
  1002. expect(response).to have_http_status(:not_found)
  1003. end
  1004. it '200 and returns viewed boolean' do
  1005. get '/posts/random'
  1006. expect(response).to have_http_status(:ok)
  1007. expect(json).to have_key('viewed')
  1008. expect([true, false]).to include(json['viewed'])
  1009. end
  1010. end
  1011. describe 'GET /posts/changes' do
  1012. let(:member) { create(:user, :member) }
  1013. it 'returns add/remove events (history) for a post' do
  1014. # add
  1015. tn2 = TagName.create!(name: 'spec_tag2')
  1016. tag2 = Tag.create!(tag_name: tn2, category: :general)
  1017. pt = PostTag.create!(post: post_record, tag: tag2, created_user: member)
  1018. # remove (discard)
  1019. pt.discard_by!(member)
  1020. get '/posts/changes', params: { id: post_record.id }
  1021. expect(response).to have_http_status(:ok)
  1022. expect(json).to include('changes', 'count')
  1023. expect(json['changes']).to be_an(Array)
  1024. expect(json['count']).to be >= 2
  1025. types = json['changes'].map { |e| e['change_type'] }.uniq
  1026. expect(types).to include('add')
  1027. expect(types).to include('remove')
  1028. end
  1029. it 'filters history by tag' do
  1030. tn2 = TagName.create!(name: 'history_tag_hit')
  1031. tag2 = Tag.create!(tag_name: tn2, category: :general)
  1032. tn3 = TagName.create!(name: 'history_tag_miss')
  1033. tag3 = Tag.create!(tag_name: tn3, category: :general)
  1034. other_post = Post.create!(
  1035. title: 'other post',
  1036. url: 'https://example.com/history-other'
  1037. )
  1038. # hit: add
  1039. PostTag.create!(post: post_record, tag: tag2, created_user: member)
  1040. # hit: add + remove
  1041. pt2 = PostTag.create!(post: other_post, tag: tag2, created_user: member)
  1042. pt2.discard_by!(member)
  1043. # miss: add + remove
  1044. pt3 = PostTag.create!(post: post_record, tag: tag3, created_user: member)
  1045. pt3.discard_by!(member)
  1046. get '/posts/changes', params: { tag: tag2.id }
  1047. expect(response).to have_http_status(:ok)
  1048. expect(json).to include('changes', 'count')
  1049. expect(json['count']).to eq(3)
  1050. changes = json.fetch('changes')
  1051. expect(changes.map { |e| e.dig('tag', 'id') }.uniq).to eq([tag2.id])
  1052. expect(changes.map { |e| e['change_type'] }).to match_array(%w[add add remove])
  1053. expect(changes.map { |e| e.dig('post', 'id') }).to match_array([
  1054. post_record.id,
  1055. other_post.id,
  1056. other_post.id
  1057. ])
  1058. end
  1059. it 'filters history by post and tag together' do
  1060. tn2 = TagName.create!(name: 'history_tag_combo_hit')
  1061. tag2 = Tag.create!(tag_name: tn2, category: :general)
  1062. tn3 = TagName.create!(name: 'history_tag_combo_miss')
  1063. tag3 = Tag.create!(tag_name: tn3, category: :general)
  1064. other_post = Post.create!(
  1065. title: 'other combo post',
  1066. url: 'https://example.com/history-combo-other'
  1067. )
  1068. # hit
  1069. PostTag.create!(post: post_record, tag: tag2, created_user: member)
  1070. # miss by post
  1071. pt2 = PostTag.create!(post: other_post, tag: tag2, created_user: member)
  1072. pt2.discard_by!(member)
  1073. # miss by tag
  1074. pt3 = PostTag.create!(post: post_record, tag: tag3, created_user: member)
  1075. pt3.discard_by!(member)
  1076. get '/posts/changes', params: { id: post_record.id, tag: tag2.id }
  1077. expect(response).to have_http_status(:ok)
  1078. expect(json).to include('changes', 'count')
  1079. expect(json['count']).to eq(1)
  1080. changes = json.fetch('changes')
  1081. expect(changes.size).to eq(1)
  1082. expect(changes[0]['change_type']).to eq('add')
  1083. expect(changes[0].dig('post', 'id')).to eq(post_record.id)
  1084. expect(changes[0].dig('tag', 'id')).to eq(tag2.id)
  1085. end
  1086. it 'returns empty history when tag does not match' do
  1087. tn2 = TagName.create!(name: 'history_tag_no_hit')
  1088. tag2 = Tag.create!(tag_name: tn2, category: :general)
  1089. get '/posts/changes', params: { tag: tag2.id }
  1090. expect(response).to have_http_status(:ok)
  1091. expect(json.fetch('changes')).to eq([])
  1092. expect(json.fetch('count')).to eq(0)
  1093. end
  1094. end
  1095. describe 'GET /posts/versions' do
  1096. let(:member) { create(:user, :member, name: 'version member') }
  1097. let(:t_v1) { Time.zone.local(2020, 1, 1, 12, 0, 0) }
  1098. let(:t_v2) { Time.zone.local(2020, 1, 2, 12, 0, 0) }
  1099. let(:t_other) { Time.zone.local(2020, 1, 3, 12, 0, 0) }
  1100. let(:oc_from) { Time.zone.local(2019, 12, 31, 0, 0, 0) }
  1101. let(:oc_before) { Time.zone.local(2020, 1, 1, 0, 0, 0) }
  1102. let!(:tag_name2) { TagName.create!(name: 'spec_tag_2') }
  1103. let!(:tag2) { Tag.create!(tag_name: tag_name2, category: :general) }
  1104. def snapshot_tags(post)
  1105. post.snapshot_tag_names.join(' ')
  1106. end
  1107. def create_post_version! post, version_no:, event_type:, created_by_user:, created_at:
  1108. PostVersion.create!(
  1109. post:,
  1110. version_no:,
  1111. event_type:,
  1112. title: post.title,
  1113. url: post.url,
  1114. thumbnail_base: post.thumbnail_base,
  1115. tags: snapshot_tags(post),
  1116. parent_post_ids: post.snapshot_parent_post_ids.join(' '),
  1117. original_created_from: post.original_created_from,
  1118. original_created_before: post.original_created_before,
  1119. created_at:,
  1120. created_by_user:
  1121. )
  1122. end
  1123. let!(:v1) do
  1124. travel_to(t_v1) do
  1125. create_post_version!(
  1126. post_record,
  1127. version_no: 1,
  1128. event_type: 'create',
  1129. created_by_user: member,
  1130. created_at: t_v1
  1131. )
  1132. end
  1133. end
  1134. let!(:v2) do
  1135. post_record.post_tags.kept.find_by!(tag: tag).discard_by!(member)
  1136. PostTag.create!(post: post_record, tag: tag2, created_user: member)
  1137. post_record.update!(
  1138. title: 'updated spec post',
  1139. original_created_from: oc_from,
  1140. original_created_before: oc_before
  1141. )
  1142. travel_to(t_v2) do
  1143. create_post_version!(
  1144. post_record.reload,
  1145. version_no: 2,
  1146. event_type: 'update',
  1147. created_by_user: member,
  1148. created_at: t_v2
  1149. )
  1150. end
  1151. end
  1152. let!(:other_post_version) do
  1153. other_post = Post.create!(
  1154. title: 'other versioned post',
  1155. url: 'https://example.com/other-versioned'
  1156. )
  1157. PostTag.create!(post: other_post, tag: tag)
  1158. travel_to(t_other) do
  1159. create_post_version!(
  1160. other_post,
  1161. version_no: 1,
  1162. event_type: 'create',
  1163. created_by_user: member,
  1164. created_at: t_other
  1165. )
  1166. end
  1167. end
  1168. it 'returns versions for the specified post in reverse chronological order' do
  1169. get '/posts/versions', params: { post: post_record.id }
  1170. expect(response).to have_http_status(:ok)
  1171. expect(json).to include('versions', 'count')
  1172. expect(json.fetch('count')).to eq(2)
  1173. versions = json.fetch('versions')
  1174. expect(versions.map { |v| v['post_id'] }.uniq).to eq([post_record.id])
  1175. expect(versions.map { |v| v['version_no'] }).to eq([2, 1])
  1176. latest = versions.first
  1177. expect(latest).to include(
  1178. 'post_id' => post_record.id,
  1179. 'version_no' => 2,
  1180. 'event_type' => 'update',
  1181. 'created_by_user' => {
  1182. 'id' => member.id,
  1183. 'name' => member.name
  1184. }
  1185. )
  1186. expect(latest.fetch('title')).to eq(
  1187. 'current' => 'updated spec post',
  1188. 'prev' => 'spec post'
  1189. )
  1190. expect(latest.fetch('url')).to eq(
  1191. 'current' => 'https://example.com/spec',
  1192. 'prev' => 'https://example.com/spec'
  1193. )
  1194. expect(latest.fetch('thumbnail')).to eq(
  1195. 'current' => nil,
  1196. 'prev' => nil
  1197. )
  1198. expect(latest.fetch('thumbnail_base')).to eq(
  1199. 'current' => nil,
  1200. 'prev' => nil
  1201. )
  1202. expect(latest.fetch('tags')).to include(
  1203. { 'name' => 'spec_tag_2', 'type' => 'added' },
  1204. { 'name' => 'spec_tag', 'type' => 'removed' }
  1205. )
  1206. expect(latest.fetch('original_created_from')).to eq(
  1207. 'current' => oc_from.iso8601,
  1208. 'prev' => nil
  1209. )
  1210. expect(latest.fetch('original_created_before')).to eq(
  1211. 'current' => oc_before.iso8601,
  1212. 'prev' => nil
  1213. )
  1214. expect(latest.fetch('created_at')).to eq(t_v2.iso8601)
  1215. first = versions.second
  1216. expect(first).to include(
  1217. 'post_id' => post_record.id,
  1218. 'version_no' => 1,
  1219. 'event_type' => 'create',
  1220. 'created_by_user' => {
  1221. 'id' => member.id,
  1222. 'name' => member.name
  1223. }
  1224. )
  1225. expect(first.fetch('title')).to eq(
  1226. 'current' => 'spec post',
  1227. 'prev' => nil
  1228. )
  1229. expect(first.fetch('tags')).to include(
  1230. { 'name' => 'spec_tag', 'type' => 'added' }
  1231. )
  1232. expect(first.fetch('created_at')).to eq(t_v1.iso8601)
  1233. end
  1234. it 'filters versions by tag when the current snapshot includes the tag' do
  1235. get '/posts/versions', params: { post: post_record.id, tag: tag2.id }
  1236. expect(response).to have_http_status(:ok)
  1237. expect(json.fetch('count')).to eq(1)
  1238. versions = json.fetch('versions')
  1239. expect(versions.size).to eq(1)
  1240. expect(versions[0]['post_id']).to eq(post_record.id)
  1241. expect(versions[0]['version_no']).to eq(2)
  1242. expect(versions[0]['tags']).to include(
  1243. { 'name' => 'spec_tag_2', 'type' => 'added' }
  1244. )
  1245. end
  1246. it 'filters versions by tag when the tag exists in either current or previous snapshot' do
  1247. get '/posts/versions', params: { post: post_record.id, tag: tag.id }
  1248. expect(response).to have_http_status(:ok)
  1249. expect(json.fetch('count')).to eq(2)
  1250. versions = json.fetch('versions')
  1251. expect(versions.map { |v| v['post_id'] }).to all(eq(post_record.id))
  1252. expect(versions.map { |v| v['version_no'] }).to eq([2, 1])
  1253. latest = versions[0]
  1254. first = versions[1]
  1255. expect(latest['tags']).to include(
  1256. { 'name' => 'spec_tag', 'type' => 'removed' }
  1257. )
  1258. expect(first['tags']).to include(
  1259. { 'name' => 'spec_tag', 'type' => 'added' }
  1260. )
  1261. end
  1262. it 'returns empty when tag does not exist' do
  1263. get '/posts/versions', params: { tag: 999_999_999 }
  1264. expect(response).to have_http_status(:ok)
  1265. expect(json.fetch('versions')).to eq([])
  1266. expect(json.fetch('count')).to eq(0)
  1267. end
  1268. it 'clamps page and limit to at least 1' do
  1269. get '/posts/versions', params: { post: post_record.id, page: 0, limit: 0 }
  1270. expect(response).to have_http_status(:ok)
  1271. expect(json.fetch('count')).to eq(2)
  1272. versions = json.fetch('versions')
  1273. expect(versions.size).to eq(1)
  1274. expect(versions[0]['version_no']).to eq(2)
  1275. end
  1276. end
  1277. describe 'POST /posts/:id/viewed' do
  1278. let(:user) { create(:user) }
  1279. it '401 when not logged in' do
  1280. sign_out
  1281. post "/posts/#{ post_record.id }/viewed"
  1282. expect(response).to have_http_status(:unauthorized)
  1283. end
  1284. it '204 and marks viewed when logged in' do
  1285. sign_in_as(user)
  1286. post "/posts/#{ post_record.id }/viewed"
  1287. expect(response).to have_http_status(:no_content)
  1288. expect(user.reload.viewed?(post_record)).to be(true)
  1289. end
  1290. end
  1291. describe 'DELETE /posts/:id/viewed' do
  1292. let(:user) { create(:user) }
  1293. it '401 when not logged in' do
  1294. sign_out
  1295. delete "/posts/#{ post_record.id }/viewed"
  1296. expect(response).to have_http_status(:unauthorized)
  1297. end
  1298. it '204 and unmarks viewed when logged in' do
  1299. sign_in_as(user)
  1300. # 先に viewed 付けてから外す
  1301. user.viewed_posts << post_record
  1302. delete "/posts/#{ post_record.id }/viewed"
  1303. expect(response).to have_http_status(:no_content)
  1304. expect(user.reload.viewed?(post_record)).to be(false)
  1305. end
  1306. end
  1307. describe 'post versioning' do
  1308. let(:member) { create(:user, :member) }
  1309. def snapshot_tags(post)
  1310. post.snapshot_tag_names.join(' ')
  1311. end
  1312. it 'creates version 1 on POST /posts' do
  1313. sign_in_as(member)
  1314. expect do
  1315. post '/posts', params: post_write_params(
  1316. title: 'versioned post',
  1317. url: 'https://example.com/versioned-post',
  1318. tags: 'spec_tag',
  1319. thumbnail: dummy_upload)
  1320. end.to change(PostVersion, :count).by(1)
  1321. expect(response).to have_http_status(:created)
  1322. created_post = Post.find(json.fetch('id'))
  1323. version = PostVersion.find_by!(post: created_post, version_no: 1)
  1324. expect(version.event_type).to eq('create')
  1325. expect(version.title).to eq('versioned post')
  1326. expect(version.url).to eq('https://example.com/versioned-post')
  1327. expect(version.created_by_user_id).to eq(member.id)
  1328. expect(version.tags).to eq(snapshot_tags(created_post))
  1329. end
  1330. it 'creates next version on PUT /posts/:id when snapshot changes' do
  1331. sign_in_as(member)
  1332. base_version = create_post_version_for!(post_record)
  1333. tag_name2 = TagName.create!(name: 'spec_tag_2')
  1334. Tag.create!(tag_name: tag_name2, category: :general)
  1335. expect do
  1336. put "/posts/#{post_record.id}", params: post_write_params(
  1337. base_version_no: base_version.version_no,
  1338. title: 'updated title',
  1339. tags: 'spec_tag_2')
  1340. end.to change(PostVersion, :count).by(1)
  1341. expect(response).to have_http_status(:ok)
  1342. version = post_record.reload.post_versions.order(:version_no).last
  1343. expect(version.version_no).to eq(2)
  1344. expect(version.event_type).to eq('update')
  1345. expect(version.title).to eq('updated title')
  1346. expect(version.created_by_user_id).to eq(member.id)
  1347. expect(version.tags).to eq(snapshot_tags(post_record.reload))
  1348. end
  1349. it 'does not create a new version on PUT /posts/:id when snapshot is unchanged' do
  1350. sign_in_as(member)
  1351. PostTag.create!(post: post_record, tag: Tag.no_deerjikist)
  1352. base_version = create_post_version_for!(post_record.reload)
  1353. expect {
  1354. put "/posts/#{post_record.id}", params: post_write_params(
  1355. base_version_no: base_version.version_no,
  1356. title: post_record.title,
  1357. tags: 'spec_tag')
  1358. }.not_to change(PostVersion, :count)
  1359. expect(response).to have_http_status(:ok)
  1360. version = post_record.reload.post_versions.order(:version_no).last
  1361. expect(version.version_no).to eq(1)
  1362. expect(version.event_type).to eq('create')
  1363. expect(version.tags).to eq(snapshot_tags(post_record))
  1364. end
  1365. it 'does not create a version when POST /posts is invalid' do
  1366. sign_in_as(member)
  1367. expect do
  1368. post '/posts', params: post_write_params(
  1369. title: 'invalid post',
  1370. url: 'ぼざクリタグ広場',
  1371. tags: 'spec_tag',
  1372. thumbnail: dummy_upload)
  1373. end.not_to change(PostVersion, :count)
  1374. expect(response).to have_http_status(:unprocessable_entity)
  1375. end
  1376. it 'does not create a version when PUT /posts/:id is invalid' do
  1377. sign_in_as(member)
  1378. base_version = create_post_version_for!(post_record)
  1379. expect do
  1380. put "/posts/#{post_record.id}", params: post_write_params(
  1381. base_version_no: base_version.version_no,
  1382. title: 'updated title',
  1383. tags: 'spec_tag',
  1384. original_created_from: Time.zone.local(2020, 1, 2, 0, 0, 0).iso8601,
  1385. original_created_before: Time.zone.local(2020, 1, 1, 0, 0, 0).iso8601)
  1386. end.not_to change(PostVersion, :count)
  1387. expect(response).to have_http_status(:unprocessable_entity)
  1388. end
  1389. end
  1390. describe 'tag versioning from post write actions' do
  1391. let(:member) { create(:user, :member) }
  1392. it 'creates tag snapshot for normalised tags on PUT /posts/:id' do
  1393. sign_in_as(member)
  1394. base_version = create_post_version_for!(post_record.reload)
  1395. tag_name2 = TagName.create!(name: 'spec_tag_2')
  1396. tag2 = Tag.create!(tag_name: tag_name2, category: :general)
  1397. expect {
  1398. put "/posts/#{post_record.id}", params: post_write_params(
  1399. base_version_no: base_version.version_no,
  1400. title: 'updated title',
  1401. tags: 'spec_tag_2')
  1402. }.to change { tag2.reload.tag_versions.count }.by(1)
  1403. expect(response).to have_http_status(:ok), response.body
  1404. end
  1405. end
  1406. end