Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-08-13 06:12:34 +00:00
parent 8f836a3022
commit dc6331c5ed
25 changed files with 513 additions and 80 deletions

View File

@ -158,9 +158,7 @@ export default {
</gl-tooltip>
</div>
<div class="gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-flex-wrap gl-min-w-0">
<div
class="gl-display-flex gl-flex-wrap flex-xl-nowrap gl-justify-content-space-between gl-gap-3 gl-min-w-0 gl-mb-2"
>
<div class="gl-flex gl-justify-between gl-gap-3 gl-min-w-0 gl-mb-2">
<div class="item-title gl-min-w-0">
<span v-if="childItem.confidential">
<gl-icon

View File

@ -54,10 +54,16 @@ export default {
required: false,
default: true,
},
disableContent: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
prefetchedWorkItem: null,
updateInProgress: false,
};
},
computed: {
@ -86,6 +92,9 @@ export default {
.map((child) => findHierarchyWidgets(child.widgets) || {})
.some((hierarchy) => hierarchy.hasChildren);
},
disableList() {
return this.disableContent || this.updateInProgress;
},
},
methods: {
async removeChild(child) {
@ -121,6 +130,7 @@ export default {
},
async undoChildRemoval(child) {
try {
this.updateInProgress = true;
const { data } = await this.$apollo.mutate({
mutation: updateWorkItemMutation,
variables: { input: { id: child.id, hierarchyWidget: { parentId: this.workItemId } } },
@ -140,6 +150,8 @@ export default {
} catch (error) {
this.$emit('error', s__('WorkItem|Something went wrong while undoing child removal.'));
Sentry.captureException(error);
} finally {
this.updateInProgress = false;
}
},
addWorkItemQuery({ iid }) {
@ -210,6 +222,7 @@ export default {
const updatedChildren = this.children.slice();
updatedChildren.splice(oldIndex, 1);
updatedChildren.splice(newIndex, 0, targetItem);
this.updateInProgress = true;
this.$apollo
.mutate({
@ -255,6 +268,9 @@ export default {
.catch((error) => {
this.$emit('error', error.message);
Sentry.captureException(error);
})
.finally(() => {
this.updateInProgress = false;
});
},
},
@ -266,7 +282,8 @@ export default {
:is="treeRootWrapper"
v-bind="treeRootOptions"
class="content-list"
:class="{ 'gl-cursor-grab sortable-container': canReorder }"
data-testid="child-items-container"
:class="{ 'gl-cursor-grab sortable-container': canReorder, 'disabled-content': disableList }"
@end="handleDragOnEnd"
>
<work-item-link-child

View File

@ -115,6 +115,7 @@ export default {
widgetName: TASKS_ANCHOR,
showLabels: true,
fetchNextPageInProgress: false,
disableContent: false,
};
},
computed: {
@ -329,6 +330,7 @@ export default {
:parent-milestone="issuableMilestone"
:form-type="formType"
:parent-work-item-type="workItem.workItemType.name"
@update-in-progress="disableContent = $event"
@cancel="hideAddForm"
/>
<work-item-children-wrapper
@ -338,6 +340,7 @@ export default {
:work-item-id="issuableGid"
:work-item-iid="iid"
:show-labels="showLabels"
:disable-content="disableContent"
@error="error = $event"
@show-modal="openChild"
/>

View File

@ -278,8 +278,12 @@ export default {
this.error = null;
this.isInputValid = true;
},
markFormSubmitInProgress(value) {
this.submitInProgress = value;
this.$emit('update-in-progress', this.submitInProgress);
},
addChild() {
this.submitInProgress = true;
this.markFormSubmitInProgress(true);
this.$apollo
.mutate({
mutation: updateWorkItemHierarchyMutation,
@ -293,6 +297,9 @@ export default {
},
})
.then(({ data }) => {
// Marking submitInProgress cannot be in finally block
// as the form may get close before the event is emitted
this.markFormSubmitInProgress(false);
if (data.workItemUpdate?.errors?.length) {
[this.error] = data.workItemUpdate.errors;
} else {
@ -304,17 +311,17 @@ export default {
.catch(() => {
this.error = this.$options.i18n.addChildErrorMessage;
this.isInputValid = false;
this.markFormSubmitInProgress(false);
})
.finally(() => {
this.search = '';
this.submitInProgress = false;
});
},
createChild() {
if (!this.canSubmitForm) {
return;
}
this.submitInProgress = true;
this.markFormSubmitInProgress(true);
this.$apollo
.mutate({
mutation: createWorkItemMutation,
@ -329,6 +336,9 @@ export default {
}),
})
.then(({ data }) => {
// Marking submitInProgress cannot be in finally block
// as the form may get close before the event is emitted
this.markFormSubmitInProgress(false);
if (data.workItemCreate?.errors?.length) {
[this.error] = data.workItemCreate.errors;
} else {
@ -340,11 +350,11 @@ export default {
.catch(() => {
this.error = this.$options.i18n.createChildErrorMessage;
this.isInputValid = false;
this.markFormSubmitInProgress(false);
})
.finally(() => {
this.search = '';
this.childToCreateTitle = null;
this.submitInProgress = false;
});
},
closeForm() {

View File

@ -106,6 +106,7 @@ export default {
showLabels: true,
fetchNextPageInProgress: false,
workItem: null,
disableContent: false,
};
},
apollo: {
@ -324,6 +325,7 @@ export default {
:parent-confidential="confidential"
@cancel="hideAddForm"
@addChild="$emit('addChild')"
@update-in-progress="disableContent = $event"
/>
<work-item-children-wrapper
:children="children"
@ -333,6 +335,7 @@ export default {
:work-item-iid="workItemIid"
:work-item-type="workItemType"
:show-labels="showLabels"
:disable-content="disableContent"
@error="error = $event"
@show-modal="showModal"
/>

View File

@ -30,10 +30,7 @@ class JsonSchemaValidator < ActiveModel::EachValidator
if options[:detail_errors]
validator.validate(value).each do |error|
message = format(
_("'%{data_pointer}' must be a valid '%{type}'"),
data_pointer: error['data_pointer'], type: error['type']
)
message = format_error_message(error)
record.errors.add(attribute, message)
end
else
@ -45,6 +42,32 @@ class JsonSchemaValidator < ActiveModel::EachValidator
attr_reader :base_directory
def format_error_message(error)
case error['type']
when 'oneOf'
format_one_of_error(error)
else
error['error']
end
end
def format_one_of_error(error)
schema_options = error['schema']['oneOf']
required_props = schema_options.flat_map { |option| option['required'] }.uniq
message = if error['root_schema']['type'] == 'array'
_("value at %{data_pointer} should use only one of: %{requirements}")
else
_("should use only one of: %{requirements}")
end
format(
message,
requirements: required_props.join(', '),
data_pointer: error['data_pointer']
)
end
def valid_schema?(value)
validator.valid?(value)
end

View File

@ -19021,6 +19021,7 @@ GitLab CI/CD configuration template.
| <a id="clusteragentid"></a>`id` | [`ID!`](#id) | ID of the cluster agent. |
| <a id="clusteragentname"></a>`name` | [`String`](#string) | Name of the cluster agent. |
| <a id="clusteragentproject"></a>`project` | [`Project`](#project) | Project this cluster agent is associated with. |
| <a id="clusteragentremotedevelopmentagentconfig"></a>`remoteDevelopmentAgentConfig` | [`RemoteDevelopmentAgentConfig`](#remotedevelopmentagentconfig) | Remote development agent config for the cluster agent. |
| <a id="clusteragenttokens"></a>`tokens` | [`ClusterAgentTokenConnection`](#clusteragenttokenconnection) | Tokens associated with the cluster agent. (see [Connections](#connections)) |
| <a id="clusteragentupdatedat"></a>`updatedAt` | [`Time`](#time) | Timestamp the cluster agent was updated. |
| <a id="clusteragentuseraccessauthorizations"></a>`userAccessAuthorizations` | [`ClusterAgentAuthorizationUserAccess`](#clusteragentauthorizationuseraccess) | User access config for the cluster agent. |
@ -31135,6 +31136,28 @@ Represents the source code attached to a release in a particular format.
| <a id="releasesourceformat"></a>`format` | [`String`](#string) | Format of the source. |
| <a id="releasesourceurl"></a>`url` | [`String`](#string) | Download URL of the source. |
### `RemoteDevelopmentAgentConfig`
Represents a remote development agent configuration.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="remotedevelopmentagentconfigclusteragent"></a>`clusterAgent` | [`ClusterAgent!`](#clusteragent) | Cluster agent that the remote development agent config belongs to. |
| <a id="remotedevelopmentagentconfigcreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of when the remote development agent config was created. |
| <a id="remotedevelopmentagentconfigdefaultmaxhoursbeforetermination"></a>`defaultMaxHoursBeforeTermination` | [`Int!`](#int) | Default max hours before worksapce termination of the remote development agent config. |
| <a id="remotedevelopmentagentconfigdnszone"></a>`dnsZone` | [`String!`](#string) | DNS zone where workspaces are available. |
| <a id="remotedevelopmentagentconfigenabled"></a>`enabled` | [`Boolean!`](#boolean) | Indicates whether remote development is enabled for the GitLab agent. |
| <a id="remotedevelopmentagentconfiggitlabworkspacesproxynamespace"></a>`gitlabWorkspacesProxyNamespace` | [`String!`](#string) | Namespace where gitlab-workspaces-proxy is installed. |
| <a id="remotedevelopmentagentconfigid"></a>`id` | [`RemoteDevelopmentRemoteDevelopmentAgentConfigID!`](#remotedevelopmentremotedevelopmentagentconfigid) | Global ID of the remote development agent config. |
| <a id="remotedevelopmentagentconfigmaxhoursbeforeterminationlimit"></a>`maxHoursBeforeTerminationLimit` | [`Int!`](#int) | Max hours before worksapce termination limit of the remote development agent config. |
| <a id="remotedevelopmentagentconfignetworkpolicyenabled"></a>`networkPolicyEnabled` | [`Boolean!`](#boolean) | Whether the network policy of the remote development agent config is enabled. |
| <a id="remotedevelopmentagentconfigprojectid"></a>`projectId` | [`ID`](#id) | ID of the project that the remote development agent config belongs to. |
| <a id="remotedevelopmentagentconfigupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of the last update to any mutable remote development agent config property. |
| <a id="remotedevelopmentagentconfigworkspacesperuserquota"></a>`workspacesPerUserQuota` | [`Int!`](#int) | Maximum number of workspaces per user. |
| <a id="remotedevelopmentagentconfigworkspacesquota"></a>`workspacesQuota` | [`Int!`](#int) | Maximum number of workspaces for the GitLab agent. |
### `Repository`
#### Fields
@ -38683,6 +38706,12 @@ A `ReleasesLinkID` is a global ID. It is encoded as a string.
An example `ReleasesLinkID` is: `"gid://gitlab/Releases::Link/1"`.
### `RemoteDevelopmentRemoteDevelopmentAgentConfigID`
A `RemoteDevelopmentRemoteDevelopmentAgentConfigID` is a global ID. It is encoded as a string.
An example `RemoteDevelopmentRemoteDevelopmentAgentConfigID` is: `"gid://gitlab/RemoteDevelopment::RemoteDevelopmentAgentConfig/1"`.
### `RemoteDevelopmentWorkspaceID`
A `RemoteDevelopmentWorkspaceID` is a global ID. It is encoded as a string.

View File

@ -35,7 +35,7 @@ Parameters:
| `search` | string | no | Return the list of authorized groups matching the search criteria |
| `order_by` | string | no | Order groups by `name`, `path`, `id`, or `similarity`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
| `statistics` | boolean | no | Include group statistics (administrators only).<br>*Note:* The REST API response does not provide the full `RootStorageStatistics` data that is shown in the UI. To match the data in the UI, use GraphQL instead of REST. For more information, see the [Group GraphQL API resources](../api/graphql/reference/index.md#group).|
| `statistics` | boolean | no | Include group statistics (administrators only).<br>*Note:* For top-level groups, the response returns the full `root_storage_statistics` data displayed in the UI. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/469254) in GitLab 17.3. |
| `visibility` | string | no | Limit to groups with `public`, `internal`, or `private` visibility. |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (administrators only) |
| `owned` | boolean | no | Limit to groups explicitly owned by the current user |
@ -95,7 +95,7 @@ GET /groups
]
```
When adding the parameter `statistics=true` and the authenticated user is an administrator, additional group statistics are returned.
When adding the parameter `statistics=true` and the authenticated user is an administrator, additional group statistics are returned. For top-level groups, `root_storage_statistics` are added as well.
```plaintext
GET /groups?statistics=true
@ -154,6 +154,20 @@ GET /groups?statistics=true
"snippets_size": 50,
"uploads_size": 0
},
"root_storage_statistics": {
"build_artifacts_size": 0,
"container_registry_size": 0,
"container_registry_size_is_estimated": false,
"dependency_proxy_size": 0,
"lfs_objects_size": 0,
"packages_size": 0,
"pipeline_artifacts_size": 0,
"repository_size": 0,
"snippets_size": 0,
"storage_size": 0,
"uploads_size": 0,
"wiki_size": 0
},
"wiki_access_level": "private",
"duo_features_enabled": true,
"lock_duo_features_enabled": false,

View File

@ -86,9 +86,24 @@ For more information about upgrading GitLab Helm Chart, see [the release notes f
## 16.11.0
- A [`groups_direct` field was added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/146881)
to the [JSON web token (ID token)](../../ci/secrets/id_token_authentication.md).
- If you use GitLab CI/CD ID tokens to authenticate with third party services,
this change can cause the HTTP header size to increase. Proxy servers might reject
the request if the headers get too big.
- If possible, increase the header limit on the receiving system.
- See [issue 467253](https://gitlab.com/gitlab-org/gitlab/-/issues/467253) for more details.
- After upgrading to GitLab 16.11 some users with large environments and databases experience
timeouts loading source code pages in the web UI.
- These timeouts are caused by slow PostgreSQL queries for pipeline data, which then
exceed the internal 60 second timeout.
- You can still clone Git repositories, and other requests for repository data works.
- See [issue 472420](https://gitlab.com/gitlab-org/gitlab/-/issues/472420) for more details,
including steps to confirm you're affected and housekeeping to run in PostgreSQL to correct it.
### Linux package installations
In GitLab 16.11, PostgreSQL will automatically be upgraded to 14.x except for the following cases:
In GitLab 16.11, PostgreSQL is automatically upgraded to 14.x except for the following cases:
- You are running the database in high availability using Patroni.
- Your database nodes are part of a GitLab Geo configuration.

View File

@ -716,6 +716,7 @@ Container Scanning for Registry populates the Vulnerability Report only when a n
- You must have at least the Maintainer role in a project to enable Container Scanning for Registry.
- The project being used must not be empty. If you are utilizing an empty project solely for storing container images, this feature won't function as intended. As a workaround, ensure the project contains an initial commit on the default branch.
- By default there is a limit of `50` scans per project per day.
- You must [configure container registry notifications](../../../administration/packages/container_registry.md#configure-container-registry-notifications).
### Enabling Container Scanning for Registry

View File

@ -709,7 +709,7 @@ To enable the analyzer, either:
- Create a [scan execution policy](../policies/scan_execution_policies.md) that enforces dependency
scanning.
- Edit the `.gitlab-ci.yml` file manually.
- [Use CI/CD components](#use-cicd-components) (Android projects only)
- [Use CI/CD components](#use-cicd-components)
#### Use a preconfigured merge request
@ -771,9 +771,9 @@ Pipelines now include a Dependency Scanning job.
Use [CI/CD components](../../../ci/components/index.md) to perform Dependency Scanning of your
application. For instructions, see the respective component's README file.
##### Available CI/CD components per language and package manager
##### Available CI/CD components
- [Android applications](https://gitlab.com/explore/catalog/components/android-dependency-scanning)
See <https://gitlab.com/explore/catalog/components/dependency-scanning>
### Running jobs in merge request pipelines

View File

@ -44,6 +44,15 @@ module API
expose :uploads_size
end
end
expose :root_storage_statistics, using: Entities::Namespace::RootStorageStatistics,
if: ->(group, opts) {
expose_root_storage_statistics?(group, opts)
}
def expose_root_storage_statistics?(group, opts)
opts[:statistics] && group.root?
end
end
end
end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
module API
module Entities
class Namespace
class RootStorageStatistics < Grape::Entity
expose :build_artifacts_size, documentation: { type: 'integer', desc: 'CI artifacts size in bytes.' }
expose :container_registry_size, documentation: { type: 'integer', desc: 'Container Registry size in bytes.' }
expose :registry_size_estimated,
as: :container_registry_size_is_estimated,
documentation: { type: 'boolean',
desc: 'Indicates whether the deduplicated Container Registry size for ' \
'the namespace is an estimated value or not.' }
expose :dependency_proxy_size, documentation: { type: 'integer', desc: 'Dependency Proxy sizes in bytes.' }
expose :lfs_objects_size, documentation: { type: 'integer', desc: 'LFS objects size in bytes.' }
expose :packages_size, documentation: { type: 'integer', desc: 'Packages size in bytes.' }
expose :pipeline_artifacts_size,
documentation: { type: 'integer', desc: 'CI pipeline artifacts size in bytes.' }
expose :repository_size, documentation: { type: 'integer', desc: 'Git repository size in bytes.' }
expose :snippets_size, documentation: { type: 'integer', desc: 'Snippets size in bytes.' }
expose :storage_size, documentation: { type: 'integer', desc: 'Total storage in bytes.' }
expose :uploads_size, documentation: { type: 'integer', desc: 'Uploads size in bytes.' }
expose :wiki_size, documentation: { type: 'integer', desc: 'Wiki size in bytes.' }
end
end
end
end

View File

@ -1533,9 +1533,6 @@ msgstr ""
msgid "%{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}v*%{code_tag_end} or %{code_tag_start}*-release%{code_tag_end} are supported."
msgstr ""
msgid "'%{data_pointer}' must be a valid '%{type}'"
msgstr ""
msgid "'%{group_name}' has been scheduled for removal on %{removal_time}."
msgstr ""
@ -64528,6 +64525,9 @@ msgstr ""
msgid "should have length between 16 to 24 characters."
msgstr ""
msgid "should use only one of: %{requirements}"
msgstr ""
msgid "show %{count} more"
msgstr ""
@ -64707,6 +64707,9 @@ msgstr ""
msgid "v%{version} published %{timeAgo}"
msgstr ""
msgid "value at %{data_pointer} should use only one of: %{requirements}"
msgstr ""
msgid "value for '%{storage}' must be an integer"
msgstr ""

View File

@ -1,9 +1,11 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Draggable from 'vuedraggable';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { isLoggedIn } from '~/lib/utils/common_utils';
import WorkItemChildrenWrapper from '~/work_items/components/work_item_links/work_item_children_wrapper.vue';
import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
@ -18,6 +20,8 @@ import {
workItemByIidResponseFactory,
} from '../../mock_data';
jest.mock('~/lib/utils/common_utils');
describe('WorkItemChildrenWrapper', () => {
let wrapper;
@ -30,6 +34,8 @@ describe('WorkItemChildrenWrapper', () => {
.mockResolvedValue(changeWorkItemParentMutationResponse);
const findWorkItemLinkChildItems = () => wrapper.findAllComponents(WorkItemLinkChild);
const findDraggable = () => wrapper.findComponent(Draggable);
const findChildItemsContainer = () => wrapper.findByTestId('child-items-container');
Vue.use(VueApollo);
@ -38,6 +44,8 @@ describe('WorkItemChildrenWrapper', () => {
confidential = false,
children = childrenWorkItems,
mutationHandler = updateWorkItemMutationHandler,
disableContent = false,
canUpdate = false,
} = {}) => {
const mockApollo = createMockApollo([
[workItemByIidQuery, getWorkItemQueryHandler],
@ -62,6 +70,8 @@ describe('WorkItemChildrenWrapper', () => {
workItemIid: '1',
confidential,
children,
disableContent,
canUpdate,
},
mocks: {
$toast,
@ -114,6 +124,44 @@ describe('WorkItemChildrenWrapper', () => {
},
);
it('does not render draggable component when user is not logged in', () => {
createComponent({ canUpdate: true });
expect(findDraggable().exists()).toBe(false);
});
it('disables list when `disableContent` is true', () => {
createComponent({ disableContent: true });
expect(findChildItemsContainer().classes('disabled-content')).toBe(true);
});
describe('when user is logged in', () => {
beforeEach(() => {
isLoggedIn.mockReturnValue(true);
});
it('renders draggable component without disabling the list', () => {
createComponent({ canUpdate: true });
expect(findDraggable().exists()).toBe(true);
expect(findDraggable().classes('disabled-content')).toBe(false);
});
it('does not render draggable component when user has no permission', () => {
createComponent({ canUpdate: false });
expect(findDraggable().exists()).toBe(false);
});
it('disables the list when `disableContent` is true', () => {
createComponent({ disableContent: true, canUpdate: true });
expect(findDraggable().exists()).toBe(true);
expect(findDraggable().classes('disabled-content')).toBe(true);
});
});
describe('when removing child work item', () => {
const workItem = { id: 'gid://gitlab/WorkItem/2' };

View File

@ -152,6 +152,19 @@ describe('WorkItemLinksForm', () => {
);
describe('creating a new work item', () => {
const submitForm = ({ title, fullPath }) => {
findInput().vm.$emit('input', title);
if (fullPath) {
findProjectSelector().vm.$emit('selectProject', fullPath);
}
// Trigger form submission
findForm().vm.$emit('submit', {
preventDefault: jest.fn(),
});
};
describe('for project level work items', () => {
beforeEach(async () => {
await createComponent();
@ -171,11 +184,10 @@ describe('WorkItemLinksForm', () => {
expect(findFormGroup().props('invalidFeedback')).toBe(null);
expect(findInput().props('state')).toBe(true);
findInput().vm.$emit('input', 'Create task test');
// Trigger form submission
findForm().vm.$emit('submit', {
preventDefault: jest.fn(),
});
submitForm({ title: 'Create task test' });
expect(wrapper.emitted('update-in-progress')).toEqual([[true]]);
await waitForPromises();
expect(findFormGroup().props('state')).toBe(false);
@ -183,14 +195,14 @@ describe('WorkItemLinksForm', () => {
'Something went wrong when trying to create a child. Please try again.',
);
expect(findInput().props('state')).toBe(false);
expect(wrapper.emitted('update-in-progress')[1]).toEqual([false]);
});
it('creates child task in non confidential parent and closes the form', async () => {
findInput().vm.$emit('input', 'Create task test');
submitForm({ title: 'Create task test' });
expect(wrapper.emitted('update-in-progress')).toEqual([[true]]);
findForm().vm.$emit('submit', {
preventDefault: jest.fn(),
});
await waitForPromises();
expect(createMutationResolver).toHaveBeenCalledWith({
@ -206,17 +218,18 @@ describe('WorkItemLinksForm', () => {
});
expect(wrapper.emitted('addChild')).toEqual([[]]);
expect(wrapper.emitted('cancel')).toEqual([[]]);
expect(wrapper.emitted('update-in-progress')[1]).toEqual([false]);
});
it('creates child task in confidential parent', async () => {
await createComponent({ parentConfidential: true });
findInput().vm.$emit('input', 'Create confidential task');
submitForm({ title: 'Create confidential task' });
expect(wrapper.emitted('update-in-progress')).toEqual([[true]]);
findForm().vm.$emit('submit', {
preventDefault: jest.fn(),
});
await waitForPromises();
expect(wrapper.vm.childWorkItemType).toEqual(workItemTypeIdForTask);
expect(createMutationResolver).toHaveBeenCalledWith({
input: {
@ -229,6 +242,7 @@ describe('WorkItemLinksForm', () => {
confidential: true,
},
});
expect(wrapper.emitted('update-in-progress')[1]).toEqual([false]);
});
});
@ -250,13 +264,9 @@ describe('WorkItemLinksForm', () => {
});
it('creates child issue in non confidential parent and closes the form', async () => {
findInput().vm.$emit('input', 'Create issue test');
submitForm({ title: 'Create issue test', fullPath: projectData[0].fullPath });
findProjectSelector().vm.$emit('selectProject', projectData[0].fullPath);
findForm().vm.$emit('submit', {
preventDefault: jest.fn(),
});
expect(wrapper.emitted('update-in-progress')).toEqual([[true]]);
await waitForPromises();
@ -272,6 +282,7 @@ describe('WorkItemLinksForm', () => {
},
});
expect(wrapper.emitted('addChild')).toEqual([[]]);
expect(wrapper.emitted('update-in-progress')[1]).toEqual([false]);
expect(wrapper.emitted('cancel')).toEqual([[]]);
});
@ -283,13 +294,9 @@ describe('WorkItemLinksForm', () => {
childrenType: WORK_ITEM_TYPE_ENUM_ISSUE,
});
findInput().vm.$emit('input', 'Create confidential issue');
submitForm({ title: 'Create confidential issue', fullPath: projectData[0].fullPath });
findProjectSelector().vm.$emit('selectProject', projectData[0].fullPath);
findForm().vm.$emit('submit', {
preventDefault: jest.fn(),
});
expect(wrapper.emitted('update-in-progress')).toEqual([[true]]);
await waitForPromises();
@ -304,6 +311,7 @@ describe('WorkItemLinksForm', () => {
confidential: true,
},
});
expect(wrapper.emitted('update-in-progress')[1]).toEqual([false]);
});
});

View File

@ -4,21 +4,67 @@ require 'spec_helper'
RSpec.describe API::Entities::Group, feature_category: :groups_and_projects do
let_it_be(:group) do
base_group = create(:group) { |g| create(:project_statistics, namespace_id: g.id) }
base_group = create(:group) do |g|
create(:project_statistics, namespace_id: g.id)
create(:namespace_root_storage_statistics, namespace_id: g.id)
end
Group.with_statistics.find(base_group.id)
end
subject(:json) { described_class.new(group, { with_custom_attributes: true, statistics: true }).as_json }
subject(:json) { described_class.new(group, options).as_json }
it 'returns expected data' do
expect(json.keys).to(
include(
:organization_id, :path, :description, :visibility, :share_with_group_lock, :require_two_factor_authentication,
:two_factor_grace_period, :project_creation_level, :auto_devops_enabled,
:subgroup_creation_level, :emails_disabled, :emails_enabled, :lfs_enabled, :default_branch_protection,
:default_branch_protection_defaults, :avatar_url, :request_access_enabled, :full_name, :full_path, :created_at,
:parent_id, :organization_id, :shared_runners_setting, :custom_attributes, :statistics, :default_branch
context 'with statistics' do
let(:options) { { with_custom_attributes: true, statistics: true } }
it 'returns expected data' do
expect(json.keys).to(
include(
:organization_id, :path, :description, :visibility, :share_with_group_lock,
:require_two_factor_authentication, :two_factor_grace_period, :project_creation_level, :auto_devops_enabled,
:subgroup_creation_level, :emails_disabled, :emails_enabled, :lfs_enabled, :default_branch_protection,
:default_branch_protection_defaults, :avatar_url, :request_access_enabled, :full_name, :full_path,
:created_at, :parent_id, :organization_id, :shared_runners_setting, :custom_attributes, :statistics,
:default_branch, :root_storage_statistics
)
)
)
end
context 'on a sub-group' do
let(:subgroup) do
subgroup = create(:group, parent: group, path: "#{group.path}-subgroup") do |g|
create(:project_statistics, namespace_id: g.id)
end
Group.with_statistics.find(subgroup.id)
end
subject(:json) { described_class.new(subgroup, options).as_json }
it 'does not expose root storage statistics' do
expect(json.keys).not_to(include(:root_storage_statistics))
end
end
context 'on a group without root storage statistics' do
let(:group_without_root_storage_statistics) do
base_group = create(:group) do |g|
create(:project_statistics, namespace_id: g.id)
end
Group.with_statistics.find(base_group.id)
end
subject(:json) { described_class.new(group_without_root_storage_statistics, options).as_json }
it 'returns nil for root storage statistics' do
expect(json[:root_storage_statistics]).to be_nil
end
end
end
context 'without statistics' do
let(:options) { { with_custom_attributes: true, statistics: false } }
it 'does not expose statistics' do
expect(json.keys).not_to(include(:statistics, :root_storage_statistics))
end
end
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Entities::Namespace::RootStorageStatistics, feature_category: :groups_and_projects do
let(:root_storage_statistics) { create(:namespace_root_storage_statistics) }
subject(:entity) { described_class.new(root_storage_statistics).as_json }
it 'exposes correct attributes' do
expect(entity.keys).to(
include(
:build_artifacts_size,
:container_registry_size,
:container_registry_size_is_estimated,
:dependency_proxy_size,
:lfs_objects_size,
:packages_size,
:pipeline_artifacts_size,
:repository_size,
:snippets_size,
:storage_size,
:uploads_size,
:wiki_size
)
)
end
end

View File

@ -136,7 +136,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do
it 'raises an error' do
expect(entry).not_to be_valid
expect(entry.errors.first)
.to match %r{image executor opts '/docker/platform' must be a valid 'string'}
.to match %r{image executor opts value at `/docker/platform` is not a string}
end
end
end
@ -184,7 +184,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do
it 'raises an error' do
expect(entry).not_to be_valid
expect(entry.errors.first)
.to match %r{image executor opts '/docker/user' must be a valid 'string'}
.to match %r{image executor opts value at `/docker/user` is not a string}
end
end
end
@ -194,8 +194,9 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do
it 'is not valid' do
expect(entry).not_to be_valid
expect(entry.errors.first)
.to match %r{image executor opts '/docker/unknown_key' must be a valid 'schema'}
expect(entry.errors.first).to match(
%r{image executor opts object property at `/docker/unknown_key` is a disallowed additional property}
)
end
end
end

View File

@ -270,14 +270,34 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_compo
using RSpec::Parameterized::TableSyntax
where(:case_name, :config, :error) do
'when only step is used without name' | { stage: 'build',
run: [{ step: 'some reference' }] } | 'job run \'/0\' must be a valid \'required\''
'when only script is used without name' | { stage: 'build',
run: [{ script: 'echo' }] } | 'job run \'/0\' must be a valid \'required\''
'when step and script are used together' | { stage: 'build',
run: [{ name: 'step1', step: 'some reference', script: 'echo' }] } | 'job run \'/0\' must be a valid \'oneof\''
'when a subkey does not exist' | { stage: 'build',
run: [{ name: 'step1', invalid_key: 'some value' }] } | 'job run \'/0\' must be a valid \'required\''
'when only step is used without name' | {
stage: 'build',
run: [{ step: 'some reference' }]
} | 'job run object at `/0` is missing required properties: name'
'when only script is used without name' | {
stage: 'build',
run: [{ script: 'echo' }]
} | 'job run object at `/0` is missing required properties: name'
'when step and script are used together' | {
stage: 'build',
run: [{
name: 'step1',
step: 'some reference',
script: 'echo'
}]
} | 'job run value at /0 should use only one of: step, script'
'when a required subkey is missing' | {
stage: 'build',
run: [{ name: 'step1' }]
} | 'job run object at `/0` is missing required properties: step'
'when a subkey is invalid' | {
stage: 'build',
run: [{ name: 'step1', step: 'some step', invalid_key: 'some value' }]
} | 'job run object property at `/0/invalid_key` is a disallowed additional property'
end
with_them do
@ -292,7 +312,7 @@ run: [{ name: 'step1', invalid_key: 'some value' }] } | 'job run \'/0\' must be
it 'returns error about invalid run' do
expect(entry).not_to be_valid
expect(entry.errors).to include 'job run \'\' must be a valid \'array\''
expect(entry.errors).to include 'job run value at root is not an array'
end
end
@ -312,7 +332,7 @@ run: [{ name: 'step1', invalid_key: 'some value' }] } | 'job run \'/0\' must be
it 'returns error about invalid env' do
expect(entry).not_to be_valid
expect(entry.errors).to include 'job run \'/0/env/my_var\' must be a valid \'string\''
expect(entry.errors).to include 'job run value at `/0/env/my_var` is not a string'
end
end
@ -321,7 +341,7 @@ run: [{ name: 'step1', invalid_key: 'some value' }] } | 'job run \'/0\' must be
it 'returns error about invalid run' do
expect(entry).not_to be_valid
expect(entry.errors).to include 'job run \'/0\' must be a valid \'required\''
expect(entry.errors).to include 'job run object at `/0` is missing required properties: step'
end
end
end

View File

@ -204,7 +204,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do
it 'is not valid' do
expect(entry).not_to be_valid
expect(entry.errors.first)
.to match %r{service executor opts '/docker/invalid' must be a valid 'schema'}
.to match %r{service executor opts object property at `/docker/invalid` is a disallowed additional property}
end
end
end
@ -216,7 +216,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do
it 'is not valid' do
expect(entry).not_to be_valid
expect(entry.errors.first)
.to match %r{service executor opts '/docker/platform' must be a valid 'string'}
.to match %r{service executor opts value at `/docker/platform` is not a string}
end
end
end

View File

@ -137,7 +137,7 @@ RSpec.describe Ci::CreatePipelineService, :ci_config_feature_flag_correctness,
it 'returns errors for invalid configuration' do
expect(pipeline).not_to be_created_successfully
expect(pipeline.errors.full_messages).to include(
"jobs:job run '/0' must be a valid 'required'"
"jobs:job run object at `/0` is missing required properties: name"
)
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe JsonSchemaValidator do
RSpec.describe JsonSchemaValidator, feature_category: :shared do
describe '#validates_each' do
let(:build_report_result) { build(:ci_build_report_result, :with_junit_success) }
@ -78,10 +78,140 @@ RSpec.describe JsonSchemaValidator do
expect(build_report_result.errors.size).to eq(1)
expect(build_report_result.errors.full_messages).to match_array(
["Data '/invalid' must be a valid 'schema'"]
["Data object property at `/invalid` is a disallowed additional property"]
)
end
end
end
context 'when validating config with oneOf JSON schema' do
let(:config) do
{
run: [
{
name: 'hello_steps',
step: 'gitlab.com/gitlab-org/ci-cd/runner-tools/echo-step',
inputs: {
echo: 'hello steps!'
}
}
]
}
end
let(:job) { Gitlab::Ci::Config::Entry::Job.new(config, name: :rspec) }
let(:errors) { ActiveModel::Errors.new(job) }
let(:validator) do
described_class.new(
attributes: [:run],
base_directory: 'app/validators/json_schemas',
filename: 'run_steps',
hash_conversion: true,
detail_errors: true
)
end
before do
job.compose!
allow(job).to receive(:errors).and_return(errors)
end
subject { validator.validate(job) }
context 'when the value is a valid array of hashes' do
before do
allow(job).to receive(:read_attribute_for_validation).and_return(config[:run])
end
it 'returns no errors' do
subject
expect(job.errors).to be_empty
end
end
context 'when a required property is missing' do
before do
config[:run] = [{ name: 'hello_steps' }]
allow(job).to receive(:read_attribute_for_validation).and_return(config[:run])
end
it 'returns an error message' do
subject
expect(job.errors).not_to be_empty
expect("#{job.errors.first.attribute} #{job.errors.first.type}").to eq("run object at `/0` is missing required properties: step")
end
end
context 'when oneOf validation fails' do
before do
config[:run] = [nil]
allow(job).to receive(:read_attribute_for_validation).and_return(config[:run])
end
it 'returns an error message' do
subject
expect(job.errors).not_to be_empty
expect("#{job.errors.first.attribute} #{job.errors.first.type}").to eq(
"run value at /0 should use only one of: step, script"
)
end
end
context 'when there is a general validation error' do
before do
config[:run] = 'not an array'
allow(job).to receive(:read_attribute_for_validation).and_return(config[:run])
end
it 'returns an error message' do
subject
expect(job.errors).not_to be_empty
expect("#{job.errors.first.attribute} #{job.errors.first.type}").to eq("run value at root is not an array")
end
end
context 'when a non-array value violates oneOf constraint' do
let(:schema) do
{
"type" => "object",
"properties" => {
"run" => {
"oneOf" => [
{ required: ["step"], title: "step" },
{ required: ["script"], title: "script" }
]
}
}
}
end
let(:validator) do
described_class.new(
attributes: [:run],
filename: 'test_schema',
detail_errors: true
)
end
before do
config[:run] = 'C'
allow(job).to receive(:read_attribute_for_validation).and_return({ run: config[:run] })
allow(JSONSchemer).to receive(:schema).and_return(JSONSchemer.schema(schema))
allow(File).to receive(:read).with(anything).and_return(schema.to_json)
end
it 'returns an error message for oneOf violation without data pointer' do
subject
expect(job.errors).not_to be_empty
expect("#{job.errors.first.attribute} #{job.errors.first.type}").to eq("run should use only one of: step, script")
end
end
end
end
end

View File

@ -25,7 +25,7 @@ require (
github.com/sirupsen/logrus v1.9.3
github.com/smartystreets/goconvey v1.8.1
github.com/stretchr/testify v1.9.0
gitlab.com/gitlab-org/gitaly/v16 v16.11.7
gitlab.com/gitlab-org/gitaly/v16 v16.11.8
gitlab.com/gitlab-org/labkit v1.21.0
go.uber.org/goleak v1.3.0
gocloud.dev v0.38.0

View File

@ -486,8 +486,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
gitlab.com/gitlab-org/gitaly/v16 v16.11.7 h1:csL3bPTHIEE0147aCo1HFgNLMgmHwlwr1Ns65OlcMuE=
gitlab.com/gitlab-org/gitaly/v16 v16.11.7/go.mod h1:lJizRUtXRd1SBHjNbbbL9OsGN4TiugvfRBd8bIsdWI0=
gitlab.com/gitlab-org/gitaly/v16 v16.11.8 h1:bL9F90+rXTlQcsSuZJivn+CIwKGXXc787IJi4g3XQEU=
gitlab.com/gitlab-org/gitaly/v16 v16.11.8/go.mod h1:lJizRUtXRd1SBHjNbbbL9OsGN4TiugvfRBd8bIsdWI0=
gitlab.com/gitlab-org/labkit v1.21.0 h1:hLmdBDtXjD1yOmZ+uJOac3a5Tlo83QaezwhES4IYik4=
gitlab.com/gitlab-org/labkit v1.21.0/go.mod h1:zeATDAaSBelPcPLbTTq8J3ZJEHyPTLVBM1q3nva+/W4=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=