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

564 lines
17 KiB

  1. require 'cgi'
  2. require 'rails_helper'
  3. require 'securerandom'
  4. RSpec.describe 'Wiki API', type: :request do
  5. def auth_headers(user)
  6. { 'X-Transfer-Code' => user.inheritance_code }
  7. end
  8. let!(:user) { create_member_user! }
  9. let!(:tn) { TagName.create!(name: 'spec_wiki_title') }
  10. let!(:page) do
  11. Wiki::Commit.create_content!(
  12. tag_name: tn,
  13. body: 'init',
  14. created_by_user: user,
  15. message: 'init')
  16. end
  17. describe 'GET /wiki' do
  18. it 'returns wiki pages with title' do
  19. get '/wiki'
  20. expect(response).to have_http_status(:ok)
  21. expect(json).to be_an(Array)
  22. expect(json).not_to be_empty
  23. expect(json[0]).to have_key('title')
  24. expect(json.map { |p| p['title'] }).to include('spec_wiki_title')
  25. end
  26. end
  27. describe 'GET /wiki/:id' do
  28. subject(:request) do
  29. get "/wiki/#{ page_id }"
  30. end
  31. let(:page_id) { page.id }
  32. context 'when wiki page exists' do
  33. it 'returns wiki page with title' do
  34. request
  35. expect(response).to have_http_status(:ok)
  36. expect(json).to include(
  37. 'id' => page.id,
  38. 'title' => 'spec_wiki_title')
  39. end
  40. end
  41. context 'when wiki page does not exist' do
  42. let(:page_id) { 9_999_999 }
  43. it 'returns 404' do
  44. request
  45. expect(response).to have_http_status(:not_found)
  46. end
  47. end
  48. end
  49. describe 'POST /wiki' do
  50. let(:endpoint) { '/wiki' }
  51. let(:member) { create(:user, role: 'member') }
  52. let(:guest) { create(:user, role: 'guest') }
  53. def auth_headers(user)
  54. { 'X-Transfer-Code' => user.inheritance_code }
  55. end
  56. context 'when not logged in' do
  57. it 'returns 401' do
  58. post endpoint, params: { title: 'Test', body: 'Hello' }
  59. expect(response).to have_http_status(:unauthorized)
  60. end
  61. end
  62. context 'when logged in but not member' do
  63. it 'returns 403' do
  64. post endpoint, params: { title: 'Test', body: 'Hello' }, headers: auth_headers(guest)
  65. expect(response).to have_http_status(:forbidden)
  66. end
  67. end
  68. context 'when params invalid' do
  69. it 'returns 422 when title blank' do
  70. post endpoint, params: { title: '', body: 'Hello' }, headers: auth_headers(member)
  71. expect(response).to have_http_status(:unprocessable_entity)
  72. end
  73. it 'returns 422 when body blank' do
  74. post endpoint, params: { title: 'Test', body: '' }, headers: auth_headers(member)
  75. expect(response).to have_http_status(:unprocessable_entity)
  76. end
  77. end
  78. context 'when success' do
  79. it 'creates wiki_page and first content revision' do
  80. expect do
  81. post endpoint, params: { title: 'TestPage', body: "a\nb\nc", message: 'init' },
  82. headers: auth_headers(member)
  83. end
  84. .to change(WikiPage, :count).by(1)
  85. .and change(WikiRevision, :count).by(1)
  86. .and change(WikiVersion, :count).by(1)
  87. expect(response).to have_http_status(:created)
  88. page_id = json.fetch('id')
  89. expect(json.fetch('title')).to eq('TestPage')
  90. expect(json.fetch('body')).to eq("a\nb\nc")
  91. created_page = WikiPage.find(page_id)
  92. version = created_page.wiki_versions.order(:version_no).last
  93. expect(version).to have_attributes(
  94. version_no: 1,
  95. event_type: 'create',
  96. title: 'TestPage',
  97. body: "a\nb\nc",
  98. created_by_user_id: member.id
  99. )
  100. rev = created_page.current_revision
  101. expect(rev).to be_present
  102. expect(rev).to be_content
  103. expect(rev.message).to eq('init')
  104. expect(created_page.body).to eq("a\nb\nc")
  105. expect(rev.lines_count).to eq(3)
  106. expect(rev.wiki_revision_lines.order(:position).pluck(:position)).to eq([0, 1, 2])
  107. expect(rev.wiki_lines.pluck(:body)).to match_array(['a', 'b', 'c'])
  108. end
  109. it 'reuses existing WikiLine rows by sha256' do
  110. # 先に同じ行を作っておく
  111. WikiLine.create!(sha256: Digest::SHA256.hexdigest('a'), body: 'a', created_at: Time.current, updated_at: Time.current)
  112. post endpoint,
  113. params: { title: 'Reuse', body: "a\na" },
  114. headers: auth_headers(member)
  115. page = WikiPage.find(JSON.parse(response.body).fetch('id'))
  116. rev = page.current_revision
  117. expect(rev.lines_count).to eq(2)
  118. # "a" の WikiLine が増殖しない(1行のはず)
  119. expect(WikiLine.where(body: 'a').count).to eq(1)
  120. end
  121. it 'deduplicates duplicated new lines before upsert' do
  122. duplicated = 'duplicated_line_for_wiki_line_upsert_spec'
  123. post endpoint,
  124. params: { title: 'DuplicateNewLine', body: "#{ duplicated }\n#{ duplicated }" },
  125. headers: auth_headers(member)
  126. expect(response).to have_http_status(:created)
  127. page = WikiPage.find(json.fetch('id'))
  128. rev = page.current_revision
  129. expect(rev.lines_count).to eq(2)
  130. expect(WikiLine.where(body: duplicated).count).to eq(1)
  131. expect(rev.wiki_revision_lines.count).to eq(2)
  132. expect(rev.wiki_revision_lines.pluck(:wiki_line_id).uniq.size).to eq(1)
  133. end
  134. it 'normalises CRLF and strips trailing newlines' do
  135. post endpoint,
  136. params: { title: 'NormalisedBody', body: "a\r\nb\r\n\r\n", message: 'normalise' },
  137. headers: auth_headers(member)
  138. expect(response).to have_http_status(:created)
  139. page = WikiPage.find(json.fetch('id'))
  140. rev = page.current_revision
  141. version = page.wiki_versions.order(:version_no).last
  142. expect(page.body).to eq("a\nb")
  143. expect(version.body).to eq("a\nb")
  144. expect(rev.lines_count).to eq(2)
  145. expect(rev.wiki_lines.order('wiki_revision_lines.position').map(&:body)).to eq(['a', 'b'])
  146. end
  147. end
  148. end
  149. describe 'PUT /wiki/:id' do
  150. let(:member) { create(:user, role: 'member', inheritance_code: SecureRandom.hex(16)) }
  151. let(:guest) { create(:user, role: 'guest', inheritance_code: SecureRandom.hex(16)) }
  152. def auth_headers(user)
  153. { 'X-Transfer-Code' => user.inheritance_code }
  154. end
  155. let!(:test_tag_name) { TagName.create!(name: 'TestPage') }
  156. let!(:page) do
  157. Wiki::Commit.create_content!(
  158. tag_name: test_tag_name,
  159. body: "a\nb",
  160. created_by_user: member,
  161. message: 'init')
  162. end
  163. context 'when not logged in' do
  164. it 'returns 401' do
  165. put "/wiki/#{page.id}", params: { title: 'TestPage', body: 'x' }
  166. expect(response).to have_http_status(:unauthorized)
  167. end
  168. end
  169. context 'when logged in but not member' do
  170. it 'returns 403' do
  171. put "/wiki/#{page.id}",
  172. params: { title: 'TestPage', body: 'x' },
  173. headers: auth_headers(guest)
  174. expect(response).to have_http_status(:forbidden)
  175. end
  176. end
  177. context 'when params invalid' do
  178. it 'returns 422 when body blank' do
  179. put "/wiki/#{page.id}",
  180. params: { title: 'TestPage', body: '' },
  181. headers: auth_headers(member)
  182. expect(response).to have_http_status(:unprocessable_entity)
  183. end
  184. end
  185. context 'when success' do
  186. it 'creates new revision and returns 200' do
  187. current_id = page.wiki_revisions.maximum(:id)
  188. expect do
  189. put "/wiki/#{page.id}",
  190. params: { title: 'TestPage', body: "x\ny", message: 'edit', base_revision_id: current_id },
  191. headers: auth_headers(member)
  192. end
  193. .to change(WikiRevision, :count).by(1)
  194. .and change(WikiVersion, :count).by(1)
  195. version = page.wiki_versions.order(:version_no).last
  196. expect(version).to have_attributes(
  197. event_type: 'update',
  198. title: 'TestPage',
  199. body: "x\ny",
  200. created_by_user_id: member.id
  201. )
  202. expect(response).to have_http_status(:ok)
  203. page.reload
  204. rev = page.current_revision
  205. expect(rev).to be_content
  206. expect(rev.message).to eq('edit')
  207. expect(page.body).to eq("x\ny")
  208. expect(rev.base_revision_id).to eq(current_id)
  209. end
  210. it 'wiki body だけを変更しても tag version は作成しない' do
  211. linked_tag_name = TagName.create!(name: 'wiki_body_only_tag')
  212. linked_tag = Tag.create!(tag_name: linked_tag_name, category: :general)
  213. TagVersionRecorder.record!(
  214. tag: linked_tag,
  215. event_type: :create,
  216. created_by_user: member)
  217. linked_page =
  218. Wiki::Commit.create_content!(
  219. tag_name: linked_tag_name,
  220. body: 'before',
  221. created_by_user: member,
  222. message: 'init')
  223. current_id = linked_page.current_revision.id
  224. before_count = linked_tag.reload.tag_versions.count
  225. expect {
  226. put "/wiki/#{ linked_page.id }",
  227. params: {
  228. title: 'wiki_body_only_tag',
  229. body: 'after',
  230. message: 'edit',
  231. base_revision_id: current_id,
  232. },
  233. headers: auth_headers(member)
  234. }
  235. .to change(WikiRevision, :count).by(1)
  236. .and change(WikiVersion, :count).by(1)
  237. expect(response).to have_http_status(:ok)
  238. expect(linked_tag.reload.tag_versions.count).to eq(before_count)
  239. end
  240. end
  241. context 'when conflict' do
  242. it 'returns 409 when base_revision_id mismatches' do
  243. # 先に別ユーザ(同じ member でもOK)が 1 回更新して先頭を進める
  244. Wiki::Commit.content!(page: page, body: "zzz", created_user: member, message: 'other edit')
  245. page.reload
  246. stale_id = page.wiki_revisions.order(:id).first.id # わざと古い id
  247. put "/wiki/#{page.id}",
  248. params: { title: 'TestPage', body: 'x', base_revision_id: stale_id },
  249. headers: auth_headers(member)
  250. expect(response).to have_http_status(:conflict)
  251. json = JSON.parse(response.body)
  252. expect(json['error']).to eq('conflict')
  253. end
  254. end
  255. context 'when page not found' do
  256. it 'returns 404' do
  257. put "/wiki/99999999",
  258. params: { title: 'X', body: 'x' },
  259. headers: auth_headers(member)
  260. expect(response).to have_http_status(:not_found)
  261. end
  262. end
  263. end
  264. describe 'GET /wiki/title/:title' do
  265. it 'returns wiki page by title' do
  266. get "/wiki/title/#{CGI.escape('spec_wiki_title')}"
  267. expect(response).to have_http_status(:ok)
  268. expect(json).to have_key('id')
  269. expect(json).to have_key('title')
  270. expect(json['id']).to eq(page.id)
  271. expect(json['title']).to eq('spec_wiki_title')
  272. end
  273. it 'returns 404 when not found' do
  274. get "/wiki/title/#{ CGI.escape('nope') }"
  275. expect(response).to have_http_status(:not_found)
  276. end
  277. end
  278. describe 'GET /wiki/search' do
  279. before do
  280. Wiki::Commit.create_content!(
  281. tag_name: TagName.create!(name: 'spec_wiki_title_2'),
  282. body: 'search body 2',
  283. created_by_user: user,
  284. message: 'init')
  285. Wiki::Commit.create_content!(
  286. tag_name: TagName.create!(name: 'unrelated_title'),
  287. body: 'unrelated body',
  288. created_by_user: user,
  289. message: 'init')
  290. end
  291. it 'returns up to 20 pages filtered by title like' do
  292. get "/wiki/search?title=#{CGI.escape('spec_wiki')}"
  293. expect(response).to have_http_status(:ok)
  294. expect(json).to be_an(Array)
  295. titles = json.map { |p| p['title'] }
  296. expect(titles).to include('spec_wiki_title')
  297. expect(titles).to include('spec_wiki_title_2')
  298. expect(titles).not_to include('unrelated_title')
  299. end
  300. it 'returns all when title param is blank' do
  301. get "/wiki/search?title=#{CGI.escape('')}"
  302. expect(response).to have_http_status(:ok)
  303. expect(json).to be_an(Array)
  304. expect(json.map { |p| p['title'] }).to include('spec_wiki_title')
  305. end
  306. end
  307. describe 'GET /wiki/changes' do
  308. let!(:rev1) do
  309. Wiki::Commit.content!(page: page, body: "a\nb", created_user: user, message: 'r1')
  310. page.current_revision
  311. end
  312. let!(:rev2) do
  313. Wiki::Commit.content!(page: page, body: "a\nc", created_user: user, message: 'r2')
  314. page.current_revision
  315. end
  316. it 'returns latest revisions (optionally filtered by page id)' do
  317. get "/wiki/changes?id=#{page.id}"
  318. expect(response).to have_http_status(:ok)
  319. expect(json).to be_an(Array)
  320. expect(json).not_to be_empty
  321. top = json.first
  322. expect(top).to include(
  323. 'revision_id' => rev2.id,
  324. 'pred' => rev2.base_revision_id,
  325. 'succ' => nil,
  326. 'kind' => 'content',
  327. 'message' => 'r2'
  328. )
  329. expect(top['wiki_page']).to include('id' => page.id, 'title' => 'spec_wiki_title')
  330. expect(top['user']).to include('id' => user.id, 'name' => user.name)
  331. expect(top).to have_key('timestamp')
  332. # order desc をざっくり担保
  333. ids = json.map { |x| x['revision_id'] }
  334. expect(ids).to eq(ids.sort.reverse)
  335. end
  336. it 'returns empty array when page has no revisions and filtered by id' do
  337. # 別ページを作って revision 無し
  338. tn2 = TagName.create!(name: 'spec_no_rev')
  339. # 異常データ: revision 無し WikiPage を直接作る
  340. p2 = WikiPage.create!(
  341. tag_name: tn2,
  342. body: 'init',
  343. created_user: user,
  344. updated_user: user)
  345. get "/wiki/changes?id=#{p2.id}"
  346. expect(response).to have_http_status(:ok)
  347. expect(json).to eq([])
  348. end
  349. end
  350. describe 'GET /wiki/title/:title/exists' do
  351. it 'returns 204 when exists' do
  352. get "/wiki/title/#{CGI.escape('spec_wiki_title')}/exists"
  353. expect(response).to have_http_status(:no_content)
  354. expect(response.body).to be_empty
  355. end
  356. it 'returns 404 when not exists' do
  357. get "/wiki/title/#{CGI.escape('nope')}/exists"
  358. expect(response).to have_http_status(:not_found)
  359. end
  360. end
  361. describe 'GET /wiki/:id/exists' do
  362. it 'returns 204 when exists' do
  363. get "/wiki/#{page.id}/exists"
  364. expect(response).to have_http_status(:no_content)
  365. expect(response.body).to be_empty
  366. end
  367. it 'returns 404 when not exists' do
  368. get "/wiki/99999999/exists"
  369. expect(response).to have_http_status(:not_found)
  370. end
  371. end
  372. describe 'GET /wiki/:id/diff' do
  373. let!(:rev_a) do
  374. Wiki::Commit.content!(page: page, body: "a\nb\nc", created_user: user, message: 'A')
  375. page.current_revision
  376. end
  377. let!(:rev_b) do
  378. Wiki::Commit.content!(page: page, body: "a\nx\nc", created_user: user, message: 'B')
  379. page.current_revision
  380. end
  381. it 'returns diff json between revisions' do
  382. get "/wiki/#{page.id}/diff?from=#{rev_a.id}&to=#{rev_b.id}"
  383. expect(response).to have_http_status(:ok)
  384. expect(json).to include(
  385. 'wiki_page_id' => page.id,
  386. 'title' => 'spec_wiki_title',
  387. 'older_revision_id' => rev_a.id,
  388. 'newer_revision_id' => rev_b.id
  389. )
  390. expect(json['diff']).to be_an(Array)
  391. # ざっくり「b が消えて x が増えた」が含まれることを確認
  392. types = json['diff'].map { |x| x['type'] }
  393. expect(types).to include('removed', 'added').or include('removed').and include('added')
  394. end
  395. it 'uses latest as "to" when to is omitted' do
  396. get "/wiki/#{page.id}/diff?from=#{rev_a.id}"
  397. expect(response).to have_http_status(:ok)
  398. expect(json['older_revision_id']).to eq(rev_a.id)
  399. expect(json['newer_revision_id']).to eq(page.current_revision.id)
  400. end
  401. end
  402. describe 'Wiki::Commit.redirect!' do
  403. it 'raises because redirect revisions are deprecated' do
  404. target_tag_name = TagName.create!(name: 'redirect_deprecated_target')
  405. target =
  406. Wiki::Commit.create_content!(
  407. tag_name: target_tag_name,
  408. body: 'target',
  409. created_by_user: user,
  410. message: 'init')
  411. expect {
  412. Wiki::Commit.redirect!(
  413. page: page,
  414. redirect_page: target,
  415. created_user: user,
  416. message: 'redirect',
  417. base_revision_id: page.current_revision.id
  418. )
  419. }.to raise_error(RuntimeError, '廃止しました.')
  420. end
  421. end
  422. it 'wiki title を変更すると対応する tag の version を作成する' do
  423. linked_tag_name = TagName.create!(name: 'wiki_linked_tag_for_version')
  424. linked_tag = Tag.create!(tag_name: linked_tag_name, category: :general)
  425. linked_page =
  426. Wiki::Commit.create_content!(
  427. tag_name: linked_tag_name,
  428. body: 'before',
  429. created_by_user: user,
  430. message: 'init')
  431. current_id = linked_page.current_revision.id
  432. expect {
  433. put "/wiki/#{ linked_page.id }",
  434. params: {
  435. title: 'wiki_linked_tag_for_version_renamed',
  436. body: 'after',
  437. message: 'rename',
  438. base_revision_id: current_id,
  439. },
  440. headers: auth_headers(user)
  441. }
  442. .to change(WikiRevision, :count).by(1)
  443. .and change(WikiVersion, :count).by(1)
  444. .and change { linked_tag.reload.tag_versions.count }.by(2)
  445. expect(response).to have_http_status(:ok)
  446. linked_tag.reload
  447. expect(linked_tag.name).to eq('wiki_linked_tag_for_version_renamed')
  448. versions = linked_tag.tag_versions.order(:version_no)
  449. expect(versions.first.event_type).to eq('create')
  450. expect(versions.first.name).to eq('wiki_linked_tag_for_version')
  451. expect(versions.second.event_type).to eq('update')
  452. expect(versions.second.name).to eq('wiki_linked_tag_for_version_renamed')
  453. end
  454. end