feat: タグ名を別管理に変更(#215) (#219)
Merge branch 'main' into feature/215 #215 ニコニコ同期テスト #215 テスト・ケース追加 #215 テスト・ケース追加 #215 テスト・ケース追加 #215 テスト・ケース追加 Merge remote-tracking branch 'origin/main' into feature/215 Merge branch 'main' into feature/215 #215 #215 Merge remote-tracking branch 'origin/main' into feature/215 #215 Co-authored-by: miteruzo <miteruzo@naver.com> Reviewed-on: #219
This commit was merged in pull request #219.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
FactoryBot.define do
|
||||
factory :tag_name do
|
||||
name { "tag-#{SecureRandom.hex(4)}" }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
FactoryBot.define do
|
||||
factory :tag do
|
||||
category { 'general' }
|
||||
post_count { 0 }
|
||||
association :tag_name
|
||||
|
||||
trait :nico do
|
||||
category { 'nico' }
|
||||
tag_name { association(:tag_name, name: "nico:#{ SecureRandom.hex(4) }") }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
FactoryBot.define do
|
||||
factory :user do
|
||||
name { "test-user" }
|
||||
inheritance_code { SecureRandom.uuid }
|
||||
role { "guest" }
|
||||
|
||||
trait :member do
|
||||
role { "member" }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
FactoryBot.define do
|
||||
factory :wiki_page do
|
||||
title { "TestPage" }
|
||||
association :created_user, factory: :user
|
||||
association :updated_user, factory: :user
|
||||
end
|
||||
end
|
||||
@@ -8,6 +8,10 @@ abort("The Rails environment is running in production mode!") if Rails.env.produ
|
||||
# that will avoid rails generators crashing because migrations haven't been run yet
|
||||
# return unless Rails.env.test?
|
||||
require 'rspec/rails'
|
||||
Dir[Rails.root.join('spec/support/**/*.rb')].each do |f|
|
||||
require f
|
||||
end
|
||||
|
||||
# Add additional requires below this line. Rails is not loaded until this point!
|
||||
|
||||
# Requires supporting ruby files with custom matchers and macros, etc, in
|
||||
@@ -34,11 +38,20 @@ begin
|
||||
rescue ActiveRecord::PendingMigrationError => e
|
||||
abort e.to_s.strip
|
||||
end
|
||||
Dir[Rails.root.join('spec/support/**/*.rb')].each do |f|
|
||||
require f
|
||||
end
|
||||
RSpec.configure do |config|
|
||||
config.before do
|
||||
I18n.locale = :en
|
||||
end
|
||||
|
||||
config.include TestRecords
|
||||
config.include RakeTaskHelper
|
||||
|
||||
# FactoryBot の create / create_list を使へるやぅにする
|
||||
config.include FactoryBot::Syntax::Methods
|
||||
|
||||
# request spec で helper 使へるやぅにする
|
||||
config.include AuthHelper, type: :request
|
||||
config.include JsonHelper, type: :request
|
||||
|
||||
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
|
||||
config.fixture_paths = [
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
require 'rails_helper'
|
||||
|
||||
|
||||
RSpec.describe 'NicoTags', type: :request do
|
||||
describe 'GET /tags/nico' do
|
||||
it 'returns tags and next_cursor when overflowing limit' do
|
||||
create_list(:tag, 21, :nico)
|
||||
get '/tags/nico', params: { limit: 20 }
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json['tags'].size).to eq(20)
|
||||
expect(json['next_cursor']).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PATCH /tags/nico/:id' do
|
||||
let(:member) { create(:user, :member) }
|
||||
let(:nico_tag) { create(:tag, :nico) }
|
||||
|
||||
it '401 when not logged in' do
|
||||
sign_out
|
||||
patch "/tags/nico/#{nico_tag.id}", params: { tags: 'a b' }
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it '403 when not member' do
|
||||
sign_in_as(create(:user, role: 'guest'))
|
||||
patch "/tags/nico/#{nico_tag.id}", params: { tags: 'a b' }
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
|
||||
it '400 when target is not nico category' do
|
||||
sign_in_as(member)
|
||||
non_nico = create(:tag, :general)
|
||||
patch "/tags/nico/#{non_nico.id}", params: { tags: 'a b' }
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,30 +1,235 @@
|
||||
require 'rails_helper'
|
||||
require 'set'
|
||||
|
||||
|
||||
RSpec.describe 'Posts API', type: :request do
|
||||
# create / update で thumbnail.attach は走るが、
|
||||
# resized_thumbnail! が MiniMagick 依存でコケやすいので request spec ではスタブしとくのが無難。
|
||||
before do
|
||||
allow_any_instance_of(Post).to receive(:resized_thumbnail!).and_return(true)
|
||||
end
|
||||
|
||||
def dummy_upload
|
||||
# 中身は何でもいい(加工処理はスタブしてる)
|
||||
Rack::Test::UploadedFile.new(StringIO.new('dummy'), 'image/jpeg', original_filename: 'dummy.jpg')
|
||||
end
|
||||
|
||||
let!(:tag_name) { TagName.create!(name: 'spec_tag') }
|
||||
let!(:tag) { Tag.create!(tag_name: tag_name, category: 'general') }
|
||||
|
||||
let!(:post_record) do
|
||||
Post.create!(title: 'spec post', url: 'https://example.com/spec').tap do |p|
|
||||
PostTag.create!(post: p, tag: tag)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /posts' do
|
||||
it 'returns tags with name in JSON' do
|
||||
tn = TagName.create!(name: 'gen:spec_tag')
|
||||
tag = Tag.create!(tag_name: tn, category: 'general')
|
||||
|
||||
post = Post.create!(title: 'spec post', url: 'https://example.com/spec')
|
||||
PostTag.create!(post: post, tag: tag)
|
||||
|
||||
it 'returns posts with tag name in JSON' do
|
||||
get '/posts'
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
json = JSON.parse(response.body)
|
||||
expect(json).to have_key('posts')
|
||||
expect(json['posts']).to be_a(Array)
|
||||
expect(json['posts']).to be_an(Array)
|
||||
expect(json['posts']).not_to be_empty
|
||||
|
||||
tags = json['posts'][0]['tags']
|
||||
expect(tags).to be_a(Array)
|
||||
expect(tags).to be_an(Array)
|
||||
expect(tags).not_to be_empty
|
||||
|
||||
# Tag は name カラムを持たないので、API 側が methods: [:name] 等で出す必要がある
|
||||
expect(tags[0]).to have_key('name')
|
||||
expect(tags[0]['name']).to eq('gen:spec_tag')
|
||||
expect(tags.map { |t| t['name'] }).to include('spec_tag')
|
||||
expect(tags[0]).to include('category')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /posts/:id' do
|
||||
subject(:request) { get "/posts/#{post_id}" }
|
||||
|
||||
context 'when post exists' do
|
||||
let(:post_id) { post_record.id }
|
||||
|
||||
it 'returns post with tag tree + related + viewed' do
|
||||
request
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
expect(json).to include('id' => post_record.id)
|
||||
expect(json).to have_key('tags')
|
||||
expect(json['tags']).to be_an(Array)
|
||||
|
||||
# show は build_tag_tree_for を使うので、tags はツリー形式(children 付き)
|
||||
node = json['tags'][0]
|
||||
expect(node).to include('id', 'name', 'category', 'post_count', 'children')
|
||||
expect(node['name']).to eq('spec_tag')
|
||||
|
||||
expect(json).to have_key('related')
|
||||
expect(json['related']).to be_an(Array)
|
||||
|
||||
expect(json).to have_key('viewed')
|
||||
expect([true, false]).to include(json['viewed'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when post does not exist' do
|
||||
let(:post_id) { 999_999_999 }
|
||||
|
||||
it 'returns 404' do
|
||||
request
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /posts' do
|
||||
let(:member) { create(:user, :member) }
|
||||
|
||||
it '401 when not logged in' do
|
||||
sign_out
|
||||
post '/posts', params: { title: 't', url: 'https://example.com/x', tags: 'a', thumbnail: dummy_upload }
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it '403 when not member' do
|
||||
sign_in_as(create(:user, role: 'guest'))
|
||||
post '/posts', params: { title: 't', url: 'https://example.com/x', tags: 'a', thumbnail: dummy_upload }
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
|
||||
it '201 and creates post + tags when member' do
|
||||
sign_in_as(member)
|
||||
|
||||
post '/posts', params: {
|
||||
title: 'new post',
|
||||
url: 'https://example.com/new',
|
||||
tags: 'spec_tag', # 既存タグ名を投げる
|
||||
thumbnail: dummy_upload
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:created)
|
||||
expect(json).to include('id', 'title', 'url')
|
||||
|
||||
# tags が name を含むこと(API 側の serialization が正しいこと)
|
||||
expect(json).to have_key('tags')
|
||||
expect(json['tags']).to be_an(Array)
|
||||
expect(json['tags'][0]).to have_key('name')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /posts/:id' do
|
||||
let(:member) { create(:user, :member) }
|
||||
|
||||
it '401 when not logged in' do
|
||||
sign_out
|
||||
put "/posts/#{post_record.id}", params: { title: 'updated', tags: 'spec_tag' }
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it '403 when not member' do
|
||||
sign_in_as(create(:user, role: 'guest'))
|
||||
put "/posts/#{post_record.id}", params: { title: 'updated', tags: 'spec_tag' }
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
|
||||
it '200 and updates title + resync tags when member' do
|
||||
sign_in_as(member)
|
||||
|
||||
# 追加で別タグも作って、更新時に入れ替わることを見る
|
||||
tn2 = TagName.create!(name: 'spec_tag_2')
|
||||
Tag.create!(tag_name: tn2, category: 'general')
|
||||
|
||||
put "/posts/#{post_record.id}", params: {
|
||||
title: 'updated title',
|
||||
tags: 'spec_tag_2'
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json).to have_key('tags')
|
||||
expect(json['tags']).to be_an(Array)
|
||||
|
||||
# show と同様、update 後レスポンスもツリー形式
|
||||
names = json['tags'].map { |n| n['name'] }
|
||||
expect(names).to include('spec_tag_2')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /posts/random' do
|
||||
it '404 when no posts' do
|
||||
PostTag.delete_all
|
||||
Post.delete_all
|
||||
get '/posts/random'
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
|
||||
it '200 and returns viewed boolean' do
|
||||
get '/posts/random'
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json).to have_key('viewed')
|
||||
expect([true, false]).to include(json['viewed'])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /posts/changes' do
|
||||
let(:member) { create(:user, :member) }
|
||||
|
||||
it 'returns add/remove events (history) for a post' do
|
||||
# add
|
||||
tn2 = TagName.create!(name: 'spec_tag2')
|
||||
tag2 = Tag.create!(tag_name: tn2, category: 'general')
|
||||
pt = PostTag.create!(post: post_record, tag: tag2, created_user: member)
|
||||
|
||||
# remove (discard)
|
||||
pt.discard_by!(member)
|
||||
|
||||
get '/posts/changes', params: { id: post_record.id }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json).to include('changes', 'count')
|
||||
expect(json['changes']).to be_an(Array)
|
||||
expect(json['count']).to be >= 2
|
||||
|
||||
types = json['changes'].map { |e| e['change_type'] }.uniq
|
||||
expect(types).to include('add')
|
||||
expect(types).to include('remove')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /posts/:id/viewed' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it '401 when not logged in' do
|
||||
sign_out
|
||||
post "/posts/#{ post_record.id }/viewed"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it '204 and marks viewed when logged in' do
|
||||
sign_in_as(user)
|
||||
post "/posts/#{ post_record.id }/viewed"
|
||||
expect(response).to have_http_status(:no_content)
|
||||
|
||||
expect(user.reload.viewed?(post_record)).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /posts/:id/unviewed' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it '401 when not logged in' do
|
||||
sign_out
|
||||
delete "/posts/#{ post_record.id }/viewed"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it '204 and unmarks viewed when logged in' do
|
||||
sign_in_as(user)
|
||||
|
||||
# 先に viewed 付けてから外す
|
||||
user.viewed_posts << post_record
|
||||
|
||||
delete "/posts/#{ post_record.id }/viewed"
|
||||
expect(response).to have_http_status(:no_content)
|
||||
|
||||
expect(user.reload.viewed?(post_record)).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
require "rails_helper"
|
||||
|
||||
|
||||
RSpec.describe "Preview", type: :request do
|
||||
describe "GET /preview/title" do
|
||||
it "401 unless logged in" do
|
||||
sign_out
|
||||
get "/preview/title", params: { url: "example.com" }
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it "400 when url blank" do
|
||||
sign_in_as(create(:user))
|
||||
get "/preview/title", params: { url: "" }
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
end
|
||||
|
||||
it "returns parsed title (stubbing URI.open)" do
|
||||
sign_in_as(create(:user))
|
||||
fake_html = "<html><head><title> Hello </title></head></html>"
|
||||
allow(URI).to receive(:open).and_return(StringIO.new(fake_html))
|
||||
|
||||
get "/preview/title", params: { url: "example.com" }
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json["title"]).to eq("Hello")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -11,23 +11,50 @@ RSpec.describe 'Tags API', type: :request do
|
||||
get '/tags'
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
json = JSON.parse(response.body)
|
||||
|
||||
expect(json).to be_a(Array)
|
||||
expect(json).to be_an(Array)
|
||||
expect(json).not_to be_empty
|
||||
expect(json[0]).to have_key('name')
|
||||
expect(json.map { |t| t['name'] }).to include('spec_tag')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /tags/:id' do
|
||||
subject(:request) do
|
||||
get "/tags/#{ tag_id }"
|
||||
end
|
||||
|
||||
let(:tag_id) { tag.id }
|
||||
|
||||
context 'when tag exists' do
|
||||
it 'returns tag with name' do
|
||||
request
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
expect(json).to include(
|
||||
'id' => tag.id,
|
||||
'name' => 'spec_tag',
|
||||
'category' => 'general')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when tag does not exist' do
|
||||
let(:tag_id) { 9_999_999 }
|
||||
|
||||
it 'returns 404' do
|
||||
request
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /tags/autocomplete' do
|
||||
it 'returns matching tags by q' do
|
||||
get '/tags/autocomplete', params: { q: 'spec' }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
json = JSON.parse(response.body)
|
||||
|
||||
expect(json).to be_a(Array)
|
||||
expect(json).to be_an(Array)
|
||||
expect(json.map { |t| t['name'] }).to include('spec_tag')
|
||||
end
|
||||
end
|
||||
@@ -37,12 +64,16 @@ RSpec.describe 'Tags API', type: :request do
|
||||
get "/tags/name/#{ CGI.escape('spec_tag') }"
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
json = JSON.parse(response.body)
|
||||
|
||||
expect(json).to have_key('id')
|
||||
expect(json).to have_key('name')
|
||||
expect(json['id']).to eq(tag.id)
|
||||
expect(json['name']).to eq('spec_tag')
|
||||
end
|
||||
|
||||
it 'returns 404 when not found' do
|
||||
get "/tags/name/#{ CGI.escape('nope') }"
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
require "rails_helper"
|
||||
|
||||
|
||||
RSpec.describe "Users", type: :request do
|
||||
describe "POST /users" do
|
||||
it "creates guest user and returns code" do
|
||||
post "/users"
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json["code"]).to be_present
|
||||
expect(json["user"]["role"]).to eq("guest")
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /users/code/renew" do
|
||||
it "returns 401 when not logged in" do
|
||||
sign_out
|
||||
post "/users/code/renew"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
describe "PUT /users/:id" do
|
||||
let(:user) { create(:user, name: "old-name", role: "guest") }
|
||||
|
||||
it "returns 401 when current_user id mismatch" do
|
||||
sign_in_as(create(:user))
|
||||
put "/users/#{user.id}", params: { name: "new-name" }
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it "returns 400 when name is blank" do
|
||||
sign_in_as(user)
|
||||
put "/users/#{user.id}", params: { name: " " }
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
end
|
||||
|
||||
it "updates name and returns 201 with user slice" do
|
||||
sign_in_as(user)
|
||||
put "/users/#{user.id}", params: { name: "new-name" }
|
||||
|
||||
expect(response).to have_http_status(:created)
|
||||
expect(json["id"]).to eq(user.id)
|
||||
expect(json["name"]).to eq("new-name")
|
||||
|
||||
user.reload
|
||||
expect(user.name).to eq("new-name")
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /users/verify" do
|
||||
it "returns valid:false when code not found" do
|
||||
post "/users/verify", params: { code: "nope" }
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json["valid"]).to eq(false)
|
||||
end
|
||||
|
||||
it "creates IpAddress and UserIp, and returns valid:true with user slice" do
|
||||
user = create(:user, inheritance_code: SecureRandom.uuid, role: "guest")
|
||||
|
||||
# request.remote_ip を固定
|
||||
allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return("203.0.113.10")
|
||||
|
||||
expect {
|
||||
post "/users/verify", params: { code: user.inheritance_code }
|
||||
}.to change(UserIp, :count).by(1)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json["valid"]).to eq(true)
|
||||
expect(json["user"]["id"]).to eq(user.id)
|
||||
expect(json["user"]["inheritance_code"]).to eq(user.inheritance_code)
|
||||
expect(json["user"]["role"]).to eq("guest")
|
||||
|
||||
# ついでに IpAddress もできてることを確認(ipの保存形式がバイナリでも count で見れる)
|
||||
expect(IpAddress.count).to be >= 1
|
||||
end
|
||||
|
||||
it "is idempotent for same user+ip (does not create duplicate UserIp)" do
|
||||
user = create(:user, inheritance_code: SecureRandom.uuid, role: "guest")
|
||||
allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return("203.0.113.10")
|
||||
|
||||
post "/users/verify", params: { code: user.inheritance_code }
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
expect {
|
||||
post "/users/verify", params: { code: user.inheritance_code }
|
||||
}.not_to change(UserIp, :count)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json["valid"]).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /users/me" do
|
||||
it "returns 404 when code not found" do
|
||||
get "/users/me", params: { code: "nope" }
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
|
||||
it "returns user slice when found" do
|
||||
user = create(:user, inheritance_code: SecureRandom.uuid, name: "me", role: "guest")
|
||||
get "/users/me", params: { code: user.inheritance_code }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json["id"]).to eq(user.id)
|
||||
expect(json["name"]).to eq("me")
|
||||
expect(json["inheritance_code"]).to eq(user.inheritance_code)
|
||||
expect(json["role"]).to eq("guest")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,7 +4,7 @@ require 'securerandom'
|
||||
|
||||
|
||||
RSpec.describe 'Wiki API', type: :request do
|
||||
let!(:user) { create_user_for_wiki! }
|
||||
let!(:user) { create_member_user! }
|
||||
|
||||
let!(:tn) { TagName.create!(name: 'spec_wiki_title') }
|
||||
let!(:page) do
|
||||
@@ -16,9 +16,8 @@ RSpec.describe 'Wiki API', type: :request do
|
||||
get '/wiki'
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
json = JSON.parse(response.body)
|
||||
|
||||
expect(json).to be_a(Array)
|
||||
expect(json).to be_an(Array)
|
||||
expect(json).not_to be_empty
|
||||
|
||||
expect(json[0]).to have_key('title')
|
||||
@@ -26,17 +25,400 @@ RSpec.describe 'Wiki API', type: :request do
|
||||
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)
|
||||
|
||||
expect(response).to have_http_status(:created)
|
||||
|
||||
page_id = json.fetch('id')
|
||||
expect(json.fetch('title')).to eq('TestPage')
|
||||
|
||||
page = WikiPage.find(page_id)
|
||||
rev = page.current_revision
|
||||
expect(rev).to be_present
|
||||
expect(rev).to be_content
|
||||
expect(rev.message).to eq('init')
|
||||
|
||||
# body が復元できること
|
||||
expect(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(%w[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
|
||||
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!(:page) { create(:wiki_page, title: 'TestPage') }
|
||||
let!(:page) do
|
||||
build(:wiki_page, title: 'TestPage').tap do |p|
|
||||
puts p.errors.full_messages unless p.valid?
|
||||
p.save!
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
# 初期版を 1 つ作っておく(更新が“2版目”になるように)
|
||||
Wiki::Commit.content!(page: page, body: "a\nb", created_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
|
||||
|
||||
it 'returns 422 when title mismatched (if you forbid rename here)' do
|
||||
put "/wiki/#{page.id}",
|
||||
params: { title: 'OtherTitle', body: 'x' },
|
||||
headers: auth_headers(member)
|
||||
# 君の controller 例だと title 変更は 422 にしてた
|
||||
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)
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
# TODO: コンフリクト未実装のため,実装したらコメント外す.
|
||||
# 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)
|
||||
json = JSON.parse(response.body)
|
||||
|
||||
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
|
||||
# 追加で検索ヒット用
|
||||
TagName.create!(name: 'spec_wiki_title_2')
|
||||
WikiPage.create!(tag_name: TagName.find_by!(name: 'spec_wiki_title_2'),
|
||||
created_user: user, updated_user: user)
|
||||
|
||||
TagName.create!(name: 'unrelated_title')
|
||||
WikiPage.create!(tag_name: TagName.find_by!(name: 'unrelated_title'),
|
||||
created_user: user, updated_user: user)
|
||||
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', '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')
|
||||
p2 = WikiPage.create!(tag_name: tn2, 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
|
||||
|
||||
it 'returns 422 when "to" is redirect revision' do
|
||||
# redirect revision を作る
|
||||
tn2 = TagName.create!(name: 'redirect_target')
|
||||
target = WikiPage.create!(tag_name: tn2, created_user: user, updated_user: user)
|
||||
|
||||
Wiki::Commit.redirect!(page: page, redirect_page: target, created_user: user, message: 'redir')
|
||||
redirect_rev = page.current_revision
|
||||
expect(redirect_rev).to be_redirect
|
||||
|
||||
get "/wiki/#{page.id}/diff?from=#{rev_a.id}&to=#{redirect_rev.id}"
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it 'returns 422 when "from" is redirect revision' do
|
||||
tn2 = TagName.create!(name: 'redirect_target2')
|
||||
target = WikiPage.create!(tag_name: tn2, created_user: user, updated_user: user)
|
||||
|
||||
Wiki::Commit.redirect!(page: page, redirect_page: target, created_user: user, message: 'redir2')
|
||||
redirect_rev = page.current_revision
|
||||
|
||||
get "/wiki/#{page.id}/diff?from=#{redirect_rev.id}&to=#{rev_b.id}"
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
module AuthHelper
|
||||
def sign_in_as(user)
|
||||
allow_any_instance_of(ApplicationController)
|
||||
.to receive(:current_user).and_return(user)
|
||||
end
|
||||
|
||||
def sign_out
|
||||
allow_any_instance_of(ApplicationController)
|
||||
.to receive(:current_user).and_return(nil)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
module JsonHelper
|
||||
def json
|
||||
JSON.parse(response.body)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
require "rake"
|
||||
|
||||
|
||||
module RakeTaskHelper
|
||||
# Railsの rake task を一度だけロードする
|
||||
def load_rails_tasks!
|
||||
return if defined?(@rails_tasks_loaded) && @rails_tasks_loaded
|
||||
@rails_tasks_loaded = true
|
||||
|
||||
Rake.application = Rake::Application.new
|
||||
Rails.application.load_tasks
|
||||
end
|
||||
|
||||
def run_rake_task(full_name)
|
||||
load_rails_tasks!
|
||||
|
||||
task = Rake::Task[full_name] # ここは rake[...] じゃなくて良い
|
||||
task.reenable
|
||||
task.invoke
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,86 @@
|
||||
require "rails_helper"
|
||||
|
||||
|
||||
RSpec.describe "nico:sync" do
|
||||
def stub_python(json_array)
|
||||
status = instance_double(Process::Status, success?: true)
|
||||
allow(Open3).to receive(:capture3).and_return([json_array.to_json, "", status])
|
||||
end
|
||||
|
||||
def create_tag!(name, category:)
|
||||
tn = TagName.find_or_create_by!(name: name.to_s.strip)
|
||||
Tag.find_or_create_by!(tag_name_id: tn.id) { |t| t.category = category }
|
||||
end
|
||||
|
||||
def link_nico_to_tag!(nico_tag, tag)
|
||||
# NicoTagRelation がある前提(君の model からそう見える)
|
||||
NicoTagRelation.create!(nico_tag_id: nico_tag.id, tag_id: tag.id)
|
||||
end
|
||||
|
||||
it "既存 post を見つけて、nico tag と linked tag を追加し、差分が出たら bot を付ける" do
|
||||
# 既存 post(正規表現で拾われるURL)
|
||||
post = Post.create!(title: "old", url: "https://www.nicovideo.jp/watch/sm9", uploaded_user: nil)
|
||||
|
||||
# 既存の非nicoタグ(kept_non_nico_ids)
|
||||
kept_general = create_tag!("spec_kept", category: "general")
|
||||
PostTag.create!(post: post, tag: kept_general)
|
||||
|
||||
# 追加される linked tag を準備(nico tag に紐付く一般タグ)
|
||||
linked = create_tag!("spec_linked", category: "general")
|
||||
nico = create_tag!("nico:AAA", category: "nico")
|
||||
link_nico_to_tag!(nico, linked)
|
||||
|
||||
# bot / tagme は task 内で使うので作っておく(Tag.bot/tagme がある前提)
|
||||
Tag.bot
|
||||
Tag.tagme
|
||||
|
||||
# pythonの出力(AAA が付く)
|
||||
stub_python([{ "code" => "sm9", "title" => "t", "tags" => ["AAA"] }])
|
||||
|
||||
# 外部HTTPは今回「既存 post なので呼ばれない」はずだが、念のため塞ぐ
|
||||
allow(URI).to receive(:open).and_return(StringIO.new("<html></html>"))
|
||||
|
||||
run_rake_task("nico:sync")
|
||||
|
||||
post.reload
|
||||
active_tag_names = post.tags.joins(:tag_name).pluck("tag_names.name")
|
||||
|
||||
expect(active_tag_names).to include("spec_kept")
|
||||
expect(active_tag_names).to include("nico:AAA")
|
||||
expect(active_tag_names).to include("spec_linked")
|
||||
|
||||
# 差分が出るので bot が付く(kept_non_nico_ids != desired_non_nico_ids)
|
||||
expect(active_tag_names).to include("bot操作")
|
||||
end
|
||||
|
||||
it "既存 post にあった古い nico tag は active から外され、履歴として discard される" do
|
||||
post = Post.create!(title: "old", url: "https://www.nicovideo.jp/watch/sm9", uploaded_user: nil)
|
||||
|
||||
# 旧nicoタグ(今回の同期結果に含まれない)
|
||||
old_nico = create_tag!("nico:OLD", category: "nico")
|
||||
old_pt = PostTag.create!(post: post, tag: old_nico)
|
||||
expect(old_pt.discarded_at).to be_nil
|
||||
|
||||
# 今回は NEW のみ欲しい
|
||||
new_nico = create_tag!("nico:NEW", category: "nico")
|
||||
|
||||
# bot/tagme 念のため
|
||||
Tag.bot
|
||||
Tag.tagme
|
||||
|
||||
stub_python([{ "code" => "sm9", "title" => "t", "tags" => ["NEW"] }])
|
||||
allow(URI).to receive(:open).and_return(StringIO.new("<html></html>"))
|
||||
|
||||
run_rake_task("nico:sync")
|
||||
|
||||
# OLD は active から外れる(discarded_at が入る)
|
||||
old_pts = PostTag.where(post_id: post.id, tag_id: old_nico.id).order(:id).to_a
|
||||
expect(old_pts.last.discarded_at).to be_present
|
||||
|
||||
# NEW は active にいる
|
||||
post.reload
|
||||
active_names = post.tags.joins(:tag_name).pluck("tag_names.name")
|
||||
expect(active_names).to include("nico:NEW")
|
||||
expect(active_names).not_to include("nico:OLD")
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user