Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-06-05 21:21:25 +00:00
parent df8f194428
commit f8eba0f714
33 changed files with 436 additions and 16 deletions

View File

@ -328,11 +328,21 @@ jest predictive:
extends:
- jest
- .frontend:rules:jest:predictive
needs:
- !reference [jest, needs]
- "detect-tests"
script:
- if [[ -s "$RSPEC_CHANGED_FILES_PATH" ]] || [[ -s "$RSPEC_MATCHING_JS_FILES_PATH" ]]; then run_timed_command "yarn jest:ci:predictive-without-fixtures"; fi
jest-with-fixtures predictive:
extends:
- jest-with-fixtures
- .frontend:rules:jest:predictive
needs:
- !reference [jest-with-fixtures, needs]
- "detect-tests"
script:
- if [[ -s "$RSPEC_CHANGED_FILES_PATH" ]] || [[ -s "$RSPEC_MATCHING_JS_FILES_PATH" ]]; then run_timed_command "yarn jest:ci:predictive"; fi
- if [[ -s "$RSPEC_CHANGED_FILES_PATH" ]] || [[ -s "$RSPEC_MATCHING_JS_FILES_PATH" ]]; then run_timed_command "yarn jest:ci:predictive-with-fixtures"; fi
jest-integration:
extends:

View File

@ -25,3 +25,10 @@ export const IID_FAILURE = 'missing_iid';
export const RETRY_ACTION_TITLE = 'Retry';
export const MANUAL_ACTION_TITLE = 'Run';
/*
this poll interval is shared between the graph,
pipeline header, jobs tab and failed jobs tab to
keep all the data relatively in sync
*/
export const POLL_INTERVAL = 10000;

View File

@ -15,6 +15,7 @@ import {
SKIP_RETRY_MODAL_KEY,
STAGE_VIEW,
VIEW_TYPE_KEY,
POLL_INTERVAL,
} from './constants';
import PipelineGraph from './components/graph_component.vue';
import GraphViewSelector from './components/graph_view_selector.vue';
@ -129,7 +130,7 @@ export default {
return getQueryHeaders(this.graphqlResourceEtag);
},
query: getPipelineDetails,
pollInterval: 10000,
pollInterval: POLL_INTERVAL,
variables() {
return {
projectPath: this.pipelineProjectPath,

View File

@ -1,7 +1,5 @@
export const DELETE_MODAL_ID = 'pipeline-delete-modal';
export const POLL_INTERVAL = 10000;
export const SCHEDULE_SOURCE = 'schedule';
export const AUTO_DEVOPS_SOURCE = 'AUTO_DEVOPS_SOURCE';
export const DETACHED_EVENT_TYPE = 'DETACHED';

View File

@ -14,10 +14,10 @@ import cancelPipelineMutation from '../graphql/mutations/cancel_pipeline.mutatio
import deletePipelineMutation from '../graphql/mutations/delete_pipeline.mutation.graphql';
import retryPipelineMutation from '../graphql/mutations/retry_pipeline.mutation.graphql';
import { getQueryHeaders } from '../graph/utils';
import { POLL_INTERVAL } from '../graph/constants';
import HeaderActions from './components/header_actions.vue';
import HeaderBadges from './components/header_badges.vue';
import getPipelineQuery from './graphql/queries/get_pipeline_header_data.query.graphql';
import { POLL_INTERVAL } from './constants';
export default {
name: 'PipelineHeader',

View File

@ -2,6 +2,8 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import { createAlert } from '~/alert';
import { getQueryHeaders } from '../graph/utils';
import { POLL_INTERVAL } from '../graph/constants';
import GetFailedJobsQuery from './graphql/queries/get_failed_jobs.query.graphql';
import FailedJobsTable from './components/failed_jobs_table.vue';
@ -17,10 +19,17 @@ export default {
pipelineIid: {
default: '',
},
graphqlResourceEtag: {
default: '',
},
},
apollo: {
failedJobs: {
context() {
return getQueryHeaders(this.graphqlResourceEtag);
},
query: GetFailedJobsQuery,
pollInterval: POLL_INTERVAL,
variables() {
return {
fullPath: this.projectPath,

View File

@ -6,6 +6,8 @@ import { __ } from '~/locale';
import eventHub from '~/ci/jobs_page/event_hub';
import JobsTable from '~/ci/jobs_page/components/jobs_table.vue';
import { JOBS_TAB_FIELDS } from '~/ci/jobs_page/constants';
import { getQueryHeaders } from '../graph/utils';
import { POLL_INTERVAL } from '../graph/constants';
import getPipelineJobs from './graphql/queries/get_pipeline_jobs.query.graphql';
export default {
@ -23,10 +25,17 @@ export default {
pipelineIid: {
default: '',
},
graphqlResourceEtag: {
default: '',
},
},
apollo: {
jobs: {
context() {
return getQueryHeaders(this.graphqlResourceEtag);
},
query: getPipelineJobs,
pollInterval: POLL_INTERVAL,
variables() {
return {
...this.queryVariables,

View File

@ -36,6 +36,11 @@ export default {
required: false,
default: null,
},
isEditing: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
@ -61,7 +66,7 @@ export default {
},
created() {
const validatePath = async () => {
if (this.isEditingGroup && this.value === this.initialValue) return;
if (this.isEditing && this.value === this.initialValue) return;
this.suggestedPath = '';

View File

@ -162,7 +162,7 @@ export default {
};
},
isEditing() {
return this.initialFormValues[FORM_FIELD_ID];
return Boolean(this.initialFormValues[FORM_FIELD_ID]);
},
},
watch: {
@ -210,6 +210,7 @@ export default {
:value="value"
:state="validation.state"
:base-path="basePath"
:is-editing="isEditing"
@input="onPathInput($event, input)"
@input-suggested-path="input"
@blur="blur"

View File

@ -1,14 +1,22 @@
<script>
import { GlSprintf } from '@gitlab/ui';
import NewEditForm from '~/groups/components/new_edit_form.vue';
import { __ } from '~/locale';
import { FORM_FIELD_NAME, FORM_FIELD_PATH, FORM_FIELD_VISIBILITY_LEVEL } from '~/groups/constants';
import { __, s__ } from '~/locale';
import { VISIBILITY_LEVELS_INTEGER_TO_STRING } from '~/visibility_level/constants';
import { visitUrlWithAlerts } from '~/lib/utils/url_utility';
import { createAlert } from '~/alert';
import FormErrorsAlert from '~/vue_shared/components/form/errors_alert.vue';
import groupUpdateMutation from '../graphql/mutations/group_update.mutation.graphql';
export default {
name: 'OrganizationGroupsEditApp',
components: { GlSprintf, NewEditForm },
components: { GlSprintf, FormErrorsAlert, NewEditForm },
i18n: {
pageTitle: __('Edit group: %{group_name}'),
submitButtonText: __('Save changes'),
errorMessage: s__('Groups|An error occurred updating this group. Please try again.'),
successMessage: __('Group was successfully updated.'),
},
inject: [
'group',
@ -21,6 +29,57 @@ export default {
'pathMaxlength',
'pathPattern',
],
data() {
return {
loading: false,
errors: [],
};
},
methods: {
async onSubmit({
[FORM_FIELD_NAME]: name,
[FORM_FIELD_PATH]: path,
[FORM_FIELD_VISIBILITY_LEVEL]: visibility,
}) {
try {
this.loading = true;
const {
data: {
groupUpdate: { group, errors },
},
} = await this.$apollo.mutate({
mutation: groupUpdateMutation,
variables: {
input: {
fullPath: this.group.fullPath,
name,
path,
visibility: VISIBILITY_LEVELS_INTEGER_TO_STRING[visibility],
},
},
});
if (errors.length) {
this.errors = errors;
this.loading = false;
return;
}
visitUrlWithAlerts(group.organizationEditPath, [
{
id: 'organization-group-successfully-updated',
message: this.$options.i18n.successMessage,
variant: 'info',
},
]);
} catch (error) {
this.loading = false;
createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
}
},
},
};
</script>
@ -31,8 +90,9 @@ export default {
<template #group_name>{{ group.fullName }}</template>
</gl-sprintf>
</h1>
<form-errors-alert v-model="errors" :scroll-on-error="true" />
<new-edit-form
:loading="false"
:loading="loading"
:base-path="basePath"
:path-maxlength="pathMaxlength"
:path-pattern="pathPattern"
@ -41,6 +101,7 @@ export default {
:available-visibility-levels="availableVisibilityLevels"
:restricted-visibility-levels="restrictedVisibilityLevels"
:initial-form-values="group"
@submit="onSubmit"
/>
</div>
</template>

View File

@ -0,0 +1,9 @@
mutation groupUpdate($input: GroupUpdateInput!) {
groupUpdate(input: $input) {
group {
id
organizationEditPath
}
errors
}
}

View File

@ -1,6 +1,8 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import createDefaultClient from '~/lib/graphql';
import App from './components/app.vue';
export const initOrganizationsGroupsEdit = () => {
@ -23,9 +25,14 @@ export const initOrganizationsGroupsEdit = () => {
pathPattern,
} = convertObjectPropsToCamelCase(JSON.parse(appData), { deep: true });
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
return new Vue({
el,
name: 'OrganizationGroupsEditRoot',
apolloProvider,
provide: {
group,
basePath,

View File

@ -22,9 +22,18 @@ module Mutations
argument :math_rendering_limits_enabled, GraphQL::Types::Boolean,
required: false,
description: copy_field_description(Types::GroupType, :math_rendering_limits_enabled)
argument :name, GraphQL::Types::String,
required: false,
description: copy_field_description(Types::GroupType, :name)
argument :path, GraphQL::Types::String,
required: false,
description: copy_field_description(Types::GroupType, :path)
argument :shared_runners_setting, Types::Namespace::SharedRunnersSettingEnum,
required: false,
description: copy_field_description(Types::GroupType, :shared_runners_setting)
argument :visibility, Types::VisibilityLevelsEnum,
required: false,
description: copy_field_description(Types::GroupType, :visibility)
def resolve(full_path:, **args)
group = authorized_find!(full_path: full_path)

View File

@ -58,7 +58,7 @@ module Organizations
def organization_groups_edit_app_data(organization, group)
{
group: group.slice(:id, :full_name, :name, :visibility_level, :path)
group: group.slice(:id, :full_name, :name, :visibility_level, :path, :full_path)
}.merge(shared_organization_groups_app_data(organization)).to_json
end

View File

@ -2392,6 +2392,8 @@ class Project < ApplicationRecord
:started
elsif export_file_exists?
:finished
elsif export_failed?
:failed
else
:none
end
@ -2409,6 +2411,12 @@ class Project < ApplicationRecord
end
end
def export_failed?
strong_memoize(:export_failed) do
::Projects::ExportJobFinder.new(self, { status: :failed }).execute.present?
end
end
def regeneration_in_progress?
(export_enqueued? || export_in_progress?) && export_file_exists?
end

View File

@ -62,7 +62,7 @@ module Projects
next if Gitlab::SidekiqStatus.running?(relation_export.jid)
next if relation_export.reset.finished?
relation_export.mark_as_failed("Exausted number of retries to export: #{relation_export.relation}")
relation_export.mark_as_failed("Exhausted number of retries to export: #{relation_export.relation}")
end
end

View File

@ -44,6 +44,11 @@ class StuckExportJobsWorker
)
completed_jobs.each do |job|
# Parallel export job completes and keeps 'started' state because it has
# multiple relation exports running in parallel. Don't mark it as failed
# until 6 hours mark
next if job.relation_exports.any? && job.created_at > EXPORT_JOBS_EXPIRATION.seconds.ago
job.fail_op
end.count
end

View File

@ -5223,7 +5223,10 @@ Input type: `GroupUpdateInput`
| <a id="mutationgroupupdatelockduofeaturesenabled"></a>`lockDuoFeaturesEnabled` | [`Boolean`](#boolean) | Indicates if the GitLab Duo features enabled setting is enforced for all subgroups. Introduced in GitLab 16.10: **Status**: Experiment. |
| <a id="mutationgroupupdatelockmathrenderinglimitsenabled"></a>`lockMathRenderingLimitsEnabled` | [`Boolean`](#boolean) | Indicates if math rendering limits are locked for all descendant groups. |
| <a id="mutationgroupupdatemathrenderinglimitsenabled"></a>`mathRenderingLimitsEnabled` | [`Boolean`](#boolean) | Indicates if math rendering limits are used for this group. |
| <a id="mutationgroupupdatename"></a>`name` | [`String`](#string) | Name of the namespace. |
| <a id="mutationgroupupdatepath"></a>`path` | [`String`](#string) | Path of the namespace. |
| <a id="mutationgroupupdatesharedrunnerssetting"></a>`sharedRunnersSetting` | [`SharedRunnersSetting`](#sharedrunnerssetting) | Shared runners availability for the namespace and its descendants. |
| <a id="mutationgroupupdatevisibility"></a>`visibility` | [`VisibilityLevelsEnum`](#visibilitylevelsenum) | Visibility of the namespace. |
#### Fields

View File

@ -174,7 +174,7 @@ The month-over-month comparison of the AI Usage unique users rate gives a more a
The baseline for the AI Usage trend is the total number of code contributors, not just users with GitLab Duo seats. This baseline gives a more accurate representation of AI usage by team members.
NOTE:
Usage rate for Code Suggestions is calculated with data starting on 2024-04-04.
Usage rate for Code Suggestions is calculated with data starting from GitLab 16.11.
For more information, see [epic 12978](https://gitlab.com/groups/gitlab-org/-/epics/12978).
## Enable or disable overview background aggregation

View File

@ -26,6 +26,8 @@ A deploy token is a pair of values:
`gitlab+deploy-token-{n}`. You can specify a custom username when you create the deploy token.
- **token**: `password` in the HTTP authentication framework.
Deploy tokens do not support [SSH authentication](../../ssh.md).
You can use a deploy token for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication)
to the following endpoints:

View File

@ -25636,6 +25636,9 @@ msgstr ""
msgid "GroupsTree|Search by name"
msgstr ""
msgid "Groups|An error occurred updating this group. Please try again."
msgstr ""
msgid "Groups|Avatar will be removed. Are you sure?"
msgstr ""

View File

@ -18,7 +18,8 @@
"jest:ci": "jest --config jest.config.js --ci --coverage --testSequencer ./scripts/frontend/parallel_ci_sequencer.js --shard \"${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1}\" --logHeapUsage",
"jest:ci:without-fixtures": "jest --config jest.config.js --ci --coverage --testSequencer ./scripts/frontend/fixture_ci_sequencer.js --shard \"${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1}\" --logHeapUsage",
"jest:ci:with-fixtures": "JEST_FIXTURE_JOBS_ONLY=1 jest --config jest.config.js --ci --coverage --testSequencer ./scripts/frontend/fixture_ci_sequencer.js --shard \"${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1}\" --logHeapUsage",
"jest:ci:predictive": "jest --config jest.config.js --ci --coverage --findRelatedTests $(cat $RSPEC_CHANGED_FILES_PATH) $(cat $RSPEC_MATCHING_JS_FILES_PATH) --passWithNoTests --testSequencer ./scripts/frontend/parallel_ci_sequencer.js --shard \"${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1}\" --logHeapUsage",
"jest:ci:predictive-without-fixtures": "jest --config jest.config.js --ci --coverage --findRelatedTests $(cat $RSPEC_CHANGED_FILES_PATH) $(cat $RSPEC_MATCHING_JS_FILES_PATH) --passWithNoTests --testSequencer ./scripts/frontend/fixture_ci_sequencer.js --shard \"${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1}\" --logHeapUsage",
"jest:ci:predictive-with-fixtures": "JEST_FIXTURE_JOBS_ONLY=1 jest --config jest.config.js --ci --coverage --findRelatedTests $(cat $RSPEC_CHANGED_FILES_PATH) $(cat $RSPEC_MATCHING_JS_FILES_PATH) --passWithNoTests --testSequencer ./scripts/frontend/fixture_ci_sequencer.js --shard \"${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1}\" --logHeapUsage",
"jest:contract": "PACT_DO_NOT_TRACK=true jest --config jest.config.contract.js --runInBand",
"jest:integration": "jest --config jest.config.integration.js",
"jest:scripts": "jest --config jest.config.scripts.js",

View File

@ -294,7 +294,7 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode, feature_category:
with_them do
let(:params) { { sort: sort } }
it 'returns ordered tokens' do
it 'returns ordered tokens', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/446283' do
expect(subject.map(&:id)).to eq(tokens.values_at(*expected_tokens).map(&:id))
end
end

View File

@ -8,6 +8,7 @@ import { createAlert } from '~/alert';
import FailedJobsApp from '~/ci/pipeline_details/jobs/failed_jobs_app.vue';
import FailedJobsTable from '~/ci/pipeline_details/jobs/components/failed_jobs_table.vue';
import GetFailedJobsQuery from '~/ci/pipeline_details/jobs/graphql/queries/get_failed_jobs.query.graphql';
import { POLL_INTERVAL } from '~/ci/pipeline_details/graph/constants';
import { mockFailedJobsQueryResponse } from 'jest/ci/pipeline_details/mock_data';
Vue.use(VueApollo);
@ -27,11 +28,14 @@ describe('Failed Jobs App', () => {
return createMockApollo(requestHandlers);
};
const graphqlResourceEtag = '/api/graphql:pipelines/id/1';
const createComponent = (resolver) => {
wrapper = shallowMount(FailedJobsApp, {
provide: {
fullPath: 'root/ci-project',
pipelineIid: 1,
graphqlResourceEtag,
},
apolloProvider: createMockApolloProvider(resolver),
});
@ -77,4 +81,18 @@ describe('Failed Jobs App', () => {
message: 'There was a problem fetching the failed jobs.',
});
});
describe('polling', () => {
beforeEach(() => {
createComponent(resolverSpy);
});
it('polls for query data', () => {
expect(resolverSpy).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(POLL_INTERVAL);
expect(resolverSpy).toHaveBeenCalledTimes(2);
});
});
});

View File

@ -8,6 +8,7 @@ import { createAlert } from '~/alert';
import JobsApp from '~/ci/pipeline_details/jobs/jobs_app.vue';
import JobsTable from '~/ci/jobs_page/components/jobs_table.vue';
import getPipelineJobsQuery from '~/ci/pipeline_details/jobs/graphql/queries/get_pipeline_jobs.query.graphql';
import { POLL_INTERVAL } from '~/ci/pipeline_details/graph/constants';
import { mockPipelineJobsQueryResponse } from '../mock_data';
Vue.use(VueApollo);
@ -31,11 +32,14 @@ describe('Jobs app', () => {
return createMockApollo(requestHandlers);
};
const graphqlResourceEtag = '/api/graphql:pipelines/id/1';
const createComponent = (resolver) => {
wrapper = shallowMount(JobsApp, {
provide: {
projectPath: 'root/ci-project',
pipelineIid: 1,
graphqlResourceEtag,
},
apolloProvider: createMockApolloProvider(resolver),
});
@ -124,4 +128,18 @@ describe('Jobs app', () => {
expect(findSkeletonLoader().exists()).toBe(false);
});
describe('polling', () => {
beforeEach(() => {
createComponent(resolverSpy);
});
it('polls for query data', () => {
expect(resolverSpy).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(POLL_INTERVAL);
expect(resolverSpy).toHaveBeenCalledTimes(2);
});
});
});

View File

@ -93,6 +93,49 @@ RSpec.describe 'Organizations (GraphQL fixtures)', feature_category: :cell do
end
end
describe 'organization update group' do
base_input_path = 'organizations/groups/edit/graphql/mutations/'
base_output_path = 'graphql/organizations/'
mutation_name = 'group_update.mutation.graphql'
it "#{base_output_path}#{mutation_name}.json" do
mutation = get_graphql_query_as_string("#{base_input_path}#{mutation_name}")
post_graphql(
mutation,
current_user: current_user,
variables: {
input: {
full_path: group.full_path,
name: "#{group.name} updated",
path: "#{group.path}-updated"
}
}
)
expect_graphql_errors_to_be_empty
end
it "#{base_output_path}#{mutation_name}_with_errors.json" do
mutation = get_graphql_query_as_string("#{base_input_path}#{mutation_name}")
post_graphql(
mutation,
current_user: current_user,
variables: {
input: {
full_path: group.full_path,
name: "#{group.name} updated",
path: "#{group.path}-updated",
visibility: 'private'
}
}
)
expect_graphql_errors_to_be_empty
end
end
describe 'organization projects' do
base_input_path = 'organizations/shared/graphql/queries/'
base_output_path = 'graphql/organizations/'

View File

@ -62,6 +62,21 @@ describe('GroupPathField', () => {
});
});
describe('when editing a group and path is set to initial path', () => {
beforeEach(async () => {
apiMockUnavailablePath();
createComponent({ propsData: { isEditing: true, value: 'foo' } });
await wrapper.setProps({ value: 'foo bar' });
await waitForPromises();
await wrapper.setProps({ value: 'foo' });
});
it('does not call API', () => {
expect(getGroupPathAvailability).toHaveBeenCalledTimes(1);
});
});
describe('when value is not the suggested path', () => {
describe('when value is an unavailable path', () => {
beforeEach(async () => {

View File

@ -136,6 +136,10 @@ describe('NewEditForm', () => {
expect(findPathField().props('value')).toBe('foo-bar');
});
it('sets `isEditing` prop to `true`', () => {
expect(findPathField().props('isEditing')).toBe(true);
});
});
describe('when form is submitted without filling in required fields', () => {

View File

@ -1,15 +1,34 @@
import { GlSprintf } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue';
import groupUpdateResponse from 'test_fixtures/graphql/organizations/group_update.mutation.graphql.json';
import groupUpdateResponseWithErrors from 'test_fixtures/graphql/organizations/group_update.mutation.graphql_with_errors.json';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import App from '~/organizations/groups/edit/components/app.vue';
import groupUpdateMutation from '~/organizations/groups/edit/graphql/mutations/group_update.mutation.graphql';
import {
VISIBILITY_LEVEL_INTERNAL_INTEGER,
VISIBILITY_LEVEL_PRIVATE_INTEGER,
VISIBILITY_LEVEL_PUBLIC_INTEGER,
VISIBILITY_LEVEL_PRIVATE_STRING,
} from '~/visibility_level/constants';
import NewEditForm from '~/groups/components/new_edit_form.vue';
import { createAlert } from '~/alert';
import { visitUrlWithAlerts } from '~/lib/utils/url_utility';
import { FORM_FIELD_NAME, FORM_FIELD_PATH, FORM_FIELD_VISIBILITY_LEVEL } from '~/groups/constants';
import FormErrorsAlert from '~/vue_shared/components/form/errors_alert.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/lib/utils/url_utility');
jest.mock('~/alert');
Vue.use(VueApollo);
describe('OrganizationGroupsEditApp', () => {
let wrapper;
let mockApollo;
const defaultProvide = {
group: {
@ -17,6 +36,7 @@ describe('OrganizationGroupsEditApp', () => {
fullName: 'Mock namespace / Foo bar',
name: 'Foo bar',
path: 'foo-bar',
fullPath: 'mock-namespace/foo-bar',
},
basePath: 'https://gitlab.com',
groupsAndProjectsOrganizationPath: '/-/organizations/carrot/groups_and_projects?display=groups',
@ -32,8 +52,15 @@ describe('OrganizationGroupsEditApp', () => {
pathPattern: 'mockPattern',
};
const createComponent = () => {
const successfulResponseHandler = jest.fn().mockResolvedValue(groupUpdateResponse);
const createComponent = ({
handlers = [[groupUpdateMutation, successfulResponseHandler]],
} = {}) => {
mockApollo = createMockApollo(handlers);
wrapper = shallowMountExtended(App, {
apolloProvider: mockApollo,
provide: defaultProvide,
stubs: {
GlSprintf,
@ -42,6 +69,18 @@ describe('OrganizationGroupsEditApp', () => {
};
const findForm = () => wrapper.findComponent(NewEditForm);
const submitForm = async () => {
findForm().vm.$emit('submit', {
[FORM_FIELD_NAME]: 'Foo bar',
[FORM_FIELD_PATH]: 'foo-bar',
[FORM_FIELD_VISIBILITY_LEVEL]: VISIBILITY_LEVEL_PRIVATE_INTEGER,
});
await nextTick();
};
afterEach(() => {
mockApollo = null;
});
it('renders page title', () => {
createComponent();
@ -66,4 +105,88 @@ describe('OrganizationGroupsEditApp', () => {
submitButtonText: 'Save changes',
});
});
describe('when form is submitted', () => {
describe('when API is loading', () => {
beforeEach(async () => {
createComponent();
await submitForm();
});
it('sets `NewEditForm` `loading` prop to `true`', () => {
expect(findForm().props('loading')).toBe(true);
});
});
describe('when API request is successful', () => {
beforeEach(async () => {
createComponent();
await submitForm();
await waitForPromises();
});
it('calls mutation with correct variables and redirects user to organization web url', () => {
expect(successfulResponseHandler).toHaveBeenCalledWith({
input: {
fullPath: defaultProvide.group.fullPath,
name: 'Foo bar',
path: 'foo-bar',
visibility: VISIBILITY_LEVEL_PRIVATE_STRING,
},
});
expect(visitUrlWithAlerts).toHaveBeenCalledWith(
groupUpdateResponse.data.groupUpdate.group.organizationEditPath,
[
{
id: 'organization-group-successfully-updated',
message: 'Group was successfully updated.',
variant: 'info',
},
],
);
});
});
describe('when API request is not successful', () => {
describe('when there is a network error', () => {
const error = new Error();
beforeEach(async () => {
createComponent({
handlers: [[groupUpdateMutation, jest.fn().mockRejectedValue(error)]],
});
await submitForm();
await waitForPromises();
});
it('displays error alert', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred updating this group. Please try again.',
error,
captureError: true,
});
});
});
describe('when there are GraphQL errors', () => {
beforeEach(async () => {
createComponent({
handlers: [
[groupUpdateMutation, jest.fn().mockResolvedValue(groupUpdateResponseWithErrors)],
],
});
await submitForm();
await waitForPromises();
});
it('displays form errors alert', () => {
expect(wrapper.findComponent(FormErrorsAlert).props()).toStrictEqual({
errors: groupUpdateResponseWithErrors.data.groupUpdate.errors,
scrollOnError: true,
});
});
});
});
});
});

View File

@ -326,6 +326,7 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
'full_name' => group.full_name,
'name' => group.name,
'path' => group.path,
'full_path' => group.full_path,
"visibility_level" => group.visibility_level
},
'base_path' => root_url,

View File

@ -7890,6 +7890,14 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
it { expect(project.export_status).to eq :queued }
end
context 'when project export is failed' do
before do
project_export_job.fail_op!
end
it { expect(project.export_status).to eq :failed }
end
context 'when project export is in progress' do
before do
project_export_job.start!

View File

@ -44,6 +44,8 @@ RSpec.describe 'GroupUpdate', feature_category: :groups_and_projects do
end
context 'when authorized' do
using RSpec::Parameterized::TableSyntax
before do
group.add_owner(user)
end
@ -65,6 +67,22 @@ RSpec.describe 'GroupUpdate', feature_category: :groups_and_projects do
expect(group.reload.shared_runners_setting).to eq(variables[:shared_runners_setting].downcase)
end
where(:field, :value) do
'name' | 'foo bar'
'path' | 'foo-bar'
'visibility' | 'private'
end
with_them do
let(:variables) { { full_path: group.full_path, field => value } }
it "updates #{params[:field]} field" do
post_graphql_mutation(mutation, current_user: user)
expect(graphql_data_at(:group_update, :group, field.to_sym)).to eq(value)
end
end
context 'when bad arguments are provided' do
let(:variables) { { full_path: '', shared_runners_setting: 'INVALID' } }

View File

@ -38,6 +38,30 @@ RSpec.describe StuckExportJobsWorker, feature_category: :importers do
expect(project_export_job.reload.failed?).to be true
end
context 'when export job has relation exports' do
before do
create(:project_relation_export, project_export_job: project_export_job)
end
context 'when export job updated at is less than expiration time' do
it 'does not mark export job as failed' do
worker.perform
expect(project_export_job.reload.failed?).to be false
end
end
context 'when export job updated at is greater than expiration time' do
it 'marks export job as failed' do
project_export_job.update!(created_at: 12.hours.ago)
worker.perform
expect(project_export_job.reload.failed?).to be true
end
end
end
end
context 'when the job is not in queue and db record in queued state' do