Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									c6f0b221b7
								
							
						
					
					
						commit
						0e30d318bb
					
				|  | @ -40,7 +40,12 @@ export default { | |||
|     <button type="button" class="btn btn-default append-right-10" @click="closeForm"> | ||||
|       {{ __('Cancel') }} | ||||
|     </button> | ||||
|     <button type="button" class="btn btn-close" @click.prevent="submitForm"> | ||||
|     <button | ||||
|       type="button" | ||||
|       class="btn btn-close" | ||||
|       data-testid="confidential-toggle" | ||||
|       @click.prevent="submitForm" | ||||
|     > | ||||
|       {{ toggleButtonText }} | ||||
|     </button> | ||||
|   </div> | ||||
|  |  | |||
|  | @ -35,7 +35,8 @@ module Ci | |||
|       lsif: 'lsif.json', | ||||
|       dotenv: '.env', | ||||
|       cobertura: 'cobertura-coverage.xml', | ||||
|       terraform: 'tfplan.json' | ||||
|       terraform: 'tfplan.json', | ||||
|       cluster_applications: 'gl-cluster-applications.json' | ||||
|     }.freeze | ||||
| 
 | ||||
|     INTERNAL_TYPES = { | ||||
|  | @ -52,6 +53,7 @@ module Ci | |||
|       lsif: :gzip, | ||||
|       dotenv: :gzip, | ||||
|       cobertura: :gzip, | ||||
|       cluster_applications: :gzip, | ||||
| 
 | ||||
|       # All these file formats use `raw` as we need to store them uncompressed | ||||
|       # for Frontend to fetch the files and do analysis | ||||
|  | @ -153,7 +155,8 @@ module Ci | |||
|       dotenv: 16, | ||||
|       cobertura: 17, | ||||
|       terraform: 18, # Transformed json | ||||
|       accessibility: 19 | ||||
|       accessibility: 19, | ||||
|       cluster_applications: 20 | ||||
|     } | ||||
| 
 | ||||
|     enum file_format: { | ||||
|  |  | |||
|  | @ -96,6 +96,8 @@ class Event < ApplicationRecord | |||
|   end | ||||
| 
 | ||||
|   scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) } | ||||
|   scope :for_wiki_meta, ->(meta) { where(target_type: 'WikiPage::Meta', target_id: meta.id) } | ||||
|   scope :created_at, ->(time) { where(created_at: time) } | ||||
| 
 | ||||
|   # Authors are required as they're used to display who pushed data. | ||||
|   # | ||||
|  |  | |||
|  | @ -295,6 +295,10 @@ class WikiPage | |||
|     'wiki_page' | ||||
|   end | ||||
| 
 | ||||
|   def version_commit_timestamp | ||||
|     version&.commit&.committed_date | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def serialize_front_matter(hash) | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ class WikiPage | |||
|     include Gitlab::Utils::StrongMemoize | ||||
| 
 | ||||
|     CanonicalSlugConflictError = Class.new(ActiveRecord::RecordInvalid) | ||||
|     WikiPageInvalid = Class.new(ArgumentError) | ||||
| 
 | ||||
|     self.table_name = 'wiki_page_meta' | ||||
| 
 | ||||
|  | @ -23,46 +24,62 @@ class WikiPage | |||
| 
 | ||||
|     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? | ||||
|     class << self | ||||
|       # 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 | ||||
|       # | ||||
|       # This method raises errors on validation issues. | ||||
|       def find_or_create(last_known_slug, wiki_page) | ||||
|         raise WikiPageInvalid unless wiki_page.valid? | ||||
| 
 | ||||
|       transaction do | ||||
|         found = find_by_canonical_slug(known_slugs, project) | ||||
|         meta = found || create(title: wiki_page.title, project_id: project.id) | ||||
|         project = wiki_page.wiki.project | ||||
|         known_slugs = [last_known_slug, wiki_page.slug].compact.uniq | ||||
|         raise 'No slugs found! This should not be possible.' if known_slugs.empty? | ||||
| 
 | ||||
|         meta.update_state(found.nil?, known_slugs, wiki_page) | ||||
|         transaction do | ||||
|           updates = wiki_page_updates(wiki_page) | ||||
|           found = find_by_canonical_slug(known_slugs, project) | ||||
|           meta = found || create!(updates.merge(project_id: project.id)) | ||||
| 
 | ||||
|           meta.update_state(found.nil?, known_slugs, wiki_page, updates) | ||||
| 
 | ||||
|           # 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 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 | ||||
| 
 | ||||
|         # 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) | ||||
|       private | ||||
| 
 | ||||
|       if conflict.present? | ||||
|         meta.errors.add(:canonical_slug, 'Duplicate value found') | ||||
|         raise CanonicalSlugConflictError.new(meta) | ||||
|       def wiki_page_updates(wiki_page) | ||||
|         last_commit_date = wiki_page.version_commit_timestamp || Time.now.utc | ||||
| 
 | ||||
|         { | ||||
|           title: wiki_page.title, | ||||
|           created_at: last_commit_date, | ||||
|           updated_at: last_commit_date | ||||
|         } | ||||
|       end | ||||
| 
 | ||||
|       meta | ||||
|     end | ||||
| 
 | ||||
|     def canonical_slug | ||||
|  | @ -85,24 +102,21 @@ class WikiPage | |||
|       @canonical_slug = slug | ||||
|     end | ||||
| 
 | ||||
|     def update_state(created, known_slugs, wiki_page) | ||||
|       update_wiki_page_attributes(wiki_page) | ||||
|     def update_state(created, known_slugs, wiki_page, updates) | ||||
|       update_wiki_page_attributes(updates) | ||||
|       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 | ||||
|     def update_wiki_page_attributes(updates) | ||||
|       # Remove all unnecessary updates: | ||||
|       updates.delete(:updated_at) if updated_at == updates[:updated_at] | ||||
|       updates.delete(:created_at) if created_at <= updates[:created_at] | ||||
|       updates.delete(:title) if title == updates[:title] | ||||
| 
 | ||||
|       update_columns(updates) unless updates.empty? | ||||
|     end | ||||
| 
 | ||||
|     def insert_slugs(strings, is_new, canonical_slug) | ||||
|  |  | |||
|  | @ -85,18 +85,40 @@ class EventCreateService | |||
|   # Create a new wiki page event | ||||
|   # | ||||
|   # @param [WikiPage::Meta] wiki_page_meta The event target | ||||
|   # @param [User] current_user The event author | ||||
|   # @param [User] author The event author | ||||
|   # @param [Integer] action One of the Event::WIKI_ACTIONS | ||||
|   def wiki_event(wiki_page_meta, current_user, action) | ||||
|   # | ||||
|   # @return a tuple of event and either :found or :created | ||||
|   def wiki_event(wiki_page_meta, author, action) | ||||
|     return unless Feature.enabled?(:wiki_events) | ||||
| 
 | ||||
|     raise IllegalActionError, action unless Event::WIKI_ACTIONS.include?(action) | ||||
| 
 | ||||
|     create_record_event(wiki_page_meta, current_user, action) | ||||
|     if duplicate = existing_wiki_event(wiki_page_meta, action) | ||||
|       return duplicate | ||||
|     end | ||||
| 
 | ||||
|     event = create_record_event(wiki_page_meta, author, action) | ||||
|     # Ensure that the event is linked in time to the metadata, for non-deletes | ||||
|     unless action == Event::DESTROYED | ||||
|       time_stamp = wiki_page_meta.updated_at | ||||
|       event.update_columns(updated_at: time_stamp, created_at: time_stamp) | ||||
|     end | ||||
| 
 | ||||
|     event | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def existing_wiki_event(wiki_page_meta, action) | ||||
|     if action == Event::DESTROYED | ||||
|       most_recent = Event.for_wiki_meta(wiki_page_meta).recent.first | ||||
|       return most_recent if most_recent.present? && most_recent.action == action | ||||
|     else | ||||
|       Event.for_wiki_meta(wiki_page_meta).created_at(wiki_page_meta.updated_at).first | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def create_record_event(record, current_user, status) | ||||
|     create_event(record.resource_parent, current_user, status, target_id: record.id, target_type: record.class.name) | ||||
|   end | ||||
|  |  | |||
|  | @ -2,8 +2,63 @@ | |||
| 
 | ||||
| module Git | ||||
|   class WikiPushService < ::BaseService | ||||
|     # Maximum number of change events we will process on any single push | ||||
|     MAX_CHANGES = 100 | ||||
| 
 | ||||
|     def execute | ||||
|       # This is used in EE | ||||
|       process_changes | ||||
|     end | ||||
| 
 | ||||
|     private | ||||
| 
 | ||||
|     def process_changes | ||||
|       return unless can_process_wiki_events? | ||||
| 
 | ||||
|       push_changes.take(MAX_CHANGES).each do |change| # rubocop:disable CodeReuse/ActiveRecord | ||||
|         next unless change.page.present? | ||||
| 
 | ||||
|         response = create_event_for(change) | ||||
|         log_error(response.message) if response.error? | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def can_process_wiki_events? | ||||
|       Feature.enabled?(:wiki_events) && Feature.enabled?(:wiki_events_on_git_push, project) | ||||
|     end | ||||
| 
 | ||||
|     def push_changes | ||||
|       default_branch_changes.flat_map do |change| | ||||
|         raw_changes(change).map { |raw| Git::WikiPushService::Change.new(wiki, change, raw) } | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def raw_changes(change) | ||||
|       wiki.repository.raw.raw_changes_between(change[:oldrev], change[:newrev]) | ||||
|     end | ||||
| 
 | ||||
|     def wiki | ||||
|       project.wiki | ||||
|     end | ||||
| 
 | ||||
|     def create_event_for(change) | ||||
|       event_service.execute(change.last_known_slug, change.page, change.event_action) | ||||
|     end | ||||
| 
 | ||||
|     def event_service | ||||
|       @event_service ||= WikiPages::EventCreateService.new(current_user) | ||||
|     end | ||||
| 
 | ||||
|     def on_default_branch?(change) | ||||
|       project.wiki.default_branch == ::Gitlab::Git.branch_name(change[:ref]) | ||||
|     end | ||||
| 
 | ||||
|     # See: [Gitlab::GitPostReceive#changes] | ||||
|     def changes | ||||
|       params[:changes] || [] | ||||
|     end | ||||
| 
 | ||||
|     def default_branch_changes | ||||
|       @default_branch_changes ||= changes.select { |change| on_default_branch?(change) } | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -0,0 +1,67 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Git | ||||
|   class WikiPushService | ||||
|     class Change | ||||
|       include Gitlab::Utils::StrongMemoize | ||||
| 
 | ||||
|       # @param [ProjectWiki] wiki | ||||
|       # @param [Hash] change - must have keys `:oldrev` and `:newrev` | ||||
|       # @param [Gitlab::Git::RawDiffChange] raw_change | ||||
|       def initialize(project_wiki, change, raw_change) | ||||
|         @wiki, @raw_change, @change = project_wiki, raw_change, change | ||||
|       end | ||||
| 
 | ||||
|       def page | ||||
|         strong_memoize(:page) { wiki.find_page(slug, revision) } | ||||
|       end | ||||
| 
 | ||||
|       # See [Gitlab::Git::RawDiffChange#extract_operation] for the | ||||
|       # definition of the full range of operation values. | ||||
|       def event_action | ||||
|         case raw_change.operation | ||||
|         when :added | ||||
|           Event::CREATED | ||||
|         when :deleted | ||||
|           Event::DESTROYED | ||||
|         else | ||||
|           Event::UPDATED | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       def last_known_slug | ||||
|         strip_extension(raw_change.old_path || raw_change.new_path) | ||||
|       end | ||||
| 
 | ||||
|       private | ||||
| 
 | ||||
|       attr_reader :raw_change, :change, :wiki | ||||
| 
 | ||||
|       def filename | ||||
|         return raw_change.old_path if deleted? | ||||
| 
 | ||||
|         raw_change.new_path | ||||
|       end | ||||
| 
 | ||||
|       def slug | ||||
|         strip_extension(filename) | ||||
|       end | ||||
| 
 | ||||
|       def revision | ||||
|         return change[:oldrev] if deleted? | ||||
| 
 | ||||
|         change[:newrev] | ||||
|       end | ||||
| 
 | ||||
|       def deleted? | ||||
|         raw_change.operation == :deleted | ||||
|       end | ||||
| 
 | ||||
|       def strip_extension(filename) | ||||
|         return unless filename | ||||
| 
 | ||||
|         File.basename(filename, File.extname(filename)) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -46,12 +46,9 @@ module WikiPages | |||
|     def create_wiki_event(page) | ||||
|       return unless ::Feature.enabled?(:wiki_events) | ||||
| 
 | ||||
|       slug = slug_for_page(page) | ||||
|       response = WikiPages::EventCreateService.new(current_user).execute(slug_for_page(page), page, event_action) | ||||
| 
 | ||||
|       Event.transaction do | ||||
|         wiki_page_meta = WikiPage::Meta.find_or_create(slug, page) | ||||
|         EventCreateService.new.wiki_event(wiki_page_meta, current_user, event_action) | ||||
|       end | ||||
|       log_error(response.message) if response.error? | ||||
|     end | ||||
| 
 | ||||
|     def slug_for_page(page) | ||||
|  |  | |||
|  | @ -0,0 +1,30 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module WikiPages | ||||
|   class EventCreateService | ||||
|     # @param [User] author The event author | ||||
|     def initialize(author) | ||||
|       raise ArgumentError, 'author must not be nil' unless author | ||||
| 
 | ||||
|       @author = author | ||||
|     end | ||||
| 
 | ||||
|     def execute(slug, page, action) | ||||
|       return ServiceResponse.success(message: 'No event created as `wiki_events` feature is disabled') unless ::Feature.enabled?(:wiki_events) | ||||
| 
 | ||||
|       event = Event.transaction do | ||||
|         wiki_page_meta = WikiPage::Meta.find_or_create(slug, page) | ||||
| 
 | ||||
|         ::EventCreateService.new.wiki_event(wiki_page_meta, author, action) | ||||
|       end | ||||
| 
 | ||||
|       ServiceResponse.success(payload: { event: event }) | ||||
|     rescue ::EventCreateService::IllegalActionError, ::ActiveRecord::ActiveRecordError => e | ||||
|       ServiceResponse.error(message: e.message, payload: { error: e }) | ||||
|     end | ||||
| 
 | ||||
|     private | ||||
| 
 | ||||
|     attr_reader :author | ||||
|   end | ||||
| end | ||||
|  | @ -271,11 +271,6 @@ | |||
|               = link_to network_admin_application_settings_path, title: _('Network'), data: { qa_selector: 'admin_settings_network_item' } do | ||||
|                 %span | ||||
|                   = _('Network') | ||||
|             - if template_exists?('admin/geo/settings/show') | ||||
|               = nav_link do | ||||
|                 = link_to geo_admin_application_settings_path, title: _('Geo') do | ||||
|                   %span | ||||
|                     = _('Geo') | ||||
|             = nav_link(path: 'application_settings#preferences') do | ||||
|               = link_to preferences_admin_application_settings_path, title: _('Preferences'), data: { qa_selector: 'admin_settings_preferences_link' } do | ||||
|                 %span | ||||
|  |  | |||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Create Wiki activity events on pushes to Wiki git repository | ||||
| merge_request: 26624 | ||||
| author: | ||||
| type: added | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Add support for cluster applications CI artifact report | ||||
| merge_request: 28866 | ||||
| author: | ||||
| type: added | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Remove Admin -> Settings -> Geo navigation | ||||
| merge_request: 21005 | ||||
| author: Lee Tickett | ||||
| type: other | ||||
|  | @ -1,3 +1,9 @@ | |||
| --- | ||||
| stage: Configure | ||||
| group: Configure | ||||
| info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers | ||||
| --- | ||||
| 
 | ||||
| # GitLab Managed Apps | ||||
| 
 | ||||
| GitLab provides **GitLab Managed Apps**, a one-click install for various applications which can | ||||
|  |  | |||
|  | @ -1,3 +1,9 @@ | |||
| --- | ||||
| stage: Configure | ||||
| group: Configure | ||||
| info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers | ||||
| --- | ||||
| 
 | ||||
| # Crossplane configuration | ||||
| 
 | ||||
| Once Crossplane [is installed](applications.md#crossplane), it must be configured for | ||||
|  |  | |||
|  | @ -1,3 +1,9 @@ | |||
| --- | ||||
| stage: Configure | ||||
| group: Configure | ||||
| info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers | ||||
| --- | ||||
| 
 | ||||
| # Cluster Environments **(PREMIUM)** | ||||
| 
 | ||||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/13392) for group-level clusters in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3. | ||||
|  |  | |||
|  | @ -1,3 +1,9 @@ | |||
| --- | ||||
| stage: Configure | ||||
| group: Configure | ||||
| info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers | ||||
| --- | ||||
| 
 | ||||
| # Cluster management project (alpha) | ||||
| 
 | ||||
| CAUTION: **Warning:** | ||||
|  |  | |||
|  | @ -1,5 +1,8 @@ | |||
| --- | ||||
| type: reference | ||||
| stage: Configure | ||||
| group: Configure | ||||
| info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers | ||||
| --- | ||||
| 
 | ||||
| # Group-level Kubernetes clusters | ||||
|  |  | |||
|  | @ -1,3 +1,9 @@ | |||
| --- | ||||
| stage: Configure | ||||
| group: Configure | ||||
| info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers | ||||
| --- | ||||
| 
 | ||||
| # Infrastructure as code with GitLab managed Terraform State | ||||
| 
 | ||||
| [Terraform remote backends](https://www.terraform.io/docs/backends/index.html) | ||||
|  |  | |||
|  | @ -1,3 +1,9 @@ | |||
| --- | ||||
| stage: Configure | ||||
| group: Configure | ||||
| info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers | ||||
| --- | ||||
| 
 | ||||
| # Adding EKS clusters | ||||
| 
 | ||||
| GitLab supports adding new and existing EKS clusters. | ||||
|  |  | |||
|  | @ -1,3 +1,9 @@ | |||
| --- | ||||
| stage: Configure | ||||
| group: Configure | ||||
| info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers | ||||
| --- | ||||
| 
 | ||||
| # Adding GKE clusters | ||||
| 
 | ||||
| GitLab supports adding new and existing GKE clusters. | ||||
|  |  | |||
|  | @ -1,3 +1,9 @@ | |||
| --- | ||||
| stage: Configure | ||||
| group: Configure | ||||
| info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers | ||||
| --- | ||||
| 
 | ||||
| # Adding and removing Kubernetes clusters | ||||
| 
 | ||||
| GitLab offers integrated cluster creation for the following Kubernetes providers: | ||||
|  |  | |||
|  | @ -1,3 +1,9 @@ | |||
| --- | ||||
| stage: Configure | ||||
| group: Configure | ||||
| info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers | ||||
| --- | ||||
| 
 | ||||
| # Kubernetes Logs | ||||
| 
 | ||||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/4752) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.0. | ||||
|  |  | |||
|  | @ -1,3 +1,9 @@ | |||
| --- | ||||
| stage: Configure | ||||
| group: Configure | ||||
| info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers | ||||
| --- | ||||
| 
 | ||||
| # Runbooks | ||||
| 
 | ||||
| Runbooks are a collection of documented procedures that explain how to | ||||
|  |  | |||
|  | @ -1,3 +1,9 @@ | |||
| --- | ||||
| stage: Configure | ||||
| group: Configure | ||||
| info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers | ||||
| --- | ||||
| 
 | ||||
| # Deploying AWS Lambda function using GitLab CI/CD | ||||
| 
 | ||||
| GitLab allows users to easily deploy AWS Lambda functions and create rich serverless applications. | ||||
|  |  | |||
|  | @ -1,3 +1,9 @@ | |||
| --- | ||||
| stage: Configure | ||||
| group: Configure | ||||
| info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers | ||||
| --- | ||||
| 
 | ||||
| # Serverless | ||||
| 
 | ||||
| > Introduced in GitLab 11.5. | ||||
|  |  | |||
|  | @ -14,7 +14,7 @@ module Gitlab | |||
|           ALLOWED_KEYS = | ||||
|             %i[junit codequality sast dependency_scanning container_scanning | ||||
|                dast performance license_management license_scanning metrics lsif | ||||
|                dotenv cobertura terraform accessibility].freeze | ||||
|                dotenv cobertura terraform accessibility cluster_applications].freeze | ||||
| 
 | ||||
|           attributes ALLOWED_KEYS | ||||
| 
 | ||||
|  | @ -38,6 +38,7 @@ module Gitlab | |||
|               validates :cobertura, array_of_strings_or_string: true | ||||
|               validates :terraform, array_of_strings_or_string: true | ||||
|               validates :accessibility, array_of_strings_or_string: true | ||||
|               validates :cluster_applications, array_of_strings_or_string: true | ||||
|             end | ||||
|           end | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,91 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Gitlab | ||||
|   module Kubernetes | ||||
|     class NetworkPolicy | ||||
|       def initialize(name:, namespace:, pod_selector:, ingress:, creation_timestamp: nil, policy_types: ["Ingress"], egress: nil) | ||||
|         @name = name | ||||
|         @namespace = namespace | ||||
|         @creation_timestamp = creation_timestamp | ||||
|         @pod_selector = pod_selector | ||||
|         @policy_types = policy_types | ||||
|         @ingress = ingress | ||||
|         @egress = egress | ||||
|       end | ||||
| 
 | ||||
|       def self.from_yaml(manifest) | ||||
|         return unless manifest | ||||
| 
 | ||||
|         policy = YAML.safe_load(manifest, symbolize_names: true) | ||||
|         return if !policy[:metadata] || !policy[:spec] | ||||
| 
 | ||||
|         metadata = policy[:metadata] | ||||
|         spec = policy[:spec] | ||||
|         self.new( | ||||
|           name: metadata[:name], | ||||
|           namespace: metadata[:namespace], | ||||
|           pod_selector: spec[:podSelector], | ||||
|           policy_types: spec[:policyTypes], | ||||
|           ingress: spec[:ingress], | ||||
|           egress: spec[:egress] | ||||
|         ) | ||||
|       rescue Psych::SyntaxError, Psych::DisallowedClass | ||||
|         nil | ||||
|       end | ||||
| 
 | ||||
|       def self.from_resource(resource) | ||||
|         return unless resource | ||||
|         return if !resource[:metadata] || !resource[:spec] | ||||
| 
 | ||||
|         metadata = resource[:metadata] | ||||
|         spec = resource[:spec].to_h | ||||
|         self.new( | ||||
|           name: metadata[:name], | ||||
|           namespace: metadata[:namespace], | ||||
|           creation_timestamp: metadata[:creationTimestamp], | ||||
|           pod_selector: spec[:podSelector], | ||||
|           policy_types: spec[:policyTypes], | ||||
|           ingress: spec[:ingress], | ||||
|           egress: spec[:egress] | ||||
|         ) | ||||
|       end | ||||
| 
 | ||||
|       def generate | ||||
|         ::Kubeclient::Resource.new.tap do |resource| | ||||
|           resource.metadata = metadata | ||||
|           resource.spec = spec | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       def as_json(opts = nil) | ||||
|         { | ||||
|           name: name, | ||||
|           namespace: namespace, | ||||
|           creation_timestamp: creation_timestamp, | ||||
|           manifest: manifest | ||||
|         } | ||||
|       end | ||||
| 
 | ||||
|       private | ||||
| 
 | ||||
|       attr_reader :name, :namespace, :creation_timestamp, :pod_selector, :policy_types, :ingress, :egress | ||||
| 
 | ||||
|       def metadata | ||||
|         { name: name, namespace: namespace } | ||||
|       end | ||||
| 
 | ||||
|       def spec | ||||
|         { | ||||
|           podSelector: pod_selector, | ||||
|           policyTypes: policy_types, | ||||
|           ingress: ingress, | ||||
|           egress: egress | ||||
|         } | ||||
|       end | ||||
| 
 | ||||
|       def manifest | ||||
|         YAML.dump(metadata: metadata, spec: spec) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| FactoryBot.define do | ||||
|   factory :design_version, class: 'DesignManagement::Version' do | ||||
|     sequence(:sha) { |n| Digest::SHA1.hexdigest("commit-like-#{n}") } | ||||
|     sha | ||||
|     issue { designs.first&.issue || create(:issue) } | ||||
|     author { issue&.author || create(:user) } | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,15 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| FactoryBot.define do | ||||
|   factory :git_wiki_commit_details, class: 'Gitlab::Git::Wiki::CommitDetails' do | ||||
|     skip_create | ||||
| 
 | ||||
|     transient do | ||||
|       author { create(:user) } | ||||
|     end | ||||
| 
 | ||||
|     sequence(:message) { |n| "Commit message #{n}" } | ||||
| 
 | ||||
|     initialize_with { new(author.id, author.username, author.name, author.email, message) } | ||||
|   end | ||||
| end | ||||
|  | @ -12,4 +12,5 @@ FactoryBot.define do | |||
|   sequence(:branch) { |n| "my-branch-#{n}" } | ||||
|   sequence(:past_time) { |n| 4.hours.ago + (2 * n).seconds } | ||||
|   sequence(:iid) | ||||
|   sequence(:sha) { |n| Digest::SHA1.hexdigest("commit-like-#{n}") } | ||||
| end | ||||
|  |  | |||
|  | @ -66,5 +66,6 @@ FactoryBot.define do | |||
|   end | ||||
| 
 | ||||
|   sequence(:wiki_page_title) { |n| "Page #{n}" } | ||||
|   sequence(:wiki_filename) { |n| "Page_#{n}.md" } | ||||
|   sequence(:sluggified_title) { |n| "slug-#{n}" } | ||||
| end | ||||
|  |  | |||
|  | @ -0,0 +1,41 @@ | |||
| import { shallowMount } from '@vue/test-utils'; | ||||
| import EditFormButtons from '~/sidebar/components/confidential/edit_form_buttons.vue'; | ||||
| 
 | ||||
| describe('Edit Form Buttons', () => { | ||||
|   let wrapper; | ||||
|   const findConfidentialToggle = () => wrapper.find('[data-testid="confidential-toggle"]'); | ||||
| 
 | ||||
|   const createComponent = props => { | ||||
|     wrapper = shallowMount(EditFormButtons, { | ||||
|       propsData: { | ||||
|         updateConfidentialAttribute: () => {}, | ||||
|         ...props, | ||||
|       }, | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   afterEach(() => { | ||||
|     wrapper.destroy(); | ||||
|     wrapper = null; | ||||
|   }); | ||||
| 
 | ||||
|   describe('when not confidential', () => { | ||||
|     it('renders Turn On in the ', () => { | ||||
|       createComponent({ | ||||
|         isConfidential: false, | ||||
|       }); | ||||
| 
 | ||||
|       expect(findConfidentialToggle().text()).toBe('Turn On'); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('when confidential', () => { | ||||
|     it('renders on or off text based on confidentiality', () => { | ||||
|       createComponent({ | ||||
|         isConfidential: true, | ||||
|       }); | ||||
| 
 | ||||
|       expect(findConfidentialToggle().text()).toBe('Turn Off'); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | @ -0,0 +1,45 @@ | |||
| import { shallowMount } from '@vue/test-utils'; | ||||
| import EditForm from '~/sidebar/components/confidential/edit_form.vue'; | ||||
| 
 | ||||
| describe('Edit Form Dropdown', () => { | ||||
|   let wrapper; | ||||
|   const toggleForm = () => {}; | ||||
|   const updateConfidentialAttribute = () => {}; | ||||
| 
 | ||||
|   const createComponent = props => { | ||||
|     wrapper = shallowMount(EditForm, { | ||||
|       propsData: { | ||||
|         ...props, | ||||
|       }, | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   afterEach(() => { | ||||
|     wrapper.destroy(); | ||||
|     wrapper = null; | ||||
|   }); | ||||
| 
 | ||||
|   describe('when not confidential', () => { | ||||
|     it('renders "You are going to turn off the confidentiality." in the ', () => { | ||||
|       createComponent({ | ||||
|         isConfidential: false, | ||||
|         toggleForm, | ||||
|         updateConfidentialAttribute, | ||||
|       }); | ||||
| 
 | ||||
|       expect(wrapper.find('p').text()).toContain('You are going to turn on the confidentiality.'); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('when confidential', () => { | ||||
|     it('renders on or off text based on confidentiality', () => { | ||||
|       createComponent({ | ||||
|         isConfidential: true, | ||||
|         toggleForm, | ||||
|         updateConfidentialAttribute, | ||||
|       }); | ||||
| 
 | ||||
|       expect(wrapper.find('p').text()).toContain('You are going to turn off the confidentiality.'); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | @ -1,35 +0,0 @@ | |||
| import Vue from 'vue'; | ||||
| import editFormButtons from '~/sidebar/components/confidential/edit_form_buttons.vue'; | ||||
| 
 | ||||
| describe('Edit Form Buttons', () => { | ||||
|   let vm1; | ||||
|   let vm2; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     const Component = Vue.extend(editFormButtons); | ||||
|     const toggleForm = () => {}; | ||||
|     const updateConfidentialAttribute = () => {}; | ||||
| 
 | ||||
|     vm1 = new Component({ | ||||
|       propsData: { | ||||
|         isConfidential: true, | ||||
|         toggleForm, | ||||
|         updateConfidentialAttribute, | ||||
|       }, | ||||
|     }).$mount(); | ||||
| 
 | ||||
|     vm2 = new Component({ | ||||
|       propsData: { | ||||
|         isConfidential: false, | ||||
|         toggleForm, | ||||
|         updateConfidentialAttribute, | ||||
|       }, | ||||
|     }).$mount(); | ||||
|   }); | ||||
| 
 | ||||
|   it('renders on or off text based on confidentiality', () => { | ||||
|     expect(vm1.$el.innerHTML.includes('Turn Off')).toBe(true); | ||||
| 
 | ||||
|     expect(vm2.$el.innerHTML.includes('Turn On')).toBe(true); | ||||
|   }); | ||||
| }); | ||||
|  | @ -1,35 +0,0 @@ | |||
| import Vue from 'vue'; | ||||
| import editForm from '~/sidebar/components/confidential/edit_form.vue'; | ||||
| 
 | ||||
| describe('Edit Form Dropdown', () => { | ||||
|   let vm1; | ||||
|   let vm2; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     const Component = Vue.extend(editForm); | ||||
|     const toggleForm = () => {}; | ||||
|     const updateConfidentialAttribute = () => {}; | ||||
| 
 | ||||
|     vm1 = new Component({ | ||||
|       propsData: { | ||||
|         isConfidential: true, | ||||
|         toggleForm, | ||||
|         updateConfidentialAttribute, | ||||
|       }, | ||||
|     }).$mount(); | ||||
| 
 | ||||
|     vm2 = new Component({ | ||||
|       propsData: { | ||||
|         isConfidential: false, | ||||
|         toggleForm, | ||||
|         updateConfidentialAttribute, | ||||
|       }, | ||||
|     }).$mount(); | ||||
|   }); | ||||
| 
 | ||||
|   it('renders on the appropriate warning text', () => { | ||||
|     expect(vm1.$el.innerHTML.includes('You are going to turn off the confidentiality.')).toBe(true); | ||||
| 
 | ||||
|     expect(vm2.$el.innerHTML.includes('You are going to turn on the confidentiality.')).toBe(true); | ||||
|   }); | ||||
| }); | ||||
|  | @ -48,6 +48,7 @@ describe Gitlab::Ci::Config::Entry::Reports do | |||
|         :cobertura | 'cobertura-coverage.xml' | ||||
|         :terraform | 'tfplan.json' | ||||
|         :accessibility | 'gl-accessibility.json' | ||||
|         :cluster_applications | 'gl-cluster-applications.json' | ||||
|       end | ||||
| 
 | ||||
|       with_them do | ||||
|  |  | |||
|  | @ -0,0 +1,224 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| describe Gitlab::Kubernetes::NetworkPolicy do | ||||
|   let(:policy) do | ||||
|     described_class.new( | ||||
|       name: name, | ||||
|       namespace: namespace, | ||||
|       creation_timestamp: '2020-04-14T00:08:30Z', | ||||
|       pod_selector: pod_selector, | ||||
|       policy_types: %w(Ingress Egress), | ||||
|       ingress: ingress, | ||||
|       egress: egress | ||||
|     ) | ||||
|   end | ||||
| 
 | ||||
|   let(:name) { 'example-name' } | ||||
|   let(:namespace) { 'example-namespace' } | ||||
|   let(:pod_selector) { { matchLabels: { role: 'db' } } } | ||||
| 
 | ||||
|   let(:ingress) do | ||||
|     [ | ||||
|       { | ||||
|         from: [ | ||||
|           { namespaceSelector: { matchLabels: { project: 'myproject' } } } | ||||
|         ] | ||||
|       } | ||||
|     ] | ||||
|   end | ||||
| 
 | ||||
|   let(:egress) do | ||||
|     [ | ||||
|       { | ||||
|         ports: [{ port: 5978 }] | ||||
|       } | ||||
|     ] | ||||
|   end | ||||
| 
 | ||||
|   describe '.from_yaml' do | ||||
|     let(:manifest) do | ||||
|       <<-POLICY | ||||
| apiVersion: networking.k8s.io/v1 | ||||
| kind: NetworkPolicy | ||||
| metadata: | ||||
|   name: example-name | ||||
|   namespace: example-namespace | ||||
| spec: | ||||
|   podSelector: | ||||
|     matchLabels: | ||||
|       role: db | ||||
|   policyTypes: | ||||
|   - Ingress | ||||
|   ingress: | ||||
|   - from: | ||||
|     - namespaceSelector: | ||||
|         matchLabels: | ||||
|           project: myproject | ||||
|       POLICY | ||||
|     end | ||||
|     let(:resource) do | ||||
|       ::Kubeclient::Resource.new( | ||||
|         metadata: { name: name, namespace: namespace }, | ||||
|         spec: { podSelector: pod_selector, policyTypes: %w(Ingress), ingress: ingress, egress: nil } | ||||
|       ) | ||||
|     end | ||||
| 
 | ||||
|     subject { Gitlab::Kubernetes::NetworkPolicy.from_yaml(manifest)&.generate } | ||||
| 
 | ||||
|     it { is_expected.to eq(resource) } | ||||
| 
 | ||||
|     context 'with nil manifest' do | ||||
|       let(:manifest) { nil } | ||||
| 
 | ||||
|       it { is_expected.to be_nil } | ||||
|     end | ||||
| 
 | ||||
|     context 'with invalid manifest' do | ||||
|       let(:manifest) { "\tfoo: bar" } | ||||
| 
 | ||||
|       it { is_expected.to be_nil } | ||||
|     end | ||||
| 
 | ||||
|     context 'with manifest without metadata' do | ||||
|       let(:manifest) do | ||||
|         <<-POLICY | ||||
| apiVersion: networking.k8s.io/v1 | ||||
| kind: NetworkPolicy | ||||
| spec: | ||||
|   podSelector: | ||||
|     matchLabels: | ||||
|       role: db | ||||
|   policyTypes: | ||||
|   - Ingress | ||||
|   ingress: | ||||
|   - from: | ||||
|     - namespaceSelector: | ||||
|         matchLabels: | ||||
|           project: myproject | ||||
|         POLICY | ||||
|       end | ||||
| 
 | ||||
|       it { is_expected.to be_nil } | ||||
|     end | ||||
| 
 | ||||
|     context 'with manifest without spec' do | ||||
|       let(:manifest) do | ||||
|         <<-POLICY | ||||
| apiVersion: networking.k8s.io/v1 | ||||
| kind: NetworkPolicy | ||||
| metadata: | ||||
|   name: example-name | ||||
|   namespace: example-namespace | ||||
|         POLICY | ||||
|       end | ||||
| 
 | ||||
|       it { is_expected.to be_nil } | ||||
|     end | ||||
| 
 | ||||
|     context 'with disallowed class' do | ||||
|       let(:manifest) do | ||||
|         <<-POLICY | ||||
| apiVersion: networking.k8s.io/v1 | ||||
| kind: NetworkPolicy | ||||
| metadata: | ||||
|   name: example-name | ||||
|   namespace: example-namespace | ||||
|   creationTimestamp: 2020-04-14T00:08:30Z | ||||
| spec: | ||||
|   podSelector: | ||||
|     matchLabels: | ||||
|       role: db | ||||
|   policyTypes: | ||||
|   - Ingress | ||||
|   ingress: | ||||
|   - from: | ||||
|     - namespaceSelector: | ||||
|         matchLabels: | ||||
|           project: myproject | ||||
|         POLICY | ||||
|       end | ||||
| 
 | ||||
|       it { is_expected.to be_nil } | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '.from_resource' do | ||||
|     let(:resource) do | ||||
|       ::Kubeclient::Resource.new( | ||||
|         metadata: { name: name, namespace: namespace, creationTimestamp: '2020-04-14T00:08:30Z', resourceVersion: '4990' }, | ||||
|         spec: { podSelector: pod_selector, policyTypes: %w(Ingress), ingress: ingress, egress: nil } | ||||
|       ) | ||||
|     end | ||||
|     let(:generated_resource) do | ||||
|       ::Kubeclient::Resource.new( | ||||
|         metadata: { name: name, namespace: namespace }, | ||||
|         spec: { podSelector: pod_selector, policyTypes: %w(Ingress), ingress: ingress, egress: nil } | ||||
|       ) | ||||
|     end | ||||
| 
 | ||||
|     subject { Gitlab::Kubernetes::NetworkPolicy.from_resource(resource)&.generate } | ||||
| 
 | ||||
|     it { is_expected.to eq(generated_resource) } | ||||
| 
 | ||||
|     context 'with nil resource' do | ||||
|       let(:resource) { nil } | ||||
| 
 | ||||
|       it { is_expected.to be_nil } | ||||
|     end | ||||
| 
 | ||||
|     context 'with resource without metadata' do | ||||
|       let(:resource) do | ||||
|         ::Kubeclient::Resource.new( | ||||
|           spec: { podSelector: pod_selector, policyTypes: %w(Ingress), ingress: ingress, egress: nil } | ||||
|         ) | ||||
|       end | ||||
| 
 | ||||
|       it { is_expected.to be_nil } | ||||
|     end | ||||
| 
 | ||||
|     context 'with resource without spec' do | ||||
|       let(:resource) do | ||||
|         ::Kubeclient::Resource.new( | ||||
|           metadata: { name: name, namespace: namespace, uid: '128cf288-7de4-11ea-aceb-42010a800089', resourceVersion: '4990' } | ||||
|         ) | ||||
|       end | ||||
| 
 | ||||
|       it { is_expected.to be_nil } | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#generate' do | ||||
|     let(:resource) do | ||||
|       ::Kubeclient::Resource.new( | ||||
|         metadata: { name: name, namespace: namespace }, | ||||
|         spec: { podSelector: pod_selector, policyTypes: %w(Ingress Egress), ingress: ingress, egress: egress } | ||||
|       ) | ||||
|     end | ||||
| 
 | ||||
|     subject { policy.generate } | ||||
| 
 | ||||
|     it { is_expected.to eq(resource) } | ||||
|   end | ||||
| 
 | ||||
|   describe '#as_json' do | ||||
|     let(:json_policy) do | ||||
|       { | ||||
|         name: name, | ||||
|         namespace: namespace, | ||||
|         creation_timestamp: '2020-04-14T00:08:30Z', | ||||
|         manifest: YAML.dump( | ||||
|           { | ||||
|             metadata: { name: name, namespace: namespace }, | ||||
|             spec: { podSelector: pod_selector, policyTypes: %w(Ingress Egress), ingress: ingress, egress: egress } | ||||
|           } | ||||
|         ) | ||||
|       } | ||||
|     end | ||||
| 
 | ||||
|     subject { policy.as_json } | ||||
| 
 | ||||
|     it { is_expected.to eq(json_policy) } | ||||
|   end | ||||
| end | ||||
|  | @ -84,6 +84,21 @@ describe Event do | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe 'scopes' do | ||||
|     describe 'created_at' do | ||||
|       it 'can find the right event' do | ||||
|         time = 1.day.ago | ||||
|         event = create(:event, created_at: time) | ||||
|         false_positive = create(:event, created_at: 2.days.ago) | ||||
| 
 | ||||
|         found = described_class.created_at(time) | ||||
| 
 | ||||
|         expect(found).to include(event) | ||||
|         expect(found).not_to include(false_positive) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe "Push event" do | ||||
|     let(:project) { create(:project, :private) } | ||||
|     let(:user) { project.owner } | ||||
|  | @ -511,6 +526,14 @@ describe Event do | |||
|         expect(described_class.not_wiki_page).to match_array(non_wiki_events) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     describe '.for_wiki_meta' do | ||||
|       it 'finds events for a given wiki page metadata object' do | ||||
|         event = events.select(&:wiki_page?).first | ||||
| 
 | ||||
|         expect(described_class.for_wiki_meta(event.target)).to contain_exactly(event) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#wiki_page and #wiki_page?' do | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| require 'spec_helper' | ||||
| 
 | ||||
| describe WikiPage::Meta do | ||||
|   let_it_be(:project) { create(:project) } | ||||
|   let_it_be(:project) { create(:project, :wiki_repo) } | ||||
|   let_it_be(:other_project) { create(:project) } | ||||
| 
 | ||||
|   describe 'Associations' do | ||||
|  | @ -169,8 +169,11 @@ describe WikiPage::Meta do | |||
|       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) | ||||
|     def create_previous_version(title: old_title, slug: last_known_slug, date: wiki_page.version.commit.committed_date) | ||||
|       create(:wiki_page_meta, | ||||
|              title: title, project: project, | ||||
|              created_at: date, updated_at: date, | ||||
|              canonical_slug: slug) | ||||
|     end | ||||
| 
 | ||||
|     def create_context | ||||
|  | @ -198,6 +201,8 @@ describe WikiPage::Meta do | |||
|           title: wiki_page.title, | ||||
|           project: wiki_page.wiki.project | ||||
|         ) | ||||
|         expect(meta.updated_at).to eq(wiki_page.version.commit.committed_date) | ||||
|         expect(meta.created_at).not_to be_after(meta.updated_at) | ||||
|         expect(meta.slugs.where(slug: last_known_slug)).to exist | ||||
|         expect(meta.slugs.canonical.where(slug: wiki_page.slug)).to exist | ||||
|       end | ||||
|  | @ -209,22 +214,32 @@ describe WikiPage::Meta do | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'the slug is too long' do | ||||
|       let(:last_known_slug) { FFaker::Lorem.characters(2050) } | ||||
|     context 'there are problems' do | ||||
|       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) | ||||
|         it 'raises an error' do | ||||
|           expect { find_record }.to raise_error ActiveRecord::ValueTooLong | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       it 'raises an error' do | ||||
|         expect { find_record }.to raise_error(ActiveRecord::RecordInvalid) | ||||
|       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 'the wiki page is not valid' do | ||||
|         let(:wiki_page) { build(:wiki_page, project: project, title: nil) } | ||||
| 
 | ||||
|         it 'raises an error' do | ||||
|           expect { find_record }.to raise_error(described_class::WikiPageInvalid) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|  | @ -258,6 +273,17 @@ describe WikiPage::Meta do | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'the commit happened a day ago' do | ||||
|       before do | ||||
|         allow(wiki_page.version.commit).to receive(:committed_date).and_return(1.day.ago) | ||||
|       end | ||||
| 
 | ||||
|       include_examples 'metadata examples' do | ||||
|         # Identical to the base case. | ||||
|         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 } | ||||
| 
 | ||||
|  | @ -292,6 +318,33 @@ describe WikiPage::Meta do | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'a record exists in the DB, but we need to update timestamps' do | ||||
|       let(:last_known_slug) { current_slug } | ||||
|       let(:old_title) { title } | ||||
| 
 | ||||
|       before do | ||||
|         create_previous_version(date: 1.week.ago) | ||||
|       end | ||||
| 
 | ||||
|       include_examples 'metadata examples' do | ||||
|         # We need the query, and the update | ||||
|         # 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 | ||||
|         # | ||||
|         # UPDATE wiki_page_meta SET updated_at = ?date WHERE id = ?id | ||||
|         # | ||||
|         # RELEASE SAVEPOINT active_record_2 | ||||
|         let(:query_limit) { 4 } | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'we need to update the slug, but not the title' do | ||||
|       let(:old_title) { title } | ||||
| 
 | ||||
|  | @ -359,14 +412,14 @@ describe WikiPage::Meta do | |||
|     end | ||||
| 
 | ||||
|     context 'we want to change the slug back to a previous version' do | ||||
|       let(:slug_1) { 'foo' } | ||||
|       let(:slug_2) { 'bar' } | ||||
|       let(:slug_1) { generate(:sluggified_title) } | ||||
|       let(:slug_2) { generate(:sluggified_title) } | ||||
| 
 | ||||
|       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 = create_previous_version(title: title, slug: slug_1) | ||||
|         meta.canonical_slug = slug_2 | ||||
|       end | ||||
| 
 | ||||
|  |  | |||
|  | @ -844,6 +844,20 @@ describe WikiPage do | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#version_commit_timestamp' do | ||||
|     context 'for a new page' do | ||||
|       it 'returns nil' do | ||||
|         expect(new_page.version_commit_timestamp).to be_nil | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'for page that exists' do | ||||
|       it 'returns the timestamp of the commit' do | ||||
|         expect(existing_page.version_commit_timestamp).to eq(existing_page.version.commit.committed_date) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def get_slugs(page_or_dir) | ||||
|  |  | |||
|  | @ -34,7 +34,7 @@ describe Ci::RetryBuildService do | |||
|        job_artifacts_container_scanning job_artifacts_dast | ||||
|        job_artifacts_license_management job_artifacts_license_scanning | ||||
|        job_artifacts_performance job_artifacts_lsif | ||||
|        job_artifacts_terraform | ||||
|        job_artifacts_terraform job_artifacts_cluster_applications | ||||
|        job_artifacts_codequality job_artifacts_metrics scheduled_at | ||||
|        job_variables waiting_for_resource_at job_artifacts_metrics_referee | ||||
|        job_artifacts_network_referee job_artifacts_dotenv | ||||
|  |  | |||
|  | @ -162,16 +162,25 @@ describe EventCreateService do | |||
|       context "The action is #{action}" do | ||||
|         let(:event) { service.wiki_event(meta, user, action) } | ||||
| 
 | ||||
|         it 'creates the event' do | ||||
|         it 'creates the event', :aggregate_failures do | ||||
|           expect(event).to have_attributes( | ||||
|             wiki_page?: true, | ||||
|             valid?: true, | ||||
|             persisted?: true, | ||||
|             action: action, | ||||
|             wiki_page: wiki_page | ||||
|             wiki_page: wiki_page, | ||||
|             author: user | ||||
|           ) | ||||
|         end | ||||
| 
 | ||||
|         it 'is idempotent', :aggregate_failures do | ||||
|           expect { event }.to change(Event, :count).by(1) | ||||
|           duplicate = nil | ||||
|           expect { duplicate = service.wiki_event(meta, user, action) }.not_to change(Event, :count) | ||||
| 
 | ||||
|           expect(duplicate).to eq(event) | ||||
|         end | ||||
| 
 | ||||
|         context 'the feature is disabled' do | ||||
|           before do | ||||
|             stub_feature_flags(wiki_events: false) | ||||
|  |  | |||
|  | @ -0,0 +1,109 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| describe Git::WikiPushService::Change do | ||||
|   subject { described_class.new(project_wiki, change, raw_change) } | ||||
| 
 | ||||
|   let(:project_wiki) { double('ProjectWiki') } | ||||
|   let(:raw_change) { double('RawChange', new_path: new_path, old_path: old_path, operation: operation) } | ||||
|   let(:change) { { oldrev: generate(:sha), newrev: generate(:sha) } } | ||||
| 
 | ||||
|   let(:new_path) do | ||||
|     case operation | ||||
|     when :deleted | ||||
|       nil | ||||
|     else | ||||
|       generate(:wiki_filename) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   let(:old_path) do | ||||
|     case operation | ||||
|     when :added | ||||
|       nil | ||||
|     when :deleted, :renamed | ||||
|       generate(:wiki_filename) | ||||
|     else | ||||
|       new_path | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#page' do | ||||
|     context 'the page does not exist' do | ||||
|       before do | ||||
|         expect(project_wiki).to receive(:find_page).with(String, String).and_return(nil) | ||||
|       end | ||||
| 
 | ||||
|       %i[added deleted renamed modified].each do |op| | ||||
|         context "the operation is #{op}" do | ||||
|           let(:operation) { op } | ||||
| 
 | ||||
|           it { is_expected.to have_attributes(page: be_nil) } | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'the page can be found' do | ||||
|       let(:wiki_page) { double('WikiPage') } | ||||
| 
 | ||||
|       before do | ||||
|         expect(project_wiki).to receive(:find_page).with(slug, revision).and_return(wiki_page) | ||||
|       end | ||||
| 
 | ||||
|       context 'the page has been deleted' do | ||||
|         let(:operation) { :deleted } | ||||
|         let(:slug) { old_path.chomp('.md') } | ||||
|         let(:revision) { change[:oldrev] } | ||||
| 
 | ||||
|         it { is_expected.to have_attributes(page: wiki_page) } | ||||
|       end | ||||
| 
 | ||||
|       %i[added renamed modified].each do |op| | ||||
|         let(:operation) { op } | ||||
|         let(:slug) { new_path.chomp('.md') } | ||||
|         let(:revision) { change[:newrev] } | ||||
| 
 | ||||
|         it { is_expected.to have_attributes(page: wiki_page) } | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#last_known_slug' do | ||||
|     context 'the page has been created' do | ||||
|       let(:operation) { :added } | ||||
| 
 | ||||
|       it { is_expected.to have_attributes(last_known_slug: new_path.chomp('.md')) } | ||||
|     end | ||||
| 
 | ||||
|     %i[renamed modified deleted].each do |op| | ||||
|       context "the operation is #{op}" do | ||||
|         let(:operation) { op } | ||||
| 
 | ||||
|         it { is_expected.to have_attributes(last_known_slug: old_path.chomp('.md')) } | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#event_action' do | ||||
|     context 'the page is deleted' do | ||||
|       let(:operation) { :deleted } | ||||
| 
 | ||||
|       it { is_expected.to have_attributes(event_action: Event::DESTROYED) } | ||||
|     end | ||||
| 
 | ||||
|     context 'the page is added' do | ||||
|       let(:operation) { :added } | ||||
| 
 | ||||
|       it { is_expected.to have_attributes(event_action: Event::CREATED) } | ||||
|     end | ||||
| 
 | ||||
|     %i[renamed modified].each do |op| | ||||
|       context "the page is #{op}" do | ||||
|         let(:operation) { op } | ||||
| 
 | ||||
|         it { is_expected.to have_attributes(event_action: Event::UPDATED) } | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,338 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| describe Git::WikiPushService, services: true do | ||||
|   include RepoHelpers | ||||
| 
 | ||||
|   let_it_be(:key_id) { create(:key, user: current_user).shell_id } | ||||
|   let_it_be(:project) { create(:project, :wiki_repo) } | ||||
|   let_it_be(:current_user) { create(:user) } | ||||
|   let_it_be(:git_wiki) { project.wiki.wiki } | ||||
|   let_it_be(:repository) { git_wiki.repository } | ||||
| 
 | ||||
|   describe '#execute' do | ||||
|     context 'the push contains more than the permitted number of changes' do | ||||
|       def run_service | ||||
|         process_changes { described_class::MAX_CHANGES.succ.times { write_new_page } } | ||||
|       end | ||||
| 
 | ||||
|       it 'creates only MAX_CHANGES events' do | ||||
|         expect { run_service }.to change(Event, :count).by(described_class::MAX_CHANGES) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'default_branch collides with a tag' do | ||||
|       it 'creates only one event' do | ||||
|         base_sha = current_sha | ||||
|         write_new_page | ||||
| 
 | ||||
|         service = create_service(base_sha, ['refs/heads/master', 'refs/tags/master']) | ||||
| 
 | ||||
|         expect { service.execute }.to change(Event, :count).by(1) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     describe 'successfully creating events' do | ||||
|       let(:count) { Event::WIKI_ACTIONS.size } | ||||
| 
 | ||||
|       def run_service | ||||
|         wiki_page_a = create(:wiki_page, project: project) | ||||
|         wiki_page_b = create(:wiki_page, project: project) | ||||
| 
 | ||||
|         process_changes do | ||||
|           write_new_page | ||||
|           update_page(wiki_page_a.title) | ||||
|           delete_page(wiki_page_b.page.path) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       it 'creates one event for every wiki action' do | ||||
|         expect { run_service }.to change(Event, :count).by(count) | ||||
|       end | ||||
| 
 | ||||
|       it 'handles all known actions' do | ||||
|         run_service | ||||
| 
 | ||||
|         expect(Event.last(count).pluck(:action)).to match_array(Event::WIKI_ACTIONS) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'two pages have been created' do | ||||
|       def run_service | ||||
|         process_changes do | ||||
|           write_new_page | ||||
|           write_new_page | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       it 'creates two events' do | ||||
|         expect { run_service }.to change(Event, :count).by(2) | ||||
|       end | ||||
| 
 | ||||
|       it 'creates two metadata records' do | ||||
|         expect { run_service }.to change(WikiPage::Meta, :count).by(2) | ||||
|       end | ||||
| 
 | ||||
|       it 'creates appropriate events' do | ||||
|         run_service | ||||
| 
 | ||||
|         expect(Event.last(2)).to all(have_attributes(wiki_page?: true, action: Event::CREATED)) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'a non-page file as been added' do | ||||
|       it 'does not create events, or WikiPage metadata' do | ||||
|         expect do | ||||
|           process_changes { write_non_page } | ||||
|         end.not_to change { [Event.count, WikiPage::Meta.count] } | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'one page, and one non-page have been created' do | ||||
|       def run_service | ||||
|         process_changes do | ||||
|           write_new_page | ||||
|           write_non_page | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       it 'creates a wiki page creation event' do | ||||
|         expect { run_service }.to change(Event, :count).by(1) | ||||
| 
 | ||||
|         expect(Event.last).to have_attributes(wiki_page?: true, action: Event::CREATED) | ||||
|       end | ||||
| 
 | ||||
|       it 'creates one metadata record' do | ||||
|         expect { run_service }.to change(WikiPage::Meta, :count).by(1) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'one page has been added, and then updated' do | ||||
|       def run_service | ||||
|         process_changes do | ||||
|           title = write_new_page | ||||
|           update_page(title) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       it 'creates just a single event' do | ||||
|         expect { run_service }.to change(Event, :count).by(1) | ||||
|       end | ||||
| 
 | ||||
|       it 'creates just one metadata record' do | ||||
|         expect { run_service }.to change(WikiPage::Meta, :count).by(1) | ||||
|       end | ||||
| 
 | ||||
|       it 'creates a new wiki page creation event' do | ||||
|         run_service | ||||
| 
 | ||||
|         expect(Event.last).to have_attributes( | ||||
|           wiki_page?: true, | ||||
|           action: Event::CREATED | ||||
|         ) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when a page we already know about has been updated' do | ||||
|       let(:wiki_page) { create(:wiki_page, project: project) } | ||||
| 
 | ||||
|       before do | ||||
|         create(:wiki_page_meta, :for_wiki_page, wiki_page: wiki_page) | ||||
|       end | ||||
| 
 | ||||
|       def run_service | ||||
|         process_changes { update_page(wiki_page.title) } | ||||
|       end | ||||
| 
 | ||||
|       it 'does not create a new meta-data record' do | ||||
|         expect { run_service }.not_to change(WikiPage::Meta, :count) | ||||
|       end | ||||
| 
 | ||||
|       it 'creates a new event' do | ||||
|         expect { run_service }.to change(Event, :count).by(1) | ||||
|       end | ||||
| 
 | ||||
|       it 'adds an update event' do | ||||
|         run_service | ||||
| 
 | ||||
|         expect(Event.last).to have_attributes( | ||||
|           wiki_page?: true, | ||||
|           action: Event::UPDATED | ||||
|         ) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when a page we do not know about has been updated' do | ||||
|       def run_service | ||||
|         wiki_page = create(:wiki_page, project: project) | ||||
|         process_changes { update_page(wiki_page.title) } | ||||
|       end | ||||
| 
 | ||||
|       it 'creates a new meta-data record' do | ||||
|         expect { run_service }.to change(WikiPage::Meta, :count).by(1) | ||||
|       end | ||||
| 
 | ||||
|       it 'creates a new event' do | ||||
|         expect { run_service }.to change(Event, :count).by(1) | ||||
|       end | ||||
| 
 | ||||
|       it 'adds an update event' do | ||||
|         run_service | ||||
| 
 | ||||
|         expect(Event.last).to have_attributes( | ||||
|           wiki_page?: true, | ||||
|           action: Event::UPDATED | ||||
|         ) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when a page we do not know about has been deleted' do | ||||
|       def run_service | ||||
|         wiki_page = create(:wiki_page, project: project) | ||||
|         process_changes { delete_page(wiki_page.page.path) } | ||||
|       end | ||||
| 
 | ||||
|       it 'create a new meta-data record' do | ||||
|         expect { run_service }.to change(WikiPage::Meta, :count).by(1) | ||||
|       end | ||||
| 
 | ||||
|       it 'creates a new event' do | ||||
|         expect { run_service }.to change(Event, :count).by(1) | ||||
|       end | ||||
| 
 | ||||
|       it 'adds an update event' do | ||||
|         run_service | ||||
| 
 | ||||
|         expect(Event.last).to have_attributes( | ||||
|           wiki_page?: true, | ||||
|           action: Event::DESTROYED | ||||
|         ) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     it 'calls log_error for every event we cannot create' do | ||||
|       base_sha = current_sha | ||||
|       count = 3 | ||||
|       count.times { write_new_page } | ||||
|       message = 'something went very very wrong' | ||||
|       allow_next_instance_of(WikiPages::EventCreateService, current_user) do |service| | ||||
|         allow(service).to receive(:execute) | ||||
|           .with(String, WikiPage, Integer) | ||||
|           .and_return(ServiceResponse.error(message: message)) | ||||
|       end | ||||
| 
 | ||||
|       service = create_service(base_sha) | ||||
| 
 | ||||
|       expect(service).to receive(:log_error).exactly(count).times.with(message) | ||||
| 
 | ||||
|       service.execute | ||||
|     end | ||||
| 
 | ||||
|     describe 'feature flags' do | ||||
|       shared_examples 'a no-op push' do | ||||
|         it 'does not create any events' do | ||||
|           expect { process_changes { write_new_page } }.not_to change(Event, :count) | ||||
|         end | ||||
| 
 | ||||
|         it 'does not even look for events to process' do | ||||
|           base_sha = current_sha | ||||
|           write_new_page | ||||
| 
 | ||||
|           service = create_service(base_sha) | ||||
| 
 | ||||
|           expect(service).not_to receive(:changed_files) | ||||
| 
 | ||||
|           service.execute | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'the wiki_events feature is disabled' do | ||||
|         before do | ||||
|           stub_feature_flags(wiki_events: false) | ||||
|         end | ||||
| 
 | ||||
|         it_behaves_like 'a no-op push' | ||||
|       end | ||||
| 
 | ||||
|       context 'the wiki_events_on_git_push feature is disabled' do | ||||
|         before do | ||||
|           stub_feature_flags(wiki_events_on_git_push: false) | ||||
|         end | ||||
| 
 | ||||
|         it_behaves_like 'a no-op push' | ||||
| 
 | ||||
|         context 'but is enabled for a given project' do | ||||
|           before do | ||||
|             stub_feature_flags(wiki_events_on_git_push: { enabled: true, thing: project }) | ||||
|           end | ||||
| 
 | ||||
|           it 'creates events' do | ||||
|             expect { process_changes { write_new_page } }.to change(Event, :count).by(1) | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   # In order to construct the correct GitPostReceive object that represents the | ||||
|   # changes we are applying, we need to describe the changes between old-ref and | ||||
|   # new-ref. Old ref (the base sha) we have to capture before we perform any | ||||
|   # changes. Once the changes have been applied, we can execute the service to | ||||
|   # process them. | ||||
|   def process_changes(&block) | ||||
|     base_sha = current_sha | ||||
|     yield | ||||
|     create_service(base_sha).execute | ||||
|   end | ||||
| 
 | ||||
|   def create_service(base, refs = ['refs/heads/master']) | ||||
|     changes = post_received(base, refs).changes | ||||
|     described_class.new(project, current_user, changes: changes) | ||||
|   end | ||||
| 
 | ||||
|   def post_received(base, refs) | ||||
|     change_str = refs.map { |ref| +"#{base} #{current_sha} #{ref}" }.join("\n") | ||||
|     post_received = ::Gitlab::GitPostReceive.new(project, key_id, change_str, {}) | ||||
|     allow(post_received).to receive(:identify).with(key_id).and_return(current_user) | ||||
| 
 | ||||
|     post_received | ||||
|   end | ||||
| 
 | ||||
|   def current_sha | ||||
|     repository.gitaly_ref_client.find_branch('master')&.dereferenced_target&.id || Gitlab::Git::BLANK_SHA | ||||
|   end | ||||
| 
 | ||||
|   # It is important not to re-use the WikiPage services here, since they create | ||||
|   # events - these helper methods below are intended to simulate actions on the repo | ||||
|   # that have not gone through our services. | ||||
| 
 | ||||
|   def write_new_page | ||||
|     generate(:wiki_page_title).tap { |t| git_wiki.write_page(t, 'markdown', 'Hello', commit_details) } | ||||
|   end | ||||
| 
 | ||||
|   # We write something to the wiki-repo that is not a page - as, for example, an | ||||
|   # attachment. This will appear as a raw-diff change, but wiki.find_page will | ||||
|   # return nil. | ||||
|   def write_non_page | ||||
|     params = { | ||||
|       file_name: 'attachment.log', | ||||
|       file_content: 'some stuff', | ||||
|       branch_name: 'master' | ||||
|     } | ||||
|     ::Wikis::CreateAttachmentService.new(project, project.owner, params).execute | ||||
|   end | ||||
| 
 | ||||
|   def update_page(title) | ||||
|     page = git_wiki.page(title: title) | ||||
|     git_wiki.update_page(page.path, title, 'markdown', 'Hey', commit_details) | ||||
|   end | ||||
| 
 | ||||
|   def delete_page(path) | ||||
|     git_wiki.delete_page(path, commit_details) | ||||
|   end | ||||
| 
 | ||||
|   def commit_details | ||||
|     create(:git_wiki_commit_details, author: current_user) | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,87 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| describe WikiPages::EventCreateService do | ||||
|   let_it_be(:project) { create(:project) } | ||||
|   let_it_be(:user) { create(:user) } | ||||
| 
 | ||||
|   subject { described_class.new(user) } | ||||
| 
 | ||||
|   describe '#execute' do | ||||
|     let_it_be(:page) { create(:wiki_page, project: project) } | ||||
|     let(:slug) { generate(:sluggified_title) } | ||||
|     let(:action) { Event::CREATED } | ||||
|     let(:response) { subject.execute(slug, page, action) } | ||||
| 
 | ||||
|     context 'feature flag is not enabled' do | ||||
|       before do | ||||
|         stub_feature_flags(wiki_events: false) | ||||
|       end | ||||
| 
 | ||||
|       it 'does not error' do | ||||
|         expect(response).to be_success | ||||
|           .and have_attributes(message: /No event created/) | ||||
|       end | ||||
| 
 | ||||
|       it 'does not create an event' do | ||||
|         expect { response }.not_to change(Event, :count) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'the user is nil' do | ||||
|       subject { described_class.new(nil) } | ||||
| 
 | ||||
|       it 'raises an error on construction' do | ||||
|         expect { subject }.to raise_error ArgumentError | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'the action is illegal' do | ||||
|       let(:action) { Event::WIKI_ACTIONS.max + 1 } | ||||
| 
 | ||||
|       it 'returns an error' do | ||||
|         expect(response).to be_error | ||||
|       end | ||||
| 
 | ||||
|       it 'does not create an event' do | ||||
|         expect { response }.not_to change(Event, :count) | ||||
|       end | ||||
| 
 | ||||
|       it 'does not create a metadata record' do | ||||
|         expect { response }.not_to change(WikiPage::Meta, :count) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     it 'returns a successful response' do | ||||
|       expect(response).to be_success | ||||
|     end | ||||
| 
 | ||||
|     context 'the action is a deletion' do | ||||
|       let(:action) { Event::DESTROYED } | ||||
| 
 | ||||
|       it 'does not synchronize the wiki metadata timestamps with the git commit' do | ||||
|         expect_next_instance_of(WikiPage::Meta) do |instance| | ||||
|           expect(instance).not_to receive(:synch_times_with_page) | ||||
|         end | ||||
| 
 | ||||
|         response | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     it 'creates a wiki page event' do | ||||
|       expect { response }.to change(Event, :count).by(1) | ||||
|     end | ||||
| 
 | ||||
|     it 'returns an event in the payload' do | ||||
|       expect(response.payload).to include(event: have_attributes(author: user, wiki_page?: true, action: action)) | ||||
|     end | ||||
| 
 | ||||
|     it 'records the slug for the page' do | ||||
|       response | ||||
|       meta = WikiPage::Meta.find_or_create(page.slug, page) | ||||
| 
 | ||||
|       expect(meta.slugs.pluck(:slug)).to include(slug) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -299,6 +299,31 @@ describe PostReceive do | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context "master" do | ||||
|       let(:default_branch) { 'master' } | ||||
|       let(:oldrev) { '012345' } | ||||
|       let(:newrev) { '6789ab' } | ||||
|       let(:changes) do | ||||
|         <<~EOF | ||||
|             #{oldrev} #{newrev} refs/heads/#{default_branch} | ||||
|             123456 789012 refs/heads/tést2 | ||||
|         EOF | ||||
|       end | ||||
| 
 | ||||
|       let(:raw_repo) { double('RawRepo') } | ||||
| 
 | ||||
|       it 'processes the changes on the master branch' do | ||||
|         expect_next_instance_of(Git::WikiPushService) do |service| | ||||
|           expect(service).to receive(:process_changes).and_call_original | ||||
|         end | ||||
|         expect(project.wiki).to receive(:default_branch).twice.and_return(default_branch) | ||||
|         expect(project.wiki.repository).to receive(:raw).and_return(raw_repo) | ||||
|         expect(raw_repo).to receive(:raw_changes_between).once.with(oldrev, newrev).and_return([]) | ||||
| 
 | ||||
|         perform | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context "branches" do | ||||
|       let(:changes) do | ||||
|         <<~EOF | ||||
|  | @ -307,6 +332,12 @@ describe PostReceive do | |||
|         EOF | ||||
|       end | ||||
| 
 | ||||
|       before do | ||||
|         allow_next_instance_of(Git::WikiPushService) do |service| | ||||
|           allow(service).to receive(:process_changes) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       it 'expires the branches cache' do | ||||
|         expect(project.wiki.repository).to receive(:expire_branches_cache).once | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue