Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fd479cd93a | |||
| 6ee621e565 | |||
| 43122a49c3 | |||
| e90c4d612e | |||
| 39101ca422 | |||
| fcd3b87b2a | |||
| 0ff7fdf78a | |||
| b2c3e02ccc |
@@ -238,18 +238,26 @@ class TagsController < ApplicationController
|
|||||||
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user)
|
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user)
|
||||||
|
|
||||||
old_name = tag.name
|
old_name = tag.name
|
||||||
|
name_changed = name != old_name
|
||||||
|
wiki_page = tag.tag_name.wiki_page if name_changed
|
||||||
|
|
||||||
tag.update!(category:)
|
tag.update!(category:)
|
||||||
tag.tag_name.update!(name:)
|
tag.tag_name.update!(name:)
|
||||||
|
|
||||||
alias_names << old_name if name != old_name
|
alias_names << old_name if name_changed
|
||||||
alias_names.delete(name)
|
alias_names.delete(name)
|
||||||
|
|
||||||
update_aliases!(tag, alias_names)
|
update_aliases!(tag, alias_names)
|
||||||
update_parent_tags!(tag, parent_names)
|
update_parent_tags!(tag, parent_names)
|
||||||
|
|
||||||
tag.reload
|
tag.reload
|
||||||
record_tag_version!(tag, event_type: :update, created_by_user: current_user)
|
|
||||||
|
record_tag_version!(
|
||||||
|
tag,
|
||||||
|
event_type: :update,
|
||||||
|
created_by_user: current_user,
|
||||||
|
name_changed:,
|
||||||
|
wiki_page:)
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: TagRepr.base(tag.reload)
|
render json: TagRepr.base(tag.reload)
|
||||||
@@ -272,10 +280,21 @@ class TagsController < ApplicationController
|
|||||||
ApplicationRecord.transaction do
|
ApplicationRecord.transaction do
|
||||||
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user)
|
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user)
|
||||||
|
|
||||||
|
old_name = tag.name
|
||||||
|
name_changed = name.present? && name != old_name
|
||||||
|
wiki_page = tag.tag_name.wiki_page if name_changed
|
||||||
|
|
||||||
tag.tag_name.update!(name:) if name.present?
|
tag.tag_name.update!(name:) if name.present?
|
||||||
tag.update!(category:) if category.present?
|
tag.update!(category:) if category.present?
|
||||||
|
|
||||||
record_tag_version!(tag, event_type: :update, created_by_user: current_user)
|
tag.reload
|
||||||
|
|
||||||
|
record_tag_version!(
|
||||||
|
tag,
|
||||||
|
event_type: :update,
|
||||||
|
created_by_user: current_user,
|
||||||
|
name_changed:,
|
||||||
|
wiki_page:)
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: TagRepr.base(tag.reload)
|
render json: TagRepr.base(tag.reload)
|
||||||
@@ -297,17 +316,47 @@ class TagsController < ApplicationController
|
|||||||
material: material.as_json&.merge(file:, content_type:))
|
material: material.as_json&.merge(file:, content_type:))
|
||||||
end
|
end
|
||||||
|
|
||||||
def record_tag_version! tag, event_type:, created_by_user:
|
def record_tag_version! tag, event_type:, created_by_user:, name_changed: false, wiki_page: nil
|
||||||
if tag.nico?
|
if tag.nico?
|
||||||
NicoTagVersionRecorder.record!(tag:, event_type:, created_by_user:)
|
NicoTagVersionRecorder.record!(tag:, event_type:, created_by_user:)
|
||||||
else
|
return
|
||||||
TagVersionRecorder.record!(tag:, event_type:, created_by_user:)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
TagVersionRecorder.record!(tag:, event_type:, created_by_user:)
|
||||||
|
|
||||||
|
return unless name_changed
|
||||||
|
|
||||||
|
wiki_page ||= tag.tag_name.wiki_page
|
||||||
|
return unless wiki_page&.wiki_versions&.exists?
|
||||||
|
|
||||||
|
WikiVersionRecorder.record!(
|
||||||
|
page: wiki_page,
|
||||||
|
event_type: :update,
|
||||||
|
created_by_user:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_aliases! tag, alias_names
|
def update_aliases! tag, alias_names
|
||||||
|
alias_names = alias_names.uniq
|
||||||
|
|
||||||
|
affected_tags = [tag]
|
||||||
|
|
||||||
current_aliases = tag.tag_name.aliases.to_a
|
current_aliases = tag.tag_name.aliases.to_a
|
||||||
|
|
||||||
|
current_aliases.each do |alias_tag_name|
|
||||||
|
next if alias_names.include?(alias_tag_name.name)
|
||||||
|
|
||||||
|
affected_tags << alias_tag_name.canonical&.tag
|
||||||
|
end
|
||||||
|
|
||||||
|
alias_names.each do |alias_name|
|
||||||
|
alias_tag_name = TagName.find_undiscard_or_create_by!(name: alias_name)
|
||||||
|
affected_tags << alias_tag_name.canonical&.tag
|
||||||
|
end
|
||||||
|
|
||||||
|
affected_tags.compact.uniq.each do |affected_tag|
|
||||||
|
TagVersioning.ensure_snapshot!(affected_tag, created_by_user: current_user)
|
||||||
|
end
|
||||||
|
|
||||||
current_aliases.each do |alias_tag_name|
|
current_aliases.each do |alias_tag_name|
|
||||||
next if alias_names.include?(alias_tag_name.name)
|
next if alias_names.include?(alias_tag_name.name)
|
||||||
|
|
||||||
@@ -318,6 +367,10 @@ class TagsController < ApplicationController
|
|||||||
alias_tag_name = TagName.find_undiscard_or_create_by!(name: alias_name)
|
alias_tag_name = TagName.find_undiscard_or_create_by!(name: alias_name)
|
||||||
alias_tag_name.update!(canonical: tag.tag_name)
|
alias_tag_name.update!(canonical: tag.tag_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
affected_tags.compact.uniq.each do |affected_tag|
|
||||||
|
record_tag_version!(affected_tag, event_type: :update, created_by_user: current_user)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_parent_tags! tag, parent_names
|
def update_parent_tags! tag, parent_names
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
class UsersController < ApplicationController
|
class UsersController < ApplicationController
|
||||||
def create
|
def create
|
||||||
user = User.create!(inheritance_code: SecureRandom.uuid, role: 'guest')
|
return head :unprocessable_entity if request.remote_ip.blank?
|
||||||
|
|
||||||
|
user = nil
|
||||||
|
|
||||||
|
User.transaction do
|
||||||
|
user = User.create!(inheritance_code: SecureRandom.uuid, role: :guest)
|
||||||
|
attach_ip_address!(user)
|
||||||
|
end
|
||||||
|
|
||||||
render json: { code: user.inheritance_code,
|
render json: { code: user.inheritance_code,
|
||||||
user: user.slice(:id, :name, :inheritance_code, :role) }
|
user: user.slice(:id, :name, :inheritance_code, :role) },
|
||||||
|
status: :created
|
||||||
end
|
end
|
||||||
|
|
||||||
def verify
|
def verify
|
||||||
ip_bin = IPAddr.new(request.remote_ip).hton
|
|
||||||
ip_address = IpAddress.find_or_create_by!(ip_address: ip_bin)
|
|
||||||
|
|
||||||
user = User.find_by(inheritance_code: params[:code])
|
user = User.find_by(inheritance_code: params[:code])
|
||||||
return render json: { valid: false } unless user
|
return render json: { valid: false } unless user
|
||||||
|
|
||||||
UserIp.find_or_create_by!(user:, ip_address:)
|
return head :unprocessable_entity if request.remote_ip.blank?
|
||||||
|
|
||||||
|
attach_ip_address!(user)
|
||||||
|
|
||||||
render json: { valid: true, user: user.slice(:id, :name, :inheritance_code, :role) }
|
render json: { valid: true, user: user.slice(:id, :name, :inheritance_code, :role) }
|
||||||
end
|
end
|
||||||
@@ -41,9 +49,18 @@ class UsersController < ApplicationController
|
|||||||
return head :bad_request if name.blank?
|
return head :bad_request if name.blank?
|
||||||
|
|
||||||
if user.update(name:)
|
if user.update(name:)
|
||||||
render json: user.slice(:id, :name, :inheritance_code, :role), status: :created
|
render json: user.slice(:id, :name, :inheritance_code, :role), status: :ok
|
||||||
else
|
else
|
||||||
render json: user.errors, status: :unprocessable_entity
|
render json: user.errors, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def attach_ip_address! user
|
||||||
|
ip_bin = IPAddr.new(request.remote_ip).hton
|
||||||
|
ip_address = IpAddress.create_or_find_by!(ip_address: ip_bin)
|
||||||
|
|
||||||
|
UserIp.create_or_find_by!(user:, ip_address:)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -85,22 +85,24 @@ class WikiPagesController < ApplicationController
|
|||||||
return head :unauthorized unless current_user
|
return head :unauthorized unless current_user
|
||||||
return head :forbidden unless current_user.gte_member?
|
return head :forbidden unless current_user.gte_member?
|
||||||
|
|
||||||
name = params[:title]&.strip
|
title = params[:title].to_s.strip
|
||||||
body = params[:body].to_s
|
body = params[:body].to_s
|
||||||
|
|
||||||
return head :unprocessable_entity if name.blank? || body.blank?
|
|
||||||
|
|
||||||
tag_name = TagName.find_undiscard_or_create_by!(name:)
|
|
||||||
page = WikiPage.new(tag_name:, created_user: current_user, updated_user: current_user)
|
|
||||||
if page.save
|
|
||||||
message = params[:message].presence
|
message = params[:message].presence
|
||||||
Wiki::Commit.content!(page:, body:, created_user: current_user, message:)
|
|
||||||
|
return head :unprocessable_entity if title.blank? || body.blank?
|
||||||
|
|
||||||
|
tag_name = TagName.find_undiscard_or_create_by!(name: title)
|
||||||
|
|
||||||
|
page =
|
||||||
|
Wiki::Commit.create_content!(
|
||||||
|
tag_name:,
|
||||||
|
body:,
|
||||||
|
created_by_user: current_user,
|
||||||
|
message:)
|
||||||
|
|
||||||
render json: WikiPageRepr.base(page), status: :created
|
render json: WikiPageRepr.base(page), status: :created
|
||||||
else
|
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
|
||||||
render json: { errors: page.errors.full_messages },
|
head :unprocessable_entity
|
||||||
status: :unprocessable_entity
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@@ -113,12 +115,21 @@ class WikiPagesController < ApplicationController
|
|||||||
return head :unprocessable_entity if title.blank? || body.blank?
|
return head :unprocessable_entity if title.blank? || body.blank?
|
||||||
|
|
||||||
page = WikiPage.find(params[:id])
|
page = WikiPage.find(params[:id])
|
||||||
base_revision_id = page.current_revision.id
|
base_revision_id = params[:base_revision_id].presence
|
||||||
|
|
||||||
if params[:title].present? && params[:title].strip != page.title
|
ApplicationRecord.transaction do
|
||||||
return head :unprocessable_entity
|
page.lock!
|
||||||
|
|
||||||
|
old_title = page.title
|
||||||
|
|
||||||
|
tag = Tag.find_by(tag_name_id: page.tag_name_id)
|
||||||
|
|
||||||
|
if tag && title != old_title
|
||||||
|
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
page.tag_name.update!(name: title) if title != old_title
|
||||||
|
|
||||||
message = params[:message].presence
|
message = params[:message].presence
|
||||||
Wiki::Commit.content!(page:,
|
Wiki::Commit.content!(page:,
|
||||||
body:,
|
body:,
|
||||||
@@ -126,6 +137,12 @@ class WikiPagesController < ApplicationController
|
|||||||
message:,
|
message:,
|
||||||
base_revision_id:)
|
base_revision_id:)
|
||||||
|
|
||||||
|
if tag && title != old_title
|
||||||
|
tag.reload
|
||||||
|
TagVersionRecorder.record!(tag:, event_type: :update, created_by_user: current_user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,6 @@ class IpAddress < ApplicationRecord
|
|||||||
validates :ip_address, presence: true, length: { maximum: 16 }
|
validates :ip_address, presence: true, length: { maximum: 16 }
|
||||||
validates :banned, inclusion: { in: [true, false] }
|
validates :banned, inclusion: { in: [true, false] }
|
||||||
|
|
||||||
has_many :users
|
has_many :user_ips, dependent: :destroy
|
||||||
|
has_many :users, through: :user_ips
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ class Tag < ApplicationRecord
|
|||||||
def self.no_deerjikist = find_or_create_by_tag_name!('ニジラー情報不詳', category: :meta)
|
def self.no_deerjikist = find_or_create_by_tag_name!('ニジラー情報不詳', category: :meta)
|
||||||
def self.video = find_or_create_by_tag_name!('動画', category: :meta)
|
def self.video = find_or_create_by_tag_name!('動画', category: :meta)
|
||||||
def self.niconico = find_or_create_by_tag_name!('ニコニコ', category: :meta)
|
def self.niconico = find_or_create_by_tag_name!('ニコニコ', category: :meta)
|
||||||
|
def self.youtube = find_or_create_by_tag_name!('YouTube', category: :meta)
|
||||||
|
|
||||||
def self.normalise_tags tag_names, with_tagme: true,
|
def self.normalise_tags tag_names, with_tagme: true,
|
||||||
with_no_deerjikist: true,
|
with_no_deerjikist: true,
|
||||||
|
|||||||
@@ -13,8 +13,11 @@ class WikiPage < ApplicationRecord
|
|||||||
foreign_key: :redirect_page_id,
|
foreign_key: :redirect_page_id,
|
||||||
dependent: :nullify
|
dependent: :nullify
|
||||||
|
|
||||||
|
has_many :wiki_versions
|
||||||
|
|
||||||
belongs_to :tag_name
|
belongs_to :tag_name
|
||||||
validates :tag_name, presence: true
|
validates :tag_name, presence: true
|
||||||
|
validates :body, presence: true
|
||||||
|
|
||||||
def title = tag_name.name
|
def title = tag_name.name
|
||||||
|
|
||||||
@@ -24,11 +27,6 @@ class WikiPage < ApplicationRecord
|
|||||||
|
|
||||||
def current_revision = wiki_revisions.order(id: :desc).first
|
def current_revision = wiki_revisions.order(id: :desc).first
|
||||||
|
|
||||||
def body
|
|
||||||
rev = current_revision
|
|
||||||
rev.body if rev&.content?
|
|
||||||
end
|
|
||||||
|
|
||||||
def resolve_redirect limit: 10
|
def resolve_redirect limit: 10
|
||||||
page = self
|
page = self
|
||||||
visited = Set.new
|
visited = Set.new
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
class WikiVersion < ApplicationRecord
|
||||||
|
include VersionRecord
|
||||||
|
|
||||||
|
belongs_to :wiki_page
|
||||||
|
|
||||||
|
validates :title, presence: true
|
||||||
|
validates :body, presence: true
|
||||||
|
end
|
||||||
@@ -7,6 +7,31 @@ module Wiki
|
|||||||
;
|
;
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.create_content! tag_name:, body:, created_by_user:, message: nil
|
||||||
|
normalised = normalise_body(body)
|
||||||
|
|
||||||
|
page = WikiPage.new(tag_name:,
|
||||||
|
body: normalised,
|
||||||
|
created_user: created_by_user,
|
||||||
|
updated_user: created_by_user)
|
||||||
|
|
||||||
|
if normalised.blank?
|
||||||
|
page.errors.add(:body, :blank)
|
||||||
|
raise ActiveRecord::RecordInvalid, page
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
page.save!
|
||||||
|
|
||||||
|
new(page:, created_user: created_by_user).content!(
|
||||||
|
body: normalised,
|
||||||
|
message:,
|
||||||
|
base_revision_id: nil)
|
||||||
|
|
||||||
|
page
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def self.content! page:, body:, created_user:, message: nil, base_revision_id: nil
|
def self.content! page:, body:, created_user:, message: nil, base_revision_id: nil
|
||||||
new(page:, created_user:).content!(body:, message:, base_revision_id:)
|
new(page:, created_user:).content!(body:, message:, base_revision_id:)
|
||||||
end
|
end
|
||||||
@@ -21,7 +46,12 @@ module Wiki
|
|||||||
end
|
end
|
||||||
|
|
||||||
def content! body:, message:, base_revision_id:
|
def content! body:, message:, base_revision_id:
|
||||||
normalised = normalise_body(body)
|
normalised = self.class.normalise_body(body)
|
||||||
|
if normalised.blank?
|
||||||
|
@page.errors.add(:body, :blank)
|
||||||
|
raise ActiveRecord::RecordInvalid, @page
|
||||||
|
end
|
||||||
|
|
||||||
lines = split_lines(normalised)
|
lines = split_lines(normalised)
|
||||||
|
|
||||||
line_shas = lines.map { |line| Digest::SHA256.hexdigest(line) }
|
line_shas = lines.map { |line| Digest::SHA256.hexdigest(line) }
|
||||||
@@ -37,10 +67,19 @@ module Wiki
|
|||||||
current_id = @page.wiki_revisions.maximum(:id)
|
current_id = @page.wiki_revisions.maximum(:id)
|
||||||
if current_id && current_id != base_revision_id.to_i
|
if current_id && current_id != base_revision_id.to_i
|
||||||
raise Conflict,
|
raise Conflict,
|
||||||
"競合が発生してゐます(現在の Id.:#{ current_id },ベース Id.:#{ base_revision_id })."
|
"競合が発生してゐます" +
|
||||||
|
"(現在の Id.:#{ current_id },ベース Id.:#{ base_revision_id })."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@page.update!(body: normalised)
|
||||||
|
|
||||||
|
WikiVersionRecorder.record!(
|
||||||
|
page: @page,
|
||||||
|
event_type: @page.wiki_versions.exists? ? :update : :create,
|
||||||
|
reason: message,
|
||||||
|
created_by_user: @created_user)
|
||||||
|
|
||||||
rev = WikiRevision.create!(
|
rev = WikiRevision.create!(
|
||||||
wiki_page: @page,
|
wiki_page: @page,
|
||||||
base_revision_id:,
|
base_revision_id:,
|
||||||
@@ -54,65 +93,45 @@ module Wiki
|
|||||||
rows = line_ids.each_with_index.map do |line_id, pos|
|
rows = line_ids.each_with_index.map do |line_id, pos|
|
||||||
{ wiki_revision_id: rev.id, wiki_line_id: line_id, position: pos }
|
{ wiki_revision_id: rev.id, wiki_line_id: line_id, position: pos }
|
||||||
end
|
end
|
||||||
WikiRevisionLine.insert_all!(rows)
|
WikiRevisionLine.insert_all!(rows) if rows.any?
|
||||||
|
|
||||||
rev
|
rev
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def redirect! redirect_page:, message:, base_revision_id:
|
def redirect!(redirect_page:, message:, base_revision_id:) = raise '廃止しました.'
|
||||||
ActiveRecord::Base.transaction do
|
|
||||||
@page.lock!
|
|
||||||
|
|
||||||
if base_revision_id.present?
|
def self.normalise_body body
|
||||||
current_id = @page.wiki_revisions.maximum(:id)
|
s = body.to_s
|
||||||
if current_id && current_id != base_revision_id.to_i
|
s.gsub!(/\r\n?/, "\n")
|
||||||
raise Conflict,
|
s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '🖕')
|
||||||
"競合が発生してゐます(現在の Id.:#{ current_id },ベース Id.:#{ base_revision_id })."
|
s.gsub(/\n+$/, '')
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
WikiRevision.create!(
|
|
||||||
wiki_page: @page,
|
|
||||||
base_revision_id:,
|
|
||||||
created_user: @created_user,
|
|
||||||
kind: :redirect,
|
|
||||||
redirect_page:,
|
|
||||||
message:,
|
|
||||||
lines_count: 0,
|
|
||||||
tree_sha256: nil)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def normalise_body body
|
def split_lines(body) = body.split("\n")
|
||||||
s = body.to_s
|
|
||||||
s.gsub!("\r\n", "\n")
|
|
||||||
s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '🖕')
|
|
||||||
end
|
|
||||||
|
|
||||||
def split_lines body
|
|
||||||
body.split("\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
def upsert_lines! lines, line_shas
|
def upsert_lines! lines, line_shas
|
||||||
now = Time.current
|
now = Time.current
|
||||||
|
|
||||||
id_by_sha = WikiLine.where(sha256: line_shas).pluck(:sha256, :id).to_h
|
id_by_sha = WikiLine.where(sha256: line_shas).pluck(:sha256, :id).to_h
|
||||||
|
|
||||||
missing_rows = []
|
missing_by_sha = { }
|
||||||
|
|
||||||
line_shas.each_with_index do |sha, i|
|
line_shas.each_with_index do |sha, i|
|
||||||
next if id_by_sha.key?(sha)
|
next if id_by_sha.key?(sha)
|
||||||
|
next if missing_by_sha.key?(sha)
|
||||||
|
|
||||||
missing_rows << { sha256: sha,
|
missing_by_sha[sha] = {
|
||||||
|
sha256: sha,
|
||||||
body: lines[i],
|
body: lines[i],
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now }
|
updated_at: now }
|
||||||
end
|
end
|
||||||
|
|
||||||
if missing_rows.any?
|
if missing_by_sha.any?
|
||||||
WikiLine.upsert_all(missing_rows)
|
WikiLine.upsert_all(missing_by_sha.values)
|
||||||
id_by_sha = WikiLine.where(sha256: line_shas).pluck(:sha256, :id).to_h
|
id_by_sha = WikiLine.where(sha256: line_shas).pluck(:sha256, :id).to_h
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
class WikiVersionRecorder < VersionRecorder
|
||||||
|
def self.record! page:, event_type:, reason: nil, created_by_user:
|
||||||
|
new(page:, event_type:, reason:, created_by_user:).record!
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize page:, event_type:, reason: nil, created_by_user:
|
||||||
|
@reason = reason
|
||||||
|
super(record: page, event_type:, created_by_user:)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def version_class = WikiVersion
|
||||||
|
def version_association = :wiki_versions
|
||||||
|
def record_key = :wiki_page
|
||||||
|
|
||||||
|
def snapshot_attributes = {
|
||||||
|
title: @record.title,
|
||||||
|
body: @record.body,
|
||||||
|
reason: @reason }
|
||||||
|
end
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
require 'json'
|
||||||
|
require 'net/http'
|
||||||
|
require 'uri'
|
||||||
|
|
||||||
|
|
||||||
|
module Youtube
|
||||||
|
class ApiClient
|
||||||
|
ENDPOINT = 'https://www.googleapis.com/youtube/v3'
|
||||||
|
|
||||||
|
def initialize api_key: ENV.fetch('YOUTUBE_API_KEY')
|
||||||
|
@api_key = api_key
|
||||||
|
end
|
||||||
|
|
||||||
|
def search_videos q:, published_after: nil, published_before: nil, page_token: nil
|
||||||
|
get_json('/search', {
|
||||||
|
part: 'snippet',
|
||||||
|
type: 'video',
|
||||||
|
q:,
|
||||||
|
order: 'date',
|
||||||
|
maxResults: 50,
|
||||||
|
regionCode: 'JP',
|
||||||
|
relevanceLanguage: 'ja',
|
||||||
|
publishedAfter: published_after&.iso8601,
|
||||||
|
publishedBefore: published_before&.iso8601,
|
||||||
|
pageToken: page_token }.compact)
|
||||||
|
end
|
||||||
|
|
||||||
|
def videos ids
|
||||||
|
return { 'items' => [] } if ids.empty?
|
||||||
|
|
||||||
|
get_json('/videos', part: 'snippet,status,contentDetails', id: ids.join(','))
|
||||||
|
end
|
||||||
|
|
||||||
|
def playlist_items playlist_id:, page_token: nil
|
||||||
|
get_json('/playlistItems', {
|
||||||
|
part: 'snippet,contentDetails,status',
|
||||||
|
playlistId: playlist_id,
|
||||||
|
maxResults: 50,
|
||||||
|
pageToken: page_token }.compact)
|
||||||
|
end
|
||||||
|
|
||||||
|
def channel id: nil, handle: nil
|
||||||
|
raise ArgumentError, 'id or handle is required' if id.present? == handle.present?
|
||||||
|
|
||||||
|
params = { part: 'snippet,contentDetails' }
|
||||||
|
params[:id] = id if id.present?
|
||||||
|
params[:forHandle] = handle if handle.present?
|
||||||
|
|
||||||
|
get_json('/channels', params)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def get_json path, params
|
||||||
|
uri = URI(ENDPOINT + path)
|
||||||
|
uri.query = URI.encode_www_form(params.merge(key: @api_key))
|
||||||
|
|
||||||
|
response = Net::HTTP.start(uri.host,
|
||||||
|
uri.port,
|
||||||
|
use_ssl: true,
|
||||||
|
open_timeout: 10,
|
||||||
|
read_timeout: 30) do |http|
|
||||||
|
http.get(uri)
|
||||||
|
end
|
||||||
|
|
||||||
|
unless response.is_a?(Net::HTTPSuccess)
|
||||||
|
raise "YouTube API error: #{ response.code } #{ response.body }"
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.parse(response.body)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
require 'open-uri'
|
||||||
|
require 'set'
|
||||||
|
require 'time'
|
||||||
|
|
||||||
|
|
||||||
|
module Youtube
|
||||||
|
class Sync
|
||||||
|
def initialize client: ApiClient.new
|
||||||
|
@client = client
|
||||||
|
end
|
||||||
|
|
||||||
|
def sync!
|
||||||
|
video_ids = discover_video_ids
|
||||||
|
return if video_ids.empty?
|
||||||
|
|
||||||
|
video_ids.each_slice(50) do |ids|
|
||||||
|
@client.videos(ids).fetch('items', []).each do |item|
|
||||||
|
sync_video!(VideoItem.new(item))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def discover_video_ids
|
||||||
|
ids = Set.new
|
||||||
|
|
||||||
|
query_terms.each do |q|
|
||||||
|
response = @client.search_videos(q:, published_after: sync_since)
|
||||||
|
|
||||||
|
response.fetch('items', []).each do |item|
|
||||||
|
video_id = item.dig('id', 'videoId')
|
||||||
|
ids << video_id if video_id.present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
playlist_ids.each do |playlist_id|
|
||||||
|
each_playlist_item(playlist_id) do |item|
|
||||||
|
video_id = item.dig('contentDetails', 'videoId')
|
||||||
|
video_id ||= item.dig('snippet', 'resourceId', 'videoId')
|
||||||
|
|
||||||
|
ids << video_id if video_id.present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ids.to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
def sync_video! video
|
||||||
|
post = Post.where('url REGEXP ?', youtube_url_regexp(video.id)).first
|
||||||
|
|
||||||
|
original_created_from = video.published_at.change(sec: 0)
|
||||||
|
original_created_before = original_created_from + 1.minute
|
||||||
|
|
||||||
|
post_created = false
|
||||||
|
post_changed = false
|
||||||
|
|
||||||
|
if post
|
||||||
|
post.assign_attributes(title: video.title,
|
||||||
|
original_created_from:,
|
||||||
|
original_created_before:,
|
||||||
|
thumbnail_base: video.thumbnail_url)
|
||||||
|
|
||||||
|
post_changed = post.changed?
|
||||||
|
post.save! if post_changed
|
||||||
|
|
||||||
|
attach_thumbnail_if_needed!(post, video.thumbnail_url)
|
||||||
|
else
|
||||||
|
post_created = true
|
||||||
|
post = Post.create!(
|
||||||
|
title: video.title,
|
||||||
|
url: video.url,
|
||||||
|
thumbnail_base: video.thumbnail_url,
|
||||||
|
uploaded_user_id: nil,
|
||||||
|
original_created_from:,
|
||||||
|
original_created_before:)
|
||||||
|
|
||||||
|
attach_thumbnail_if_needed!(post, video.thumbnail_url)
|
||||||
|
|
||||||
|
sync_post_tags!(post, [Tag.tagme.id, Tag.bot.id, Tag.youtube.id, Tag.video.id])
|
||||||
|
end
|
||||||
|
|
||||||
|
kept_tag_ids = post.tags.pluck(:id).to_set
|
||||||
|
desired_tag_ids = kept_tag_ids.to_a
|
||||||
|
|
||||||
|
deerjikist = Deerjikist.find_by(platform: :youtube, code: video.channel_id)
|
||||||
|
if deerjikist
|
||||||
|
desired_tag_ids.delete(Tag.no_deerjikist.id)
|
||||||
|
desired_tag_ids << deerjikist.tag_id
|
||||||
|
elsif post.tags.where(category: :deerjikist).none?
|
||||||
|
desired_tag_ids << Tag.no_deerjikist.id
|
||||||
|
end
|
||||||
|
|
||||||
|
desired_tag_ids.uniq!
|
||||||
|
|
||||||
|
sync_post_tags!(post, desired_tag_ids, current_tag_ids: kept_tag_ids)
|
||||||
|
|
||||||
|
if post_created
|
||||||
|
PostVersionRecorder.record!(post:, event_type: :create, created_by_user: nil)
|
||||||
|
elsif post_changed || kept_tag_ids != desired_tag_ids.to_set
|
||||||
|
PostVersionRecorder.ensure_snapshot!(post, created_by_user: nil)
|
||||||
|
PostVersionRecorder.record!(post:, event_type: :update, created_by_user: nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def sync_post_tags! post, desired_tag_ids, current_tag_ids: nil
|
||||||
|
current_tag_ids ||= PostTag.kept.where(post_id: post.id).pluck(:tag_id).to_set
|
||||||
|
desired_tag_ids = desired_tag_ids.compact.to_set
|
||||||
|
|
||||||
|
to_add = desired_tag_ids - current_tag_ids
|
||||||
|
to_remove = current_tag_ids - desired_tag_ids
|
||||||
|
|
||||||
|
Tag.where(id: to_add.to_a).find_each do |tag|
|
||||||
|
begin
|
||||||
|
PostTag.create!(post:, tag:)
|
||||||
|
rescue ActiveRecord::RecordNotUnique
|
||||||
|
;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
PostTag.where(post_id: post.id, tag_id: to_remove.to_a).kept.find_each do |pt|
|
||||||
|
pt.discard_by!(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def attach_thumbnail_if_needed! post, thumbnail_url
|
||||||
|
return if post.thumbnail.attached?
|
||||||
|
return if thumbnail_url.blank?
|
||||||
|
|
||||||
|
post.thumbnail.attach(
|
||||||
|
io: URI.open(thumbnail_url),
|
||||||
|
filename: File.basename(URI.parse(thumbnail_url).path),
|
||||||
|
content_type: 'image/jpeg')
|
||||||
|
|
||||||
|
post.resized_thumbnail!
|
||||||
|
end
|
||||||
|
|
||||||
|
def youtube_url_regexp id
|
||||||
|
escaped = Regexp.escape(id)
|
||||||
|
"(youtube\\.com/watch\\?v=#{ escaped }|youtu\\.be/#{ escaped })([^A-Za-z0-9_-]|$)"
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_terms = ['ぼざろクリーチャーシリーズ', '伊地知ニジカ', '伊地知虹鹿']
|
||||||
|
|
||||||
|
def playlist_ids
|
||||||
|
['PLrOch4zHkI5vu29b-f9umUQQ4tQkuWLPX',
|
||||||
|
'PLrOch4zHkI5vOK0RaytQq6PbucxQkkL0K',
|
||||||
|
'PLrOch4zHkI5tdwm9vSegiDQJOM-hgpcOC']
|
||||||
|
end
|
||||||
|
|
||||||
|
def sync_since = 14.days.ago
|
||||||
|
|
||||||
|
def each_playlist_item playlist_id
|
||||||
|
page_token = nil
|
||||||
|
|
||||||
|
loop do
|
||||||
|
response = @client.playlist_items(playlist_id:, page_token:)
|
||||||
|
|
||||||
|
response.fetch('items', []).each do |item|
|
||||||
|
yield item
|
||||||
|
end
|
||||||
|
|
||||||
|
page_token = response['nextPageToken']
|
||||||
|
break if page_token.blank?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
require 'time'
|
||||||
|
|
||||||
|
|
||||||
|
module Youtube
|
||||||
|
class VideoItem
|
||||||
|
attr_reader :id, :title, :channel_id, :published_at, :thumbnail_url, :raw_tags
|
||||||
|
|
||||||
|
def initialize item
|
||||||
|
snippet = item.fetch('snippet')
|
||||||
|
|
||||||
|
@id = item.fetch('id')
|
||||||
|
@title = snippet['title']
|
||||||
|
@channel_id = snippet['channelId']
|
||||||
|
@published_at = Time.iso8601(snippet['publishedAt'])
|
||||||
|
@thumbnail_url = pick_thumbnail(snippet['thumbnails'] || { })
|
||||||
|
@raw_tags = snippet['tags'] || []
|
||||||
|
end
|
||||||
|
|
||||||
|
def url = "https://www.youtube.com/watch?v=#{ @id }"
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def pick_thumbnail thumbnails
|
||||||
|
['maxres', 'standard', 'high', 'medium', 'default'].each do |key|
|
||||||
|
url = thumbnails.dig(key, 'url')
|
||||||
|
return url if url.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -17,3 +17,11 @@ every 1.day, at: '0:00 am' do
|
|||||||
rake 'post_similarity:calc', environment: 'production'
|
rake 'post_similarity:calc', environment: 'production'
|
||||||
rake 'tag_similarity:calc', environment: 'production'
|
rake 'tag_similarity:calc', environment: 'production'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
every 1.day, at: '7:50 am' do
|
||||||
|
rake 'nico:export', environment: 'production'
|
||||||
|
end
|
||||||
|
|
||||||
|
every :hour do
|
||||||
|
rake 'post:sync', environment: 'production'
|
||||||
|
end
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
class CreateWikiVersions < ActiveRecord::Migration[8.0]
|
||||||
|
class WikiPage < ActiveRecord::Base
|
||||||
|
self.table_name = 'wiki_pages'
|
||||||
|
end
|
||||||
|
|
||||||
|
class WikiRevision < ActiveRecord::Base
|
||||||
|
self.table_name = 'wiki_revisions'
|
||||||
|
end
|
||||||
|
|
||||||
|
class WikiRevisionLine < ActiveRecord::Base
|
||||||
|
self.table_name = 'wiki_revision_lines'
|
||||||
|
end
|
||||||
|
|
||||||
|
class WikiLine < ActiveRecord::Base
|
||||||
|
self.table_name = 'wiki_lines'
|
||||||
|
end
|
||||||
|
|
||||||
|
class WikiVersion < ActiveRecord::Base
|
||||||
|
self.table_name = 'wiki_versions'
|
||||||
|
end
|
||||||
|
|
||||||
|
class TagName < ActiveRecord::Base
|
||||||
|
self.table_name = 'tag_names'
|
||||||
|
end
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_column :wiki_pages, :body, :text, after: :tag_name_id
|
||||||
|
|
||||||
|
create_table :wiki_versions do |t|
|
||||||
|
t.references :wiki_page, null: false, foreign_key: true
|
||||||
|
t.integer :version_no, null: false
|
||||||
|
t.string :event_type, null: false
|
||||||
|
t.string :title, null: false
|
||||||
|
t.text :body, null: false
|
||||||
|
t.text :reason
|
||||||
|
t.datetime :created_at, null: false
|
||||||
|
t.references :created_by_user, foreign_key: { to_table: :users }
|
||||||
|
|
||||||
|
t.index [:wiki_page_id, :version_no], unique: true
|
||||||
|
t.check_constraint 'version_no > 0',
|
||||||
|
name: 'wiki_versions_version_no_positive'
|
||||||
|
t.check_constraint "event_type IN ('create', 'update', 'discard', 'restore')",
|
||||||
|
name: 'wiki_versions_event_type_valid'
|
||||||
|
end
|
||||||
|
|
||||||
|
WikiPage.reset_column_information
|
||||||
|
WikiVersion.reset_column_information
|
||||||
|
|
||||||
|
say_with_time 'Backfilling wiki_versions' do
|
||||||
|
WikiPage.find_each do |page|
|
||||||
|
base_revision_id = nil
|
||||||
|
version_no = 1
|
||||||
|
title = TagName.find(page.tag_name_id).name
|
||||||
|
body = nil
|
||||||
|
loop do
|
||||||
|
rev = WikiRevision.where(wiki_page_id: page.id).find_by(base_revision_id:)
|
||||||
|
break unless rev
|
||||||
|
|
||||||
|
body = WikiRevisionLine.where(wiki_revision_id: rev.id).order(:position).map { |wrl|
|
||||||
|
WikiLine.find(wrl.wiki_line_id).body
|
||||||
|
}.join("\n")
|
||||||
|
|
||||||
|
WikiVersion.create!(
|
||||||
|
wiki_page_id: page.id,
|
||||||
|
version_no:,
|
||||||
|
event_type: version_no == 1 ? 'create' : 'update',
|
||||||
|
title:,
|
||||||
|
body:,
|
||||||
|
reason: rev.message,
|
||||||
|
created_at: rev.created_at,
|
||||||
|
created_by_user_id: rev.created_user_id)
|
||||||
|
|
||||||
|
version_no += 1
|
||||||
|
base_revision_id = rev.id
|
||||||
|
end
|
||||||
|
if body
|
||||||
|
page.update!(body:)
|
||||||
|
else
|
||||||
|
page.destroy!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
change_column_null :wiki_pages, :body, false
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
drop_table :wiki_versions
|
||||||
|
remove_column :wiki_pages, :body
|
||||||
|
end
|
||||||
|
end
|
||||||
Generated
+20
-1
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2026_04_19_035400) do
|
ActiveRecord::Schema[8.0].define(version: 2026_04_26_120600) do
|
||||||
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "record_type", null: false
|
t.string "record_type", null: false
|
||||||
@@ -354,6 +354,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_04_19_035400) do
|
|||||||
|
|
||||||
create_table "wiki_pages", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "wiki_pages", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.bigint "tag_name_id", null: false
|
t.bigint "tag_name_id", null: false
|
||||||
|
t.text "body", null: false
|
||||||
t.bigint "created_user_id", null: false
|
t.bigint "created_user_id", null: false
|
||||||
t.bigint "updated_user_id", null: false
|
t.bigint "updated_user_id", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
@@ -396,6 +397,22 @@ ActiveRecord::Schema[8.0].define(version: 2026_04_19_035400) do
|
|||||||
t.index ["wiki_page_id"], name: "index_wiki_revisions_on_wiki_page_id"
|
t.index ["wiki_page_id"], name: "index_wiki_revisions_on_wiki_page_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "wiki_versions", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
|
t.bigint "wiki_page_id", null: false
|
||||||
|
t.integer "version_no", null: false
|
||||||
|
t.string "event_type", null: false
|
||||||
|
t.string "title", null: false
|
||||||
|
t.text "body", null: false
|
||||||
|
t.text "reason"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.bigint "created_by_user_id"
|
||||||
|
t.index ["created_by_user_id"], name: "index_wiki_versions_on_created_by_user_id"
|
||||||
|
t.index ["wiki_page_id", "version_no"], name: "index_wiki_versions_on_wiki_page_id_and_version_no", unique: true
|
||||||
|
t.index ["wiki_page_id"], name: "index_wiki_versions_on_wiki_page_id"
|
||||||
|
t.check_constraint "`event_type` in (_utf8mb4'create',_utf8mb4'update',_utf8mb4'discard',_utf8mb4'restore')", name: "wiki_versions_event_type_valid"
|
||||||
|
t.check_constraint "`version_no` > 0", name: "wiki_versions_version_no_positive"
|
||||||
|
end
|
||||||
|
|
||||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||||
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
||||||
add_foreign_key "material_versions", "materials"
|
add_foreign_key "material_versions", "materials"
|
||||||
@@ -453,4 +470,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_04_19_035400) do
|
|||||||
add_foreign_key "wiki_revisions", "wiki_pages"
|
add_foreign_key "wiki_revisions", "wiki_pages"
|
||||||
add_foreign_key "wiki_revisions", "wiki_pages", column: "redirect_page_id"
|
add_foreign_key "wiki_revisions", "wiki_pages", column: "redirect_page_id"
|
||||||
add_foreign_key "wiki_revisions", "wiki_revisions", column: "base_revision_id"
|
add_foreign_key "wiki_revisions", "wiki_revisions", column: "base_revision_id"
|
||||||
|
add_foreign_key "wiki_versions", "users", column: "created_by_user_id"
|
||||||
|
add_foreign_key "wiki_versions", "wiki_pages"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace :post do
|
||||||
|
desc '投稿同期(ニコニコ以外)'
|
||||||
|
task sync: :environment do
|
||||||
|
Youtube::Sync.new.sync!
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,5 +3,7 @@ FactoryBot.define do
|
|||||||
title { "TestPage" }
|
title { "TestPage" }
|
||||||
association :created_user, factory: :user
|
association :created_user, factory: :user
|
||||||
association :updated_user, factory: :user
|
association :updated_user, factory: :user
|
||||||
|
|
||||||
|
body { ' ' }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -107,11 +107,13 @@ RSpec.describe Tag, type: :model do
|
|||||||
context 'when the source tag_name has a wiki_page' do
|
context 'when the source tag_name has a wiki_page' do
|
||||||
let!(:source_post_tag) { PostTag.create!(post: post_record, tag: source_tag) }
|
let!(:source_post_tag) { PostTag.create!(post: post_record, tag: source_tag) }
|
||||||
let!(:wiki_page) do
|
let!(:wiki_page) do
|
||||||
WikiPage.create!(
|
admin = create_admin_user!
|
||||||
|
|
||||||
|
Wiki::Commit.create_content!(
|
||||||
tag_name: source_tag_name,
|
tag_name: source_tag_name,
|
||||||
created_user: create_admin_user!,
|
body: 'source wiki body',
|
||||||
updated_user: create_admin_user!
|
created_by_user: admin,
|
||||||
)
|
message: 'init')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'rolls back the transaction' do
|
it 'rolls back the transaction' do
|
||||||
|
|||||||
@@ -214,5 +214,30 @@ RSpec.describe 'TagVersions API', type: :request do
|
|||||||
expect(versions.size).to eq(1)
|
expect(versions.size).to eq(1)
|
||||||
expect(versions.first['version_no']).to eq(2)
|
expect(versions.first['version_no']).to eq(2)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not create tag versions by wiki updates when tag has no versions yet' do
|
||||||
|
wiki_tag_name = TagName.create!(name: 'tag_versions_from_wiki')
|
||||||
|
wiki_tag = Tag.create!(tag_name: wiki_tag_name, category: :general)
|
||||||
|
|
||||||
|
wiki_page =
|
||||||
|
Wiki::Commit.create_content!(
|
||||||
|
tag_name: wiki_tag_name,
|
||||||
|
body: 'before',
|
||||||
|
created_by_user: member,
|
||||||
|
message: 'init')
|
||||||
|
|
||||||
|
Wiki::Commit.content!(
|
||||||
|
page: wiki_page,
|
||||||
|
body: 'after',
|
||||||
|
created_user: member,
|
||||||
|
message: 'edit',
|
||||||
|
base_revision_id: wiki_page.current_revision.id)
|
||||||
|
|
||||||
|
get '/tags/versions', params: { id: wiki_tag.id }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json.fetch('versions')).to eq([])
|
||||||
|
expect(json.fetch('count')).to eq(0)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Tag and wiki history integrity', type: :request do
|
||||||
|
let(:member_user) { create(:user, role: 'member') }
|
||||||
|
|
||||||
|
def stub_current_user user
|
||||||
|
allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_tag! name:, category: :general
|
||||||
|
tag_name = TagName.create!(name:)
|
||||||
|
Tag.create!(tag_name:, category:)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_wiki_for_tag! tag:, body: 'wiki body', user: member_user
|
||||||
|
Wiki::Commit.create_content!(
|
||||||
|
tag_name: tag.tag_name,
|
||||||
|
body:,
|
||||||
|
created_by_user: user,
|
||||||
|
message: 'init')
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_current_user(member_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PATCH /tags/:id' do
|
||||||
|
it 'records wiki_version when tag name changes and tag has wiki' do
|
||||||
|
tag = create_tag!(name: 'patch_tag_wiki_before')
|
||||||
|
wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
patch "/tags/#{ tag.id }", params: {
|
||||||
|
name: 'patch_tag_wiki_after',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.to change(TagVersion, :count).by(2)
|
||||||
|
.and change(WikiVersion, :count).by(1)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
tag.reload
|
||||||
|
wiki_page.reload
|
||||||
|
version = wiki_page.wiki_versions.order(:version_no).last
|
||||||
|
|
||||||
|
expect(tag.name).to eq('patch_tag_wiki_after')
|
||||||
|
expect(wiki_page.title).to eq('patch_tag_wiki_after')
|
||||||
|
|
||||||
|
expect(version).to have_attributes(
|
||||||
|
event_type: 'update',
|
||||||
|
title: 'patch_tag_wiki_after',
|
||||||
|
body: 'wiki body before',
|
||||||
|
created_by_user_id: member_user.id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not record wiki_version when only category changes' do
|
||||||
|
tag = create_tag!(name: 'patch_tag_category_only')
|
||||||
|
wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before')
|
||||||
|
|
||||||
|
before_wiki_versions = wiki_page.wiki_versions.count
|
||||||
|
|
||||||
|
expect {
|
||||||
|
patch "/tags/#{ tag.id }", params: {
|
||||||
|
category: 'meme',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.to change(TagVersion, :count).by(2)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
tag.reload
|
||||||
|
wiki_page.reload
|
||||||
|
|
||||||
|
expect(tag.name).to eq('patch_tag_category_only')
|
||||||
|
expect(tag.category).to eq('meme')
|
||||||
|
expect(wiki_page.wiki_versions.count).to eq(before_wiki_versions)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PUT /tags/:id' do
|
||||||
|
it 'records wiki_version when tag name changes and tag has wiki' do
|
||||||
|
tag = create_tag!(name: 'put_tag_wiki_before')
|
||||||
|
wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
put "/tags/#{ tag.id }", params: {
|
||||||
|
name: 'put_tag_wiki_after',
|
||||||
|
category: 'general',
|
||||||
|
aliases: '',
|
||||||
|
parent_tags: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.to change(TagVersion, :count).by(2)
|
||||||
|
.and change(WikiVersion, :count).by(1)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
tag.reload
|
||||||
|
wiki_page.reload
|
||||||
|
version = wiki_page.wiki_versions.order(:version_no).last
|
||||||
|
|
||||||
|
expect(tag.name).to eq('put_tag_wiki_after')
|
||||||
|
expect(wiki_page.title).to eq('put_tag_wiki_after')
|
||||||
|
|
||||||
|
expect(version).to have_attributes(
|
||||||
|
event_type: 'update',
|
||||||
|
title: 'put_tag_wiki_after',
|
||||||
|
body: 'wiki body before',
|
||||||
|
created_by_user_id: member_user.id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not record wiki_version when only category changes' do
|
||||||
|
tag = create_tag!(name: 'put_tag_category_only')
|
||||||
|
wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before')
|
||||||
|
|
||||||
|
before_wiki_versions = wiki_page.wiki_versions.count
|
||||||
|
|
||||||
|
expect {
|
||||||
|
put "/tags/#{ tag.id }", params: {
|
||||||
|
name: 'put_tag_category_only',
|
||||||
|
category: 'meme',
|
||||||
|
aliases: '',
|
||||||
|
parent_tags: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.to change(TagVersion, :count).by(2)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
tag.reload
|
||||||
|
wiki_page.reload
|
||||||
|
|
||||||
|
expect(tag.name).to eq('put_tag_category_only')
|
||||||
|
expect(tag.category).to eq('meme')
|
||||||
|
expect(wiki_page.wiki_versions.count).to eq(before_wiki_versions)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not record wiki_version when only aliases change' do
|
||||||
|
tag = create_tag!(name: 'put_tag_alias_only')
|
||||||
|
wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before')
|
||||||
|
|
||||||
|
before_wiki_versions = wiki_page.wiki_versions.count
|
||||||
|
|
||||||
|
expect {
|
||||||
|
put "/tags/#{ tag.id }", params: {
|
||||||
|
name: 'put_tag_alias_only',
|
||||||
|
category: 'general',
|
||||||
|
aliases: 'put_tag_alias_only_alias',
|
||||||
|
parent_tags: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.to change(TagVersion, :count).by(2)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(wiki_page.reload.wiki_versions.count).to eq(before_wiki_versions)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -471,6 +471,54 @@ RSpec.describe 'Tags API', type: :request do
|
|||||||
expect(response).to have_http_status(:unprocessable_entity)
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
expect(nico_tag.reload.category).to eq('nico')
|
expect(nico_tag.reload.category).to eq('nico')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'PATCH で tag の name を変更すると対応する wiki version を作成する' do
|
||||||
|
wiki_page =
|
||||||
|
Wiki::Commit.create_content!(
|
||||||
|
tag_name: tag.tag_name,
|
||||||
|
body: 'wiki body before',
|
||||||
|
created_by_user: member_user,
|
||||||
|
message: 'init')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
patch "/tags/#{ tag.id }", params: {
|
||||||
|
name: 'patch_wiki_renamed_tag',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.to change(TagVersion, :count).by(2)
|
||||||
|
.and change(WikiVersion, :count).by(1)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
version = wiki_page.reload.wiki_versions.order(:version_no).last
|
||||||
|
|
||||||
|
expect(version).to have_attributes(
|
||||||
|
event_type: 'update',
|
||||||
|
title: 'patch_wiki_renamed_tag',
|
||||||
|
body: 'wiki body before',
|
||||||
|
created_by_user_id: member_user.id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'tag の category だけを変更しても wiki version は作成しない' do
|
||||||
|
wiki_page =
|
||||||
|
Wiki::Commit.create_content!(
|
||||||
|
tag_name: tag.tag_name,
|
||||||
|
body: 'wiki body before',
|
||||||
|
created_by_user: member_user,
|
||||||
|
message: 'init')
|
||||||
|
|
||||||
|
before_wiki_version_count = wiki_page.reload.wiki_versions.count
|
||||||
|
|
||||||
|
expect {
|
||||||
|
patch "/tags/#{ tag.id }", params: {
|
||||||
|
category: 'meme',
|
||||||
|
}
|
||||||
|
}.to change(TagVersion, :count).by(2)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(wiki_page.reload.wiki_versions.count).to eq(before_wiki_version_count)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -876,6 +924,134 @@ RSpec.describe 'Tags API', type: :request do
|
|||||||
expect(system_tag.reload.name).to eq(old_name)
|
expect(system_tag.reload.name).to eq(old_name)
|
||||||
expect(system_tag.category).to eq(old_category)
|
expect(system_tag.category).to eq(old_category)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'wiki を持つ tag を更新すると wiki version も作成する' do
|
||||||
|
wiki_page =
|
||||||
|
Wiki::Commit.create_content!(
|
||||||
|
tag_name: tag.tag_name,
|
||||||
|
body: 'wiki body before',
|
||||||
|
created_by_user: member_user,
|
||||||
|
message: 'init')
|
||||||
|
|
||||||
|
Wiki::Commit.content!(
|
||||||
|
page: wiki_page,
|
||||||
|
body: 'wiki body before',
|
||||||
|
created_user: member_user,
|
||||||
|
message: 'init'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect {
|
||||||
|
put "/tags/#{ tag.id }", params: {
|
||||||
|
name: 'put_wiki_version_tag',
|
||||||
|
category: 'meme',
|
||||||
|
aliases: 'unko',
|
||||||
|
parent_tags: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.to change(TagVersion, :count).by(2)
|
||||||
|
.and change(WikiVersion, :count).by(1)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
version = wiki_page.reload.wiki_versions.order(:version_no).last
|
||||||
|
|
||||||
|
expect(version).to have_attributes(
|
||||||
|
event_type: 'update',
|
||||||
|
title: 'put_wiki_version_tag',
|
||||||
|
body: 'wiki body before',
|
||||||
|
created_by_user_id: member_user.id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it '別名を他 tag から奪った場合、奪はれた側の tag version も作成する' do
|
||||||
|
old_owner = Tag.create!(
|
||||||
|
tag_name: TagName.create!(name: 'put_alias_old_owner'),
|
||||||
|
category: :general
|
||||||
|
)
|
||||||
|
stolen_alias = TagName.create!(
|
||||||
|
name: 'put_stolen_alias',
|
||||||
|
canonical: old_owner.tag_name
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(old_owner.tag_name.aliases.map(&:name)).to include('put_stolen_alias')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
put "/tags/#{ tag.id }", params: {
|
||||||
|
name: 'spec_tag',
|
||||||
|
category: 'general',
|
||||||
|
aliases: 'unko put_stolen_alias',
|
||||||
|
parent_tags: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.to change { tag.reload.tag_versions.count }.by(2)
|
||||||
|
.and change { old_owner.reload.tag_versions.count }.by(2)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
expect(stolen_alias.reload.canonical).to eq(tag.tag_name)
|
||||||
|
expect(old_owner.reload.tag_name.aliases.map(&:name)).not_to include('put_stolen_alias')
|
||||||
|
|
||||||
|
old_owner_versions = old_owner.tag_versions.order(:version_no)
|
||||||
|
|
||||||
|
expect(old_owner_versions.first.event_type).to eq('create')
|
||||||
|
expect(old_owner_versions.first.aliases.split).to include('put_stolen_alias')
|
||||||
|
|
||||||
|
expect(old_owner_versions.second.event_type).to eq('update')
|
||||||
|
expect(old_owner_versions.second.aliases.split).not_to include('put_stolen_alias')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'parent_tags に指定すると循環する tag は 422 にする' do
|
||||||
|
pending '#332 で対応予定'
|
||||||
|
|
||||||
|
child = Tag.create!(
|
||||||
|
tag_name: TagName.create!(name: 'put_cycle_child'),
|
||||||
|
category: :general
|
||||||
|
)
|
||||||
|
|
||||||
|
TagImplication.create!(tag: child, parent_tag: tag)
|
||||||
|
|
||||||
|
put "/tags/#{ tag.id }", params: {
|
||||||
|
name: 'spec_tag',
|
||||||
|
category: 'general',
|
||||||
|
aliases: 'unko',
|
||||||
|
parent_tags: child.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
|
||||||
|
expect(TagImplication.where(tag:, parent_tag: child)).not_to exist
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'tag の name を変更すると対応する wiki version を作成する' do
|
||||||
|
wiki_page =
|
||||||
|
Wiki::Commit.create_content!(
|
||||||
|
tag_name: tag.tag_name,
|
||||||
|
body: 'wiki body before',
|
||||||
|
created_by_user: member_user,
|
||||||
|
message: 'init')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
put "/tags/#{ tag.id }", params: {
|
||||||
|
name: 'put_wiki_renamed_tag',
|
||||||
|
category: 'general',
|
||||||
|
aliases: 'unko',
|
||||||
|
parent_tags: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.to change(TagVersion, :count).by(2)
|
||||||
|
.and change(WikiVersion, :count).by(1)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
version = wiki_page.reload.wiki_versions.order(:version_no).last
|
||||||
|
|
||||||
|
expect(version).to have_attributes(
|
||||||
|
event_type: 'update',
|
||||||
|
title: 'put_wiki_renamed_tag',
|
||||||
|
body: 'wiki body before',
|
||||||
|
created_by_user_id: member_user.id
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
require "rails_helper"
|
require "rails_helper"
|
||||||
|
|
||||||
|
|
||||||
RSpec.describe "Users", type: :request do
|
RSpec.describe "Users", type: :request do
|
||||||
describe "POST /users" do
|
describe "POST /users" do
|
||||||
it "creates guest user and returns code" do
|
it "creates guest user and returns code" do
|
||||||
post "/users"
|
post "/users"
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:created)
|
||||||
expect(json["code"]).to be_present
|
expect(json["code"]).to be_present
|
||||||
expect(json["user"]["role"]).to eq("guest")
|
expect(json["user"]["role"]).to eq("guest")
|
||||||
end
|
end
|
||||||
@@ -38,7 +37,7 @@ RSpec.describe "Users", type: :request do
|
|||||||
sign_in_as(user)
|
sign_in_as(user)
|
||||||
put "/users/#{user.id}", params: { name: "new-name" }
|
put "/users/#{user.id}", params: { name: "new-name" }
|
||||||
|
|
||||||
expect(response).to have_http_status(:created)
|
expect(response).to have_http_status(:ok)
|
||||||
expect(json["id"]).to eq(user.id)
|
expect(json["id"]).to eq(user.id)
|
||||||
expect(json["name"]).to eq("new-name")
|
expect(json["name"]).to eq("new-name")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Wiki body search', type: :request do
|
||||||
|
let!(:user) { create_member_user! }
|
||||||
|
|
||||||
|
it 'searches wiki pages by body text' do
|
||||||
|
pending '#336 で対応予定'
|
||||||
|
|
||||||
|
Wiki::Commit.create_content!(
|
||||||
|
tag_name: TagName.create!(name: 'wiki_body_search_hit'),
|
||||||
|
body: 'unique body keyword for wiki search',
|
||||||
|
created_by_user: user,
|
||||||
|
message: 'init')
|
||||||
|
|
||||||
|
Wiki::Commit.create_content!(
|
||||||
|
tag_name: TagName.create!(name: 'wiki_body_search_miss'),
|
||||||
|
body: 'ordinary body',
|
||||||
|
created_by_user: user,
|
||||||
|
message: 'init')
|
||||||
|
|
||||||
|
get '/wiki/search', params: { body: 'unique body keyword' }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json.map { |page| page['title'] }).to include('wiki_body_search_hit')
|
||||||
|
expect(json.map { |page| page['title'] }).not_to include('wiki_body_search_miss')
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Wiki conflict handling', type: :request do
|
||||||
|
let!(:user) { create_member_user! }
|
||||||
|
|
||||||
|
def auth_headers user
|
||||||
|
{ 'X-Transfer-Code' => user.inheritance_code }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 409 when base_revision_id is stale' do
|
||||||
|
page =
|
||||||
|
Wiki::Commit.create_content!(
|
||||||
|
tag_name: TagName.create!(name: 'wiki_conflict_request'),
|
||||||
|
body: 'first',
|
||||||
|
created_by_user: user,
|
||||||
|
message: 'init')
|
||||||
|
|
||||||
|
stale_id = page.current_revision.id
|
||||||
|
|
||||||
|
Wiki::Commit.content!(
|
||||||
|
page:,
|
||||||
|
body: 'second',
|
||||||
|
created_user: user,
|
||||||
|
message: 'other edit',
|
||||||
|
base_revision_id: stale_id)
|
||||||
|
|
||||||
|
put "/wiki/#{ page.id }",
|
||||||
|
params: {
|
||||||
|
title: 'wiki_conflict_request',
|
||||||
|
body: 'third',
|
||||||
|
message: 'stale',
|
||||||
|
base_revision_id: stale_id,
|
||||||
|
},
|
||||||
|
headers: auth_headers(user)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:conflict)
|
||||||
|
|
||||||
|
page.reload
|
||||||
|
expect(page.body).to eq('second')
|
||||||
|
expect(page.current_revision.message).to eq('other edit')
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
require 'cgi'
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Wiki history integrity', type: :request do
|
||||||
|
let!(:user) { create_member_user! }
|
||||||
|
|
||||||
|
def auth_headers user
|
||||||
|
{ 'X-Transfer-Code' => user.inheritance_code }
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_wiki_page title:, body: 'body', message: 'init', user: self.user
|
||||||
|
Wiki::Commit.create_content!(
|
||||||
|
tag_name: TagName.create!(name: title),
|
||||||
|
body:,
|
||||||
|
created_by_user: user,
|
||||||
|
message:)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST /wiki' do
|
||||||
|
it 'creates wiki_page, wiki_revision, and wiki_version atomically' do
|
||||||
|
expect {
|
||||||
|
post '/wiki',
|
||||||
|
params: {
|
||||||
|
title: 'wiki_history_create_atomic',
|
||||||
|
body: "a\nb\nc",
|
||||||
|
message: 'initial commit',
|
||||||
|
},
|
||||||
|
headers: auth_headers(user)
|
||||||
|
}
|
||||||
|
.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 = WikiPage.find(json.fetch('id'))
|
||||||
|
revision = page.current_revision
|
||||||
|
version = page.wiki_versions.order(:version_no).last
|
||||||
|
|
||||||
|
expect(page.title).to eq('wiki_history_create_atomic')
|
||||||
|
expect(page.body).to eq("a\nb\nc")
|
||||||
|
|
||||||
|
expect(revision).to be_content
|
||||||
|
expect(revision.message).to eq('initial commit')
|
||||||
|
expect(revision.lines_count).to eq(3)
|
||||||
|
|
||||||
|
expect(version).to have_attributes(
|
||||||
|
version_no: 1,
|
||||||
|
event_type: 'create',
|
||||||
|
title: 'wiki_history_create_atomic',
|
||||||
|
body: "a\nb\nc",
|
||||||
|
reason: 'initial commit',
|
||||||
|
created_by_user_id: user.id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 422 and creates nothing when normalised body is blank' do
|
||||||
|
expect {
|
||||||
|
post '/wiki',
|
||||||
|
params: {
|
||||||
|
title: 'wiki_history_blank_body',
|
||||||
|
body: "\r\n\r\n",
|
||||||
|
message: 'blank',
|
||||||
|
},
|
||||||
|
headers: auth_headers(user)
|
||||||
|
}
|
||||||
|
.not_to change(WikiPage, :count)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(WikiPage.joins(:tag_name).where(tag_names: { name: 'wiki_history_blank_body' })).not_to exist
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 422 and creates no partial page when title already exists' do
|
||||||
|
create_wiki_page(title: 'wiki_history_duplicate_title', body: 'first')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
post '/wiki',
|
||||||
|
params: {
|
||||||
|
title: 'wiki_history_duplicate_title',
|
||||||
|
body: 'second',
|
||||||
|
message: 'duplicate',
|
||||||
|
},
|
||||||
|
headers: auth_headers(user)
|
||||||
|
}
|
||||||
|
.not_to change(WikiPage, :count)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(WikiPage.joins(:tag_name).where(tag_names: { name: 'wiki_history_duplicate_title' }).count).to eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PUT /wiki/:id' do
|
||||||
|
it 'updates body and records wiki_revision and wiki_version' do
|
||||||
|
page = create_wiki_page(title: 'wiki_history_update_body', body: 'before')
|
||||||
|
current_id = page.current_revision.id
|
||||||
|
|
||||||
|
expect {
|
||||||
|
put "/wiki/#{ page.id }",
|
||||||
|
params: {
|
||||||
|
title: 'wiki_history_update_body',
|
||||||
|
body: 'after',
|
||||||
|
message: 'edit body',
|
||||||
|
base_revision_id: current_id,
|
||||||
|
},
|
||||||
|
headers: auth_headers(user)
|
||||||
|
}
|
||||||
|
.to change(WikiRevision, :count).by(1)
|
||||||
|
.and change(WikiVersion, :count).by(1)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
page.reload
|
||||||
|
version = page.wiki_versions.order(:version_no).last
|
||||||
|
|
||||||
|
expect(page.title).to eq('wiki_history_update_body')
|
||||||
|
expect(page.body).to eq('after')
|
||||||
|
|
||||||
|
expect(version).to have_attributes(
|
||||||
|
event_type: 'update',
|
||||||
|
title: 'wiki_history_update_body',
|
||||||
|
body: 'after',
|
||||||
|
reason: 'edit body',
|
||||||
|
created_by_user_id: user.id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renames title and records wiki_version with new title' do
|
||||||
|
page = create_wiki_page(title: 'wiki_history_rename_before', body: 'before')
|
||||||
|
current_id = page.current_revision.id
|
||||||
|
|
||||||
|
expect {
|
||||||
|
put "/wiki/#{ page.id }",
|
||||||
|
params: {
|
||||||
|
title: 'wiki_history_rename_after',
|
||||||
|
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)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
page.reload
|
||||||
|
version = page.wiki_versions.order(:version_no).last
|
||||||
|
|
||||||
|
expect(page.title).to eq('wiki_history_rename_after')
|
||||||
|
expect(page.body).to eq('after')
|
||||||
|
|
||||||
|
expect(version).to have_attributes(
|
||||||
|
event_type: 'update',
|
||||||
|
title: 'wiki_history_rename_after',
|
||||||
|
body: 'after',
|
||||||
|
reason: 'rename',
|
||||||
|
created_by_user_id: user.id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not change title, body, revision, or version on stale base_revision_id' do
|
||||||
|
page = create_wiki_page(title: 'wiki_history_conflict_page', body: 'first')
|
||||||
|
stale_id = page.current_revision.id
|
||||||
|
|
||||||
|
Wiki::Commit.content!(
|
||||||
|
page:,
|
||||||
|
body: 'second',
|
||||||
|
created_user: user,
|
||||||
|
message: 'other edit',
|
||||||
|
base_revision_id: stale_id)
|
||||||
|
|
||||||
|
page.reload
|
||||||
|
current_title = page.title
|
||||||
|
current_body = page.body
|
||||||
|
revision_count = page.wiki_revisions.count
|
||||||
|
version_count = page.wiki_versions.count
|
||||||
|
|
||||||
|
put "/wiki/#{ page.id }",
|
||||||
|
params: {
|
||||||
|
title: 'wiki_history_conflict_renamed',
|
||||||
|
body: 'third',
|
||||||
|
message: 'stale edit',
|
||||||
|
base_revision_id: stale_id,
|
||||||
|
},
|
||||||
|
headers: auth_headers(user)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:conflict)
|
||||||
|
|
||||||
|
page.reload
|
||||||
|
expect(page.title).to eq(current_title)
|
||||||
|
expect(page.body).to eq(current_body)
|
||||||
|
expect(page.wiki_revisions.count).to eq(revision_count)
|
||||||
|
expect(page.wiki_versions.count).to eq(version_count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Wiki restore', type: :request do
|
||||||
|
let!(:user) { create_member_user! }
|
||||||
|
|
||||||
|
def auth_headers user
|
||||||
|
{ 'X-Transfer-Code' => user.inheritance_code }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'restores wiki page to previous version' do
|
||||||
|
pending '#337 で対応予定'
|
||||||
|
|
||||||
|
page =
|
||||||
|
Wiki::Commit.create_content!(
|
||||||
|
tag_name: TagName.create!(name: 'wiki_restore_page'),
|
||||||
|
body: 'v1',
|
||||||
|
created_by_user: user,
|
||||||
|
message: 'init')
|
||||||
|
|
||||||
|
v1 = page.wiki_versions.order(:version_no).last
|
||||||
|
|
||||||
|
Wiki::Commit.content!(
|
||||||
|
page:,
|
||||||
|
body: 'v2',
|
||||||
|
created_user: user,
|
||||||
|
message: 'edit',
|
||||||
|
base_revision_id: page.current_revision.id)
|
||||||
|
|
||||||
|
post "/wiki/#{ page.id }/restore",
|
||||||
|
params: { version_no: v1.version_no },
|
||||||
|
headers: auth_headers(user)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(page.reload.body).to eq('v1')
|
||||||
|
expect(page.wiki_versions.order(:version_no).last.event_type).to eq('restore')
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,13 +4,19 @@ require 'securerandom'
|
|||||||
|
|
||||||
|
|
||||||
RSpec.describe 'Wiki API', type: :request do
|
RSpec.describe 'Wiki API', type: :request do
|
||||||
|
def auth_headers(user)
|
||||||
|
{ 'X-Transfer-Code' => user.inheritance_code }
|
||||||
|
end
|
||||||
|
|
||||||
let!(:user) { create_member_user! }
|
let!(:user) { create_member_user! }
|
||||||
|
|
||||||
let!(:tn) { TagName.create!(name: 'spec_wiki_title') }
|
let!(:tn) { TagName.create!(name: 'spec_wiki_title') }
|
||||||
let!(:page) do
|
let!(:page) do
|
||||||
WikiPage.create!(tag_name: tn, created_user: user, updated_user: user).tap do |p|
|
Wiki::Commit.create_content!(
|
||||||
Wiki::Commit.content!(page: p, body: 'init', created_user: user, message: 'init')
|
tag_name: tn,
|
||||||
end
|
body: 'init',
|
||||||
|
created_by_user: user,
|
||||||
|
message: 'init')
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET /wiki' do
|
describe 'GET /wiki' do
|
||||||
@@ -37,6 +43,7 @@ RSpec.describe 'Wiki API', type: :request do
|
|||||||
context 'when wiki page exists' do
|
context 'when wiki page exists' do
|
||||||
it 'returns wiki page with title' do
|
it 'returns wiki page with title' do
|
||||||
request
|
request
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
expect(json).to include(
|
expect(json).to include(
|
||||||
@@ -50,6 +57,7 @@ RSpec.describe 'Wiki API', type: :request do
|
|||||||
|
|
||||||
it 'returns 404' do
|
it 'returns 404' do
|
||||||
request
|
request
|
||||||
|
|
||||||
expect(response).to have_http_status(:not_found)
|
expect(response).to have_http_status(:not_found)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -99,25 +107,34 @@ RSpec.describe 'Wiki API', type: :request do
|
|||||||
end
|
end
|
||||||
.to change(WikiPage, :count).by(1)
|
.to change(WikiPage, :count).by(1)
|
||||||
.and change(WikiRevision, :count).by(1)
|
.and change(WikiRevision, :count).by(1)
|
||||||
|
.and change(WikiVersion, :count).by(1)
|
||||||
|
|
||||||
expect(response).to have_http_status(:created)
|
expect(response).to have_http_status(:created)
|
||||||
|
|
||||||
page_id = json.fetch('id')
|
page_id = json.fetch('id')
|
||||||
expect(json.fetch('title')).to eq('TestPage')
|
expect(json.fetch('title')).to eq('TestPage')
|
||||||
|
|
||||||
page = WikiPage.find(page_id)
|
created_page = WikiPage.find(page_id)
|
||||||
rev = page.current_revision
|
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_present
|
||||||
expect(rev).to be_content
|
expect(rev).to be_content
|
||||||
expect(rev.message).to eq('init')
|
expect(rev.message).to eq('init')
|
||||||
|
|
||||||
# body が復元できること
|
expect(created_page.body).to eq("a\nb\nc")
|
||||||
expect(page.body).to eq("a\nb\nc")
|
|
||||||
|
|
||||||
# 行数とリレーションの整合
|
|
||||||
expect(rev.lines_count).to eq(3)
|
expect(rev.lines_count).to eq(3)
|
||||||
expect(rev.wiki_revision_lines.order(:position).pluck(:position)).to eq([0, 1, 2])
|
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])
|
expect(rev.wiki_lines.pluck(:body)).to match_array(['a', 'b', 'c'])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'reuses existing WikiLine rows by sha256' do
|
it 'reuses existing WikiLine rows by sha256' do
|
||||||
@@ -135,6 +152,41 @@ RSpec.describe 'Wiki API', type: :request do
|
|||||||
# "a" の WikiLine が増殖しない(1行のはず)
|
# "a" の WikiLine が増殖しない(1行のはず)
|
||||||
expect(WikiLine.where(body: 'a').count).to eq(1)
|
expect(WikiLine.where(body: 'a').count).to eq(1)
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -146,17 +198,14 @@ RSpec.describe 'Wiki API', type: :request do
|
|||||||
{ 'X-Transfer-Code' => user.inheritance_code }
|
{ 'X-Transfer-Code' => user.inheritance_code }
|
||||||
end
|
end
|
||||||
|
|
||||||
#let!(:page) { create(:wiki_page, title: 'TestPage') }
|
let!(:test_tag_name) { TagName.create!(name: '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
|
let!(:page) do
|
||||||
# 初期版を 1 つ作っておく(更新が“2版目”になるように)
|
Wiki::Commit.create_content!(
|
||||||
Wiki::Commit.content!(page: page, body: "a\nb", created_user: member, message: 'init')
|
tag_name: test_tag_name,
|
||||||
|
body: "a\nb",
|
||||||
|
created_by_user: member,
|
||||||
|
message: 'init')
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when not logged in' do
|
context 'when not logged in' do
|
||||||
@@ -182,14 +231,6 @@ RSpec.describe 'Wiki API', type: :request do
|
|||||||
headers: auth_headers(member)
|
headers: auth_headers(member)
|
||||||
expect(response).to have_http_status(:unprocessable_entity)
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
context 'when success' do
|
context 'when success' do
|
||||||
@@ -200,7 +241,18 @@ RSpec.describe 'Wiki API', type: :request do
|
|||||||
put "/wiki/#{page.id}",
|
put "/wiki/#{page.id}",
|
||||||
params: { title: 'TestPage', body: "x\ny", message: 'edit', base_revision_id: current_id },
|
params: { title: 'TestPage', body: "x\ny", message: 'edit', base_revision_id: current_id },
|
||||||
headers: auth_headers(member)
|
headers: auth_headers(member)
|
||||||
end.to change(WikiRevision, :count).by(1)
|
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)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
@@ -211,25 +263,60 @@ RSpec.describe 'Wiki API', type: :request do
|
|||||||
expect(page.body).to eq("x\ny")
|
expect(page.body).to eq("x\ny")
|
||||||
expect(rev.base_revision_id).to eq(current_id)
|
expect(rev.base_revision_id).to eq(current_id)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
# TODO: コンフリクト未実装のため,実装したらコメント外す.
|
context 'when conflict' do
|
||||||
# context 'when conflict' do
|
it 'returns 409 when base_revision_id mismatches' do
|
||||||
# it 'returns 409 when base_revision_id mismatches' do
|
# 先に別ユーザ(同じ member でもOK)が 1 回更新して先頭を進める
|
||||||
# # 先に別ユーザ(同じ member でもOK)が 1 回更新して先頭を進める
|
Wiki::Commit.content!(page: page, body: "zzz", created_user: member, message: 'other edit')
|
||||||
# Wiki::Commit.content!(page: page, body: "zzz", created_user: member, message: 'other edit')
|
page.reload
|
||||||
# page.reload
|
|
||||||
|
|
||||||
# stale_id = page.wiki_revisions.order(:id).first.id # わざと古い id
|
stale_id = page.wiki_revisions.order(:id).first.id # わざと古い id
|
||||||
# put "/wiki/#{page.id}",
|
put "/wiki/#{page.id}",
|
||||||
# params: { title: 'TestPage', body: 'x', base_revision_id: stale_id },
|
params: { title: 'TestPage', body: 'x', base_revision_id: stale_id },
|
||||||
# headers: auth_headers(member)
|
headers: auth_headers(member)
|
||||||
|
|
||||||
# expect(response).to have_http_status(:conflict)
|
expect(response).to have_http_status(:conflict)
|
||||||
# json = JSON.parse(response.body)
|
json = JSON.parse(response.body)
|
||||||
# expect(json['error']).to eq('conflict')
|
expect(json['error']).to eq('conflict')
|
||||||
# end
|
end
|
||||||
# end
|
end
|
||||||
|
|
||||||
context 'when page not found' do
|
context 'when page not found' do
|
||||||
it 'returns 404' do
|
it 'returns 404' do
|
||||||
@@ -261,14 +348,17 @@ RSpec.describe 'Wiki API', type: :request do
|
|||||||
|
|
||||||
describe 'GET /wiki/search' do
|
describe 'GET /wiki/search' do
|
||||||
before do
|
before do
|
||||||
# 追加で検索ヒット用
|
Wiki::Commit.create_content!(
|
||||||
TagName.create!(name: 'spec_wiki_title_2')
|
tag_name: TagName.create!(name: 'spec_wiki_title_2'),
|
||||||
WikiPage.create!(tag_name: TagName.find_by!(name: 'spec_wiki_title_2'),
|
body: 'search body 2',
|
||||||
created_user: user, updated_user: user)
|
created_by_user: user,
|
||||||
|
message: 'init')
|
||||||
|
|
||||||
TagName.create!(name: 'unrelated_title')
|
Wiki::Commit.create_content!(
|
||||||
WikiPage.create!(tag_name: TagName.find_by!(name: 'unrelated_title'),
|
tag_name: TagName.create!(name: 'unrelated_title'),
|
||||||
created_user: user, updated_user: user)
|
body: 'unrelated body',
|
||||||
|
created_by_user: user,
|
||||||
|
message: 'init')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns up to 20 pages filtered by title like' do
|
it 'returns up to 20 pages filtered by title like' do
|
||||||
@@ -278,7 +368,9 @@ RSpec.describe 'Wiki API', type: :request do
|
|||||||
expect(json).to be_an(Array)
|
expect(json).to be_an(Array)
|
||||||
|
|
||||||
titles = json.map { |p| p['title'] }
|
titles = json.map { |p| p['title'] }
|
||||||
expect(titles).to include('spec_wiki_title', 'spec_wiki_title_2')
|
|
||||||
|
expect(titles).to include('spec_wiki_title')
|
||||||
|
expect(titles).to include('spec_wiki_title_2')
|
||||||
expect(titles).not_to include('unrelated_title')
|
expect(titles).not_to include('unrelated_title')
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -329,7 +421,12 @@ RSpec.describe 'Wiki API', type: :request do
|
|||||||
it 'returns empty array when page has no revisions and filtered by id' do
|
it 'returns empty array when page has no revisions and filtered by id' do
|
||||||
# 別ページを作って revision 無し
|
# 別ページを作って revision 無し
|
||||||
tn2 = TagName.create!(name: 'spec_no_rev')
|
tn2 = TagName.create!(name: 'spec_no_rev')
|
||||||
p2 = WikiPage.create!(tag_name: tn2, created_user: user, updated_user: user)
|
# 異常データ: revision 無し WikiPage を直接作る
|
||||||
|
p2 = WikiPage.create!(
|
||||||
|
tag_name: tn2,
|
||||||
|
body: 'init',
|
||||||
|
created_user: user,
|
||||||
|
updated_user: user)
|
||||||
|
|
||||||
get "/wiki/changes?id=#{p2.id}"
|
get "/wiki/changes?id=#{p2.id}"
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
@@ -398,29 +495,68 @@ RSpec.describe 'Wiki API', type: :request do
|
|||||||
expect(json['older_revision_id']).to eq(rev_a.id)
|
expect(json['older_revision_id']).to eq(rev_a.id)
|
||||||
expect(json['newer_revision_id']).to eq(page.current_revision.id)
|
expect(json['newer_revision_id']).to eq(page.current_revision.id)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
it 'returns 422 when "from" is redirect revision' do
|
describe 'Wiki::Commit.redirect!' do
|
||||||
tn2 = TagName.create!(name: 'redirect_target2')
|
it 'raises because redirect revisions are deprecated' do
|
||||||
target = WikiPage.create!(tag_name: tn2, created_user: user, updated_user: user)
|
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')
|
||||||
|
|
||||||
Wiki::Commit.redirect!(page: page, redirect_page: target, created_user: user, message: 'redir2')
|
expect {
|
||||||
redirect_rev = page.current_revision
|
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
|
||||||
|
|
||||||
get "/wiki/#{page.id}/diff?from=#{redirect_rev.id}&to=#{rev_b.id}"
|
it 'wiki title を変更すると対応する tag の version を作成する' do
|
||||||
expect(response).to have_http_status(:unprocessable_entity)
|
linked_tag_name = TagName.create!(name: 'wiki_linked_tag_for_version')
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Wiki title collision', type: :request do
|
||||||
|
let!(:user) { create_member_user! }
|
||||||
|
|
||||||
|
def auth_headers user
|
||||||
|
{ 'X-Transfer-Code' => user.inheritance_code }
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_wiki_page title:, body:
|
||||||
|
Wiki::Commit.create_content!(
|
||||||
|
tag_name: TagName.create!(name: title),
|
||||||
|
body:,
|
||||||
|
created_by_user: user,
|
||||||
|
message: 'init')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 422 when renaming wiki title to existing title' do
|
||||||
|
source = create_wiki_page(title: 'wiki_collision_source', body: 'source body')
|
||||||
|
create_wiki_page(title: 'wiki_collision_target', body: 'target body')
|
||||||
|
|
||||||
|
source_revision_count = source.wiki_revisions.count
|
||||||
|
source_version_count = source.wiki_versions.count
|
||||||
|
old_title = source.title
|
||||||
|
old_body = source.body
|
||||||
|
|
||||||
|
put "/wiki/#{ source.id }",
|
||||||
|
params: {
|
||||||
|
title: 'wiki_collision_target',
|
||||||
|
body: 'new body',
|
||||||
|
message: 'rename collision',
|
||||||
|
base_revision_id: source.current_revision.id,
|
||||||
|
},
|
||||||
|
headers: auth_headers(user)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
|
||||||
|
source.reload
|
||||||
|
|
||||||
|
expect(source.title).to eq(old_title)
|
||||||
|
expect(source.body).to eq(old_body)
|
||||||
|
expect(source.wiki_revisions.count).to eq(source_revision_count)
|
||||||
|
expect(source.wiki_versions.count).to eq(source_version_count)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 422 when creating wiki with existing title' do
|
||||||
|
create_wiki_page(title: 'wiki_collision_create', body: 'already exists')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
post '/wiki',
|
||||||
|
params: {
|
||||||
|
title: 'wiki_collision_create',
|
||||||
|
body: 'new body',
|
||||||
|
message: 'duplicate create',
|
||||||
|
},
|
||||||
|
headers: auth_headers(user)
|
||||||
|
}
|
||||||
|
.not_to change(WikiPage, :count)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
require 'digest'
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Wiki::Commit do
|
||||||
|
let(:user) { create_member_user! }
|
||||||
|
|
||||||
|
def create_page title:, body: 'initial body'
|
||||||
|
described_class.create_content!(
|
||||||
|
tag_name: TagName.create!(name: title),
|
||||||
|
body:,
|
||||||
|
created_by_user: user,
|
||||||
|
message: 'init')
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.create_content!' do
|
||||||
|
it 'creates page, revision, and version with normalised body' do
|
||||||
|
expect {
|
||||||
|
described_class.create_content!(
|
||||||
|
tag_name: TagName.create!(name: 'commit_integrity_create'),
|
||||||
|
body: "a\r\nb\r\n\r\n",
|
||||||
|
created_by_user: user,
|
||||||
|
message: 'init')
|
||||||
|
}
|
||||||
|
.to change(WikiPage, :count).by(1)
|
||||||
|
.and change(WikiRevision, :count).by(1)
|
||||||
|
.and change(WikiVersion, :count).by(1)
|
||||||
|
|
||||||
|
page = WikiPage.joins(:tag_name).find_by!(tag_names: { name: 'commit_integrity_create' })
|
||||||
|
revision = page.current_revision
|
||||||
|
version = page.wiki_versions.order(:version_no).last
|
||||||
|
|
||||||
|
expect(page.body).to eq("a\nb")
|
||||||
|
expect(revision.lines_count).to eq(2)
|
||||||
|
expect(version.body).to eq("a\nb")
|
||||||
|
expect(version.reason).to eq('init')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'rejects body that becomes blank after normalisation' do
|
||||||
|
tag_name = TagName.create!(name: 'commit_integrity_blank')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
described_class.create_content!(
|
||||||
|
tag_name:,
|
||||||
|
body: "\r\n\r\n",
|
||||||
|
created_by_user: user,
|
||||||
|
message: 'blank')
|
||||||
|
}
|
||||||
|
.to raise_error(ActiveRecord::RecordInvalid)
|
||||||
|
|
||||||
|
expect(WikiPage.where(tag_name:)).not_to exist
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.content!' do
|
||||||
|
it 'updates page body, revision, and version' do
|
||||||
|
page = create_page(title: 'commit_integrity_update', body: 'before')
|
||||||
|
current_id = page.current_revision.id
|
||||||
|
|
||||||
|
expect {
|
||||||
|
described_class.content!(
|
||||||
|
page:,
|
||||||
|
body: 'after',
|
||||||
|
created_user: user,
|
||||||
|
message: 'edit',
|
||||||
|
base_revision_id: current_id)
|
||||||
|
}
|
||||||
|
.to change(WikiRevision, :count).by(1)
|
||||||
|
.and change(WikiVersion, :count).by(1)
|
||||||
|
|
||||||
|
page.reload
|
||||||
|
version = page.wiki_versions.order(:version_no).last
|
||||||
|
|
||||||
|
expect(page.body).to eq('after')
|
||||||
|
expect(version.body).to eq('after')
|
||||||
|
expect(version.reason).to eq('edit')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not record tag_version on body-only wiki update' do
|
||||||
|
tag_name = TagName.create!(name: 'commit_integrity_linked_tag')
|
||||||
|
tag = Tag.create!(tag_name:, category: :general)
|
||||||
|
|
||||||
|
page =
|
||||||
|
described_class.create_content!(
|
||||||
|
tag_name:,
|
||||||
|
body: 'before',
|
||||||
|
created_by_user: user,
|
||||||
|
message: 'init')
|
||||||
|
|
||||||
|
TagVersionRecorder.record!(
|
||||||
|
tag:,
|
||||||
|
event_type: :create,
|
||||||
|
created_by_user: user)
|
||||||
|
|
||||||
|
before_count = tag.reload.tag_versions.count
|
||||||
|
|
||||||
|
described_class.content!(
|
||||||
|
page:,
|
||||||
|
body: 'after',
|
||||||
|
created_user: user,
|
||||||
|
message: 'edit',
|
||||||
|
base_revision_id: page.current_revision.id)
|
||||||
|
|
||||||
|
expect(tag.reload.tag_versions.count).to eq(before_count)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises conflict and leaves page, revision, and version unchanged' do
|
||||||
|
page = create_page(title: 'commit_integrity_conflict', body: 'first')
|
||||||
|
stale_id = page.current_revision.id
|
||||||
|
|
||||||
|
described_class.content!(
|
||||||
|
page:,
|
||||||
|
body: 'second',
|
||||||
|
created_user: user,
|
||||||
|
message: 'second',
|
||||||
|
base_revision_id: stale_id)
|
||||||
|
|
||||||
|
page.reload
|
||||||
|
before_body = page.body
|
||||||
|
before_revision_count = page.wiki_revisions.count
|
||||||
|
before_version_count = page.wiki_versions.count
|
||||||
|
|
||||||
|
expect {
|
||||||
|
described_class.content!(
|
||||||
|
page:,
|
||||||
|
body: 'third',
|
||||||
|
created_user: user,
|
||||||
|
message: 'stale',
|
||||||
|
base_revision_id: stale_id)
|
||||||
|
}
|
||||||
|
.to raise_error(Wiki::Commit::Conflict)
|
||||||
|
|
||||||
|
page.reload
|
||||||
|
expect(page.body).to eq(before_body)
|
||||||
|
expect(page.wiki_revisions.count).to eq(before_revision_count)
|
||||||
|
expect(page.wiki_versions.count).to eq(before_version_count)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'deduplicates duplicated missing wiki lines' do
|
||||||
|
page = create_page(title: 'commit_integrity_dedup', body: 'before')
|
||||||
|
duplicated = 'commit_integrity_duplicate_line'
|
||||||
|
|
||||||
|
described_class.content!(
|
||||||
|
page:,
|
||||||
|
body: "#{ duplicated }\n#{ duplicated }",
|
||||||
|
created_user: user,
|
||||||
|
message: 'dedup',
|
||||||
|
base_revision_id: page.current_revision.id)
|
||||||
|
|
||||||
|
revision = page.reload.current_revision
|
||||||
|
|
||||||
|
expect(WikiLine.where(body: duplicated).count).to eq(1)
|
||||||
|
expect(revision.wiki_revision_lines.count).to eq(2)
|
||||||
|
expect(revision.wiki_revision_lines.pluck(:wiki_line_id).uniq.size).to eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.redirect!' do
|
||||||
|
it 'raises because redirect revisions are deprecated' do
|
||||||
|
page = create_page(title: 'commit_integrity_redirect_source', body: 'source')
|
||||||
|
target = create_page(title: 'commit_integrity_redirect_target', body: 'target')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
described_class.redirect!(
|
||||||
|
page:,
|
||||||
|
redirect_page: target,
|
||||||
|
created_user: user,
|
||||||
|
message: 'redirect',
|
||||||
|
base_revision_id: page.current_revision.id)
|
||||||
|
}
|
||||||
|
.to raise_error(RuntimeError, '廃止しました.')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Wiki::Commit do
|
||||||
|
let(:user) { create_member_user! }
|
||||||
|
|
||||||
|
def create_page(title: 'commit_spec_page', body: 'initial body')
|
||||||
|
tag_name = TagName.create!(name: title)
|
||||||
|
|
||||||
|
Wiki::Commit.create_content!(
|
||||||
|
tag_name:,
|
||||||
|
body:,
|
||||||
|
created_by_user: user,
|
||||||
|
message: 'init')
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.content!' do
|
||||||
|
it 'stores normalised body in wiki_pages and wiki_versions' do
|
||||||
|
page = create_page(title: 'commit_normalised_page')
|
||||||
|
|
||||||
|
described_class.content!(
|
||||||
|
page:,
|
||||||
|
body: "a\r\nb\r\n\r\n",
|
||||||
|
created_user: user,
|
||||||
|
message: 'init'
|
||||||
|
)
|
||||||
|
|
||||||
|
page.reload
|
||||||
|
version = page.wiki_versions.order(:version_no).last
|
||||||
|
|
||||||
|
expect(page.body).to eq("a\nb")
|
||||||
|
expect(version.body).to eq("a\nb")
|
||||||
|
expect(page.current_revision.lines_count).to eq(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'deduplicates duplicated missing wiki lines before upsert' do
|
||||||
|
page = create_page(title: 'commit_duplicate_line_page')
|
||||||
|
duplicated = 'commit_duplicate_line'
|
||||||
|
|
||||||
|
described_class.content!(
|
||||||
|
page:,
|
||||||
|
body: "#{ duplicated }\n#{ duplicated }",
|
||||||
|
created_user: user,
|
||||||
|
message: 'init'
|
||||||
|
)
|
||||||
|
|
||||||
|
page.reload
|
||||||
|
|
||||||
|
expect(WikiLine.where(body: duplicated).count).to eq(1)
|
||||||
|
expect(page.current_revision.lines_count).to eq(2)
|
||||||
|
expect(page.current_revision.wiki_revision_lines.count).to eq(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises conflict when base_revision_id is stale' do
|
||||||
|
page = create_page(title: 'commit_conflict_page')
|
||||||
|
|
||||||
|
first = described_class.content!(
|
||||||
|
page:,
|
||||||
|
body: 'first',
|
||||||
|
created_user: user,
|
||||||
|
message: 'first'
|
||||||
|
)
|
||||||
|
|
||||||
|
described_class.content!(
|
||||||
|
page:,
|
||||||
|
body: 'second',
|
||||||
|
created_user: user,
|
||||||
|
message: 'second',
|
||||||
|
base_revision_id: first.id
|
||||||
|
)
|
||||||
|
|
||||||
|
expect {
|
||||||
|
described_class.content!(
|
||||||
|
page:,
|
||||||
|
body: 'third',
|
||||||
|
created_user: user,
|
||||||
|
message: 'third',
|
||||||
|
base_revision_id: first.id
|
||||||
|
)
|
||||||
|
}.to raise_error(Wiki::Commit::Conflict)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not record tag version when corresponding tag has no versions' do
|
||||||
|
tag_name = TagName.create!(name: 'commit_linked_tag_without_versions')
|
||||||
|
tag = Tag.create!(tag_name:, category: :general)
|
||||||
|
|
||||||
|
page =
|
||||||
|
described_class.create_content!(
|
||||||
|
tag_name:,
|
||||||
|
body: 'before',
|
||||||
|
created_by_user: user,
|
||||||
|
message: 'init')
|
||||||
|
|
||||||
|
expect(tag.reload.tag_versions.count).to eq(0)
|
||||||
|
|
||||||
|
current_revision_id = page.current_revision.id
|
||||||
|
|
||||||
|
expect {
|
||||||
|
described_class.content!(
|
||||||
|
page:,
|
||||||
|
body: 'after',
|
||||||
|
created_user: user,
|
||||||
|
message: 'edit',
|
||||||
|
base_revision_id: current_revision_id)
|
||||||
|
}.to change(WikiVersion, :count).by(1)
|
||||||
|
|
||||||
|
expect(tag.reload.tag_versions.count).to eq(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not record tag version when corresponding tag has no versions' do
|
||||||
|
tag_name = TagName.create!(name: 'commit_linked_tag_without_versions')
|
||||||
|
tag = Tag.create!(tag_name:, category: :general)
|
||||||
|
|
||||||
|
page =
|
||||||
|
described_class.create_content!(
|
||||||
|
tag_name:,
|
||||||
|
body: 'before',
|
||||||
|
created_by_user: user,
|
||||||
|
message: 'init')
|
||||||
|
|
||||||
|
current_revision_id = page.current_revision.id
|
||||||
|
|
||||||
|
expect {
|
||||||
|
described_class.content!(
|
||||||
|
page:,
|
||||||
|
body: 'after',
|
||||||
|
created_user: user,
|
||||||
|
message: 'edit',
|
||||||
|
base_revision_id: current_revision_id)
|
||||||
|
}.to change(WikiVersion, :count).by(1)
|
||||||
|
|
||||||
|
expect(tag.reload.tag_versions.count).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.redirect!' do
|
||||||
|
it 'raises because redirect revisions are deprecated' do
|
||||||
|
page = create_page(title: 'commit_redirect_source')
|
||||||
|
target = create_page(title: 'commit_redirect_target')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
described_class.redirect!(
|
||||||
|
page:,
|
||||||
|
redirect_page: target,
|
||||||
|
created_user: user,
|
||||||
|
message: 'redirect'
|
||||||
|
)
|
||||||
|
}.to raise_error(RuntimeError, '廃止しました.')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe WikiVersionRecorder do
|
||||||
|
let(:user) { create_member_user! }
|
||||||
|
|
||||||
|
def create_page title:, body: 'body'
|
||||||
|
Wiki::Commit.create_content!(
|
||||||
|
tag_name: TagName.create!(name: title),
|
||||||
|
body:,
|
||||||
|
created_by_user: user,
|
||||||
|
message: 'init')
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.record!' do
|
||||||
|
it 'records title, body, reason, user, and version number' do
|
||||||
|
page = create_page(title: 'wiki_version_recorder_basic', body: 'body')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
described_class.record!(
|
||||||
|
page:,
|
||||||
|
event_type: :update,
|
||||||
|
reason: 'manual reason',
|
||||||
|
created_by_user: user)
|
||||||
|
}
|
||||||
|
.to change { page.reload.wiki_versions.count }.by(1)
|
||||||
|
|
||||||
|
version = page.wiki_versions.order(:version_no).last
|
||||||
|
|
||||||
|
expect(version).to have_attributes(
|
||||||
|
version_no: 2,
|
||||||
|
event_type: 'update',
|
||||||
|
title: 'wiki_version_recorder_basic',
|
||||||
|
body: 'body',
|
||||||
|
reason: 'manual reason',
|
||||||
|
created_by_user_id: user.id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create duplicated update version for identical snapshot' do
|
||||||
|
page = create_page(title: 'wiki_version_recorder_duplicate', body: 'body')
|
||||||
|
|
||||||
|
described_class.record!(
|
||||||
|
page:,
|
||||||
|
event_type: :update,
|
||||||
|
reason: nil,
|
||||||
|
created_by_user: user)
|
||||||
|
|
||||||
|
before_count = page.reload.wiki_versions.count
|
||||||
|
|
||||||
|
described_class.record!(
|
||||||
|
page:,
|
||||||
|
event_type: :update,
|
||||||
|
reason: nil,
|
||||||
|
created_by_user: user)
|
||||||
|
|
||||||
|
expect(page.reload.wiki_versions.count).to eq(before_count)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates update version when title changes' do
|
||||||
|
page = create_page(title: 'wiki_version_recorder_title_before', body: 'body')
|
||||||
|
page.tag_name.update!(name: 'wiki_version_recorder_title_after')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
described_class.record!(
|
||||||
|
page:,
|
||||||
|
event_type: :update,
|
||||||
|
reason: 'rename',
|
||||||
|
created_by_user: user)
|
||||||
|
}
|
||||||
|
.to change { page.reload.wiki_versions.count }.by(1)
|
||||||
|
|
||||||
|
version = page.wiki_versions.order(:version_no).last
|
||||||
|
|
||||||
|
expect(version.title).to eq('wiki_version_recorder_title_after')
|
||||||
|
expect(version.body).to eq('body')
|
||||||
|
expect(version.reason).to eq('rename')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates update version when body changes' do
|
||||||
|
page = create_page(title: 'wiki_version_recorder_body', body: 'before')
|
||||||
|
page.update!(body: 'after')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
described_class.record!(
|
||||||
|
page:,
|
||||||
|
event_type: :update,
|
||||||
|
reason: 'body',
|
||||||
|
created_by_user: user)
|
||||||
|
}
|
||||||
|
.to change { page.reload.wiki_versions.count }.by(1)
|
||||||
|
|
||||||
|
version = page.wiki_versions.order(:version_no).last
|
||||||
|
|
||||||
|
expect(version.title).to eq('wiki_version_recorder_body')
|
||||||
|
expect(version.body).to eq('after')
|
||||||
|
expect(version.reason).to eq('body')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Youtube::ApiClient do
|
||||||
|
let(:api_key) { 'test-api-key' }
|
||||||
|
let(:client) { described_class.new(api_key:) }
|
||||||
|
|
||||||
|
describe '#search_videos' do
|
||||||
|
it 'calls YouTube search API with expected params' do
|
||||||
|
published_after = Time.zone.parse('2026-05-01 00:00:00')
|
||||||
|
published_before = Time.zone.parse('2026-05-02 00:00:00')
|
||||||
|
|
||||||
|
expect(client).to receive(:get_json).with(
|
||||||
|
'/search',
|
||||||
|
{
|
||||||
|
part: 'snippet',
|
||||||
|
type: 'video',
|
||||||
|
q: 'ぼざろクリーチャー',
|
||||||
|
order: 'date',
|
||||||
|
maxResults: 50,
|
||||||
|
regionCode: 'JP',
|
||||||
|
relevanceLanguage: 'ja',
|
||||||
|
publishedAfter: published_after.iso8601,
|
||||||
|
publishedBefore: published_before.iso8601,
|
||||||
|
pageToken: 'NEXT'
|
||||||
|
}
|
||||||
|
).and_return({ 'items' => [] })
|
||||||
|
|
||||||
|
client.search_videos(
|
||||||
|
q: 'ぼざろクリーチャー',
|
||||||
|
published_after:,
|
||||||
|
published_before:,
|
||||||
|
page_token: 'NEXT'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'omits nil optional params' do
|
||||||
|
expect(client).to receive(:get_json).with(
|
||||||
|
'/search',
|
||||||
|
hash_excluding(:publishedAfter, :publishedBefore, :pageToken)
|
||||||
|
).and_return({ 'items' => [] })
|
||||||
|
|
||||||
|
client.search_videos(q: 'ぼざろクリーチャー')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#videos' do
|
||||||
|
it 'returns empty items when ids are empty' do
|
||||||
|
expect(client).not_to receive(:get_json)
|
||||||
|
|
||||||
|
expect(client.videos([])).to eq({ 'items' => [] })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calls videos API with comma separated ids' do
|
||||||
|
expect(client).to receive(:get_json).with(
|
||||||
|
'/videos',
|
||||||
|
{
|
||||||
|
part: 'snippet,status,contentDetails',
|
||||||
|
id: 'video-1,video-2'
|
||||||
|
}
|
||||||
|
).and_return({ 'items' => [] })
|
||||||
|
|
||||||
|
client.videos(['video-1', 'video-2'])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#playlist_items' do
|
||||||
|
it 'calls playlistItems API with page token' do
|
||||||
|
expect(client).to receive(:get_json).with(
|
||||||
|
'/playlistItems',
|
||||||
|
{
|
||||||
|
part: 'snippet,contentDetails,status',
|
||||||
|
playlistId: 'PL123',
|
||||||
|
maxResults: 50,
|
||||||
|
pageToken: 'NEXT'
|
||||||
|
}
|
||||||
|
).and_return({ 'items' => [] })
|
||||||
|
|
||||||
|
client.playlist_items(playlist_id: 'PL123', page_token: 'NEXT')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'omits page token when nil' do
|
||||||
|
expect(client).to receive(:get_json).with(
|
||||||
|
'/playlistItems',
|
||||||
|
{
|
||||||
|
part: 'snippet,contentDetails,status',
|
||||||
|
playlistId: 'PL123',
|
||||||
|
maxResults: 50
|
||||||
|
}
|
||||||
|
).and_return({ 'items' => [] })
|
||||||
|
|
||||||
|
client.playlist_items(playlist_id: 'PL123')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#channel' do
|
||||||
|
it 'calls channels API by id' do
|
||||||
|
expect(client).to receive(:get_json).with(
|
||||||
|
'/channels',
|
||||||
|
{
|
||||||
|
part: 'snippet,contentDetails',
|
||||||
|
id: 'UC123'
|
||||||
|
}
|
||||||
|
).and_return({ 'items' => [] })
|
||||||
|
|
||||||
|
client.channel(id: 'UC123')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calls channels API by handle' do
|
||||||
|
expect(client).to receive(:get_json).with(
|
||||||
|
'/channels',
|
||||||
|
{
|
||||||
|
part: 'snippet,contentDetails',
|
||||||
|
forHandle: '@some_handle'
|
||||||
|
}
|
||||||
|
).and_return({ 'items' => [] })
|
||||||
|
|
||||||
|
client.channel(handle: '@some_handle')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises when neither id nor handle is given' do
|
||||||
|
expect { client.channel }.to raise_error(ArgumentError, 'id or handle is required')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises when both id and handle are given' do
|
||||||
|
expect do
|
||||||
|
client.channel(id: 'UC123', handle: '@some_handle')
|
||||||
|
end.to raise_error(ArgumentError, 'id or handle is required')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Youtube::Sync do
|
||||||
|
let(:client) { instance_double(Youtube::ApiClient) }
|
||||||
|
let(:sync) { described_class.new(client:) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(PostVersionRecorder).to receive(:record!)
|
||||||
|
allow(PostVersionRecorder).to receive(:ensure_snapshot!)
|
||||||
|
allow(sync).to receive(:attach_thumbnail_if_needed!)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#sync!' do
|
||||||
|
it 'returns without fetching video details when no video ids are discovered' do
|
||||||
|
allow(sync).to receive(:query_terms).and_return([])
|
||||||
|
allow(sync).to receive(:playlist_ids).and_return([])
|
||||||
|
|
||||||
|
expect(client).not_to receive(:videos)
|
||||||
|
|
||||||
|
sync.sync!
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'discovers ids from search and all playlist pages' do
|
||||||
|
allow(sync).to receive(:query_terms).and_return(['ぼざろクリーチャー'])
|
||||||
|
allow(sync).to receive(:playlist_ids).and_return(['PL123'])
|
||||||
|
allow(sync).to receive(:sync_since).and_return(Time.zone.parse('2026-05-01 00:00:00'))
|
||||||
|
|
||||||
|
allow(client).to receive(:search_videos).with(
|
||||||
|
q: 'ぼざろクリーチャー',
|
||||||
|
published_after: Time.zone.parse('2026-05-01 00:00:00')
|
||||||
|
).and_return({
|
||||||
|
'items' => [
|
||||||
|
{
|
||||||
|
'id' => {
|
||||||
|
'videoId' => 'search-video-1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
allow(client).to receive(:playlist_items).with(
|
||||||
|
playlist_id: 'PL123',
|
||||||
|
page_token: nil
|
||||||
|
).and_return({
|
||||||
|
'items' => [
|
||||||
|
{
|
||||||
|
'contentDetails' => {
|
||||||
|
'videoId' => 'playlist-video-1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'nextPageToken' => 'NEXT'
|
||||||
|
})
|
||||||
|
|
||||||
|
allow(client).to receive(:playlist_items).with(
|
||||||
|
playlist_id: 'PL123',
|
||||||
|
page_token: 'NEXT'
|
||||||
|
).and_return({
|
||||||
|
'items' => [
|
||||||
|
{
|
||||||
|
'snippet' => {
|
||||||
|
'resourceId' => {
|
||||||
|
'videoId' => 'playlist-video-2'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(client).to receive(:videos).with(
|
||||||
|
satisfy do |ids|
|
||||||
|
ids.sort == ['playlist-video-1', 'playlist-video-2', 'search-video-1']
|
||||||
|
end
|
||||||
|
).and_return({ 'items' => [] })
|
||||||
|
|
||||||
|
sync.sync!
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a YouTube post with default tags and no_deerjikist when no deerjikist mapping exists' do
|
||||||
|
Tag.tagme
|
||||||
|
Tag.bot
|
||||||
|
Tag.youtube
|
||||||
|
Tag.video
|
||||||
|
Tag.no_deerjikist
|
||||||
|
|
||||||
|
allow(sync).to receive(:query_terms).and_return([])
|
||||||
|
allow(sync).to receive(:playlist_ids).and_return(['PL123'])
|
||||||
|
|
||||||
|
allow(client).to receive(:playlist_items).with(
|
||||||
|
playlist_id: 'PL123',
|
||||||
|
page_token: nil
|
||||||
|
).and_return({
|
||||||
|
'items' => [
|
||||||
|
{
|
||||||
|
'contentDetails' => {
|
||||||
|
'videoId' => 'video-1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
allow(client).to receive(:videos).with(['video-1']).and_return({
|
||||||
|
'items' => [
|
||||||
|
youtube_video_item(
|
||||||
|
id: 'video-1',
|
||||||
|
title: 'YouTube テスト動画',
|
||||||
|
channel_id: 'UC_NO_MAPPING'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
expect do
|
||||||
|
sync.sync!
|
||||||
|
end.to change(Post, :count).by(1)
|
||||||
|
|
||||||
|
post = Post.find_by!(url: 'https://www.youtube.com/watch?v=video-1')
|
||||||
|
tag_ids = post.tags.pluck(:id)
|
||||||
|
|
||||||
|
expect(post.title).to eq('YouTube テスト動画')
|
||||||
|
expect(post.uploaded_user_id).to be_nil
|
||||||
|
expect(post.original_created_from).to eq(Time.zone.parse('2026-05-01 12:34:00'))
|
||||||
|
expect(post.original_created_before).to eq(Time.zone.parse('2026-05-01 12:35:00'))
|
||||||
|
|
||||||
|
expect(tag_ids).to include(Tag.tagme.id)
|
||||||
|
expect(tag_ids).to include(Tag.bot.id)
|
||||||
|
expect(tag_ids).to include(Tag.youtube.id)
|
||||||
|
expect(tag_ids).to include(Tag.video.id)
|
||||||
|
expect(tag_ids).to include(Tag.no_deerjikist.id)
|
||||||
|
|
||||||
|
expect(PostVersionRecorder).to have_received(:record!).with(
|
||||||
|
post:,
|
||||||
|
event_type: :create,
|
||||||
|
created_by_user: nil
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'uses deerjikist tag when channel id is mapped' do
|
||||||
|
Tag.tagme
|
||||||
|
Tag.bot
|
||||||
|
Tag.youtube
|
||||||
|
Tag.video
|
||||||
|
Tag.no_deerjikist
|
||||||
|
|
||||||
|
deerjikist_tag = Tag.find_or_create_by_tag_name!('テスト投稿者', category: :deerjikist)
|
||||||
|
Deerjikist.create!(
|
||||||
|
platform: 'youtube',
|
||||||
|
code: 'UC_MAPPED',
|
||||||
|
tag: deerjikist_tag
|
||||||
|
)
|
||||||
|
|
||||||
|
allow(sync).to receive(:query_terms).and_return([])
|
||||||
|
allow(sync).to receive(:playlist_ids).and_return(['PL123'])
|
||||||
|
|
||||||
|
allow(client).to receive(:playlist_items).with(
|
||||||
|
playlist_id: 'PL123',
|
||||||
|
page_token: nil
|
||||||
|
).and_return({
|
||||||
|
'items' => [
|
||||||
|
{
|
||||||
|
'contentDetails' => {
|
||||||
|
'videoId' => 'video-1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
allow(client).to receive(:videos).with(['video-1']).and_return({
|
||||||
|
'items' => [
|
||||||
|
youtube_video_item(
|
||||||
|
id: 'video-1',
|
||||||
|
title: 'YouTube テスト動画',
|
||||||
|
channel_id: 'UC_MAPPED'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
sync.sync!
|
||||||
|
|
||||||
|
post = Post.find_by!(url: 'https://www.youtube.com/watch?v=video-1')
|
||||||
|
tag_ids = post.tags.pluck(:id)
|
||||||
|
|
||||||
|
expect(tag_ids).to include(deerjikist_tag.id)
|
||||||
|
expect(tag_ids).not_to include(Tag.no_deerjikist.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes no_deerjikist when deerjikist mapping is added later' do
|
||||||
|
Tag.no_deerjikist
|
||||||
|
|
||||||
|
post = Post.create!(
|
||||||
|
title: '旧タイトル',
|
||||||
|
url: 'https://www.youtube.com/watch?v=video-1',
|
||||||
|
uploaded_user_id: nil,
|
||||||
|
original_created_from: Time.zone.parse('2026-05-01 00:00:00'),
|
||||||
|
original_created_before: Time.zone.parse('2026-05-01 00:01:00')
|
||||||
|
)
|
||||||
|
PostTag.create!(post:, tag: Tag.no_deerjikist)
|
||||||
|
|
||||||
|
deerjikist_tag = Tag.find_or_create_by_tag_name!('後から判明した投稿者', category: :deerjikist)
|
||||||
|
Deerjikist.create!(
|
||||||
|
platform: 'youtube',
|
||||||
|
code: 'UC_MAPPED_LATER',
|
||||||
|
tag: deerjikist_tag
|
||||||
|
)
|
||||||
|
|
||||||
|
allow(sync).to receive(:query_terms).and_return([])
|
||||||
|
allow(sync).to receive(:playlist_ids).and_return(['PL123'])
|
||||||
|
|
||||||
|
allow(client).to receive(:playlist_items).with(
|
||||||
|
playlist_id: 'PL123',
|
||||||
|
page_token: nil
|
||||||
|
).and_return({
|
||||||
|
'items' => [
|
||||||
|
{
|
||||||
|
'contentDetails' => {
|
||||||
|
'videoId' => 'video-1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
allow(client).to receive(:videos).with(['video-1']).and_return({
|
||||||
|
'items' => [
|
||||||
|
youtube_video_item(
|
||||||
|
id: 'video-1',
|
||||||
|
title: '新タイトル',
|
||||||
|
channel_id: 'UC_MAPPED_LATER'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
sync.sync!
|
||||||
|
|
||||||
|
post.reload
|
||||||
|
tag_ids = post.tags.pluck(:id)
|
||||||
|
|
||||||
|
expect(post.title).to eq('新タイトル')
|
||||||
|
expect(tag_ids).to include(deerjikist_tag.id)
|
||||||
|
expect(tag_ids).not_to include(Tag.no_deerjikist.id)
|
||||||
|
|
||||||
|
expect(PostVersionRecorder).to have_received(:ensure_snapshot!).with(
|
||||||
|
post,
|
||||||
|
created_by_user: nil
|
||||||
|
)
|
||||||
|
expect(PostVersionRecorder).to have_received(:record!).with(
|
||||||
|
post:,
|
||||||
|
event_type: :update,
|
||||||
|
created_by_user: nil
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'matches existing youtu.be URL and does not create duplicate post' do
|
||||||
|
post = Post.create!(
|
||||||
|
title: '旧タイトル',
|
||||||
|
url: 'https://youtu.be/video-1',
|
||||||
|
uploaded_user_id: nil,
|
||||||
|
original_created_from: Time.zone.parse('2026-05-01 00:00:00'),
|
||||||
|
original_created_before: Time.zone.parse('2026-05-01 00:01:00')
|
||||||
|
)
|
||||||
|
|
||||||
|
allow(sync).to receive(:query_terms).and_return([])
|
||||||
|
allow(sync).to receive(:playlist_ids).and_return(['PL123'])
|
||||||
|
|
||||||
|
allow(client).to receive(:playlist_items).with(
|
||||||
|
playlist_id: 'PL123',
|
||||||
|
page_token: nil
|
||||||
|
).and_return({
|
||||||
|
'items' => [
|
||||||
|
{
|
||||||
|
'contentDetails' => {
|
||||||
|
'videoId' => 'video-1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
allow(client).to receive(:videos).with(['video-1']).and_return({
|
||||||
|
'items' => [
|
||||||
|
youtube_video_item(
|
||||||
|
id: 'video-1',
|
||||||
|
title: '新タイトル',
|
||||||
|
channel_id: 'UC_NO_MAPPING'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
expect do
|
||||||
|
sync.sync!
|
||||||
|
end.not_to change(Post, :count)
|
||||||
|
|
||||||
|
expect(post.reload.title).to eq('新タイトル')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def youtube_video_item(id:, title:, channel_id:)
|
||||||
|
{
|
||||||
|
'id' => id,
|
||||||
|
'snippet' => {
|
||||||
|
'title' => title,
|
||||||
|
'channelId' => channel_id,
|
||||||
|
'publishedAt' => '2026-05-01T12:34:56Z',
|
||||||
|
'thumbnails' => {
|
||||||
|
'high' => {
|
||||||
|
'url' => "https://img.youtube.com/#{id}.jpg"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'tags' => ['tag-a', 'tag-b']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Youtube::VideoItem do
|
||||||
|
describe '#initialize' do
|
||||||
|
it 'extracts fields from YouTube video API item' do
|
||||||
|
item = {
|
||||||
|
'id' => 'video-1',
|
||||||
|
'snippet' => {
|
||||||
|
'title' => 'テスト動画',
|
||||||
|
'channelId' => 'UC123',
|
||||||
|
'publishedAt' => '2026-05-01T12:34:56Z',
|
||||||
|
'tags' => ['tag-a', 'tag-b'],
|
||||||
|
'thumbnails' => {
|
||||||
|
'high' => {
|
||||||
|
'url' => 'https://img.youtube.com/high.jpg'
|
||||||
|
},
|
||||||
|
'medium' => {
|
||||||
|
'url' => 'https://img.youtube.com/medium.jpg'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
video = described_class.new(item)
|
||||||
|
|
||||||
|
expect(video.id).to eq('video-1')
|
||||||
|
expect(video.title).to eq('テスト動画')
|
||||||
|
expect(video.channel_id).to eq('UC123')
|
||||||
|
expect(video.published_at).to eq(Time.iso8601('2026-05-01T12:34:56Z'))
|
||||||
|
expect(video.thumbnail_url).to eq('https://img.youtube.com/high.jpg')
|
||||||
|
expect(video.raw_tags).to eq(['tag-a', 'tag-b'])
|
||||||
|
expect(video.url).to eq('https://www.youtube.com/watch?v=video-1')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'uses highest priority thumbnail' do
|
||||||
|
item = {
|
||||||
|
'id' => 'video-1',
|
||||||
|
'snippet' => {
|
||||||
|
'title' => 'テスト動画',
|
||||||
|
'channelId' => 'UC123',
|
||||||
|
'publishedAt' => '2026-05-01T12:34:56Z',
|
||||||
|
'thumbnails' => {
|
||||||
|
'default' => {
|
||||||
|
'url' => 'https://img.youtube.com/default.jpg'
|
||||||
|
},
|
||||||
|
'standard' => {
|
||||||
|
'url' => 'https://img.youtube.com/standard.jpg'
|
||||||
|
},
|
||||||
|
'maxres' => {
|
||||||
|
'url' => 'https://img.youtube.com/maxres.jpg'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
video = described_class.new(item)
|
||||||
|
|
||||||
|
expect(video.thumbnail_url).to eq('https://img.youtube.com/maxres.jpg')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'falls back to empty raw tags' do
|
||||||
|
item = {
|
||||||
|
'id' => 'video-1',
|
||||||
|
'snippet' => {
|
||||||
|
'title' => 'テスト動画',
|
||||||
|
'channelId' => 'UC123',
|
||||||
|
'publishedAt' => '2026-05-01T12:34:56Z',
|
||||||
|
'thumbnails' => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
video = described_class.new(item)
|
||||||
|
|
||||||
|
expect(video.raw_tags).to eq([])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns nil thumbnail when no thumbnail exists' do
|
||||||
|
item = {
|
||||||
|
'id' => 'video-1',
|
||||||
|
'snippet' => {
|
||||||
|
'title' => 'テスト動画',
|
||||||
|
'channelId' => 'UC123',
|
||||||
|
'publishedAt' => '2026-05-01T12:34:56Z',
|
||||||
|
'thumbnails' => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
video = described_class.new(item)
|
||||||
|
|
||||||
|
expect(video.thumbnail_url).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
require 'rake'
|
||||||
|
|
||||||
|
RSpec.describe 'post:sync' do
|
||||||
|
around do |example|
|
||||||
|
original_application = Rake.application
|
||||||
|
Rake.application = Rake::Application.new
|
||||||
|
|
||||||
|
Rake::Task.define_task(:environment)
|
||||||
|
load Rails.root.join('lib/tasks/sync_posts.rake')
|
||||||
|
|
||||||
|
example.run
|
||||||
|
ensure
|
||||||
|
Rake.application = original_application
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'runs Youtube::Sync' do
|
||||||
|
sync = instance_double(Youtube::Sync)
|
||||||
|
|
||||||
|
expect(Youtube::Sync).to receive(:new).once.and_return(sync)
|
||||||
|
expect(sync).to receive(:sync!).once
|
||||||
|
|
||||||
|
Rake::Task['post:sync'].invoke
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user