diff --git a/app/models/clusters/agents/group_authorization.rb b/app/models/clusters/agents/group_authorization.rb
index 74c0cec3b7e..28a711aaf17 100644
--- a/app/models/clusters/agents/group_authorization.rb
+++ b/app/models/clusters/agents/group_authorization.rb
@@ -10,7 +10,9 @@ module Clusters
validates :config, json_schema: { filename: 'cluster_agent_authorization_configuration' }
- delegate :project, to: :agent
+ def config_project
+ agent.project
+ end
end
end
end
diff --git a/app/models/clusters/agents/implicit_authorization.rb b/app/models/clusters/agents/implicit_authorization.rb
index 967cc686045..9f7f653ed65 100644
--- a/app/models/clusters/agents/implicit_authorization.rb
+++ b/app/models/clusters/agents/implicit_authorization.rb
@@ -6,12 +6,15 @@ module Clusters
attr_reader :agent
delegate :id, to: :agent, prefix: true
- delegate :project, to: :agent
def initialize(agent:)
@agent = agent
end
+ def config_project
+ agent.project
+ end
+
def config
nil
end
diff --git a/app/models/clusters/agents/project_authorization.rb b/app/models/clusters/agents/project_authorization.rb
index 1c71a0a432a..f6d19086751 100644
--- a/app/models/clusters/agents/project_authorization.rb
+++ b/app/models/clusters/agents/project_authorization.rb
@@ -9,6 +9,10 @@ module Clusters
belongs_to :project, class_name: '::Project', optional: false
validates :config, json_schema: { filename: 'cluster_agent_authorization_configuration' }
+
+ def config_project
+ agent.project
+ end
end
end
end
diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb
index 55f56c6277a..a4cc43d1f13 100644
--- a/app/models/users/credit_card_validation.rb
+++ b/app/models/users/credit_card_validation.rb
@@ -12,5 +12,13 @@ module Users
validates :last_digits, allow_nil: true, numericality: {
greater_than_or_equal_to: 0, less_than_or_equal_to: 9999
}
+
+ def similar_records
+ self.class.where(
+ expiration_date: expiration_date,
+ last_digits: last_digits,
+ holder_name: holder_name
+ ).order(credit_card_validated_at: :desc).includes(:user)
+ end
end
end
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index ad8d9d1f04f..2a9b4694e7b 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -61,7 +61,6 @@
= _('Disabled')
= render_if_exists 'admin/namespace_plan_info', namespace: @user.namespace
- = render_if_exists 'admin/users/credit_card_info', user: @user
%li
%span.light= _('External User:')
@@ -139,6 +138,8 @@
= render_if_exists 'namespaces/shared_runner_status', namespace: @user.namespace
+ = render_if_exists 'admin/users/credit_card_info', user: @user, link_to_match_page: true
+
= render 'shared/custom_attributes', custom_attributes: @user.custom_attributes
-# Rendered on desktop only so order of cards can be different on desktop vs mobile
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 8ef53c40b11..3e6acdb130a 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -3,7 +3,7 @@
- breadcrumb_title @snippet.to_reference
- page_title "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
-#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet) } }
+#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet), 'can-report-spam': @snippet.submittable_as_spam_by?(current_user).to_s } }
.row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true, api_awards_path: project_snippets_award_api_path(@snippet)
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index ca52a1f8f46..f1093a3b730 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -12,7 +12,7 @@
- content_for :prefetch_asset_tags do
- webpack_preload_asset_tag('monaco', prefetch: true)
-#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet) } }
+#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet), 'can-report-spam': @snippet.submittable_as_spam_by?(current_user).to_s } }
.row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true
diff --git a/config/feature_flags/development/finding_ci_pipeline_disable_joins.yml b/config/feature_flags/development/finding_ci_pipeline_disable_joins.yml
new file mode 100644
index 00000000000..8987b729cac
--- /dev/null
+++ b/config/feature_flags/development/finding_ci_pipeline_disable_joins.yml
@@ -0,0 +1,8 @@
+---
+name: finding_ci_pipeline_disable_joins
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70216
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338665
+milestone: '14.3'
+type: development
+group: group::threat insights
+default_enabled: true
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index da055d22695..30def6ae80f 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -192,6 +192,20 @@ cannot change them:
This ensures that your job uses the settings you intend and that they are not overridden by
project-level pipelines.
+##### Avoid parent and child pipelines
+
+Compliance pipelines start on the run of _every_ pipeline in a relevant project. This means that if a pipeline in the relevant project
+triggers a child pipeline, the compliance pipeline runs first. This can trigger the parent pipeline, instead of the child pipeline.
+
+Therefore, in projects with compliance frameworks, we recommend replacing
+[parent-child pipelines](../../../ci/pipelines/parent_child_pipelines.md) with the following:
+
+- Direct [`include`](../../../ci/yaml/index.md#include) statements that provide the parent pipeline with child pipeline configuration.
+- Child pipelines placed in another project that are run using the [trigger API](../../../ci/triggers/) rather than the parent-child
+ pipeline feature.
+
+This alternative ensures the compliance pipeline does not re-start the parent pipeline.
+
### Sharing and permissions
For your repository, you can set up features such as public access, repository features,
diff --git a/lib/api/entities/clusters/agent_authorization.rb b/lib/api/entities/clusters/agent_authorization.rb
index 6c533fff105..7bbe0f1ec45 100644
--- a/lib/api/entities/clusters/agent_authorization.rb
+++ b/lib/api/entities/clusters/agent_authorization.rb
@@ -5,7 +5,7 @@ module API
module Clusters
class AgentAuthorization < Grape::Entity
expose :agent_id, as: :id
- expose :project, with: Entities::ProjectIdentity, as: :config_project
+ expose :config_project, with: Entities::ProjectIdentity
expose :config, as: :configuration
end
end
diff --git a/lib/api/entities/user.rb b/lib/api/entities/user.rb
index 5c46233a639..051f8b49031 100644
--- a/lib/api/entities/user.rb
+++ b/lib/api/entities/user.rb
@@ -4,6 +4,7 @@ module API
module Entities
class User < UserBasic
include UsersHelper
+ include TimeZoneHelper
include ActionView::Helpers::SanitizeHelper
expose :created_at, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) }
@@ -24,6 +25,10 @@ module API
expose :bio_html do |user|
strip_tags(user.bio)
end
+
+ expose :local_time do |user|
+ local_time(user.timezone)
+ end
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 1603365d0af..557df5c0ded 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1141,6 +1141,9 @@ msgstr ""
msgid "(revoked)"
msgstr ""
+msgid "(target)"
+msgstr ""
+
msgid "(we need your current password to confirm your changes)"
msgstr ""
@@ -3341,6 +3344,9 @@ msgstr ""
msgid "All users must have a name."
msgstr ""
+msgid "All users with matching cards"
+msgstr ""
+
msgid "Allow \"%{group_name}\" to sign you in"
msgstr ""
@@ -6309,6 +6315,9 @@ msgstr ""
msgid "Capacity threshold"
msgstr ""
+msgid "Card number:"
+msgstr ""
+
msgid "CascadingSettings|Enforce for all subgroups"
msgstr ""
@@ -9924,10 +9933,7 @@ msgstr ""
msgid "CredentialsInventory|SSH Keys"
msgstr ""
-msgid "Credit card validated at:"
-msgstr ""
-
-msgid "Credit card validated:"
+msgid "Credit card:"
msgstr ""
msgid "Critical vulnerabilities present"
@@ -9984,6 +9990,9 @@ msgstr ""
msgid "Current sign-in at:"
msgstr ""
+msgid "Current sign-in ip"
+msgstr ""
+
msgid "Current vulnerabilities count"
msgstr ""
@@ -13806,6 +13815,9 @@ msgstr ""
msgid "Expiration date (optional)"
msgstr ""
+msgid "Expiration date:"
+msgstr ""
+
msgid "Expired"
msgstr ""
@@ -16853,6 +16865,9 @@ msgstr ""
msgid "History of authentications"
msgstr ""
+msgid "Holder name:"
+msgstr ""
+
msgid "Home page URL"
msgstr ""
@@ -22939,6 +22954,9 @@ msgstr ""
msgid "No contributions were found"
msgstr ""
+msgid "No credit card data for matching"
+msgstr ""
+
msgid "No credit card required."
msgstr ""
@@ -31589,6 +31607,9 @@ msgstr ""
msgid "Smartcard authentication failed: client certificate header is missing."
msgstr ""
+msgid "Snippet"
+msgstr ""
+
msgid "Snippets"
msgstr ""
@@ -31613,6 +31634,9 @@ msgstr ""
msgid "SnippetsEmptyState|There are no snippets to show."
msgstr ""
+msgid "Snippets|%{spammable_titlecase} was submitted to Akismet successfully."
+msgstr ""
+
msgid "Snippets|Add another file %{num}/%{total}"
msgstr ""
@@ -31622,6 +31646,9 @@ msgstr ""
msgid "Snippets|Description (optional)"
msgstr ""
+msgid "Snippets|Error with Akismet. Please check the logs for more info."
+msgstr ""
+
msgid "Snippets|Files"
msgstr ""
@@ -36953,6 +36980,9 @@ msgstr ""
msgid "User and IP rate limits"
msgstr ""
+msgid "User created at"
+msgstr ""
+
msgid "User does not have a pending request"
msgstr ""
@@ -37313,6 +37343,15 @@ msgstr ""
msgid "Validate your GitLab CI configuration file"
msgstr ""
+msgid "Validated at"
+msgstr ""
+
+msgid "Validated at:"
+msgstr ""
+
+msgid "Validated:"
+msgstr ""
+
msgid "Validations failed."
msgstr ""
@@ -40851,6 +40890,9 @@ msgstr ""
msgid "originating vulnerability"
msgstr ""
+msgid "other card matches"
+msgstr ""
+
msgid "out of %d total test"
msgid_plural "out of %d total tests"
msgstr[0] ""
diff --git a/scripts/rspec_helpers.sh b/scripts/rspec_helpers.sh
index 280a1586de3..455aaa37692 100644
--- a/scripts/rspec_helpers.sh
+++ b/scripts/rspec_helpers.sh
@@ -98,7 +98,7 @@ function rspec_simple_job() {
}
function rspec_db_library_code() {
- local db_files="spec/lib/gitlab/database/ spec/support/helpers/database/"
+ local db_files="spec/lib/gitlab/database/"
rspec_simple_job "-- ${db_files}"
}
diff --git a/spec/frontend/cycle_analytics/utils_spec.js b/spec/frontend/cycle_analytics/utils_spec.js
index 69fed879fd8..74d64cd8d71 100644
--- a/spec/frontend/cycle_analytics/utils_spec.js
+++ b/spec/frontend/cycle_analytics/utils_spec.js
@@ -1,7 +1,6 @@
import { useFakeDate } from 'helpers/fake_date';
import {
transformStagesForPathNavigation,
- timeSummaryForPathNavigation,
medianTimeToParsedSeconds,
formatMedianValues,
filterStagesByHiddenStatus,
@@ -47,21 +46,6 @@ describe('Value stream analytics utils', () => {
});
});
- describe('timeSummaryForPathNavigation', () => {
- it.each`
- unit | value | result
- ${'months'} | ${1.5} | ${'1.5M'}
- ${'weeks'} | ${1.25} | ${'1.5w'}
- ${'days'} | ${2} | ${'2d'}
- ${'hours'} | ${10} | ${'10h'}
- ${'minutes'} | ${20} | ${'20m'}
- ${'seconds'} | ${10} | ${'<1m'}
- ${'seconds'} | ${0} | ${'-'}
- `('will format $value $unit to $result', ({ unit, value, result }) => {
- expect(timeSummaryForPathNavigation({ [unit]: value })).toBe(result);
- });
- });
-
describe('medianTimeToParsedSeconds', () => {
it.each`
value | result
diff --git a/spec/frontend/lib/utils/datetime/date_format_utility_spec.js b/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
index 942ba56196e..1adc70450e8 100644
--- a/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
@@ -118,3 +118,18 @@ describe('date_format_utility.js', () => {
});
});
});
+
+describe('formatTimeAsSummary', () => {
+ it.each`
+ unit | value | result
+ ${'months'} | ${1.5} | ${'1.5M'}
+ ${'weeks'} | ${1.25} | ${'1.5w'}
+ ${'days'} | ${2} | ${'2d'}
+ ${'hours'} | ${10} | ${'10h'}
+ ${'minutes'} | ${20} | ${'20m'}
+ ${'seconds'} | ${10} | ${'<1m'}
+ ${'seconds'} | ${0} | ${'-'}
+ `('will format $value $unit to $result', ({ unit, value, result }) => {
+ expect(utils.formatTimeAsSummary({ [unit]: value })).toBe(result);
+ });
+});
diff --git a/spec/frontend/snippets/components/show_spec.js b/spec/frontend/snippets/components/show_spec.js
index b7b638b5137..af61f4ea54f 100644
--- a/spec/frontend/snippets/components/show_spec.js
+++ b/spec/frontend/snippets/components/show_spec.js
@@ -41,19 +41,23 @@ describe('Snippet view app', () => {
},
});
}
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findEmbedDropdown = () => wrapper.findComponent(EmbedDropdown);
+
afterEach(() => {
wrapper.destroy();
});
it('renders loader while the query is in flight', () => {
createComponent({ loading: true });
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(true);
});
- it('renders all simple components after the query is finished', () => {
+ it('renders all simple components required after the query is finished', () => {
createComponent();
- expect(wrapper.find(SnippetHeader).exists()).toBe(true);
- expect(wrapper.find(SnippetTitle).exists()).toBe(true);
+ expect(wrapper.findComponent(SnippetHeader).exists()).toBe(true);
+ expect(wrapper.findComponent(SnippetTitle).exists()).toBe(true);
});
it('renders embed dropdown component if visibility allows', () => {
@@ -65,7 +69,7 @@ describe('Snippet view app', () => {
},
},
});
- expect(wrapper.find(EmbedDropdown).exists()).toBe(true);
+ expect(findEmbedDropdown().exists()).toBe(true);
});
it('renders correct snippet-blob components', () => {
@@ -98,7 +102,7 @@ describe('Snippet view app', () => {
},
},
});
- expect(wrapper.find(EmbedDropdown).exists()).toBe(isRendered);
+ expect(findEmbedDropdown().exists()).toBe(isRendered);
});
});
@@ -120,7 +124,7 @@ describe('Snippet view app', () => {
},
},
});
- expect(wrapper.find(CloneDropdownButton).exists()).toBe(isRendered);
+ expect(wrapper.findComponent(CloneDropdownButton).exists()).toBe(isRendered);
},
);
});
diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js
index fb95be3a77c..552a1c6fcde 100644
--- a/spec/frontend/snippets/components/snippet_header_spec.js
+++ b/spec/frontend/snippets/components/snippet_header_spec.js
@@ -1,23 +1,30 @@
import { GlButton, GlModal, GlDropdown } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { ApolloMutation } from 'vue-apollo';
+import MockAdapter from 'axios-mock-adapter';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
-import SnippetHeader from '~/snippets/components/snippet_header.vue';
+import SnippetHeader, { i18n } from '~/snippets/components/snippet_header.vue';
import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.graphql';
+import axios from '~/lib/utils/axios_utils';
+import createFlash, { FLASH_TYPES } from '~/flash';
+
+jest.mock('~/flash');
describe('Snippet header component', () => {
let wrapper;
let snippet;
let mutationTypes;
let mutationVariables;
+ let mock;
let errorMsg;
let err;
const originalRelativeUrlRoot = gon.relative_url_root;
const reportAbusePath = '/-/snippets/42/mark_as_spam';
+ const canReportSpam = true;
const GlEmoji = { template: '
' };
@@ -47,6 +54,7 @@ describe('Snippet header component', () => {
mocks: { $apollo },
provide: {
reportAbusePath,
+ canReportSpam,
...provide,
},
propsData: {
@@ -118,10 +126,13 @@ describe('Snippet header component', () => {
RESOLVE: jest.fn(() => Promise.resolve({ data: { destroySnippet: { errors: [] } } })),
REJECT: jest.fn(() => Promise.reject(err)),
};
+
+ mock = new MockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
+ mock.restore();
gon.relative_url_root = originalRelativeUrlRoot;
});
@@ -186,7 +197,6 @@ describe('Snippet header component', () => {
{
category: 'primary',
disabled: false,
- href: reportAbusePath,
text: 'Submit as spam',
variant: 'default',
},
@@ -205,7 +215,6 @@ describe('Snippet header component', () => {
text: 'Delete',
},
{
- href: reportAbusePath,
text: 'Submit as spam',
title: 'Submit as spam',
},
@@ -249,6 +258,31 @@ describe('Snippet header component', () => {
);
});
+ describe('submit snippet as spam', () => {
+ beforeEach(async () => {
+ createComponent();
+ });
+
+ it.each`
+ request | variant | text
+ ${200} | ${'SUCCESS'} | ${i18n.snippetSpamSuccess}
+ ${500} | ${'DANGER'} | ${i18n.snippetSpamFailure}
+ `(
+ 'renders a "$variant" flash message with "$text" message for a request with a "$request" response',
+ async ({ request, variant, text }) => {
+ const submitAsSpamBtn = findButtons().at(2);
+ mock.onPost(reportAbusePath).reply(request);
+ submitAsSpamBtn.trigger('click');
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenLastCalledWith({
+ message: expect.stringContaining(text),
+ type: FLASH_TYPES[variant],
+ });
+ },
+ );
+ });
+
describe('with guest user', () => {
beforeEach(() => {
createComponent({
@@ -258,6 +292,7 @@ describe('Snippet header component', () => {
},
provide: {
reportAbusePath: null,
+ canReportSpam: false,
},
});
});
diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
index 926223e0670..09633daf587 100644
--- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
+++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
@@ -9,6 +9,7 @@ const DEFAULT_PROPS = {
username: 'root',
name: 'Administrator',
location: 'Vienna',
+ localTime: '2:30 PM',
bot: false,
bio: null,
workInformation: null,
@@ -31,10 +32,11 @@ describe('User Popover Component', () => {
wrapper.destroy();
});
- const findUserStatus = () => wrapper.find('.js-user-status');
+ const findUserStatus = () => wrapper.findByTestId('user-popover-status');
const findTarget = () => document.querySelector('.js-user-link');
const findUserName = () => wrapper.find(UserNameWithStatus);
const findSecurityBotDocsLink = () => wrapper.findByTestId('user-popover-bot-docs-link');
+ const findUserLocalTime = () => wrapper.findByTestId('user-popover-local-time');
const createWrapper = (props = {}, options = {}) => {
wrapper = mountExtended(UserPopover, {
@@ -71,7 +73,6 @@ describe('User Popover Component', () => {
expect(wrapper.text()).toContain(DEFAULT_PROPS.user.name);
expect(wrapper.text()).toContain(DEFAULT_PROPS.user.username);
- expect(wrapper.text()).toContain(DEFAULT_PROPS.user.location);
});
it('shows icon for location', () => {
@@ -164,6 +165,25 @@ describe('User Popover Component', () => {
});
});
+ describe('local time', () => {
+ it('should show local time when it is available', () => {
+ createWrapper();
+
+ expect(findUserLocalTime().exists()).toBe(true);
+ });
+
+ it('should not show local time when it is not available', () => {
+ const user = {
+ ...DEFAULT_PROPS.user,
+ localTime: null,
+ };
+
+ createWrapper({ user });
+
+ expect(findUserLocalTime().exists()).toBe(false);
+ });
+ });
+
describe('status data', () => {
it('should show only message', () => {
const user = { ...DEFAULT_PROPS.user, status: { message_html: 'Hello World' } };
@@ -256,5 +276,11 @@ describe('User Popover Component', () => {
const securityBotDocsLink = findSecurityBotDocsLink();
expect(securityBotDocsLink.text()).toBe('Learn more about %<>\';"');
});
+
+ it('does not display local time', () => {
+ createWrapper({ user: SECURITY_BOT_USER });
+
+ expect(findUserLocalTime().exists()).toBe(false);
+ });
});
});
diff --git a/spec/lib/api/entities/clusters/agent_authorization_spec.rb b/spec/lib/api/entities/clusters/agent_authorization_spec.rb
index 101a8af4ac4..3a1deb43bf8 100644
--- a/spec/lib/api/entities/clusters/agent_authorization_spec.rb
+++ b/spec/lib/api/entities/clusters/agent_authorization_spec.rb
@@ -3,15 +3,34 @@
require 'spec_helper'
RSpec.describe API::Entities::Clusters::AgentAuthorization do
- let_it_be(:authorization) { create(:agent_group_authorization) }
-
subject { described_class.new(authorization).as_json }
- it 'includes basic fields' do
- expect(subject).to include(
- id: authorization.agent_id,
- config_project: a_hash_including(id: authorization.agent.project_id),
- configuration: authorization.config
- )
+ shared_examples 'generic authorization' do
+ it 'includes shared fields' do
+ expect(subject).to include(
+ id: authorization.agent_id,
+ config_project: a_hash_including(id: authorization.agent.project_id),
+ configuration: authorization.config
+ )
+ end
+ end
+
+ context 'project authorization' do
+ let(:authorization) { create(:agent_project_authorization) }
+
+ include_examples 'generic authorization'
+ end
+
+ context 'group authorization' do
+ let(:authorization) { create(:agent_group_authorization) }
+
+ include_examples 'generic authorization'
+ end
+
+ context 'implicit authorization' do
+ let(:agent) { create(:cluster_agent) }
+ let(:authorization) { Clusters::Agents::ImplicitAuthorization.new(agent: agent) }
+
+ include_examples 'generic authorization'
end
end
diff --git a/spec/lib/api/entities/user_spec.rb b/spec/lib/api/entities/user_spec.rb
index 860f007f284..9c9a157d68a 100644
--- a/spec/lib/api/entities/user_spec.rb
+++ b/spec/lib/api/entities/user_spec.rb
@@ -3,10 +3,13 @@
require 'spec_helper'
RSpec.describe API::Entities::User do
- let(:user) { create(:user) }
- let(:current_user) { create(:user) }
+ let_it_be(:timezone) { 'America/Los_Angeles' }
- subject { described_class.new(user, current_user: current_user).as_json }
+ let(:user) { create(:user, timezone: timezone) }
+ let(:current_user) { create(:user) }
+ let(:entity) { described_class.new(user, current_user: current_user) }
+
+ subject { entity.as_json }
it 'exposes correct attributes' do
expect(subject).to include(:bio, :location, :public_email, :skype, :linkedin, :twitter, :website_url, :organization, :job_title, :work_information, :pronouns)
@@ -35,4 +38,10 @@ RSpec.describe API::Entities::User do
expect(subject[:bot]).to eq(true)
end
end
+
+ it 'exposes local_time' do
+ local_time = '2:30 PM'
+ expect(entity).to receive(:local_time).with(timezone).and_return(local_time)
+ expect(subject[:local_time]).to eq(local_time)
+ end
end
diff --git a/spec/models/clusters/agents/group_authorization_spec.rb b/spec/models/clusters/agents/group_authorization_spec.rb
index 2a99fb26e3f..baeb8f5464e 100644
--- a/spec/models/clusters/agents/group_authorization_spec.rb
+++ b/spec/models/clusters/agents/group_authorization_spec.rb
@@ -7,4 +7,10 @@ RSpec.describe Clusters::Agents::GroupAuthorization do
it { is_expected.to belong_to(:group).class_name('::Group').required }
it { expect(described_class).to validate_jsonb_schema(['config']) }
+
+ describe '#config_project' do
+ let(:record) { create(:agent_group_authorization) }
+
+ it { expect(record.config_project).to eq(record.agent.project) }
+ end
end
diff --git a/spec/models/clusters/agents/implicit_authorization_spec.rb b/spec/models/clusters/agents/implicit_authorization_spec.rb
index 69aa55a350e..2d6c3ddb426 100644
--- a/spec/models/clusters/agents/implicit_authorization_spec.rb
+++ b/spec/models/clusters/agents/implicit_authorization_spec.rb
@@ -9,6 +9,6 @@ RSpec.describe Clusters::Agents::ImplicitAuthorization do
it { expect(subject.agent).to eq(agent) }
it { expect(subject.agent_id).to eq(agent.id) }
- it { expect(subject.project).to eq(agent.project) }
+ it { expect(subject.config_project).to eq(agent.project) }
it { expect(subject.config).to be_nil }
end
diff --git a/spec/models/clusters/agents/project_authorization_spec.rb b/spec/models/clusters/agents/project_authorization_spec.rb
index 134c70739ac..9ba259356c7 100644
--- a/spec/models/clusters/agents/project_authorization_spec.rb
+++ b/spec/models/clusters/agents/project_authorization_spec.rb
@@ -7,4 +7,10 @@ RSpec.describe Clusters::Agents::ProjectAuthorization do
it { is_expected.to belong_to(:project).class_name('Project').required }
it { expect(described_class).to validate_jsonb_schema(['config']) }
+
+ describe '#config_project' do
+ let(:record) { create(:agent_project_authorization) }
+
+ it { expect(record.config_project).to eq(record.agent.project) }
+ end
end
diff --git a/spec/models/users/credit_card_validation_spec.rb b/spec/models/users/credit_card_validation_spec.rb
index 667649bd5ed..d2b4f5ebd65 100644
--- a/spec/models/users/credit_card_validation_spec.rb
+++ b/spec/models/users/credit_card_validation_spec.rb
@@ -7,4 +7,19 @@ RSpec.describe Users::CreditCardValidation do
it { is_expected.to validate_length_of(:holder_name).is_at_most(26) }
it { is_expected.to validate_numericality_of(:last_digits).is_less_than_or_equal_to(9999) }
+
+ describe '.similar_records' do
+ let(:card_details) { subject.attributes.slice(:expiration_date, :last_digits, :holder_name) }
+
+ subject(:credit_card_validation) { create(:credit_card_validation) }
+
+ let!(:match1) { create(:credit_card_validation, card_details) }
+ let!(:other1) { create(:credit_card_validation, card_details.merge(last_digits: 9)) }
+ let!(:match2) { create(:credit_card_validation, card_details) }
+ let!(:other2) { create(:credit_card_validation, card_details.merge(holder_name: 'foo bar')) }
+
+ it 'returns records with matching credit card, ordered by credit_card_validated_at' do
+ expect(subject.similar_records).to eq([match2, match1, subject])
+ end
+ end
end