Commit 99b3ee82 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch '30526-a-be-wiki-activity-Models' into 'master'

#30526 (A) [BE] Wiki Events (models)

See merge request gitlab-org/gitlab!26529
parents 6f412cb9 82a0cc88
......@@ -6,6 +6,7 @@ class Event < ApplicationRecord
include Presentable
include DeleteWithLimit
include CreatedAtFilterable
include Gitlab::Utils::StrongMemoize
default_scope { reorder(nil) }
......@@ -42,7 +43,8 @@ class Event < ApplicationRecord
note: Note,
project: Project,
snippet: Snippet,
user: User
user: User,
wiki: WikiPage::Meta
).freeze
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
......@@ -79,6 +81,7 @@ class Event < ApplicationRecord
scope :recent, -> { reorder(id: :desc) }
scope :code_push, -> { where(action: PUSHED) }
scope :merged, -> { where(action: MERGED) }
scope :for_wiki_page, -> { where(target_type: WikiPage::Meta.name) }
scope :with_associations, -> do
# We're using preload for "push_event_payload" as otherwise the association
......@@ -197,6 +200,14 @@ class Event < ApplicationRecord
created_action? && !target && target_type.nil?
end
def created_wiki_page?
wiki_page? && action == CREATED
end
def updated_wiki_page?
wiki_page? && action == UPDATED
end
def created_target?
created_action? && target
end
......@@ -217,6 +228,10 @@ class Event < ApplicationRecord
target_type == "MergeRequest"
end
def wiki_page?
target_type == WikiPage::Meta.name
end
def milestone
target if milestone?
end
......@@ -229,6 +244,14 @@ class Event < ApplicationRecord
target if merge_request?
end
def wiki_page
strong_memoize(:wiki_page) do
next unless wiki_page?
ProjectWiki.new(project, author).find_page(target.canonical_slug)
end
end
def note
target if note?
end
......@@ -250,6 +273,10 @@ class Event < ApplicationRecord
'destroyed'
elsif commented_action?
"commented on"
elsif created_wiki_page?
'created'
elsif updated_wiki_page?
'updated'
elsif created_project_action?
created_project_action_name
else
......@@ -362,6 +389,8 @@ class Event < ApplicationRecord
:read_snippet
elsif milestone?
:read_milestone
elsif wiki_page?
:read_wiki
end
end
end
......
......@@ -19,10 +19,11 @@ class ProjectWiki
DIRECTION_DESC = 'desc'
DIRECTION_ASC = 'asc'
attr_reader :project, :user
# Returns a string describing what went wrong after
# an operation fails.
attr_reader :error_message
attr_reader :project
def initialize(project, user = nil)
@project = project
......@@ -196,9 +197,9 @@ class ProjectWiki
def commit_details(action, message = nil, title = nil)
commit_message = message.presence || default_message(action, title)
git_user = Gitlab::Git::User.from_gitlab(@user)
git_user = Gitlab::Git::User.from_gitlab(user)
Gitlab::Git::Wiki::CommitDetails.new(@user.id,
Gitlab::Git::Wiki::CommitDetails.new(user.id,
git_user.username,
git_user.name,
git_user.email,
......@@ -206,7 +207,7 @@ class ProjectWiki
end
def default_message(action, title)
"#{@user.username} #{action} page: #{title}"
"#{user.username} #{action} page: #{title}"
end
def update_project_activity
......
......@@ -21,6 +21,14 @@ class WikiPage
ActiveModel::Name.new(self, nil, 'wiki')
end
def eql?(other)
return false unless other.present? && other.is_a?(self.class)
slug == other.slug && wiki.project == other.wiki.project
end
alias_method :==, :eql?
# Sorts and groups pages by directory.
#
# pages - an array of WikiPage objects.
......@@ -58,6 +66,7 @@ class WikiPage
# The GitLab ProjectWiki instance.
attr_reader :wiki
delegate :project, to: :wiki
# The raw Gitlab::Git::WikiPage instance.
attr_reader :page
......@@ -70,6 +79,10 @@ class WikiPage
Gitlab::HookData::WikiPageBuilder.new(self).build
end
# Construct a new WikiPage
#
# @param [ProjectWiki] wiki
# @param [Gitlab::Git::WikiPage] page
def initialize(wiki, page = nil)
@wiki = wiki
@page = page
......
# frozen_string_literal: true
class WikiPage
class Meta < ApplicationRecord
include Gitlab::Utils::StrongMemoize
CanonicalSlugConflictError = Class.new(ActiveRecord::RecordInvalid)
self.table_name = 'wiki_page_meta'
belongs_to :project
has_many :slugs, class_name: 'WikiPage::Slug', foreign_key: 'wiki_page_meta_id', inverse_of: :wiki_page_meta
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
validates :title, presence: true
validates :project_id, presence: true
validate :no_two_metarecords_in_same_project_can_have_same_canonical_slug
scope :with_canonical_slug, ->(slug) do
joins(:slugs).where(wiki_page_slugs: { canonical: true, slug: slug })
end
alias_method :resource_parent, :project
# Return the (updated) WikiPage::Meta record for a given wiki page
#
# If none is found, then a new record is created, and its fields are set
# to reflect the wiki_page passed.
#
# @param [String] last_known_slug
# @param [WikiPage] wiki_page
#
# As with all `find_or_create` methods, this one raises errors on
# validation issues.
def self.find_or_create(last_known_slug, wiki_page)
project = wiki_page.wiki.project
known_slugs = [last_known_slug, wiki_page.slug].compact.uniq
raise 'no slugs!' if known_slugs.empty?
transaction do
found = find_by_canonical_slug(known_slugs, project)
meta = found || create(title: wiki_page.title, project_id: project.id)
meta.update_state(found.nil?, known_slugs, wiki_page)
# We don't need to run validations here, since find_by_canonical_slug
# guarantees that there is no conflict in canonical_slug, and DB
# constraints on title and project_id enforce our other invariants
# This saves us a query.
meta
end
end
def self.find_by_canonical_slug(canonical_slug, project)
meta, conflict = with_canonical_slug(canonical_slug)
.where(project_id: project.id)
.limit(2)
if conflict.present?
meta.errors.add(:canonical_slug, 'Duplicate value found')
raise CanonicalSlugConflictError.new(meta)
end
meta
end
def canonical_slug
strong_memoize(:canonical_slug) { slugs.canonical.first&.slug }
end
def canonical_slug=(slug)
return if @canonical_slug == slug
if persisted?
transaction do
slugs.canonical.update_all(canonical: false)
page_slug = slugs.create_with(canonical: true).find_or_create_by(slug: slug)
page_slug.update_columns(canonical: true) unless page_slug.canonical?
end
else
slugs.new(slug: slug, canonical: true)
end
@canonical_slug = slug
end
def update_state(created, known_slugs, wiki_page)
update_wiki_page_attributes(wiki_page)
insert_slugs(known_slugs, created, wiki_page.slug)
self.canonical_slug = wiki_page.slug
end
def update_columns(attrs = {})
super(attrs.reverse_merge(updated_at: Time.now.utc))
end
def self.update_all(attrs = {})
super(attrs.reverse_merge(updated_at: Time.now.utc))
end
private
def update_wiki_page_attributes(page)
update_columns(title: page.title) unless page.title == title
end
def insert_slugs(strings, is_new, canonical_slug)
creation = Time.now.utc
slug_attrs = strings.map do |slug|
{
wiki_page_meta_id: id,
slug: slug,
canonical: (is_new && slug == canonical_slug),
created_at: creation,
updated_at: creation
}
end
slugs.insert_all(slug_attrs) unless !is_new && slug_attrs.size == 1
@canonical_slug = canonical_slug if is_new || strings.size == 1
end
def no_two_metarecords_in_same_project_can_have_same_canonical_slug
return unless project_id.present? && canonical_slug.present?
offending = self.class.with_canonical_slug(canonical_slug).where(project_id: project_id)
offending = offending.where.not(id: id) if persisted?
if offending.exists?
errors.add(:canonical_slug, 'each page in a wiki must have a distinct canonical slug')
end
end
end
end
# frozen_string_literal: true
class WikiPage
class Slug < ApplicationRecord
self.table_name = 'wiki_page_slugs'
belongs_to :wiki_page_meta, class_name: 'WikiPage::Meta', inverse_of: :slugs
validates :slug, presence: true, uniqueness: { scope: :wiki_page_meta_id }
validates :canonical, uniqueness: {
scope: :wiki_page_meta_id,
if: :canonical?,
message: 'Only one slug can be canonical per wiki metadata record'
}
scope :canonical, -> { where(canonical: true) }
def update_columns(attrs = {})
super(attrs.reverse_merge(updated_at: Time.now.utc))
end
def self.update_all(attrs = {})
super(attrs.reverse_merge(updated_at: Time.now.utc))
end
end
end
# frozen_string_literal: true
class WikiPage::MetaPolicy < BasePolicy
delegate { @subject.project }
end
---
title: Adds wiki metadata models
merge_request: 26529
author:
type: added
# frozen_string_literal: true
class AddWikiSlug < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
create_table :wiki_page_meta, id: :serial do |t|
t.references :project, index: true, foreign_key: { on_delete: :cascade }, null: false
t.timestamps_with_timezone null: false
t.string :title, null: false, limit: 255
end
create_table :wiki_page_slugs, id: :serial do |t|
t.boolean :canonical, default: false, null: false
t.references :wiki_page_meta, index: true, foreign_key: { on_delete: :cascade }, null: false
t.timestamps_with_timezone null: false
t.string :slug, null: false, limit: 2048
t.index [:slug, :wiki_page_meta_id], unique: true
t.index [:wiki_page_meta_id], name: 'one_canonical_wiki_page_slug_per_metadata', unique: true, where: "(canonical = true)"
end
end
end
......@@ -4668,6 +4668,25 @@ ActiveRecord::Schema.define(version: 2020_03_13_123934) do
t.index ["type"], name: "index_web_hooks_on_type"
end
create_table "wiki_page_meta", id: :serial, force: :cascade do |t|
t.bigint "project_id", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.string "title", limit: 255, null: false
t.index ["project_id"], name: "index_wiki_page_meta_on_project_id"
end
create_table "wiki_page_slugs", id: :serial, force: :cascade do |t|
t.boolean "canonical", default: false, null: false
t.bigint "wiki_page_meta_id", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.string "slug", limit: 2048, null: false
t.index ["slug", "wiki_page_meta_id"], name: "index_wiki_page_slugs_on_slug_and_wiki_page_meta_id", unique: true
t.index ["wiki_page_meta_id"], name: "index_wiki_page_slugs_on_wiki_page_meta_id"
t.index ["wiki_page_meta_id"], name: "one_canonical_wiki_page_slug_per_metadata", unique: true, where: "(canonical = true)"
end
create_table "x509_certificates", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
......@@ -5212,6 +5231,8 @@ ActiveRecord::Schema.define(version: 2020_03_13_123934) do
add_foreign_key "vulnerability_scanners", "projects", on_delete: :cascade
add_foreign_key "web_hook_logs", "web_hooks", on_delete: :cascade
add_foreign_key "web_hooks", "projects", name: "fk_0c8ca6d9d1", on_delete: :cascade
add_foreign_key "wiki_page_meta", "projects", on_delete: :cascade
add_foreign_key "wiki_page_slugs", "wiki_page_meta", column: "wiki_page_meta_id", on_delete: :cascade
add_foreign_key "x509_certificates", "x509_issuers", on_delete: :cascade
add_foreign_key "x509_commit_signatures", "projects", on_delete: :cascade
add_foreign_key "x509_commit_signatures", "x509_certificates", on_delete: :cascade
......
......@@ -12,6 +12,10 @@ module Gitlab
'content' => absolute_image_urls(wiki_page.content)
)
end
def uploads_prefix
''
end
end
end
end
......@@ -22,6 +22,16 @@ FactoryBot.define do
action { Event::CLOSED }
target factory: :closed_issue
end
factory :wiki_page_event do
action { Event::CREATED }
transient do
wiki_page { create(:wiki_page, project: project) }
end
target { create(:wiki_page_meta, :for_wiki_page, wiki_page: wiki_page) }
end
end
factory :push_event, class: 'PushEvent' do
......
......@@ -5,17 +5,22 @@ require 'ostruct'
FactoryBot.define do
factory :wiki_page do
transient do
title { generate(:wiki_page_title) }
content { 'Content for wiki page' }
format { 'markdown' }
project { create(:project) }
attrs do
{
title: 'Title.with.dot',
content: 'Content for wiki page',
format: 'markdown'
title: title,
content: content,
format: format
}
end
end
page { OpenStruct.new(url_path: 'some-name') }
association :wiki, factory: :project_wiki, strategy: :build
wiki { build(:project_wiki, project: project) }
initialize_with { new(wiki, page) }
before(:create) do |page, evaluator|
......@@ -25,5 +30,48 @@ FactoryBot.define do
to_create do |page|
page.create
end
trait :with_real_page do
project { create(:project, :repository) }
page do
wiki.create_page(title, content)
page_title, page_dir = wiki.page_title_and_dir(title)
wiki.wiki.page(title: page_title, dir: page_dir, version: nil)
end
end
end
factory :wiki_page_meta, class: 'WikiPage::Meta' do
title { generate(:wiki_page_title) }
project { create(:project) }
trait :for_wiki_page do
transient do
wiki_page { create(:wiki_page, project: project) }
end
project { @overrides[:wiki_page]&.project || create(:project) }
title { wiki_page.title }
initialize_with do
raise 'Metadata only available for valid pages' unless wiki_page.valid?
WikiPage::Meta.find_or_create(wiki_page.slug, wiki_page)
end
end
end
factory :wiki_page_slug, class: 'WikiPage::Slug' do
wiki_page_meta { create(:wiki_page_meta) }
slug { generate(:sluggified_title) }
canonical { false }
trait :canonical do
canonical { true }
end
end
sequence(:wiki_page_title) { |n| "Page #{n}" }
sequence(:sluggified_title) { |n| "slug-#{n}" }
end
......@@ -40,6 +40,14 @@ Event:
- updated_at
- action
- author_id
WikiPage::Meta:
- id
- title
- project_id
WikiPage::Slug:
- id
- wiki_page_meta_id
- slug
PushEventPayload:
- commit_count
- action
......
......@@ -99,6 +99,39 @@ describe Event do
end
end
describe '#target_title' do
let_it_be(:project) { create(:project) }
let(:author) { project.owner }
let(:target) { nil }
let(:event) do
described_class.new(project: project,
target: target,
author_id: author.id)
end
context 'for an issue' do
let(:title) { generate(:title) }
let(:issue) { create(:issue, title: title, project: project) }
let(:target) { issue }
it 'delegates to issue title' do
expect(event.target_title).to eq(title)
end
end
context 'for a wiki page' do
let(:title) { generate(:wiki_page_title) }
let(:wiki_page) { create(:wiki_page, title: title, project: project) }
let(:event) { create(:wiki_page_event, project: project, wiki_page: wiki_page) }
it 'delegates to wiki page title' do
expect(event.target_title).to eq(wiki_page.title)
end
end
end
describe '#membership_changed?' do
context "created" do
subject { build(:event, :created).membership_changed? }
......@@ -148,13 +181,16 @@ describe Event do
end
describe '#visible_to_user?' do
let(:project) { create(:project, :public) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
let(:guest) { create(:user) }
let(:author) { create(:author) }
let(:assignee) { create(:user) }
let(:admin) { create(:admin) }
let_it_be(:non_member) { create(:user) }
let_it_be(:member) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:author) { create(:author) }
let_it_be(:assignee) { create(:user) }
let_it_be(:admin) { create(:admin) }
let_it_be(:public_project) { create(:project, :public) }
let_it_be(:private_project) { create(:project, :private) }
let(:project) { public_project }
let(:issue) { create(:issue, project: project, author: author, assignees: [assignee]) }
let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignees: [assignee]) }
let(:project_snippet) { create(:project_snippet, :public, project: project, author: author) }
......@@ -165,36 +201,77 @@ describe Event do
let(:note_on_project_snippet) { create(:note_on_project_snippet, author: author, noteable: project_snippet, project: project) }
let(:note_on_personal_snippet) { create(:note_on_personal_snippet, author: author, noteable: personal_snippet, project: nil) }
let(:milestone_on_project) { create(:milestone, project: project) }
let(:event) { described_class.new(project: project, target: target, author_id: author.id) }
let(:event) do
described_class.new(project: project,
target: target,
author_id: author.id)
end
before do
project.add_developer(member)
project.add_guest(guest)
end
def visible_to_all
{
logged_out: true,
non_member: true,
guest: true,
member: true,
admin: true
}
end
def visible_to_none
visible_to_all.transform_values { |_| false }
end
def visible_to_none_except(*roles)
visible_to_none.merge(roles.map { |role| [role, true] }.to_h)
end
def visible_to_all_except(*roles)
visible_to_all.merge(roles.map { |role| [role, false] }.to_h)
end
shared_examples 'visibility examples' do
it 'has the correct visibility' do
expect({
logged_out: event.visible_to_user?(nil),
non_member: event.visible_to_user?(non_member),
guest: event.visible_to_user?(guest),
member: event.visible_to_user?(member),
admin: event.visible_to_user?(admin)
}).to match(visibility)
end
end
shared_examples 'visible to assignee' do |visible|
it { expect(event.visible_to_user?(assignee)).to eq(visible) }
end
shared_examples 'visible to author' do |visible|
it { expect(event.visible_to_user?(author)).to eq(visible) }
end
shared_examples 'visible to assignee and author' do |visible|
include_examples 'visible to assignee', visible
include_examples 'visible to author', visible
end
context 'commit note event' do
let(:project) { create(:project, :public, :repository) }
let(:target) { note_on_commit }
it do
aggregate_failures do
expect(event.visible_to_user?(non_member)).to eq true
expect(event.visible_to_user?(member)).to eq true
expect(event.visible_to_user?(guest)).to eq true
expect(event.visible_to_user?(admin)).to eq true
end
include_examples 'visibility examples' do
let(:visibility) { visible_to_all }
end
context 'private project' do
let(:project) { create(:project, :private, :repository) }
it do
aggregate_failures do
expect(event.visible_to_user?(non_member)).to eq false
expect(event.visible_to_user?(member)).to eq true
expect(event.visible_to_user?(guest)).to eq false
expect(event.visible_to_user?(admin)).to eq true
end
include_examples 'visibility examples' do
let(:visibility) { visible_to_none_except(:member, :admin) }
end
end
end
......@@ -203,27 +280,19 @@ describe Event do
context 'for non confidential issues' do
let(:target) { issue }
it do
expect(event.visible_to_user?(non_member)).to eq true
expect(event.visible_to_user?(author)).to eq true
expect(event.visible_to_user?(assignee)).to eq true
expect(event.visible_to_user?(member)).to eq true
expect(event.visible_to_user?(guest)).to eq true
expect(event.visible_to_user?(admin)).to eq true
include_examples 'visibility examples' do
let(:visibility) { visible_to_all }
end
include_examples 'visible to assignee and author', true
end
context 'for confidential issues' do
let(:target) { confidential_issue }
it do
expect(event.visible_to_user?(non_member)).to eq false
expect(event.visible_to_user?(author)).to eq true
expect(event.visible_to_user?(assignee)).to eq true
expect(event.visible_to_user?(member)).to eq true
expect(event.visible_to_user?(guest)).to eq false
expect(event.visible_to_user?(admin)).to eq true
include_examples 'visibility examples' do
let(:visibility) { visible_to_none_except(:member, :admin) }
end
include_examples 'visible to assignee and author', true
end
end
......@@ -231,105 +300,99 @@ describe Event do
context 'on non confidential issues' do
let(:target) { note_on_issue }
it do
expect(event.visible_to_user?(non_member)).to eq true
expect(event.visible_to_user?(author)).to eq true
expect(event.visible_to_user?(assignee)).to eq true
expect(event.visible_to_user?(member)).to eq true
expect(event.visible_to_user?(guest)).to eq true
expect(event.visible_to_user?(admin)).to eq true
include_examples 'visibility examples' do
let(:visibility) { visible_to_all }
end
include_examples 'visible to assignee and author', true
end
context 'on confidential issues' do
let(:target) { note_on_confidential_issue }
it do
expect(event.visible_to_user?(non_member)).to eq false
expect(event.visible_to_user?(author)).to eq true
expect(event.visible_to_user?(assignee)).to eq true
expect(event.visible_to_user?(member)).to eq true
expect(event.visible_to_user?(guest)).to eq false
expect(event.visible_to_user?(admin)).to eq true
include_examples 'visibility examples' do
let(:visibility) { visible_to_none_except(:member, :admin) }
end
include_examples 'visible to assignee and author', true
end
context 'private project' do
let(:project) { create(:project, :private) }
let(:project) { private_project }
let(:target) { note_on_issue }
it do
expect(event.visible_to_user?(non_member)).to eq false
expect(event.visible_to_user?(author)).to eq false
expect(event.visible_to_user?(assignee)).to eq false
expect(event.visible_to_user?(member)).to eq true
expect(event.visible_to_user?(guest)).to eq true
expect(event.visible_to_user?(admin)).to eq true
include_examples 'visibility examples' do
let(:visibility) { visible_to_none_except(:guest, :member, :admin) }
end
include_examples 'visible to assignee and author', false
end
end
context 'merge request diff note event' do
let(:project) { create(:project, :public) }
let(:merge_request) { create(:merge_request, source_project: project, author: author, assignees: [assignee]) }
let(:note_on_merge_request) { create(:legacy_diff_note_on_merge_request, noteable: merge_request, project: project) }
let(:target) { note_on_merge_request }
it do
expect(event.visible_to_user?(non_member)).to eq true
expect(event.visible_to_user?(author)).to eq true
expect(event.visible_to_user?(assignee)).to eq true
expect(event.visible_to_user?(member)).to eq true
expect(event.visible_to_user?(guest)).to eq true
expect(event.visible_to_user?(admin)).to eq true
context 'public project' do
let(:project) { public_project }
include_examples 'visibility examples' do
let(:visibility) { visible_to_all }
end
include_examples 'visible to assignee', true
end
context 'private project' do
let(:project) { create(:project, :private) }
let(:project) { private_project }
it do
expect(event.visible_to_user?(non_member)).to eq false
expect(event.visible_to_user?(author)).to eq false
expect(event.visible_to_user?(assignee)).to eq false
expect(event.visible_to_user?(member)).to eq true
expect(event.visible_to_user?(guest)).to eq false
expect(event.visible_to_user?(admin)).to eq true
include_examples 'visibility examples' do
let(:visibility) { visible_to_none_except(:member, :admin) }
end
include_examples 'visible to assignee', false
end
end
context 'milestone event' do
let(:target) { milestone_on_project }
it do
expect(event.visible_to_user?(nil)).to be_truthy
expect(event.visible_to_user?(non_member)).to be_truthy
expect(event.visible_to_user?(member)).to be_truthy
expect(event.visible_to_user?(guest)).to be_truthy
expect(event.visible_to_user?(admin)).to be_truthy
include_examples 'visibility examples' do
let(:visibility) { visible_to_all }
end
context 'on public project with private issue tracker and merge requests' do
let(:project) { create(:project, :public, :issues_private, :merge_requests_private) }
it do
expect(event.visible_to_user?(nil)).to be_falsy
expect(event.visible_to_user?(non_member)).to be_falsy
expect(event.visible_to_user?(member)).to be_truthy
expect(event.visible_to_user?(guest)).to be_truthy
expect(event.visible_to_user?(admin)).to be_truthy
include_examples 'visibility examples' do
let(:visibility) { visible_to_all_except(:logged_out, :non_member) }
end
end
context 'on private project' do
let(:project) { create(:project, :private) }
it do
expect(event.visible_to_user?(nil)).to be_falsy
expect(event.visible_to_user?(non_member)).to be_falsy
expect(event.visible_to_user?(member)).to be_truthy
expect(event.visible_to_user?(guest)).to be_truthy
expect(event.visible_to_user?(admin)).to be_truthy
include_examples 'visibility examples' do
let(:visibility) { visible_to_all_except(:logged_out, :non_member) }
end
end
end
context 'wiki-page event', :aggregate_failures do
let(:event) { create(:wiki_page_event, project: project) }
context 'on private project', :aggregate_failures do
let(:project) { create(:project, :wiki_repo) }
include_examples 'visibility examples' do
let(:visibility) { visible_to_all_except(:logged_out, :non_member) }
end
end
context 'wiki-page event on public project', :aggregate_failures do
let(:project) { create(:project, :public, :wiki_repo) }
include_examples 'visibility examples' do
let(:visibility) { visible_to_all }
end
end
end
......@@ -337,79 +400,98 @@ describe Event do
context 'project snippet note event' do
let(:target) { note_on_project_snippet }
it do
expect(event.visible_to_user?(nil)).to be_truthy
expect(event.visible_to_user?(non_member)).to be_truthy
expect(event.visible_to_user?(author)).to be_truthy
expect(event.visible_to_user?(member)).to be_truthy
expect(event.visible_to_user?(guest)).to be_truthy
expect(event.visible_to_user?(admin)).to be_truthy
include_examples 'visibility examples' do
let(:visibility) { visible_to_all }
end
context 'on public project with private snippets' do
let(:project) { create(:project, :public, :snippets_private) }
it do
expect(event.visible_to_user?(nil)).to be_falsy
expect(event.visible_to_user?(non_member)).to be_falsy
# Normally, we'd expect the author of a comment to be able to view it.
# However, this doesn't seem to be the case for comments on snippets.
expect(event.visible_to_user?(author)).to be_falsy
expect(event.visible_to_user?(member)).to be_truthy
expect(event.visible_to_user?(guest)).to be_truthy
expect(event.visible_to_user?(admin)).to be_truthy
include_examples 'visibility examples' do
let(:visibility) { visible_to_none_except(:guest, :member, :admin) }
end
# Normally, we'd expect the author of a comment to be able to view it.
# However, this doesn't seem to be the case for comments on snippets.
include_examples 'visible to author', false
end
context 'on private project' do
let(:project) { create(:project, :private) }
it do
expect(event.visible_to_user?(nil)).to be_falsy
expect(event.visible_to_user?(non_member)).to be_falsy
# Normally, we'd expect the author of a comment to be able to view it.
# However, this doesn't seem to be the case for comments on snippets.
expect(event.visible_to_user?(author)).to be_falsy
expect(event.visible_to_user?(member)).to be_truthy
expect(event.visible_to_user?(guest)).to be_truthy
expect(event.visible_to_user?(admin)).to be_truthy
include_examples 'visibility examples' do
let(:visibility) { visible_to_none_except(:guest, :member, :admin) }
end
# Normally, we'd expect the author of a comment to be able to view it.
# However, this doesn't seem to be the case for comments on snippets.
include_examples 'visible to author', false
end
end
context 'personal snippet note event' do
let(:target) { note_on_personal_snippet }
it do
expect(event.visible_to_user?(nil)).to be_truthy
expect(event.visible_to_user?(non_member)).to be_truthy
expect(event.visible_to_user?(author)).to be_truthy
expect(event.visible_to_user?(admin)).to be_truthy
include_examples 'visibility examples' do
let(:visibility) { visible_to_all }
end
include_examples 'visible to author', true
context 'on internal snippet' do
let(:personal_snippet) { create(:personal_snippet, :internal, author: author) }
it do
expect(event.visible_to_user?(nil)).to be_falsy
expect(event.visible_to_user?(non_member)).to be_truthy
expect(event.visible_to_user?(author)).to be_truthy
expect(event.visible_to_user?(admin)).to be_truthy
include_examples 'visibility examples' do
let(:visibility) { visible_to_all_except(:logged_out) }
end
end
context 'on private snippet' do
let(:personal_snippet) { create(:personal_snippet, :private, author: author) }
it do
expect(event.visible_to_user?(nil)).to be_falsy
expect(event.visible_to_user?(non_member)).to be_falsy
expect(event.visible_to_user?(author)).to be_truthy
expect(event.visible_to_user?(admin)).to be_truthy
include_examples 'visibility examples' do
let(:visibility) { visible_to_none_except(:admin) }
end
include_examples 'visible to author', true
end
end
end
describe '.for_wiki_page' do
let_it_be(:events) do
[
create(:closed_issue_event),
create(:wiki_page_event),
create(:closed_issue_event),
create(:event, :created),
create(:wiki_page_event)
]
end
it 'only contains the wiki page events' do
wiki_events = events.select(&:wiki_page?)
expect(described_class.for_wiki_page).to match_array(wiki_events)
end
end
describe '#wiki_page and #wiki_page?' do
let_it_be(:project) { create(:project, :repository) }
context 'for a wiki page event' do
let(:wiki_page) do
create(:wiki_page, :with_real_page, project: project)
end
subject(:event) { create(:wiki_page_event, project: project, wiki_page: wiki_page) }
it { is_expected.to have_attributes(wiki_page?: be_truthy, wiki_page: wiki_page) }
end
[:issue, :user, :merge_request, :snippet, :milestone, nil].each do |kind|
context "for a #{kind} event" do
it 'is nil' do
target = create(kind) if kind
event = create(:event, project: project, target: target)
expect(event).to have_attributes(wiki_page: be_nil, wiki_page?: be_falsy)
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
describe WikiPage::Meta do
let_it_be(:project) { create(:project) }
let_it_be(:other_project) { create(:project) }
describe 'Associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:slugs) }
it { is_expected.to have_many(:events) }
it 'can find slugs' do
meta = create(:wiki_page_meta)
slugs = create_list(:wiki_page_slug, 3, wiki_page_meta: meta)
expect(meta.slugs).to match_array(slugs)
end
end
describe 'Validations' do
subject do
described_class.new(title: 'some title', project: project)
end
it { is_expected.to validate_presence_of(:project_id) }
it { is_expected.to validate_presence_of(:title) }
it 'is forbidden to add extremely long titles' do
expect do
create(:wiki_page_meta, project: project, title: FFaker::Lorem.characters(300))
end.to raise_error(ActiveRecord::ValueTooLong)
end
it 'is forbidden to have two records for the same project with the same canonical_slug' do
the_slug = generate(:sluggified_title)
create(:wiki_page_meta, canonical_slug: the_slug, project: project)
in_violation = build(:wiki_page_meta, canonical_slug: the_slug, project: project)
expect(in_violation).not_to be_valid
end
end
describe '#canonical_slug' do
subject { described_class.find(meta.id) }
let_it_be(:meta) do
described_class.create(title: generate(:wiki_page_title), project: project)
end
context 'there are no slugs' do
it { is_expected.to have_attributes(canonical_slug: be_nil) }
end
it 'can be set on initialization' do
meta = create(:wiki_page_meta, canonical_slug: 'foo')
expect(meta.canonical_slug).to eq('foo')
end
context 'we have some non-canonical slugs' do
before do
create_list(:wiki_page_slug, 2, wiki_page_meta: subject)
end
it { is_expected.to have_attributes(canonical_slug: be_nil) }
it 'issues at most one query' do
expect { subject.canonical_slug }.not_to exceed_query_limit(1)
end
it 'issues no queries if we already know the slug' do
subject.canonical_slug
expect { subject.canonical_slug }.not_to exceed_query_limit(0)
end
end
context 'we have a canonical slug' do
before do
create_list(:wiki_page_slug, 2, wiki_page_meta: subject)
end
it 'has the correct value' do
slug = create(:wiki_page_slug, :canonical, wiki_page_meta: subject)
is_expected.to have_attributes(canonical_slug: slug.slug)
end
end
describe 'canonical_slug=' do
shared_examples 'canonical_slug setting examples' do
# Constant overhead of two queries for the transaction
let(:upper_query_limit) { query_limit + 2 }
let(:lower_query_limit) { [upper_query_limit - 1, 0].max}
let(:other_slug) { generate(:sluggified_title) }
it 'changes it to the correct value' do
subject.canonical_slug = slug
expect(subject).to have_attributes(canonical_slug: slug)
end
it 'ensures the slug is in the db' do
subject.canonical_slug = slug
expect(subject.slugs.canonical.where(slug: slug)).to exist
end
it 'issues at most N queries' do
expect { subject.canonical_slug = slug }.not_to exceed_query_limit(upper_query_limit)
end
it 'issues fewer queries if we already know the current slug' do
subject.canonical_slug = other_slug
expect { subject.canonical_slug = slug }.not_to exceed_query_limit(lower_query_limit)
end
end
context 'the slug is not known to us' do
let(:slug) { generate(:sluggified_title) }
let(:query_limit) { 8 }
include_examples 'canonical_slug setting examples'
end
context 'the slug is already in the DB (but not canonical)' do
let_it_be(:slug_record) { create(:wiki_page_slug, wiki_page_meta: meta) }
let(:slug) { slug_record.slug }
let(:query_limit) { 4 }
include_examples 'canonical_slug setting examples'
end
context 'the slug is already in the DB (and canonical)' do
let_it_be(:slug_record) { create(:wiki_page_slug, :canonical, wiki_page_meta: meta) }
let(:slug) { slug_record.slug }
let(:query_limit) { 4 }
include_examples 'canonical_slug setting examples'
end
context 'the slug is up to date and in the DB' do
let(:slug) { generate(:sluggified_title) }
before do
subject.canonical_slug = slug
end
include_examples 'canonical_slug setting examples' do
let(:other_slug) { slug }
let(:upper_query_limit) { 0 }
end
end
end
end
describe '.find_or_create' do
let(:old_title) { generate(:wiki_page_title) }
let(:last_known_slug) { generate(:sluggified_title) }
let(:current_slug) { wiki_page.slug }
let(:title) { wiki_page.title }
let(:wiki_page) { create(:wiki_page, project: project) }
def find_record
described_class.find_or_create(last_known_slug, wiki_page)
end
def create_previous_version(title = old_title, slug = last_known_slug)
create(:wiki_page_meta, title: title, project: project, canonical_slug: slug)
end
def create_context
# Ensure that we behave nicely with respect to other projects
# We have:
# - page in other project with same canonical_slug
create(:wiki_page_meta, project: other_project, canonical_slug: wiki_page.slug)
# - page in same project with different canonical_slug, but with
# an old slug that = canonical_slug
different_slug = generate(:sluggified_title)
create(:wiki_page_meta, project: project, canonical_slug: different_slug)
.slugs.create(slug: wiki_page.slug)
end
shared_examples 'metadata examples' do
it 'establishes the correct state', :aggregate_failures do
create_context
meta = find_record
expect(meta).to have_attributes(
valid?: true,
canonical_slug: wiki_page.slug,
title: wiki_page.title,
project: wiki_page.wiki.project
)
expect(meta.slugs.where(slug: last_known_slug)).to exist
expect(meta.slugs.canonical.where(slug: wiki_page.slug)).to exist
end
it 'makes a reasonable number of DB queries' do
expect(project).to eq(wiki_page.wiki.project)
expect { find_record }.not_to exceed_query_limit(query_limit)
end
end
context 'the slug is too long' do
let(:last_known_slug) { FFaker::Lorem.characters(2050) }
it 'raises an error' do
expect { find_record }.to raise_error ActiveRecord::ValueTooLong
end
end
context 'a conflicting record exists' do
before do
create(:wiki_page_meta, project: project, canonical_slug: last_known_slug)
create(:wiki_page_meta, project: project, canonical_slug: current_slug)
end
it 'raises an error' do
expect { find_record }.to raise_error(ActiveRecord::RecordInvalid)
end
end
context 'no existing record exists' do
include_examples 'metadata examples' do
# The base case is 5 queries:
# - 2 for the outer transaction
# - 1 to find the metadata object if it exists
# - 1 to create it if it does not
# - 1 to insert last_known_slug and current_slug
#
# (Log has been edited for clarity)
# SAVEPOINT active_record_2
#
# SELECT * FROM wiki_page_meta
# INNER JOIN wiki_page_slugs
# ON wiki_page_slugs.wiki_page_meta_id = wiki_page_meta.id
# WHERE wiki_page_meta.project_id = ?
# AND wiki_page_slugs.canonical = TRUE
# AND wiki_page_slugs.slug IN (?,?)
# LIMIT 2
#
# INSERT INTO wiki_page_meta (project_id, title) VALUES (?, ?) RETURNING id
#
# INSERT INTO wiki_page_slugs (wiki_page_meta_id,slug,canonical)
# VALUES (?, ?, ?) (?, ?, ?)
# ON CONFLICT DO NOTHING RETURNING id
#
# RELEASE SAVEPOINT active_record_2
let(:query_limit) { 5 }
end
end
context 'the last_known_slug is the same as the current slug, as on creation' do
let(:last_known_slug) { current_slug }
include_examples 'metadata examples' do
# Identical to the base case.
let(:query_limit) { 5 }
end
end
context 'a record exists in the DB in the correct state' do
let(:last_known_slug) { current_slug }
let(:old_title) { title }
before do
create_previous_version
end
include_examples 'metadata examples' do
# We just need to do the initial query, and the outer transaction
# SAVEPOINT active_record_2
#
# SELECT * FROM wiki_page_meta
# INNER JOIN wiki_page_slugs
# ON wiki_page_slugs.wiki_page_meta_id = wiki_page_meta.id
# WHERE wiki_page_meta.project_id = ?
# AND wiki_page_slugs.canonical = TRUE
# AND wiki_page_slugs.slug = ?
# LIMIT 2
#
# RELEASE SAVEPOINT active_record_2
let(:query_limit) { 3 }
end
end
context 'we need to update the slug, but not the title' do
let(:old_title) { title }
before do
create_previous_version
end
include_examples 'metadata examples' do
# Here we need:
# - 2 for the outer transaction
# - 1 to find the record
# - 1 to insert the new slug
# - 3 to set canonical state correctly
#
# SAVEPOINT active_record_2
#
# SELECT * FROM wiki_page_meta
# INNER JOIN wiki_page_slugs
# ON wiki_page_slugs.wiki_page_meta_id = wiki_page_meta.id
# WHERE wiki_page_meta.project_id = ?
# AND wiki_page_slugs.canonical = TRUE
# AND wiki_page_slugs.slug = ?
# LIMIT 1
#
# INSERT INTO wiki_page_slugs (wiki_page_meta_id,slug,canonical)
# VALUES (?, ?, ?) ON CONFLICT DO NOTHING RETURNING id
#
# SELECT * FROM wiki_page_slugs
# WHERE wiki_page_slugs.wiki_page_meta_id = ?
# AND wiki_page_slugs.slug = ?
# LIMIT 1
# UPDATE wiki_page_slugs SET canonical = FALSE WHERE wiki_page_meta_id = ?
# UPDATE wiki_page_slugs SET canonical = TRUE WHERE id = ?
#
# RELEASE SAVEPOINT active_record_2
let(:query_limit) { 7 }
end
end
context 'we need to update the title, but not the slug' do
let(:last_known_slug) { wiki_page.slug }
before do
create_previous_version
end
include_examples 'metadata examples' do
# Same as minimal case, plus one query to update the title.
#
# SAVEPOINT active_record_2
#
# SELECT * FROM wiki_page_meta
# INNER JOIN wiki_page_slugs
# ON wiki_page_slugs.wiki_page_meta_id = wiki_page_meta.id
# WHERE wiki_page_meta.project_id = ?
# AND wiki_page_slugs.canonical = TRUE
# AND wiki_page_slugs.slug = ?
# LIMIT 1
#
# UPDATE wiki_page_meta SET title = ? WHERE id = ?
#
# RELEASE SAVEPOINT active_record_2
let(:query_limit) { 4 }
end
end
context 'we want to change the slug back to a previous version' do
let(:slug_1) { 'foo' }
let(:slug_2) { 'bar' }
let(:wiki_page) { create(:wiki_page, title: slug_1, project: project) }
let(:last_known_slug) { slug_2 }
before do
meta = create_previous_version(title, slug_1)
meta.canonical_slug = slug_2
end
include_examples 'metadata examples' do
let(:query_limit) { 7 }
end
end
context 'we want to change the slug a bunch of times' do
let(:slugs) { generate_list(:sluggified_title, 3) }
before do
meta = create_previous_version
slugs.each { |slug| meta.canonical_slug = slug }
end
include_examples 'metadata examples' do
let(:query_limit) { 7 }
end
end
context 'we need to update the title and the slug' do
before do
create_previous_version
end
include_examples 'metadata examples' do
# -- outer transaction
# SAVEPOINT active_record_2
#
# -- to find the record
# SELECT * FROM wiki_page_meta
# INNER JOIN wiki_page_slugs
# ON wiki_page_slugs.wiki_page_meta_id = wiki_page_meta.id
# WHERE wiki_page_meta.project_id = ?
# AND wiki_page_slugs.canonical = TRUE
# AND wiki_page_slugs.slug IN (?,?)
# LIMIT 2
#
# -- to update the title
# UPDATE wiki_page_meta SET title = ? WHERE id = ?
#
# -- to update slug
# INSERT INTO wiki_page_slugs (wiki_page_meta_id,slug,canonical)
# VALUES (?, ?, ?) ON CONFLICT DO NOTHING RETURNING id
#
# UPDATE wiki_page_slugs SET canonical = FALSE WHERE wiki_page_meta_id = ?
#
# SELECT * FROM wiki_page_slugs
# WHERE wiki_page_slugs.wiki_page_meta_id = ?
# AND wiki_page_slugs.slug = ?
# LIMIT 1
#
# UPDATE wiki_page_slugs SET canonical = TRUE WHERE id = ?
#
# RELEASE SAVEPOINT active_record_2
let(:query_limit) { 8 }
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe WikiPage::Slug do
let_it_be(:meta) { create(:wiki_page_meta) }
describe 'Associations' do
it { is_expected.to belong_to(:wiki_page_meta) }
it 'refers correctly to the wiki_page_meta' do
created = create(:wiki_page_slug, wiki_page_meta: meta)
expect(created.reload.wiki_page_meta).to eq(meta)
end
end
describe 'scopes' do
describe 'canonical' do
subject { described_class.canonical }
context 'there are no slugs' do
it { is_expected.to be_empty }
end
context 'there are some non-canonical slugs' do
before do
create(:wiki_page_slug)
end
it { is_expected.to be_empty }
end
context 'there is at least one canonical slugs' do
before do
create(:wiki_page_slug, :canonical)
end
it { is_expected.not_to be_empty }
end
end
end
describe 'Validations' do
let(:canonical) { false }
subject do
build(:wiki_page_slug, canonical: canonical, wiki_page_meta: meta)
end
it { is_expected.to validate_presence_of(:slug) }
it { is_expected.to validate_uniqueness_of(:slug).scoped_to(:wiki_page_meta_id) }
describe 'only_one_slug_can_be_canonical_per_meta_record' do
context 'there are no other slugs' do
it { is_expected.to be_valid }
context 'the current slug is canonical' do
let(:canonical) { true }
it { is_expected.to be_valid }
end
end
context 'there are other slugs, but they are not canonical' do
before do
create(:wiki_page_slug, wiki_page_meta: meta)
end
it { is_expected.to be_valid }
context 'the current slug is canonical' do
let(:canonical) { true }
it { is_expected.to be_valid }
end
end
context 'there is already a canonical slug' do
before do
create(:wiki_page_slug, canonical: true, wiki_page_meta: meta)
end
it { is_expected.to be_valid }
context 'the current slug is canonical' do
let(:canonical) { true }
it { is_expected.not_to be_valid }
end
end
end
end
end
......@@ -606,12 +606,36 @@ describe WikiPage do
expect(subject).to eq(subject)
end
it 'returns false for updated wiki page' do
it 'returns true for updated wiki page' do
subject.update(content: "Updated content")
updated_page = wiki.find_page('test page')
updated_page = wiki.find_page(existing_page.slug)
expect(updated_page).not_to be_nil
expect(updated_page).not_to eq(subject)
expect(updated_page).to eq(subject)
end
it 'returns false for a completely different wiki page' do
other_page = create(:wiki_page)
expect(subject.slug).not_to eq(other_page.slug)
expect(subject.project).not_to eq(other_page.project)
expect(subject).not_to eq(other_page)
end
it 'returns false for page with different slug on same project' do
other_page = create(:wiki_page, project: subject.project)
expect(subject.slug).not_to eq(other_page.slug)
expect(subject.project).to eq(other_page.project)
expect(subject).not_to eq(other_page)
end
it 'returns false for page with the same slug on a different project' do
other_page = create(:wiki_page, title: existing_page.slug)
expect(subject.slug).to eq(other_page.slug)
expect(subject.project).not_to eq(other_page.project)
expect(subject).not_to eq(other_page)
end
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment