Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-12-06 09:10:26 +00:00
parent fde9b7a784
commit 1b9b475faa
24 changed files with 372 additions and 151 deletions

View File

@ -124,6 +124,9 @@ export default {
isLoading() {
return this.$apollo.queries.issuable.loading || this.loading;
},
initialLoading() {
return this.$apollo.queries.issuable.loading;
},
hasDate() {
return this.dateValue !== null;
},
@ -271,10 +274,10 @@ export default {
<span class="collapse-truncated-title">{{ formattedDate }}</span>
</div>
<sidebar-inherit-date
v-if="canInherit"
v-if="canInherit && !initialLoading"
:issuable="issuable"
:is-loading="isLoading"
:date-type="dateType"
:is-loading="isLoading"
@reset-date="setDate(null)"
@set-date="setFixedDate"
/>

View File

@ -17,8 +17,9 @@ export default {
type: Object,
},
isLoading: {
required: true,
required: false,
type: Boolean,
default: false,
},
dateType: {
type: String,
@ -31,6 +32,7 @@ export default {
return this.issuable?.[dateFields[this.dateType].isDateFixed] || false;
},
set(fixed) {
if (fixed === this.issuable[dateFields[this.dateType].isDateFixed]) return;
this.$emit('set-date', fixed);
},
},

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
module Resolvers
module Ci
# NOTE: This class was introduced to allow modifying the meaning of certain values in RunnerStatusEnum
# while preserving backward compatibility. It can be removed in 15.0 once the API has stabilized.
class RunnerStatusResolver < BaseResolver
type Types::Ci::RunnerStatusEnum, null: false
alias_method :runner, :object
argument :legacy_mode,
type: GraphQL::Types::String,
default_value: '14.5',
required: false,
description: 'Compatibility mode. A null value turns off compatibility mode.',
deprecated: { reason: 'Will be removed in 15.0. From that release onward, the field will behave as if legacyMode is null', milestone: '14.6' }
def resolve(legacy_mode:, **args)
runner.status(legacy_mode)
end
end
end
end

View File

@ -5,24 +5,33 @@ module Types
class RunnerStatusEnum < BaseEnum
graphql_name 'CiRunnerStatus'
::Ci::Runner::AVAILABLE_STATUSES.each do |status|
description = case status
when 'active'
"A runner that is not paused."
when 'online'
"A runner that contacted this instance within the last #{::Ci::Runner::ONLINE_CONTACT_TIMEOUT.inspect}."
when 'offline'
"A runner that has not contacted this instance within the last #{::Ci::Runner::ONLINE_CONTACT_TIMEOUT.inspect}."
when 'not_connected'
"A runner that has never contacted this instance."
else
"A runner that is #{status.to_s.tr('_', ' ')}."
end
value 'ACTIVE',
description: 'Runner that is not paused.',
deprecated: { reason: 'Use CiRunnerType.active instead', milestone: '14.6' },
value: :active
value status.to_s.upcase,
description: description,
value: status.to_sym
end
value 'PAUSED',
description: 'Runner that is paused.',
deprecated: { reason: 'Use CiRunnerType.active instead', milestone: '14.6' },
value: :paused
value 'ONLINE',
description: "Runner that contacted this instance within the last #{::Ci::Runner::ONLINE_CONTACT_TIMEOUT.inspect}.",
value: :online
value 'OFFLINE',
description: "Runner that has not contacted this instance within the last #{::Ci::Runner::ONLINE_CONTACT_TIMEOUT.inspect}.",
deprecated: { reason: 'This field will have a slightly different scope starting in 15.0, with STALE being returned after a certain period offline', milestone: '14.6' },
value: :offline
value 'STALE',
description: "Runner that has not contacted this instance within the last #{::Ci::Runner::STALE_TIMEOUT.inspect}. Only available if legacyMode is null. Will be a possible return value starting in 15.0",
value: :stale
value 'NOT_CONNECTED',
description: 'Runner that has never contacted this instance.',
deprecated: { reason: 'This field will have a slightly different scope starting in 15.0, with STALE being returned after a certain period of no contact', milestone: '14.6' },
value: :not_connected
end
end
end

View File

@ -27,8 +27,11 @@ module Types
description: 'Access level of the runner.'
field :active, GraphQL::Types::Boolean, null: false,
description: 'Indicates the runner is allowed to receive jobs.'
field :status, ::Types::Ci::RunnerStatusEnum, null: false,
description: 'Status of the runner.'
field :status,
Types::Ci::RunnerStatusEnum,
null: false,
description: 'Status of the runner.',
resolver: ::Resolvers::Ci::RunnerStatusResolver
field :version, GraphQL::Types::String, null: true,
description: 'Version of the runner.'
field :short_sha, GraphQL::Types::String, null: true,
@ -50,7 +53,7 @@ module Types
field :job_count, GraphQL::Types::Int, null: true,
description: "Number of jobs processed by the runner (limited to #{JOB_COUNT_LIMIT}, plus one to indicate that more items exist)."
field :admin_url, GraphQL::Types::String, null: true,
description: 'Admin URL of the runner. Only available for adminstrators.'
description: 'Admin URL of the runner. Only available for administrators.'
def job_count
# We limit to 1 above the JOB_COUNT_LIMIT to indicate that more items exist after JOB_COUNT_LIMIT

View File

@ -61,6 +61,11 @@ module ProfilesHelper
def ssh_key_expires_field_description
s_('Profiles|Key can still be used after expiration.')
end
# Overridden in EE::ProfilesHelper#ssh_key_expiration_policy_enabled?
def ssh_key_expiration_policy_enabled?
false
end
end
ProfilesHelper.prepend_mod

View File

@ -44,7 +44,7 @@ module Ci
AVAILABLE_TYPES_LEGACY = %w[specific shared].freeze
AVAILABLE_TYPES = runner_types.keys.freeze
AVAILABLE_STATUSES = %w[active paused online offline not_connected].freeze
AVAILABLE_STATUSES = %w[active paused online offline not_connected stale].freeze
AVAILABLE_SCOPES = (AVAILABLE_TYPES_LEGACY + AVAILABLE_TYPES + AVAILABLE_STATUSES).freeze
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
@ -287,10 +287,15 @@ module Ci
end
def stale?
return false unless created_at
[created_at, contacted_at].compact.max < self.class.stale_deadline
end
def status
def status(legacy_mode = nil)
return deprecated_rest_status if legacy_mode == '14.5'
return :stale if stale?
return :not_connected unless contacted_at
online? ? :online : :offline

View File

@ -32,6 +32,7 @@
= render_if_exists 'admin/application_settings/git_two_factor_session_expiry', form: f
= render_if_exists 'admin/application_settings/personal_access_token_expiration_policy', form: f
= render_if_exists 'admin/application_settings/enforce_pat_expiration', form: f
= render_if_exists 'admin/application_settings/ssh_key_expiration_policy', form: f
= render_if_exists 'admin/application_settings/enforce_ssh_key_expiration', form: f
.form-group

View File

@ -1,3 +1,4 @@
- max_date = ::Gitlab::CurrentSettings.max_ssh_key_lifetime_from_now.to_date if ssh_key_expiration_policy_enabled?
%div
= form_for [:profile, @key], html: { class: 'js-requires-input' } do |f|
= form_errors(@key)
@ -13,8 +14,8 @@
%p.form-text.text-muted= s_('Profiles|Give your individual key a title. This will be publicly visible.')
.col.form-group
= f.label :expires_at, s_('Profiles|Expires at'), class: 'label-bold'
= f.date_field :expires_at, class: "form-control input-lg", min: Date.tomorrow, data: { qa_selector: 'key_expiry_date_field' }
= f.label :expires_at, s_('Profiles|Expiration date'), class: 'label-bold'
= f.date_field :expires_at, class: "form-control input-lg", min: Date.tomorrow, max: max_date, data: { qa_selector: 'key_expiry_date_field' }
%p.form-text.text-muted{ data: { qa_selector: 'key_expiry_date_field_description' } }= ssh_key_expires_field_description
.js-add-ssh-key-validation-warning.hide

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddMaxSshKeyLifetimeToApplicationSettings < Gitlab::Database::Migration[1.0]
def change
add_column :application_settings, :max_ssh_key_lifetime, :integer
end
end

View File

@ -0,0 +1 @@
7686fd3e33b25b811aba459aba514cde8e88102277edb3be7e12378cb7e8de85

View File

@ -10477,6 +10477,7 @@ CREATE TABLE application_settings (
sentry_dsn text,
sentry_clientside_dsn text,
sentry_environment text,
max_ssh_key_lifetime integer,
static_objects_external_storage_auth_token_encrypted text,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)),

View File

@ -8750,7 +8750,7 @@ Represents the total number of issues and their weights for a particular day.
| ---- | ---- | ----------- |
| <a id="cirunneraccesslevel"></a>`accessLevel` | [`CiRunnerAccessLevel!`](#cirunneraccesslevel) | Access level of the runner. |
| <a id="cirunneractive"></a>`active` | [`Boolean!`](#boolean) | Indicates the runner is allowed to receive jobs. |
| <a id="cirunneradminurl"></a>`adminUrl` | [`String`](#string) | Admin URL of the runner. Only available for adminstrators. |
| <a id="cirunneradminurl"></a>`adminUrl` | [`String`](#string) | Admin URL of the runner. Only available for administrators. |
| <a id="cirunnercontactedat"></a>`contactedAt` | [`Time`](#time) | Last contact from the runner. |
| <a id="cirunnerdescription"></a>`description` | [`String`](#string) | Description of the runner. |
| <a id="cirunnerid"></a>`id` | [`CiRunnerID!`](#cirunnerid) | ID of the runner. |
@ -8765,11 +8765,24 @@ Represents the total number of issues and their weights for a particular day.
| <a id="cirunnerrununtagged"></a>`runUntagged` | [`Boolean!`](#boolean) | Indicates the runner is able to run untagged jobs. |
| <a id="cirunnerrunnertype"></a>`runnerType` | [`CiRunnerType!`](#cirunnertype) | Type of the runner. |
| <a id="cirunnershortsha"></a>`shortSha` | [`String`](#string) | First eight characters of the runner's token used to authenticate new job requests. Used as the runner's unique ID. |
| <a id="cirunnerstatus"></a>`status` | [`CiRunnerStatus!`](#cirunnerstatus) | Status of the runner. |
| <a id="cirunnertaglist"></a>`tagList` | [`[String!]`](#string) | Tags associated with the runner. |
| <a id="cirunneruserpermissions"></a>`userPermissions` | [`RunnerPermissions!`](#runnerpermissions) | Permissions for the current user on the resource. |
| <a id="cirunnerversion"></a>`version` | [`String`](#string) | Version of the runner. |
#### Fields with arguments
##### `CiRunner.status`
Status of the runner.
Returns [`CiRunnerStatus!`](#cirunnerstatus).
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="cirunnerstatuslegacymode"></a>`legacyMode` **{warning-solid}** | [`String`](#string) | **Deprecated** in 14.6. Will be removed in 15.0. From that release onward, the field will behave as if legacyMode is null. |
### `CiStage`
#### Fields
@ -15956,11 +15969,12 @@ Values for sorting runners.
| Value | Description |
| ----- | ----------- |
| <a id="cirunnerstatusactive"></a>`ACTIVE` | A runner that is not paused. |
| <a id="cirunnerstatusnot_connected"></a>`NOT_CONNECTED` | A runner that has never contacted this instance. |
| <a id="cirunnerstatusoffline"></a>`OFFLINE` | A runner that has not contacted this instance within the last 2 hours. |
| <a id="cirunnerstatusonline"></a>`ONLINE` | A runner that contacted this instance within the last 2 hours. |
| <a id="cirunnerstatuspaused"></a>`PAUSED` | A runner that is paused. |
| <a id="cirunnerstatusactive"></a>`ACTIVE` **{warning-solid}** | **Deprecated** in 14.6. Use CiRunnerType.active instead. |
| <a id="cirunnerstatusnot_connected"></a>`NOT_CONNECTED` **{warning-solid}** | **Deprecated** in 14.6. This field will have a slightly different scope starting in 15.0, with STALE being returned after a certain period of no contact. |
| <a id="cirunnerstatusoffline"></a>`OFFLINE` **{warning-solid}** | **Deprecated** in 14.6. This field will have a slightly different scope starting in 15.0, with STALE being returned after a certain period offline. |
| <a id="cirunnerstatusonline"></a>`ONLINE` | Runner that contacted this instance within the last 2 hours. |
| <a id="cirunnerstatuspaused"></a>`PAUSED` **{warning-solid}** | **Deprecated** in 14.6. Use CiRunnerType.active instead. |
| <a id="cirunnerstatusstale"></a>`STALE` | Runner that has not contacted this instance within the last 3 months. Only available if legacyMode is null. Will be a possible return value starting in 15.0. |
### `CiRunnerType`

View File

@ -10,9 +10,11 @@ type: reference, api
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10242) in GitLab 12.6.
WARNING:
This API is in an alpha stage and considered unstable.
This API is in the process of being deprecated and considered unstable.
The response payload may be subject to change or breakage
across GitLab releases.
across GitLab releases. Please use the
[GraphQL API](graphql/reference/index.md#queryvulnerabilities)
instead.
Every API call to vulnerabilities must be [authenticated](index.md#authentication).

View File

@ -343,6 +343,7 @@ listed in the descriptions of the relevant settings.
| `max_import_size` | integer | no | Maximum import size in MB. 0 for unlimited. Default = 0 (unlimited) [Modified](https://gitlab.com/gitlab-org/gitlab/-/issues/251106) from 50MB to 0 in GitLab 13.8. |
| `max_pages_size` | integer | no | Maximum size of pages repositories in MB. |
| `max_personal_access_token_lifetime` | integer | no | **(ULTIMATE SELF)** Maximum allowable lifetime for personal access tokens in days. |
| `max_ssh_key_lifetime` | integer | no | **(ULTIMATE SELF)** Maximum allowable lifetime for SSH keys in days. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1007) in GitLab 14.6. |
| `metrics_method_call_threshold` | integer | no | A method call is only tracked when it takes longer than the given amount of milliseconds. |
| `mirror_available` | boolean | no | Allow repository mirroring to configured by project Maintainers. If disabled, only Administrators can configure repository mirroring. |
| `mirror_capacity_threshold` | integer | no | **(PREMIUM)** Minimum capacity to be available before scheduling more mirrors preemptively. |

View File

@ -192,6 +192,62 @@ To set a limit on how long these sessions are valid:
1. Fill in the **Session duration for Git operations when 2FA is enabled (minutes)** field.
1. Click **Save changes**.
## Limit the lifetime of SSH keys **(ULTIMATE SELF)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1007) in GitLab 14.6 [with a flag](../../../administration/feature_flags.md) named `ff_limit_ssh_key_lifetime`. Disabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available,
ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `ff_limit_ssh_key_lifetime`.
On GitLab.com, this feature is not available. The feature is not ready for production use.
Users can optionally specify a lifetime for
[SSH keys](../../../ssh/index.md).
This lifetime is not a requirement, and can be set to any arbitrary number of days.
SSH keys are user credentials to access GitLab.
However, organizations with security requirements may want to enforce more protection by
requiring the regular rotation of these keys.
### Set a lifetime
Only a GitLab administrator can set a lifetime. Leaving it empty means
there are no restrictions.
To set a lifetime on how long SSH keys are valid:
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Settings > General**.
1. Expand the **Account and limit** section.
1. Fill in the **Maximum allowable lifetime for SSH keys (days)** field.
1. Click **Save changes**.
Once a lifetime for SSH keys is set, GitLab:
- Requires users to set an expiration date that is no later than the allowed lifetime on new
SSH keys.
- Applies the lifetime restriction to existing SSH keys. Keys with no expiry or a lifetime
greater than the maximum immediately become invalid.
NOTE:
When a user's SSH key becomes invalid they can delete and re-add the same key again.
## Allow expired SSH keys to be used **(ULTIMATE SELF)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/250480) in GitLab 13.9.
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/320970) in GitLab 14.0.
By default, expired SSH keys **are not usable**.
To allow the use of expired SSH keys:
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Settings > General**.
1. Expand the **Account and limit** section.
1. Uncheck the **Enforce SSH key expiration** checkbox.
Disabling SSH key expiration immediately enables all expired SSH keys.
## Limit the lifetime of personal access tokens **(ULTIMATE SELF)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3649) in GitLab 12.6.
@ -225,22 +281,6 @@ Once a lifetime for personal access tokens is set, GitLab:
allowed lifetime. Three hours is given to allow administrators to change the allowed lifetime,
or remove it, before revocation takes place.
## Allow expired SSH keys to be used **(ULTIMATE SELF)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/250480) in GitLab 13.9.
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/320970) in GitLab 14.0.
By default, expired SSH keys **are not usable**.
To allow the use of expired SSH keys:
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Settings > General**.
1. Expand the **Account and limit** section.
1. Uncheck the **Enforce SSH key expiration** checkbox.
Disabling SSH key expiration immediately enables all expired SSH keys.
## Allow expired Personal Access Tokens to be used **(ULTIMATE SELF)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214723) in GitLab 13.1.

View File

@ -21496,6 +21496,9 @@ msgstr ""
msgid "Maximum allowable lifetime for personal access token (days)"
msgstr ""
msgid "Maximum allowed lifetime for SSH keys (in days)"
msgstr ""
msgid "Maximum artifacts size"
msgstr ""
@ -26705,15 +26708,15 @@ msgstr ""
msgid "Profiles|Enter your pronouns to let people know how to refer to you"
msgstr ""
msgid "Profiles|Expiration date"
msgstr ""
msgid "Profiles|Expired key is not valid."
msgstr ""
msgid "Profiles|Expired:"
msgstr ""
msgid "Profiles|Expires at"
msgstr ""
msgid "Profiles|Expires:"
msgstr ""
@ -26753,15 +26756,18 @@ msgstr ""
msgid "Profiles|Key"
msgstr ""
msgid "Profiles|Key becomes invalid on this date."
msgstr ""
msgid "Profiles|Key becomes invalid on this date. Maximum lifetime for SSH keys is %{max_ssh_key_lifetime} days"
msgstr ""
msgid "Profiles|Key can still be used after expiration."
msgstr ""
msgid "Profiles|Key usable beyond expiration date."
msgstr ""
msgid "Profiles|Key will be deleted on this date."
msgstr ""
msgid "Profiles|Last used:"
msgstr ""
@ -39221,6 +39227,9 @@ msgstr ""
msgid "When a runner is locked, it cannot be assigned to other projects"
msgstr ""
msgid "When enabled, SSH keys with no expiry date or an invalid expiration date are no longer accepted. Leave blank for no limit."
msgstr ""
msgid "When enabled, existing personal access tokens may be revoked. Leave blank for no limit."
msgstr ""

View File

@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import { mapValues } from 'lodash';
import App from '~/google_cloud/components/app.vue';
import Home from '~/google_cloud/components/home.vue';
import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
@ -8,103 +9,59 @@ import NoGcpProjects from '~/google_cloud/components/errors/no_gcp_projects.vue'
const BASE_FEEDBACK_URL =
'https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/meta/-/issues/new';
const SCREEN_COMPONENTS = {
Home,
ServiceAccountsForm,
GcpError,
NoGcpProjects,
};
const SERVICE_ACCOUNTS_FORM_PROPS = {
gcpProjects: [1, 2, 3],
environments: [4, 5, 6],
cancelPath: '',
};
const HOME_PROPS = {
serviceAccounts: [{}, {}],
createServiceAccountUrl: '#url-create-service-account',
emptyIllustrationUrl: '#url-empty-illustration',
};
describe('google_cloud App component', () => {
let wrapper;
const findIncubationBanner = () => wrapper.findComponent(IncubationBanner);
const findGcpError = () => wrapper.findComponent(GcpError);
const findNoGcpProjects = () => wrapper.findComponent(NoGcpProjects);
const findServiceAccountsForm = () => wrapper.findComponent(ServiceAccountsForm);
const findHome = () => wrapper.findComponent(Home);
afterEach(() => {
wrapper.destroy();
});
describe('for gcp_error screen', () => {
describe.each`
screen | extraProps | componentName
${'gcp_error'} | ${{ error: 'mock_gcp_client_error' }} | ${'GcpError'}
${'no_gcp_projects'} | ${{}} | ${'NoGcpProjects'}
${'service_accounts_form'} | ${SERVICE_ACCOUNTS_FORM_PROPS} | ${'ServiceAccountsForm'}
${'home'} | ${HOME_PROPS} | ${'Home'}
`('for screen=$screen', ({ screen, extraProps, componentName }) => {
const component = SCREEN_COMPONENTS[componentName];
beforeEach(() => {
const propsData = {
screen: 'gcp_error',
error: 'mock_gcp_client_error',
};
wrapper = shallowMount(App, { propsData });
wrapper = shallowMount(App, { propsData: { screen, ...extraProps } });
});
it('renders the gcp_error screen', () => {
expect(findGcpError().exists()).toBe(true);
});
it(`renders only ${componentName}`, () => {
const existences = mapValues(SCREEN_COMPONENTS, (x) => wrapper.findComponent(x).exists());
it('should contain incubation banner', () => {
expect(findIncubationBanner().props()).toEqual({
shareFeedbackUrl: `${BASE_FEEDBACK_URL}?issuable_template=general_feedback`,
reportBugUrl: `${BASE_FEEDBACK_URL}?issuable_template=report_bug`,
featureRequestUrl: `${BASE_FEEDBACK_URL}?issuable_template=feature_request`,
expect(existences).toEqual({
...mapValues(SCREEN_COMPONENTS, () => false),
[componentName]: true,
});
});
});
describe('for no_gcp_projects screen', () => {
beforeEach(() => {
const propsData = {
screen: 'no_gcp_projects',
};
wrapper = shallowMount(App, { propsData });
it(`renders the ${componentName} with props`, () => {
expect(wrapper.findComponent(component).props()).toEqual(extraProps);
});
it('renders the no_gcp_projects screen', () => {
expect(findNoGcpProjects().exists()).toBe(true);
});
it('should contain incubation banner', () => {
expect(findIncubationBanner().props()).toEqual({
shareFeedbackUrl: `${BASE_FEEDBACK_URL}?issuable_template=general_feedback`,
reportBugUrl: `${BASE_FEEDBACK_URL}?issuable_template=report_bug`,
featureRequestUrl: `${BASE_FEEDBACK_URL}?issuable_template=feature_request`,
});
});
});
describe('for service_accounts_form screen', () => {
beforeEach(() => {
const propsData = {
screen: 'service_accounts_form',
gcpProjects: [1, 2, 3],
environments: [4, 5, 6],
cancelPath: '',
};
wrapper = shallowMount(App, { propsData });
});
it('renders the service_accounts_form screen', () => {
expect(findServiceAccountsForm().exists()).toBe(true);
});
it('should contain incubation banner', () => {
expect(findIncubationBanner().props()).toEqual({
shareFeedbackUrl: `${BASE_FEEDBACK_URL}?issuable_template=general_feedback`,
reportBugUrl: `${BASE_FEEDBACK_URL}?issuable_template=report_bug`,
featureRequestUrl: `${BASE_FEEDBACK_URL}?issuable_template=feature_request`,
});
});
});
describe('for home screen', () => {
beforeEach(() => {
const propsData = {
screen: 'home',
serviceAccounts: [{}, {}],
createServiceAccountUrl: '#url-create-service-account',
emptyIllustrationUrl: '#url-empty-illustration',
};
wrapper = shallowMount(App, { propsData });
});
it('renders the home screen', () => {
expect(findHome().exists()).toBe(true);
});
it('should contain incubation banner', () => {
it('renders incubation banner', () => {
expect(findIncubationBanner().props()).toEqual({
shareFeedbackUrl: `${BASE_FEEDBACK_URL}?issuable_template=general_feedback`,
reportBugUrl: `${BASE_FEEDBACK_URL}?issuable_template=report_bug`,

View File

@ -145,13 +145,20 @@ describe('Sidebar date Widget', () => {
${false} | ${SidebarInheritDate} | ${'SidebarInheritDate'} | ${false}
`(
'when canInherit is $canInherit, $componentName display is $expected',
({ canInherit, component, expected }) => {
async ({ canInherit, component, expected }) => {
createComponent({ canInherit });
await waitForPromises();
expect(wrapper.find(component).exists()).toBe(expected);
},
);
it('does not render SidebarInheritDate when canInherit is true and date is loading', async () => {
createComponent({ canInherit: true });
expect(wrapper.find(SidebarInheritDate).exists()).toBe(false);
});
it('displays a flash message when query is rejected', async () => {
createComponent({
dueDateQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),

View File

@ -10,7 +10,7 @@ describe('SidebarInheritDate', () => {
const findFixedRadio = () => wrapper.findAll(GlFormRadio).at(0);
const findInheritRadio = () => wrapper.findAll(GlFormRadio).at(1);
const createComponent = () => {
const createComponent = ({ dueDateIsFixed = false } = {}) => {
wrapper = shallowMount(SidebarInheritDate, {
provide: {
canUpdate: true,
@ -18,11 +18,10 @@ describe('SidebarInheritDate', () => {
propsData: {
issuable: {
dueDate: '2021-04-15',
dueDateIsFixed: true,
dueDateIsFixed,
dueDateFixed: '2021-04-15',
dueDateFromMilestones: '2021-05-15',
},
isLoading: false,
dateType: 'dueDate',
},
});
@ -45,6 +44,13 @@ describe('SidebarInheritDate', () => {
expect(findInheritRadio().text()).toBe('Inherited:');
});
it('does not emit set-date if fixed value does not change', () => {
createComponent({ dueDateIsFixed: true });
findFixedRadio().vm.$emit('input', true);
expect(wrapper.emitted('set-date')).toBeUndefined();
});
it('emits set-date event on click on radio button', () => {
findFixedRadio().vm.$emit('input', true);

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::Ci::RunnerStatusResolver do
include GraphqlHelpers
describe '#resolve' do
let(:user) { build(:user) }
let(:runner) { build(:ci_runner) }
subject(:resolve_subject) { resolve(described_class, ctx: { current_user: user }, obj: runner, args: args) }
context 'with legacy_mode' do
context 'set to 14.5' do
let(:args) do
{ legacy_mode: '14.5' }
end
it 'calls runner.status with specified legacy_mode' do
expect(runner).to receive(:status).with('14.5').once.and_return(:online)
expect(resolve_subject).to eq(:online)
end
end
context 'set to nil' do
let(:args) do
{ legacy_mode: nil }
end
it 'calls runner.status with specified legacy_mode' do
expect(runner).to receive(:status).with(nil).once.and_return(:stale)
expect(resolve_subject).to eq(:stale)
end
end
end
end
end

View File

@ -342,6 +342,7 @@ RSpec.describe Ci::Runner do
using RSpec::Parameterized::TableSyntax
where(:created_at, :contacted_at, :expected_stale?) do
nil | nil | false
3.months.ago - 1.second | 3.months.ago - 0.001.seconds | true
3.months.ago - 1.second | 3.months.ago + 1.hour | false
3.months.ago - 1.second | nil | true
@ -376,6 +377,8 @@ RSpec.describe Ci::Runner do
end
def stub_redis_runner_contacted_at(value)
return unless created_at
Gitlab::Redis::Cache.with do |redis|
cache_key = runner.send(:cache_attribute_key)
expect(redis).to receive(:get).with(cache_key)
@ -419,7 +422,7 @@ RSpec.describe Ci::Runner do
it { is_expected.to be_falsey }
end
context 'contacted long time ago time' do
context 'contacted long time ago' do
before do
runner.contacted_at = 1.year.ago
end
@ -437,7 +440,7 @@ RSpec.describe Ci::Runner do
end
context 'with cache value' do
context 'contacted long time ago time' do
context 'contacted long time ago' do
before do
runner.contacted_at = 1.year.ago
stub_redis_runner_contacted_at(1.year.ago.to_s)
@ -699,16 +702,33 @@ RSpec.describe Ci::Runner do
end
describe '#status' do
let(:runner) { build(:ci_runner, :instance) }
let(:runner) { build(:ci_runner, :instance, created_at: 4.months.ago) }
let(:legacy_mode) { }
subject { runner.status }
subject { runner.status(legacy_mode) }
context 'never connected' do
before do
runner.contacted_at = nil
end
it { is_expected.to eq(:not_connected) }
context 'with legacy_mode enabled' do
let(:legacy_mode) { '14.5' }
it { is_expected.to eq(:not_connected) }
end
context 'with legacy_mode disabled' do
it { is_expected.to eq(:stale) }
end
context 'created recently' do
before do
runner.created_at = 1.day.ago
end
it { is_expected.to eq(:not_connected) }
end
end
context 'inactive but online' do
@ -717,7 +737,15 @@ RSpec.describe Ci::Runner do
runner.active = false
end
it { is_expected.to eq(:online) }
context 'with legacy_mode enabled' do
let(:legacy_mode) { '14.5' }
it { is_expected.to eq(:paused) }
end
context 'with legacy_mode disabled' do
it { is_expected.to eq(:online) }
end
end
context 'contacted 1s ago' do
@ -728,13 +756,29 @@ RSpec.describe Ci::Runner do
it { is_expected.to eq(:online) }
end
context 'contacted long time ago' do
context 'contacted recently' do
before do
runner.contacted_at = 1.year.ago
runner.contacted_at = (3.months - 1.hour).ago
end
it { is_expected.to eq(:offline) }
end
context 'contacted long time ago' do
before do
runner.contacted_at = (3.months + 1.second).ago
end
context 'with legacy_mode enabled' do
let(:legacy_mode) { '14.5' }
it { is_expected.to eq(:offline) }
end
context 'with legacy_mode disabled' do
it { is_expected.to eq(:stale) }
end
end
end
describe '#deprecated_rest_status' do

View File

@ -63,7 +63,7 @@ RSpec.describe 'Query.runner(id)' do
'revision' => runner.revision,
'locked' => false,
'active' => runner.active,
'status' => runner.status.to_s.upcase,
'status' => runner.status('14.5').to_s.upcase,
'maximumTimeout' => runner.maximum_timeout,
'accessLevel' => runner.access_level.to_s.upcase,
'runUntagged' => runner.run_untagged,
@ -221,6 +221,45 @@ RSpec.describe 'Query.runner(id)' do
end
end
describe 'for runner with status' do
let_it_be(:stale_runner) { create(:ci_runner, description: 'Stale runner 1', created_at: 3.months.ago) }
let(:query) do
%(
query {
staleRunner: runner(id: "#{stale_runner.to_global_id}") {
status
legacyStatusWithExplicitVersion: status(legacyMode: "14.5")
newStatus: status(legacyMode: null)
}
pausedRunner: runner(id: "#{inactive_instance_runner.to_global_id}") {
status
legacyStatusWithExplicitVersion: status(legacyMode: "14.5")
newStatus: status(legacyMode: null)
}
}
)
end
it 'retrieves status fields with expected values' do
post_graphql(query, current_user: user)
stale_runner_data = graphql_data_at(:stale_runner)
expect(stale_runner_data).to match a_hash_including(
'status' => 'NOT_CONNECTED',
'legacyStatusWithExplicitVersion' => 'NOT_CONNECTED',
'newStatus' => 'STALE'
)
paused_runner_data = graphql_data_at(:paused_runner)
expect(paused_runner_data).to match a_hash_including(
'status' => 'PAUSED',
'legacyStatusWithExplicitVersion' => 'PAUSED',
'newStatus' => 'OFFLINE'
)
end
end
describe 'for multiple runners' do
let_it_be(:project1) { create(:project, :test_repo) }
let_it_be(:project2) { create(:project, :test_repo) }

View File

@ -33,8 +33,8 @@ RSpec.describe 'profiles/keys/_form.html.haml' do
end
it 'has the expires at field', :aggregate_failures do
expect(rendered).to have_field('Expires at', type: 'date')
expect(page.find_field('Expires at')['min']).to eq(l(1.day.from_now, format: "%Y-%m-%d"))
expect(rendered).to have_field('Expiration date', type: 'date')
expect(page.find_field('Expiration date')['min']).to eq(l(1.day.from_now, format: "%Y-%m-%d"))
expect(rendered).to have_text('Key can still be used after expiration.')
end