0ff7fdf78a
#317 #317 #317 #317 #317 #317 Co-authored-by: miteruzo <miteruzo@naver.com> Reviewed-on: #333
563 lines
17 KiB
Ruby
563 lines
17 KiB
Ruby
require 'cgi'
|
|
require 'rails_helper'
|
|
require 'securerandom'
|
|
|
|
|
|
RSpec.describe 'Wiki API', type: :request do
|
|
def auth_headers(user)
|
|
{ 'X-Transfer-Code' => user.inheritance_code }
|
|
end
|
|
|
|
let!(:user) { create_member_user! }
|
|
|
|
let!(:tn) { TagName.create!(name: 'spec_wiki_title') }
|
|
let!(:page) do
|
|
Wiki::Commit.create_content!(
|
|
tag_name: tn,
|
|
body: 'init',
|
|
created_by_user: user,
|
|
message: 'init')
|
|
end
|
|
|
|
describe 'GET /wiki' do
|
|
it 'returns wiki pages with title' do
|
|
get '/wiki'
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
expect(json).to be_an(Array)
|
|
expect(json).not_to be_empty
|
|
|
|
expect(json[0]).to have_key('title')
|
|
expect(json.map { |p| p['title'] }).to include('spec_wiki_title')
|
|
end
|
|
end
|
|
|
|
describe 'GET /wiki/:id' do
|
|
subject(:request) do
|
|
get "/wiki/#{ page_id }"
|
|
end
|
|
|
|
let(:page_id) { page.id }
|
|
|
|
context 'when wiki page exists' do
|
|
it 'returns wiki page with title' do
|
|
request
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
expect(json).to include(
|
|
'id' => page.id,
|
|
'title' => 'spec_wiki_title')
|
|
end
|
|
end
|
|
|
|
context 'when wiki page does not exist' do
|
|
let(:page_id) { 9_999_999 }
|
|
|
|
it 'returns 404' do
|
|
request
|
|
|
|
expect(response).to have_http_status(:not_found)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'POST /wiki' do
|
|
let(:endpoint) { '/wiki' }
|
|
|
|
let(:member) { create(:user, role: 'member') }
|
|
let(:guest) { create(:user, role: 'guest') }
|
|
|
|
def auth_headers(user)
|
|
{ 'X-Transfer-Code' => user.inheritance_code }
|
|
end
|
|
|
|
context 'when not logged in' do
|
|
it 'returns 401' do
|
|
post endpoint, params: { title: 'Test', body: 'Hello' }
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when logged in but not member' do
|
|
it 'returns 403' do
|
|
post endpoint, params: { title: 'Test', body: 'Hello' }, headers: auth_headers(guest)
|
|
expect(response).to have_http_status(:forbidden)
|
|
end
|
|
end
|
|
|
|
context 'when params invalid' do
|
|
it 'returns 422 when title blank' do
|
|
post endpoint, params: { title: '', body: 'Hello' }, headers: auth_headers(member)
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
end
|
|
|
|
it 'returns 422 when body blank' do
|
|
post endpoint, params: { title: 'Test', body: '' }, headers: auth_headers(member)
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
end
|
|
end
|
|
|
|
context 'when success' do
|
|
it 'creates wiki_page and first content revision' do
|
|
expect do
|
|
post endpoint, params: { title: 'TestPage', body: "a\nb\nc", message: 'init' },
|
|
headers: auth_headers(member)
|
|
end
|
|
.to change(WikiPage, :count).by(1)
|
|
.and change(WikiRevision, :count).by(1)
|
|
.and change(WikiVersion, :count).by(1)
|
|
|
|
expect(response).to have_http_status(:created)
|
|
|
|
page_id = json.fetch('id')
|
|
expect(json.fetch('title')).to eq('TestPage')
|
|
|
|
created_page = WikiPage.find(page_id)
|
|
version = created_page.wiki_versions.order(:version_no).last
|
|
|
|
expect(version).to have_attributes(
|
|
version_no: 1,
|
|
event_type: 'create',
|
|
title: 'TestPage',
|
|
body: "a\nb\nc",
|
|
created_by_user_id: member.id
|
|
)
|
|
|
|
rev = created_page.current_revision
|
|
expect(rev).to be_present
|
|
expect(rev).to be_content
|
|
expect(rev.message).to eq('init')
|
|
|
|
expect(created_page.body).to eq("a\nb\nc")
|
|
|
|
expect(rev.lines_count).to eq(3)
|
|
expect(rev.wiki_revision_lines.order(:position).pluck(:position)).to eq([0, 1, 2])
|
|
expect(rev.wiki_lines.pluck(:body)).to match_array(['a', 'b', 'c'])
|
|
end
|
|
|
|
it 'reuses existing WikiLine rows by sha256' do
|
|
# 先に同じ行を作っておく
|
|
WikiLine.create!(sha256: Digest::SHA256.hexdigest('a'), body: 'a', created_at: Time.current, updated_at: Time.current)
|
|
|
|
post endpoint,
|
|
params: { title: 'Reuse', body: "a\na" },
|
|
headers: auth_headers(member)
|
|
|
|
page = WikiPage.find(JSON.parse(response.body).fetch('id'))
|
|
rev = page.current_revision
|
|
expect(rev.lines_count).to eq(2)
|
|
|
|
# "a" の WikiLine が増殖しない(1行のはず)
|
|
expect(WikiLine.where(body: 'a').count).to eq(1)
|
|
end
|
|
|
|
it 'deduplicates duplicated new lines before upsert' do
|
|
duplicated = 'duplicated_line_for_wiki_line_upsert_spec'
|
|
|
|
post endpoint,
|
|
params: { title: 'DuplicateNewLine', body: "#{ duplicated }\n#{ duplicated }" },
|
|
headers: auth_headers(member)
|
|
|
|
expect(response).to have_http_status(:created)
|
|
|
|
page = WikiPage.find(json.fetch('id'))
|
|
rev = page.current_revision
|
|
|
|
expect(rev.lines_count).to eq(2)
|
|
expect(WikiLine.where(body: duplicated).count).to eq(1)
|
|
expect(rev.wiki_revision_lines.count).to eq(2)
|
|
expect(rev.wiki_revision_lines.pluck(:wiki_line_id).uniq.size).to eq(1)
|
|
end
|
|
|
|
it 'normalises CRLF and strips trailing newlines' do
|
|
post endpoint,
|
|
params: { title: 'NormalisedBody', body: "a\r\nb\r\n\r\n", message: 'normalise' },
|
|
headers: auth_headers(member)
|
|
|
|
expect(response).to have_http_status(:created)
|
|
|
|
page = WikiPage.find(json.fetch('id'))
|
|
rev = page.current_revision
|
|
version = page.wiki_versions.order(:version_no).last
|
|
|
|
expect(page.body).to eq("a\nb")
|
|
expect(version.body).to eq("a\nb")
|
|
expect(rev.lines_count).to eq(2)
|
|
expect(rev.wiki_lines.order('wiki_revision_lines.position').map(&:body)).to eq(['a', 'b'])
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'PUT /wiki/:id' do
|
|
let(:member) { create(:user, role: 'member', inheritance_code: SecureRandom.hex(16)) }
|
|
let(:guest) { create(:user, role: 'guest', inheritance_code: SecureRandom.hex(16)) }
|
|
|
|
def auth_headers(user)
|
|
{ 'X-Transfer-Code' => user.inheritance_code }
|
|
end
|
|
|
|
let!(:test_tag_name) { TagName.create!(name: 'TestPage') }
|
|
|
|
let!(:page) do
|
|
Wiki::Commit.create_content!(
|
|
tag_name: test_tag_name,
|
|
body: "a\nb",
|
|
created_by_user: member,
|
|
message: 'init')
|
|
end
|
|
|
|
context 'when not logged in' do
|
|
it 'returns 401' do
|
|
put "/wiki/#{page.id}", params: { title: 'TestPage', body: 'x' }
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when logged in but not member' do
|
|
it 'returns 403' do
|
|
put "/wiki/#{page.id}",
|
|
params: { title: 'TestPage', body: 'x' },
|
|
headers: auth_headers(guest)
|
|
expect(response).to have_http_status(:forbidden)
|
|
end
|
|
end
|
|
|
|
context 'when params invalid' do
|
|
it 'returns 422 when body blank' do
|
|
put "/wiki/#{page.id}",
|
|
params: { title: 'TestPage', body: '' },
|
|
headers: auth_headers(member)
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
end
|
|
end
|
|
|
|
context 'when success' do
|
|
it 'creates new revision and returns 200' do
|
|
current_id = page.wiki_revisions.maximum(:id)
|
|
|
|
expect do
|
|
put "/wiki/#{page.id}",
|
|
params: { title: 'TestPage', body: "x\ny", message: 'edit', base_revision_id: current_id },
|
|
headers: auth_headers(member)
|
|
end
|
|
.to change(WikiRevision, :count).by(1)
|
|
.and change(WikiVersion, :count).by(1)
|
|
|
|
version = page.wiki_versions.order(:version_no).last
|
|
|
|
expect(version).to have_attributes(
|
|
event_type: 'update',
|
|
title: 'TestPage',
|
|
body: "x\ny",
|
|
created_by_user_id: member.id
|
|
)
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
page.reload
|
|
rev = page.current_revision
|
|
expect(rev).to be_content
|
|
expect(rev.message).to eq('edit')
|
|
expect(page.body).to eq("x\ny")
|
|
expect(rev.base_revision_id).to eq(current_id)
|
|
end
|
|
|
|
it 'wiki body だけを変更しても tag version は作成しない' do
|
|
linked_tag_name = TagName.create!(name: 'wiki_body_only_tag')
|
|
linked_tag = Tag.create!(tag_name: linked_tag_name, category: :general)
|
|
|
|
TagVersionRecorder.record!(
|
|
tag: linked_tag,
|
|
event_type: :create,
|
|
created_by_user: member)
|
|
|
|
linked_page =
|
|
Wiki::Commit.create_content!(
|
|
tag_name: linked_tag_name,
|
|
body: 'before',
|
|
created_by_user: member,
|
|
message: 'init')
|
|
|
|
current_id = linked_page.current_revision.id
|
|
before_count = linked_tag.reload.tag_versions.count
|
|
|
|
expect {
|
|
put "/wiki/#{ linked_page.id }",
|
|
params: {
|
|
title: 'wiki_body_only_tag',
|
|
body: 'after',
|
|
message: 'edit',
|
|
base_revision_id: current_id,
|
|
},
|
|
headers: auth_headers(member)
|
|
}
|
|
.to change(WikiRevision, :count).by(1)
|
|
.and change(WikiVersion, :count).by(1)
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
expect(linked_tag.reload.tag_versions.count).to eq(before_count)
|
|
end
|
|
end
|
|
|
|
context 'when conflict' do
|
|
it 'returns 409 when base_revision_id mismatches' do
|
|
# 先に別ユーザ(同じ member でもOK)が 1 回更新して先頭を進める
|
|
Wiki::Commit.content!(page: page, body: "zzz", created_user: member, message: 'other edit')
|
|
page.reload
|
|
|
|
stale_id = page.wiki_revisions.order(:id).first.id # わざと古い id
|
|
put "/wiki/#{page.id}",
|
|
params: { title: 'TestPage', body: 'x', base_revision_id: stale_id },
|
|
headers: auth_headers(member)
|
|
|
|
expect(response).to have_http_status(:conflict)
|
|
json = JSON.parse(response.body)
|
|
expect(json['error']).to eq('conflict')
|
|
end
|
|
end
|
|
|
|
context 'when page not found' do
|
|
it 'returns 404' do
|
|
put "/wiki/99999999",
|
|
params: { title: 'X', body: 'x' },
|
|
headers: auth_headers(member)
|
|
expect(response).to have_http_status(:not_found)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'GET /wiki/title/:title' do
|
|
it 'returns wiki page by title' do
|
|
get "/wiki/title/#{CGI.escape('spec_wiki_title')}"
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
expect(json).to have_key('id')
|
|
expect(json).to have_key('title')
|
|
expect(json['id']).to eq(page.id)
|
|
expect(json['title']).to eq('spec_wiki_title')
|
|
end
|
|
|
|
it 'returns 404 when not found' do
|
|
get "/wiki/title/#{ CGI.escape('nope') }"
|
|
expect(response).to have_http_status(:not_found)
|
|
end
|
|
end
|
|
|
|
describe 'GET /wiki/search' do
|
|
before do
|
|
Wiki::Commit.create_content!(
|
|
tag_name: TagName.create!(name: 'spec_wiki_title_2'),
|
|
body: 'search body 2',
|
|
created_by_user: user,
|
|
message: 'init')
|
|
|
|
Wiki::Commit.create_content!(
|
|
tag_name: TagName.create!(name: 'unrelated_title'),
|
|
body: 'unrelated body',
|
|
created_by_user: user,
|
|
message: 'init')
|
|
end
|
|
|
|
it 'returns up to 20 pages filtered by title like' do
|
|
get "/wiki/search?title=#{CGI.escape('spec_wiki')}"
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
expect(json).to be_an(Array)
|
|
|
|
titles = json.map { |p| p['title'] }
|
|
|
|
expect(titles).to include('spec_wiki_title')
|
|
expect(titles).to include('spec_wiki_title_2')
|
|
expect(titles).not_to include('unrelated_title')
|
|
end
|
|
|
|
it 'returns all when title param is blank' do
|
|
get "/wiki/search?title=#{CGI.escape('')}"
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
expect(json).to be_an(Array)
|
|
expect(json.map { |p| p['title'] }).to include('spec_wiki_title')
|
|
end
|
|
end
|
|
|
|
describe 'GET /wiki/changes' do
|
|
let!(:rev1) do
|
|
Wiki::Commit.content!(page: page, body: "a\nb", created_user: user, message: 'r1')
|
|
page.current_revision
|
|
end
|
|
|
|
let!(:rev2) do
|
|
Wiki::Commit.content!(page: page, body: "a\nc", created_user: user, message: 'r2')
|
|
page.current_revision
|
|
end
|
|
|
|
it 'returns latest revisions (optionally filtered by page id)' do
|
|
get "/wiki/changes?id=#{page.id}"
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
expect(json).to be_an(Array)
|
|
expect(json).not_to be_empty
|
|
|
|
top = json.first
|
|
expect(top).to include(
|
|
'revision_id' => rev2.id,
|
|
'pred' => rev2.base_revision_id,
|
|
'succ' => nil,
|
|
'kind' => 'content',
|
|
'message' => 'r2'
|
|
)
|
|
expect(top['wiki_page']).to include('id' => page.id, 'title' => 'spec_wiki_title')
|
|
expect(top['user']).to include('id' => user.id, 'name' => user.name)
|
|
expect(top).to have_key('timestamp')
|
|
|
|
# order desc をざっくり担保
|
|
ids = json.map { |x| x['revision_id'] }
|
|
expect(ids).to eq(ids.sort.reverse)
|
|
end
|
|
|
|
it 'returns empty array when page has no revisions and filtered by id' do
|
|
# 別ページを作って revision 無し
|
|
tn2 = TagName.create!(name: 'spec_no_rev')
|
|
# 異常データ: revision 無し WikiPage を直接作る
|
|
p2 = WikiPage.create!(
|
|
tag_name: tn2,
|
|
body: 'init',
|
|
created_user: user,
|
|
updated_user: user)
|
|
|
|
get "/wiki/changes?id=#{p2.id}"
|
|
expect(response).to have_http_status(:ok)
|
|
expect(json).to eq([])
|
|
end
|
|
end
|
|
|
|
describe 'GET /wiki/title/:title/exists' do
|
|
it 'returns 204 when exists' do
|
|
get "/wiki/title/#{CGI.escape('spec_wiki_title')}/exists"
|
|
expect(response).to have_http_status(:no_content)
|
|
expect(response.body).to be_empty
|
|
end
|
|
|
|
it 'returns 404 when not exists' do
|
|
get "/wiki/title/#{CGI.escape('nope')}/exists"
|
|
expect(response).to have_http_status(:not_found)
|
|
end
|
|
end
|
|
|
|
describe 'GET /wiki/:id/exists' do
|
|
it 'returns 204 when exists' do
|
|
get "/wiki/#{page.id}/exists"
|
|
expect(response).to have_http_status(:no_content)
|
|
expect(response.body).to be_empty
|
|
end
|
|
|
|
it 'returns 404 when not exists' do
|
|
get "/wiki/99999999/exists"
|
|
expect(response).to have_http_status(:not_found)
|
|
end
|
|
end
|
|
|
|
describe 'GET /wiki/:id/diff' do
|
|
let!(:rev_a) do
|
|
Wiki::Commit.content!(page: page, body: "a\nb\nc", created_user: user, message: 'A')
|
|
page.current_revision
|
|
end
|
|
|
|
let!(:rev_b) do
|
|
Wiki::Commit.content!(page: page, body: "a\nx\nc", created_user: user, message: 'B')
|
|
page.current_revision
|
|
end
|
|
|
|
it 'returns diff json between revisions' do
|
|
get "/wiki/#{page.id}/diff?from=#{rev_a.id}&to=#{rev_b.id}"
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
expect(json).to include(
|
|
'wiki_page_id' => page.id,
|
|
'title' => 'spec_wiki_title',
|
|
'older_revision_id' => rev_a.id,
|
|
'newer_revision_id' => rev_b.id
|
|
)
|
|
expect(json['diff']).to be_an(Array)
|
|
# ざっくり「b が消えて x が増えた」が含まれることを確認
|
|
types = json['diff'].map { |x| x['type'] }
|
|
expect(types).to include('removed', 'added').or include('removed').and include('added')
|
|
end
|
|
|
|
it 'uses latest as "to" when to is omitted' do
|
|
get "/wiki/#{page.id}/diff?from=#{rev_a.id}"
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
expect(json['older_revision_id']).to eq(rev_a.id)
|
|
expect(json['newer_revision_id']).to eq(page.current_revision.id)
|
|
end
|
|
end
|
|
|
|
describe 'Wiki::Commit.redirect!' do
|
|
it 'raises because redirect revisions are deprecated' do
|
|
target_tag_name = TagName.create!(name: 'redirect_deprecated_target')
|
|
target =
|
|
Wiki::Commit.create_content!(
|
|
tag_name: target_tag_name,
|
|
body: 'target',
|
|
created_by_user: user,
|
|
message: 'init')
|
|
|
|
expect {
|
|
Wiki::Commit.redirect!(
|
|
page: page,
|
|
redirect_page: target,
|
|
created_user: user,
|
|
message: 'redirect',
|
|
base_revision_id: page.current_revision.id
|
|
)
|
|
}.to raise_error(RuntimeError, '廃止しました.')
|
|
end
|
|
end
|
|
|
|
it 'wiki title を変更すると対応する tag の version を作成する' do
|
|
linked_tag_name = TagName.create!(name: 'wiki_linked_tag_for_version')
|
|
linked_tag = Tag.create!(tag_name: linked_tag_name, category: :general)
|
|
|
|
linked_page =
|
|
Wiki::Commit.create_content!(
|
|
tag_name: linked_tag_name,
|
|
body: 'before',
|
|
created_by_user: user,
|
|
message: 'init')
|
|
|
|
current_id = linked_page.current_revision.id
|
|
|
|
expect {
|
|
put "/wiki/#{ linked_page.id }",
|
|
params: {
|
|
title: 'wiki_linked_tag_for_version_renamed',
|
|
body: 'after',
|
|
message: 'rename',
|
|
base_revision_id: current_id,
|
|
},
|
|
headers: auth_headers(user)
|
|
}
|
|
.to change(WikiRevision, :count).by(1)
|
|
.and change(WikiVersion, :count).by(1)
|
|
.and change { linked_tag.reload.tag_versions.count }.by(2)
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
linked_tag.reload
|
|
expect(linked_tag.name).to eq('wiki_linked_tag_for_version_renamed')
|
|
|
|
versions = linked_tag.tag_versions.order(:version_no)
|
|
|
|
expect(versions.first.event_type).to eq('create')
|
|
expect(versions.first.name).to eq('wiki_linked_tag_for_version')
|
|
|
|
expect(versions.second.event_type).to eq('update')
|
|
expect(versions.second.name).to eq('wiki_linked_tag_for_version_renamed')
|
|
end
|
|
end
|