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

427 lines
14 KiB

  1. require 'cgi'
  2. require 'rails_helper'
  3. require 'securerandom'
  4. RSpec.describe 'Wiki API', type: :request do
  5. let!(:user) { create_member_user! }
  6. let!(:tn) { TagName.create!(name: 'spec_wiki_title') }
  7. let!(:page) do
  8. WikiPage.create!(tag_name: tn, created_user: user, updated_user: user).tap do |p|
  9. Wiki::Commit.content!(page: p, body: 'init', created_user: user, message: 'init')
  10. end
  11. end
  12. describe 'GET /wiki' do
  13. it 'returns wiki pages with title' do
  14. get '/wiki'
  15. expect(response).to have_http_status(:ok)
  16. expect(json).to be_an(Array)
  17. expect(json).not_to be_empty
  18. expect(json[0]).to have_key('title')
  19. expect(json.map { |p| p['title'] }).to include('spec_wiki_title')
  20. end
  21. end
  22. describe 'GET /wiki/:id' do
  23. subject(:request) do
  24. get "/wiki/#{ page_id }"
  25. end
  26. let(:page_id) { page.id }
  27. context 'when wiki page exists' do
  28. it 'returns wiki page with title' do
  29. request
  30. expect(response).to have_http_status(:ok)
  31. expect(json).to include(
  32. 'id' => page.id,
  33. 'title' => 'spec_wiki_title')
  34. end
  35. end
  36. context 'when wiki page does not exist' do
  37. let(:page_id) { 9_999_999 }
  38. it 'returns 404' do
  39. request
  40. expect(response).to have_http_status(:not_found)
  41. end
  42. end
  43. end
  44. describe 'POST /wiki' do
  45. let(:endpoint) { '/wiki' }
  46. let(:member) { create(:user, role: 'member') }
  47. let(:guest) { create(:user, role: 'guest') }
  48. def auth_headers(user)
  49. { 'X-Transfer-Code' => user.inheritance_code }
  50. end
  51. context 'when not logged in' do
  52. it 'returns 401' do
  53. post endpoint, params: { title: 'Test', body: 'Hello' }
  54. expect(response).to have_http_status(:unauthorized)
  55. end
  56. end
  57. context 'when logged in but not member' do
  58. it 'returns 403' do
  59. post endpoint, params: { title: 'Test', body: 'Hello' }, headers: auth_headers(guest)
  60. expect(response).to have_http_status(:forbidden)
  61. end
  62. end
  63. context 'when params invalid' do
  64. it 'returns 422 when title blank' do
  65. post endpoint, params: { title: '', body: 'Hello' }, headers: auth_headers(member)
  66. expect(response).to have_http_status(:unprocessable_entity)
  67. end
  68. it 'returns 422 when body blank' do
  69. post endpoint, params: { title: 'Test', body: '' }, headers: auth_headers(member)
  70. expect(response).to have_http_status(:unprocessable_entity)
  71. end
  72. end
  73. context 'when success' do
  74. it 'creates wiki_page and first content revision' do
  75. expect do
  76. post endpoint, params: { title: 'TestPage', body: "a\nb\nc", message: 'init' },
  77. headers: auth_headers(member)
  78. end
  79. .to change(WikiPage, :count).by(1)
  80. .and change(WikiRevision, :count).by(1)
  81. expect(response).to have_http_status(:created)
  82. page_id = json.fetch('id')
  83. expect(json.fetch('title')).to eq('TestPage')
  84. page = WikiPage.find(page_id)
  85. rev = page.current_revision
  86. expect(rev).to be_present
  87. expect(rev).to be_content
  88. expect(rev.message).to eq('init')
  89. # body が復元できること
  90. expect(page.body).to eq("a\nb\nc")
  91. # 行数とリレーションの整合
  92. expect(rev.lines_count).to eq(3)
  93. expect(rev.wiki_revision_lines.order(:position).pluck(:position)).to eq([0, 1, 2])
  94. expect(rev.wiki_lines.pluck(:body)).to match_array(%w[a b c])
  95. end
  96. it 'reuses existing WikiLine rows by sha256' do
  97. # 先に同じ行を作っておく
  98. WikiLine.create!(sha256: Digest::SHA256.hexdigest('a'), body: 'a', created_at: Time.current, updated_at: Time.current)
  99. post endpoint,
  100. params: { title: 'Reuse', body: "a\na" },
  101. headers: auth_headers(member)
  102. page = WikiPage.find(JSON.parse(response.body).fetch('id'))
  103. rev = page.current_revision
  104. expect(rev.lines_count).to eq(2)
  105. # "a" の WikiLine が増殖しない(1行のはず)
  106. expect(WikiLine.where(body: 'a').count).to eq(1)
  107. end
  108. end
  109. end
  110. describe 'PUT /wiki/:id' do
  111. let(:member) { create(:user, role: 'member', inheritance_code: SecureRandom.hex(16)) }
  112. let(:guest) { create(:user, role: 'guest', inheritance_code: SecureRandom.hex(16)) }
  113. def auth_headers(user)
  114. { 'X-Transfer-Code' => user.inheritance_code }
  115. end
  116. #let!(:page) { create(:wiki_page, title: 'TestPage') }
  117. let!(:page) do
  118. build(:wiki_page, title: 'TestPage').tap do |p|
  119. puts p.errors.full_messages unless p.valid?
  120. p.save!
  121. end
  122. end
  123. before do
  124. # 初期版を 1 つ作っておく(更新が“2版目”になるように)
  125. Wiki::Commit.content!(page: page, body: "a\nb", created_user: member, message: 'init')
  126. end
  127. context 'when not logged in' do
  128. it 'returns 401' do
  129. put "/wiki/#{page.id}", params: { title: 'TestPage', body: 'x' }
  130. expect(response).to have_http_status(:unauthorized)
  131. end
  132. end
  133. context 'when logged in but not member' do
  134. it 'returns 403' do
  135. put "/wiki/#{page.id}",
  136. params: { title: 'TestPage', body: 'x' },
  137. headers: auth_headers(guest)
  138. expect(response).to have_http_status(:forbidden)
  139. end
  140. end
  141. context 'when params invalid' do
  142. it 'returns 422 when body blank' do
  143. put "/wiki/#{page.id}",
  144. params: { title: 'TestPage', body: '' },
  145. headers: auth_headers(member)
  146. expect(response).to have_http_status(:unprocessable_entity)
  147. end
  148. it 'returns 422 when title mismatched (if you forbid rename here)' do
  149. put "/wiki/#{page.id}",
  150. params: { title: 'OtherTitle', body: 'x' },
  151. headers: auth_headers(member)
  152. # 君の controller 例だと title 変更は 422 にしてた
  153. expect(response).to have_http_status(:unprocessable_entity)
  154. end
  155. end
  156. context 'when success' do
  157. it 'creates new revision and returns 200' do
  158. current_id = page.wiki_revisions.maximum(:id)
  159. expect do
  160. put "/wiki/#{page.id}",
  161. params: { title: 'TestPage', body: "x\ny", message: 'edit', base_revision_id: current_id },
  162. headers: auth_headers(member)
  163. end.to change(WikiRevision, :count).by(1)
  164. expect(response).to have_http_status(:ok)
  165. page.reload
  166. rev = page.current_revision
  167. expect(rev).to be_content
  168. expect(rev.message).to eq('edit')
  169. expect(page.body).to eq("x\ny")
  170. expect(rev.base_revision_id).to eq(current_id)
  171. end
  172. end
  173. # TODO: コンフリクト未実装のため,実装したらコメント外す.
  174. # context 'when conflict' do
  175. # it 'returns 409 when base_revision_id mismatches' do
  176. # # 先に別ユーザ(同じ member でもOK)が 1 回更新して先頭を進める
  177. # Wiki::Commit.content!(page: page, body: "zzz", created_user: member, message: 'other edit')
  178. # page.reload
  179. # stale_id = page.wiki_revisions.order(:id).first.id # わざと古い id
  180. # put "/wiki/#{page.id}",
  181. # params: { title: 'TestPage', body: 'x', base_revision_id: stale_id },
  182. # headers: auth_headers(member)
  183. # expect(response).to have_http_status(:conflict)
  184. # json = JSON.parse(response.body)
  185. # expect(json['error']).to eq('conflict')
  186. # end
  187. # end
  188. context 'when page not found' do
  189. it 'returns 404' do
  190. put "/wiki/99999999",
  191. params: { title: 'X', body: 'x' },
  192. headers: auth_headers(member)
  193. expect(response).to have_http_status(:not_found)
  194. end
  195. end
  196. end
  197. describe 'GET /wiki/title/:title' do
  198. it 'returns wiki page by title' do
  199. get "/wiki/title/#{CGI.escape('spec_wiki_title')}"
  200. expect(response).to have_http_status(:ok)
  201. expect(json).to have_key('id')
  202. expect(json).to have_key('title')
  203. expect(json['id']).to eq(page.id)
  204. expect(json['title']).to eq('spec_wiki_title')
  205. end
  206. it 'returns 404 when not found' do
  207. get "/wiki/title/#{ CGI.escape('nope') }"
  208. expect(response).to have_http_status(:not_found)
  209. end
  210. end
  211. describe 'GET /wiki/search' do
  212. before do
  213. # 追加で検索ヒット用
  214. TagName.create!(name: 'spec_wiki_title_2')
  215. WikiPage.create!(tag_name: TagName.find_by!(name: 'spec_wiki_title_2'),
  216. created_user: user, updated_user: user)
  217. TagName.create!(name: 'unrelated_title')
  218. WikiPage.create!(tag_name: TagName.find_by!(name: 'unrelated_title'),
  219. created_user: user, updated_user: user)
  220. end
  221. it 'returns up to 20 pages filtered by title like' do
  222. get "/wiki/search?title=#{CGI.escape('spec_wiki')}"
  223. expect(response).to have_http_status(:ok)
  224. expect(json).to be_an(Array)
  225. titles = json.map { |p| p['title'] }
  226. expect(titles).to include('spec_wiki_title', 'spec_wiki_title_2')
  227. expect(titles).not_to include('unrelated_title')
  228. end
  229. it 'returns all when title param is blank' do
  230. get "/wiki/search?title=#{CGI.escape('')}"
  231. expect(response).to have_http_status(:ok)
  232. expect(json).to be_an(Array)
  233. expect(json.map { |p| p['title'] }).to include('spec_wiki_title')
  234. end
  235. end
  236. describe 'GET /wiki/changes' do
  237. let!(:rev1) do
  238. Wiki::Commit.content!(page: page, body: "a\nb", created_user: user, message: 'r1')
  239. page.current_revision
  240. end
  241. let!(:rev2) do
  242. Wiki::Commit.content!(page: page, body: "a\nc", created_user: user, message: 'r2')
  243. page.current_revision
  244. end
  245. it 'returns latest revisions (optionally filtered by page id)' do
  246. get "/wiki/changes?id=#{page.id}"
  247. expect(response).to have_http_status(:ok)
  248. expect(json).to be_an(Array)
  249. expect(json).not_to be_empty
  250. top = json.first
  251. expect(top).to include(
  252. 'revision_id' => rev2.id,
  253. 'pred' => rev2.base_revision_id,
  254. 'succ' => nil,
  255. 'kind' => 'content',
  256. 'message' => 'r2'
  257. )
  258. expect(top['wiki_page']).to include('id' => page.id, 'title' => 'spec_wiki_title')
  259. expect(top['user']).to include('id' => user.id, 'name' => user.name)
  260. expect(top).to have_key('timestamp')
  261. # order desc をざっくり担保
  262. ids = json.map { |x| x['revision_id'] }
  263. expect(ids).to eq(ids.sort.reverse)
  264. end
  265. it 'returns empty array when page has no revisions and filtered by id' do
  266. # 別ページを作って revision 無し
  267. tn2 = TagName.create!(name: 'spec_no_rev')
  268. p2 = WikiPage.create!(tag_name: tn2, created_user: user, updated_user: user)
  269. get "/wiki/changes?id=#{p2.id}"
  270. expect(response).to have_http_status(:ok)
  271. expect(json).to eq([])
  272. end
  273. end
  274. describe 'GET /wiki/title/:title/exists' do
  275. it 'returns 204 when exists' do
  276. get "/wiki/title/#{CGI.escape('spec_wiki_title')}/exists"
  277. expect(response).to have_http_status(:no_content)
  278. expect(response.body).to be_empty
  279. end
  280. it 'returns 404 when not exists' do
  281. get "/wiki/title/#{CGI.escape('nope')}/exists"
  282. expect(response).to have_http_status(:not_found)
  283. end
  284. end
  285. describe 'GET /wiki/:id/exists' do
  286. it 'returns 204 when exists' do
  287. get "/wiki/#{page.id}/exists"
  288. expect(response).to have_http_status(:no_content)
  289. expect(response.body).to be_empty
  290. end
  291. it 'returns 404 when not exists' do
  292. get "/wiki/99999999/exists"
  293. expect(response).to have_http_status(:not_found)
  294. end
  295. end
  296. describe 'GET /wiki/:id/diff' do
  297. let!(:rev_a) do
  298. Wiki::Commit.content!(page: page, body: "a\nb\nc", created_user: user, message: 'A')
  299. page.current_revision
  300. end
  301. let!(:rev_b) do
  302. Wiki::Commit.content!(page: page, body: "a\nx\nc", created_user: user, message: 'B')
  303. page.current_revision
  304. end
  305. it 'returns diff json between revisions' do
  306. get "/wiki/#{page.id}/diff?from=#{rev_a.id}&to=#{rev_b.id}"
  307. expect(response).to have_http_status(:ok)
  308. expect(json).to include(
  309. 'wiki_page_id' => page.id,
  310. 'title' => 'spec_wiki_title',
  311. 'older_revision_id' => rev_a.id,
  312. 'newer_revision_id' => rev_b.id
  313. )
  314. expect(json['diff']).to be_an(Array)
  315. # ざっくり「b が消えて x が増えた」が含まれることを確認
  316. types = json['diff'].map { |x| x['type'] }
  317. expect(types).to include('removed', 'added').or include('removed').and include('added')
  318. end
  319. it 'uses latest as "to" when to is omitted' do
  320. get "/wiki/#{page.id}/diff?from=#{rev_a.id}"
  321. expect(response).to have_http_status(:ok)
  322. expect(json['older_revision_id']).to eq(rev_a.id)
  323. expect(json['newer_revision_id']).to eq(page.current_revision.id)
  324. end
  325. it 'returns 422 when "to" is redirect revision' do
  326. # redirect revision を作る
  327. tn2 = TagName.create!(name: 'redirect_target')
  328. target = WikiPage.create!(tag_name: tn2, created_user: user, updated_user: user)
  329. Wiki::Commit.redirect!(page: page, redirect_page: target, created_user: user, message: 'redir')
  330. redirect_rev = page.current_revision
  331. expect(redirect_rev).to be_redirect
  332. get "/wiki/#{page.id}/diff?from=#{rev_a.id}&to=#{redirect_rev.id}"
  333. expect(response).to have_http_status(:unprocessable_entity)
  334. end
  335. it 'returns 422 when "from" is redirect revision' do
  336. tn2 = TagName.create!(name: 'redirect_target2')
  337. target = WikiPage.create!(tag_name: tn2, created_user: user, updated_user: user)
  338. Wiki::Commit.redirect!(page: page, redirect_page: target, created_user: user, message: 'redir2')
  339. redirect_rev = page.current_revision
  340. get "/wiki/#{page.id}/diff?from=#{redirect_rev.id}&to=#{rev_b.id}"
  341. expect(response).to have_http_status(:unprocessable_entity)
  342. end
  343. end
  344. end