diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
index 2a792dfa19a..feb023fb3d5 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
@@ -52,7 +52,7 @@ export default {
},
toggleFormDropdown() {
this.isDropdownShowing = !this.isDropdownShowing;
- const { dropdown } = this.$children[2].$refs.dropdown.$refs;
+ const { dropdown } = this.$refs.status.$refs.dropdown.$refs;
if (dropdown && this.isDropdownShowing) {
dropdown.show();
}
@@ -102,6 +102,7 @@ export default {
(user, group) { where(user: user, group: group) }
+ scope :by_user_parent, -> (user, group) { where(user: user, group: group) }
- def self.visited!(user, board)
- visit = find_or_create_by(user: user, group: board.group, board: board)
- visit.touch if visit.updated_at < Time.current
- rescue ActiveRecord::RecordNotUnique
- retry
- end
-
- def self.latest(user, group, count: nil)
- visits = by_user_group(user, group).order(updated_at: :desc)
- visits = visits.preload(:board) if count && count > 1
-
- visits.first(count)
+ def self.board_parent_relation
+ :group
end
end
diff --git a/app/models/board_project_recent_visit.rb b/app/models/board_project_recent_visit.rb
index 509c8f97b83..723afd6feab 100644
--- a/app/models/board_project_recent_visit.rb
+++ b/app/models/board_project_recent_visit.rb
@@ -2,27 +2,19 @@
# Tracks which boards in a specific project a user has visited
class BoardProjectRecentVisit < ApplicationRecord
+ include BoardRecentVisit
+
belongs_to :user
belongs_to :project
belongs_to :board
- validates :user, presence: true
+ validates :user, presence: true
validates :project, presence: true
validates :board, presence: true
- scope :by_user_project, -> (user, project) { where(user: user, project: project) }
+ scope :by_user_parent, -> (user, project) { where(user: user, project: project) }
- def self.visited!(user, board)
- visit = find_or_create_by(user: user, project: board.project, board: board)
- visit.touch if visit.updated_at < Time.current
- rescue ActiveRecord::RecordNotUnique
- retry
- end
-
- def self.latest(user, project, count: nil)
- visits = by_user_project(user, project).order(updated_at: :desc)
- visits = visits.preload(:board) if count && count > 1
-
- visits.first(count)
+ def self.board_parent_relation
+ :project
end
end
diff --git a/app/models/concerns/board_recent_visit.rb b/app/models/concerns/board_recent_visit.rb
new file mode 100644
index 00000000000..fd4d574ac58
--- /dev/null
+++ b/app/models/concerns/board_recent_visit.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module BoardRecentVisit
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def visited!(user, board)
+ find_or_create_by(
+ "user" => user,
+ board_parent_relation => board.resource_parent,
+ board_relation => board
+ ).tap do |visit|
+ visit.touch
+ end
+ rescue ActiveRecord::RecordNotUnique
+ retry
+ end
+
+ def latest(user, parent, count: nil)
+ visits = by_user_parent(user, parent).order(updated_at: :desc)
+ visits = visits.preload(board_relation)
+
+ visits.first(count)
+ end
+
+ def board_relation
+ :board
+ end
+
+ def board_parent_relation
+ raise NotImplementedError
+ end
+ end
+end
diff --git a/app/services/boards/visits/create_service.rb b/app/services/boards/visits/create_service.rb
index 428ed1a8bcc..4d659596803 100644
--- a/app/services/boards/visits/create_service.rb
+++ b/app/services/boards/visits/create_service.rb
@@ -5,13 +5,17 @@ module Boards
class CreateService < Boards::BaseService
def execute(board)
return unless current_user && Gitlab::Database.read_write?
- return unless board.is_a?(Board) # other board types do not support board visits yet
+ return unless board
- if parent.is_a?(Group)
- BoardGroupRecentVisit.visited!(current_user, board)
- else
- BoardProjectRecentVisit.visited!(current_user, board)
- end
+ model.visited!(current_user, board)
+ end
+
+ private
+
+ def model
+ return BoardGroupRecentVisit if parent.is_a?(Group)
+
+ BoardProjectRecentVisit
end
end
end
diff --git a/changelogs/unreleased/321625-epic_boards-redirect.yml b/changelogs/unreleased/321625-epic_boards-redirect.yml
new file mode 100644
index 00000000000..9f5600a1e12
--- /dev/null
+++ b/changelogs/unreleased/321625-epic_boards-redirect.yml
@@ -0,0 +1,5 @@
+---
+title: Redirect to the last visited epic board
+merge_request: 60720
+author:
+type: added
diff --git a/db/migrate/20260421104929_add_epic_board_recent_visits_table.rb b/db/migrate/20260421104929_add_epic_board_recent_visits_table.rb
new file mode 100644
index 00000000000..da69f14e34c
--- /dev/null
+++ b/db/migrate/20260421104929_add_epic_board_recent_visits_table.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class AddEpicBoardRecentVisitsTable < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ def up
+ with_lock_retries do
+ create_table :boards_epic_board_recent_visits do |t|
+ t.references :user, index: true, null: false, foreign_key: { on_delete: :cascade }
+ t.references :epic_board, index: true, foreign_key: { to_table: :boards_epic_boards, on_delete: :cascade }, null: false
+ t.references :group, index: true, foreign_key: { to_table: :namespaces, on_delete: :cascade }, null: false
+ t.timestamps_with_timezone null: false
+ end
+ end
+ end
+
+ def down
+ with_lock_retries do
+ drop_table :boards_epic_board_recent_visits
+ end
+ end
+end
diff --git a/db/migrate/20260421104930_add_index_to_epic_board_recent_visits.rb b/db/migrate/20260421104930_add_index_to_epic_board_recent_visits.rb
new file mode 100644
index 00000000000..1341886c50c
--- /dev/null
+++ b/db/migrate/20260421104930_add_index_to_epic_board_recent_visits.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class AddIndexToEpicBoardRecentVisits < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ INDEX_NAME = 'index_epic_board_recent_visits_on_user_group_and_board'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :boards_epic_board_recent_visits,
+ [:user_id, :group_id, :epic_board_id],
+ name: INDEX_NAME,
+ unique: true
+ end
+
+ def down
+ remove_concurrent_index_by_name :boards_epic_board_recent_visits, INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20260421104929 b/db/schema_migrations/20260421104929
new file mode 100644
index 00000000000..58ae3012455
--- /dev/null
+++ b/db/schema_migrations/20260421104929
@@ -0,0 +1 @@
+c92824844732aad7277fc8b50bac0bf6919ad0a8d72e73b4ec3b89eafc085b7d
\ No newline at end of file
diff --git a/db/schema_migrations/20260421104930 b/db/schema_migrations/20260421104930
new file mode 100644
index 00000000000..d32c8ad7c7a
--- /dev/null
+++ b/db/schema_migrations/20260421104930
@@ -0,0 +1 @@
+ae3c8336cb25efa7d23357a6777c0656dbe1a216efb5d4edcf923d1128f7e1e3
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 8f05390ae4a..8b7d6737916 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -10057,6 +10057,24 @@ CREATE SEQUENCE boards_epic_board_positions_id_seq
ALTER SEQUENCE boards_epic_board_positions_id_seq OWNED BY boards_epic_board_positions.id;
+CREATE TABLE boards_epic_board_recent_visits (
+ id bigint NOT NULL,
+ user_id bigint NOT NULL,
+ epic_board_id bigint NOT NULL,
+ group_id bigint NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL
+);
+
+CREATE SEQUENCE boards_epic_board_recent_visits_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE boards_epic_board_recent_visits_id_seq OWNED BY boards_epic_board_recent_visits.id;
+
CREATE TABLE boards_epic_boards (
id bigint NOT NULL,
hide_backlog_list boolean DEFAULT false NOT NULL,
@@ -19353,6 +19371,8 @@ ALTER TABLE ONLY boards_epic_board_labels ALTER COLUMN id SET DEFAULT nextval('b
ALTER TABLE ONLY boards_epic_board_positions ALTER COLUMN id SET DEFAULT nextval('boards_epic_board_positions_id_seq'::regclass);
+ALTER TABLE ONLY boards_epic_board_recent_visits ALTER COLUMN id SET DEFAULT nextval('boards_epic_board_recent_visits_id_seq'::regclass);
+
ALTER TABLE ONLY boards_epic_boards ALTER COLUMN id SET DEFAULT nextval('boards_epic_boards_id_seq'::regclass);
ALTER TABLE ONLY boards_epic_list_user_preferences ALTER COLUMN id SET DEFAULT nextval('boards_epic_list_user_preferences_id_seq'::regclass);
@@ -20468,6 +20488,9 @@ ALTER TABLE ONLY boards_epic_board_labels
ALTER TABLE ONLY boards_epic_board_positions
ADD CONSTRAINT boards_epic_board_positions_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY boards_epic_board_recent_visits
+ ADD CONSTRAINT boards_epic_board_recent_visits_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY boards_epic_boards
ADD CONSTRAINT boards_epic_boards_pkey PRIMARY KEY (id);
@@ -22293,6 +22316,12 @@ CREATE INDEX index_boards_epic_board_positions_on_epic_id ON boards_epic_board_p
CREATE INDEX index_boards_epic_board_positions_on_scoped_relative_position ON boards_epic_board_positions USING btree (epic_board_id, epic_id, relative_position);
+CREATE INDEX index_boards_epic_board_recent_visits_on_epic_board_id ON boards_epic_board_recent_visits USING btree (epic_board_id);
+
+CREATE INDEX index_boards_epic_board_recent_visits_on_group_id ON boards_epic_board_recent_visits USING btree (group_id);
+
+CREATE INDEX index_boards_epic_board_recent_visits_on_user_id ON boards_epic_board_recent_visits USING btree (user_id);
+
CREATE INDEX index_boards_epic_boards_on_group_id ON boards_epic_boards USING btree (group_id);
CREATE INDEX index_boards_epic_list_user_preferences_on_epic_list_id ON boards_epic_list_user_preferences USING btree (epic_list_id);
@@ -22857,6 +22886,8 @@ CREATE INDEX index_environments_on_state_and_auto_stop_at ON environments USING
CREATE UNIQUE INDEX index_epic_board_list_preferences_on_user_and_list ON boards_epic_list_user_preferences USING btree (user_id, epic_list_id);
+CREATE UNIQUE INDEX index_epic_board_recent_visits_on_user_group_and_board ON boards_epic_board_recent_visits USING btree (user_id, group_id, epic_board_id);
+
CREATE INDEX index_epic_issues_on_epic_id ON epic_issues USING btree (epic_id);
CREATE INDEX index_epic_issues_on_epic_id_and_issue_id ON epic_issues USING btree (epic_id, issue_id);
@@ -26626,6 +26657,9 @@ ALTER TABLE ONLY packages_rubygems_metadata
ALTER TABLE ONLY packages_pypi_metadata
ADD CONSTRAINT fk_rails_9698717cdd FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE;
+ALTER TABLE ONLY boards_epic_board_recent_visits
+ ADD CONSTRAINT fk_rails_96c2c18642 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY packages_dependency_links
ADD CONSTRAINT fk_rails_96ef1c00d3 FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE;
@@ -26890,6 +26924,9 @@ ALTER TABLE ONLY pages_deployments
ALTER TABLE ONLY merge_request_user_mentions
ADD CONSTRAINT fk_rails_c440b9ea31 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
+ALTER TABLE ONLY boards_epic_board_recent_visits
+ ADD CONSTRAINT fk_rails_c4dcba4a3e FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY ci_job_artifacts
ADD CONSTRAINT fk_rails_c5137cb2c1 FOREIGN KEY (job_id) REFERENCES ci_builds(id) ON DELETE CASCADE;
@@ -27067,6 +27104,9 @@ ALTER TABLE ONLY draft_notes
ALTER TABLE ONLY namespace_package_settings
ADD CONSTRAINT fk_rails_e773444769 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+ALTER TABLE ONLY boards_epic_board_recent_visits
+ ADD CONSTRAINT fk_rails_e77911cf03 FOREIGN KEY (epic_board_id) REFERENCES boards_epic_boards(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY dast_site_tokens
ADD CONSTRAINT fk_rails_e84f721a8e FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md
index ef82c556468..a072cc73c43 100644
--- a/doc/administration/auth/README.md
+++ b/doc/administration/auth/README.md
@@ -6,7 +6,7 @@ group: Access
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/#assignments
---
-# GitLab authentication and authorization
+# GitLab authentication and authorization **(FREE SELF)**
GitLab integrates with the following external authentication and authorization
providers:
diff --git a/doc/administration/auth/atlassian.md b/doc/administration/auth/atlassian.md
index 9c5da558b0d..365236748b9 100644
--- a/doc/administration/auth/atlassian.md
+++ b/doc/administration/auth/atlassian.md
@@ -5,7 +5,7 @@ group: Access
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/#assignments
---
-# Atlassian OmniAuth Provider
+# Atlassian OmniAuth Provider **(FREE SELF)**
To enable the Atlassian OmniAuth provider for passwordless authentication you must register an application with Atlassian.
diff --git a/doc/administration/auth/authentiq.md b/doc/administration/auth/authentiq.md
index dbc5e446287..2eab4555c85 100644
--- a/doc/administration/auth/authentiq.md
+++ b/doc/administration/auth/authentiq.md
@@ -5,7 +5,7 @@ group: Access
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/#assignments
---
-# Authentiq OmniAuth Provider
+# Authentiq OmniAuth Provider **(FREE SELF)**
To enable the Authentiq OmniAuth provider for passwordless authentication you must register an application with Authentiq.
diff --git a/doc/administration/auth/cognito.md b/doc/administration/auth/cognito.md
index f264321ea6c..de5fa991abe 100644
--- a/doc/administration/auth/cognito.md
+++ b/doc/administration/auth/cognito.md
@@ -5,7 +5,7 @@ group: Access
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/#assignments
---
-# Amazon Web Services Cognito
+# Amazon Web Services Cognito **(FREE SELF)**
Amazon Cognito lets you add user sign-up, sign-in, and access control to your GitLab instance.
The following documentation enables Cognito as an OAuth2 provider.
diff --git a/doc/administration/auth/crowd.md b/doc/administration/auth/crowd.md
index 440d956fc8f..f83710ef4c7 100644
--- a/doc/administration/auth/crowd.md
+++ b/doc/administration/auth/crowd.md
@@ -5,7 +5,7 @@ group: Access
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/#assignments
---
-# Atlassian Crowd OmniAuth Provider
+# Atlassian Crowd OmniAuth Provider **(FREE SELF)**
Authenticate to GitLab using the Atlassian Crowd OmniAuth provider. Enabling
this provider also allows Crowd authentication for Git-over-https requests.
diff --git a/doc/administration/auth/jwt.md b/doc/administration/auth/jwt.md
index 59d00265a1b..b74b70ee8c0 100644
--- a/doc/administration/auth/jwt.md
+++ b/doc/administration/auth/jwt.md
@@ -5,7 +5,7 @@ group: Access
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/#assignments
---
-# JWT OmniAuth provider
+# JWT OmniAuth provider **(FREE SELF)**
To enable the JWT OmniAuth provider, you must register your application with JWT.
JWT will provide you with a secret key for you to use.
diff --git a/doc/administration/auth/ldap/ldap-troubleshooting.md b/doc/administration/auth/ldap/ldap-troubleshooting.md
index ecb521b0689..1e6684751ed 100644
--- a/doc/administration/auth/ldap/ldap-troubleshooting.md
+++ b/doc/administration/auth/ldap/ldap-troubleshooting.md
@@ -5,7 +5,7 @@ group: Access
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/#assignments
---
-# LDAP Troubleshooting for Administrators
+# LDAP Troubleshooting for Administrators **(FREE SELF)**
## Common Problems & Workflows
diff --git a/doc/administration/auth/oidc.md b/doc/administration/auth/oidc.md
index 9e3f6cd51de..30ca7d94a1e 100644
--- a/doc/administration/auth/oidc.md
+++ b/doc/administration/auth/oidc.md
@@ -5,7 +5,7 @@ group: Access
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/#assignments
---
-# OpenID Connect OmniAuth provider
+# OpenID Connect OmniAuth provider **(FREE SELF)**
GitLab can use [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html) as an OmniAuth provider.
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 8628f956b99..3626fe428b1 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -2563,6 +2563,25 @@ Input type: `IterationCadenceUpdateInput`
| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| `iterationCadence` | [`IterationCadence`](#iterationcadence) | The updated iteration cadence. |
+### `Mutation.iterationDelete`
+
+Input type: `IterationDeleteInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `id` | [`IterationID!`](#iterationid) | ID of the iteration. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| `group` | [`Group!`](#group) | Group the iteration belongs to. |
+
### `Mutation.jiraImportStart`
Input type: `JiraImportStartInput`
diff --git a/doc/user/application_security/api_fuzzing/index.md b/doc/user/application_security/api_fuzzing/index.md
index bc6ce132566..bb5ab2e4c79 100644
--- a/doc/user/application_security/api_fuzzing/index.md
+++ b/doc/user/application_security/api_fuzzing/index.md
@@ -588,7 +588,7 @@ NOTE:
| `SECURE_ANALYZERS_PREFIX` | Specify the Docker registry base address from which to download the analyzer. |
| `FUZZAPI_VERSION` | Specify API Fuzzing container version. Defaults to `latest`. |
| `FUZZAPI_TARGET_URL` | Base URL of API testing target. |
-|[`FUZZAPI_CONFIG`](#configuration-files) | API Fuzzing configuration file. Defaults to `.gitlab-apifuzzer.yml`. |
+|[`FUZZAPI_CONFIG`](#configuration-files) | [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/276395) in GitLab 13.12, replaced with default `.gitlab/gitlab-api-fuzzing-config.yml`. API Fuzzing configuration file. |
|[`FUZZAPI_PROFILE`](#configuration-files) | Configuration profile to use during testing. Defaults to `Quick`. |
|[`FUZZAPI_OPENAPI`](#openapi-specification) | OpenAPI specification file or URL. |
|[`FUZZAPI_HAR`](#http-archive-har) | HTTP Archive (HAR) file. |
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
index 34669eb3815..ca0eeb2403c 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
@@ -3,7 +3,7 @@
module QA
RSpec.describe 'Create', quarantine: { only: { subdomain: :staging }, issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/323990', type: :flaky } do
describe 'Merge request rebasing' do
- it 'user rebases source branch of merge request', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1274' do
+ it 'user rebases source branch of merge request', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1800' do
Flow::Login.sign_in
project = Resource::Project.fabricate_via_api! do |project|
diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js
index 1b069c1fe90..80eea6c19c2 100644
--- a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js
+++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js
@@ -10,9 +10,9 @@ const mockAlert = mockAlerts[0];
describe('Alert Details Sidebar Status', () => {
let wrapper;
- const findStatusDropdown = () => wrapper.find(GlDropdown);
- const findStatusDropdownItem = () => wrapper.find(GlDropdownItem);
- const findStatusLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findStatusDropdown = () => wrapper.findComponent(GlDropdown);
+ const findStatusDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+ const findStatusLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findStatusDropdownHeader = () => wrapper.findByTestId('dropdown-header');
const findAlertStatus = () => wrapper.findComponent(AlertStatus);
const findStatus = () => wrapper.findByTestId('status');
diff --git a/spec/models/board_group_recent_visit_spec.rb b/spec/models/board_group_recent_visit_spec.rb
index c6fbd263072..d2d287d8e24 100644
--- a/spec/models/board_group_recent_visit_spec.rb
+++ b/spec/models/board_group_recent_visit_spec.rb
@@ -3,9 +3,8 @@
require 'spec_helper'
RSpec.describe BoardGroupRecentVisit do
- let(:user) { create(:user) }
- let(:group) { create(:group) }
- let(:board) { create(:board, group: group) }
+ let_it_be(:board_parent) { create(:group) }
+ let_it_be(:board) { create(:board, group: board_parent) }
describe 'relationships' do
it { is_expected.to belong_to(:user) }
@@ -19,56 +18,9 @@ RSpec.describe BoardGroupRecentVisit do
it { is_expected.to validate_presence_of(:board) }
end
- describe '#visited' do
- it 'creates a visit if one does not exists' do
- expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1)
- end
-
- shared_examples 'was visited previously' do
- let!(:visit) { create :board_group_recent_visit, group: board.group, board: board, user: user, updated_at: 7.days.ago }
-
- it 'updates the timestamp' do
- freeze_time do
- described_class.visited!(user, board)
-
- expect(described_class.count).to eq 1
- expect(described_class.first.updated_at).to be_like_time(Time.zone.now)
- end
- end
- end
-
- it_behaves_like 'was visited previously'
-
- context 'when we try to create a visit that is not unique' do
- before do
- expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique')
- expect(described_class).to receive(:find_or_create_by).and_return(visit)
- end
-
- it_behaves_like 'was visited previously'
- end
- end
-
- describe '#latest' do
- def create_visit(time)
- create :board_group_recent_visit, group: group, user: user, updated_at: time
- end
-
- it 'returns the most recent visited' do
- create_visit(7.days.ago)
- create_visit(5.days.ago)
- recent = create_visit(1.day.ago)
-
- expect(described_class.latest(user, group)).to eq recent
- end
-
- it 'returns last 3 visited boards' do
- create_visit(7.days.ago)
- visit1 = create_visit(3.days.ago)
- visit2 = create_visit(2.days.ago)
- visit3 = create_visit(5.days.ago)
-
- expect(described_class.latest(user, group, count: 3)).to eq([visit2, visit1, visit3])
- end
+ it_behaves_like 'boards recent visit' do
+ let_it_be(:board_relation) { :board }
+ let_it_be(:board_parent_relation) { :group }
+ let_it_be(:visit_relation) { :board_group_recent_visit }
end
end
diff --git a/spec/models/board_project_recent_visit_spec.rb b/spec/models/board_project_recent_visit_spec.rb
index 145a4f5b1a7..262c3a8faaa 100644
--- a/spec/models/board_project_recent_visit_spec.rb
+++ b/spec/models/board_project_recent_visit_spec.rb
@@ -3,9 +3,8 @@
require 'spec_helper'
RSpec.describe BoardProjectRecentVisit do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:board) { create(:board, project: project) }
+ let_it_be(:board_parent) { create(:project) }
+ let_it_be(:board) { create(:board, project: board_parent) }
describe 'relationships' do
it { is_expected.to belong_to(:user) }
@@ -19,56 +18,9 @@ RSpec.describe BoardProjectRecentVisit do
it { is_expected.to validate_presence_of(:board) }
end
- describe '#visited' do
- it 'creates a visit if one does not exists' do
- expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1)
- end
-
- shared_examples 'was visited previously' do
- let!(:visit) { create :board_project_recent_visit, project: board.project, board: board, user: user, updated_at: 7.days.ago }
-
- it 'updates the timestamp' do
- freeze_time do
- described_class.visited!(user, board)
-
- expect(described_class.count).to eq 1
- expect(described_class.first.updated_at).to be_like_time(Time.zone.now)
- end
- end
- end
-
- it_behaves_like 'was visited previously'
-
- context 'when we try to create a visit that is not unique' do
- before do
- expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique')
- expect(described_class).to receive(:find_or_create_by).and_return(visit)
- end
-
- it_behaves_like 'was visited previously'
- end
- end
-
- describe '#latest' do
- def create_visit(time)
- create :board_project_recent_visit, project: project, user: user, updated_at: time
- end
-
- it 'returns the most recent visited' do
- create_visit(7.days.ago)
- create_visit(5.days.ago)
- recent = create_visit(1.day.ago)
-
- expect(described_class.latest(user, project)).to eq recent
- end
-
- it 'returns last 3 visited boards' do
- create_visit(7.days.ago)
- visit1 = create_visit(3.days.ago)
- visit2 = create_visit(2.days.ago)
- visit3 = create_visit(5.days.ago)
-
- expect(described_class.latest(user, project, count: 3)).to eq([visit2, visit1, visit3])
- end
+ it_behaves_like 'boards recent visit' do
+ let_it_be(:board_relation) { :board }
+ let_it_be(:board_parent_relation) { :project }
+ let_it_be(:visit_relation) { :board_project_recent_visit }
end
end
diff --git a/spec/services/boards/visits/create_service_spec.rb b/spec/services/boards/visits/create_service_spec.rb
index 64faa2cf07b..8910345d170 100644
--- a/spec/services/boards/visits/create_service_spec.rb
+++ b/spec/services/boards/visits/create_service_spec.rb
@@ -7,47 +7,20 @@ RSpec.describe Boards::Visits::CreateService do
let(:user) { create(:user) }
context 'when a project board' do
- let(:project) { create(:project) }
- let(:project_board) { create(:board, project: project) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:board) { create(:board, project: project) }
- subject(:service) { described_class.new(project_board.resource_parent, user) }
+ let_it_be(:model) { BoardProjectRecentVisit }
- it 'returns nil when there is no user' do
- service.current_user = nil
-
- expect(service.execute(project_board)).to eq nil
- end
-
- it 'returns nil when database is read-only' do
- allow(Gitlab::Database).to receive(:read_only?) { true }
-
- expect(service.execute(project_board)).to eq nil
- end
-
- it 'records the visit' do
- expect(BoardProjectRecentVisit).to receive(:visited!).once
-
- service.execute(project_board)
- end
+ it_behaves_like 'boards recent visit create service'
end
context 'when a group board' do
- let(:group) { create(:group) }
- let(:group_board) { create(:board, group: group) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:board) { create(:board, group: group) }
+ let_it_be(:model) { BoardGroupRecentVisit }
- subject(:service) { described_class.new(group_board.resource_parent, user) }
-
- it 'returns nil when there is no user' do
- service.current_user = nil
-
- expect(service.execute(group_board)).to eq nil
- end
-
- it 'records the visit' do
- expect(BoardGroupRecentVisit).to receive(:visited!).once
-
- service.execute(group_board)
- end
+ it_behaves_like 'boards recent visit create service'
end
end
end
diff --git a/spec/support/shared_examples/services/boards/boards_recent_visit_shared_examples.rb b/spec/support/shared_examples/services/boards/boards_recent_visit_shared_examples.rb
new file mode 100644
index 00000000000..68ea460dabc
--- /dev/null
+++ b/spec/support/shared_examples/services/boards/boards_recent_visit_shared_examples.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'boards recent visit' do
+ let_it_be(:user) { create(:user) }
+
+ describe '#visited' do
+ it 'creates a visit if one does not exists' do
+ expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1)
+ end
+
+ shared_examples 'was visited previously' do
+ let_it_be(:visit) do
+ create(visit_relation,
+ board_parent_relation => board_parent,
+ board_relation => board,
+ user: user,
+ updated_at: 7.days.ago
+ )
+ end
+
+ it 'updates the timestamp' do
+ freeze_time do
+ described_class.visited!(user, board)
+
+ expect(described_class.count).to eq 1
+ expect(described_class.first.updated_at).to be_like_time(Time.zone.now)
+ end
+ end
+ end
+
+ it_behaves_like 'was visited previously'
+
+ context 'when we try to create a visit that is not unique' do
+ before do
+ expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique')
+ expect(described_class).to receive(:find_or_create_by).and_return(visit)
+ end
+
+ it_behaves_like 'was visited previously'
+ end
+ end
+
+ describe '#latest' do
+ def create_visit(time)
+ create(visit_relation, board_parent_relation => board_parent, user: user, updated_at: time)
+ end
+
+ it 'returns the most recent visited' do
+ create_visit(7.days.ago)
+ create_visit(5.days.ago)
+ recent = create_visit(1.day.ago)
+
+ expect(described_class.latest(user, board_parent)).to eq recent
+ end
+
+ it 'returns last 3 visited boards' do
+ create_visit(7.days.ago)
+ visit1 = create_visit(3.days.ago)
+ visit2 = create_visit(2.days.ago)
+ visit3 = create_visit(5.days.ago)
+
+ expect(described_class.latest(user, board_parent, count: 3)).to eq([visit2, visit1, visit3])
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/boards/create_service_shared_examples.rb b/spec/support/shared_examples/services/boards/create_service_shared_examples.rb
new file mode 100644
index 00000000000..63b5e3a5a84
--- /dev/null
+++ b/spec/support/shared_examples/services/boards/create_service_shared_examples.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'boards recent visit create service' do
+ let_it_be(:user) { create(:user) }
+
+ subject(:service) { described_class.new(board.resource_parent, user) }
+
+ it 'returns nil when there is no user' do
+ service.current_user = nil
+
+ expect(service.execute(board)).to be_nil
+ end
+
+ it 'returns nil when database is read only' do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+
+ expect(service.execute(board)).to be_nil
+ end
+
+ it 'records the visit' do
+ expect(model).to receive(:visited!).once
+
+ service.execute(board)
+ end
+end