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

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