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

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