Commit f2b4e1cd authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 08031760 1b33e388
<script> <script>
import { GlAlert, GlLink, GlSprintf, GlEmptyState } from '@gitlab/ui'; import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { mapState, mapMutations } from 'vuex'; import { mapState, mapMutations } from 'vuex';
import { retrieveAlert } from '~/jira_connect/subscriptions/utils'; import { retrieveAlert } from '~/jira_connect/subscriptions/utils';
import { SET_ALERT } from '../store/mutation_types'; import { SET_ALERT } from '../store/mutation_types';
import SubscriptionsList from './subscriptions_list.vue'; import SignInPage from '../pages/sign_in.vue';
import AddNamespaceButton from './add_namespace_button.vue'; import SubscriptionsPage from '../pages/subscriptions.vue';
import SignInButton from './sign_in_button.vue';
import UserLink from './user_link.vue'; import UserLink from './user_link.vue';
import CompatibilityAlert from './compatibility_alert.vue'; import CompatibilityAlert from './compatibility_alert.vue';
...@@ -16,12 +15,10 @@ export default { ...@@ -16,12 +15,10 @@ export default {
GlAlert, GlAlert,
GlLink, GlLink,
GlSprintf, GlSprintf,
GlEmptyState,
SubscriptionsList,
AddNamespaceButton,
SignInButton,
UserLink, UserLink,
CompatibilityAlert, CompatibilityAlert,
SignInPage,
SubscriptionsPage,
}, },
inject: { inject: {
usersPath: { usersPath: {
...@@ -84,43 +81,9 @@ export default { ...@@ -84,43 +81,9 @@ export default {
<user-link :user-signed-in="userSignedIn" :has-subscriptions="hasSubscriptions" /> <user-link :user-signed-in="userSignedIn" :has-subscriptions="hasSubscriptions" />
<h2 class="gl-text-center gl-mb-7">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2> <h2 class="gl-text-center gl-mb-7">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2>
<div class="jira-connect-app-body gl-mx-auto gl-px-5 gl-mb-7"> <div class="gl-layout-w-limited gl-mx-auto gl-px-5 gl-mb-7">
<template v-if="hasSubscriptions"> <sign-in-page v-if="!userSignedIn" :has-subscriptions="hasSubscriptions" />
<div class="gl-display-flex gl-justify-content-end"> <subscriptions-page v-else :has-subscriptions="hasSubscriptions" />
<sign-in-button v-if="!userSignedIn" :users-path="usersPath" />
<add-namespace-button v-else />
</div>
<subscriptions-list />
</template>
<template v-else>
<div v-if="!userSignedIn" class="gl-text-center">
<p class="gl-mb-7">{{ s__('JiraService|Sign in to GitLab.com to get started.') }}</p>
<sign-in-button class="gl-mb-7" :users-path="usersPath">
{{ __('Sign in to GitLab') }}
</sign-in-button>
<p>
{{
s__(
'Integrations|Note: this integration only works with accounts on GitLab.com (SaaS).',
)
}}
</p>
</div>
<gl-empty-state
v-else
:title="s__('Integrations|No linked namespaces')"
:description="
s__(
'Integrations|Namespaces are the GitLab groups and subgroups you link to this Jira instance.',
)
"
>
<template #actions>
<add-namespace-button />
</template>
</gl-empty-state>
</template>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import { getGitlabSignInURL } from '~/jira_connect/subscriptions/utils'; import { getGitlabSignInURL } from '~/jira_connect/subscriptions/utils';
import { s__ } from '~/locale';
export default { export default {
components: { components: {
...@@ -25,12 +26,15 @@ export default { ...@@ -25,12 +26,15 @@ export default {
this.signInURL = await getGitlabSignInURL(this.usersPath); this.signInURL = await getGitlabSignInURL(this.usersPath);
}, },
}, },
i18n: {
defaultButtonText: s__('Integrations|Sign in to GitLab'),
},
}; };
</script> </script>
<template> <template>
<gl-button category="primary" variant="info" :href="signInURL" target="_blank"> <gl-button category="primary" variant="info" :href="signInURL" target="_blank">
<slot> <slot>
{{ s__('Integrations|Sign in to add namespaces') }} {{ $options.i18n.defaultButtonText }}
</slot> </slot>
</gl-button> </gl-button>
</template> </template>
<script>
import { s__ } from '~/locale';
import SubscriptionsList from '../components/subscriptions_list.vue';
import SignInButton from '../components/sign_in_button.vue';
export default {
name: 'SignInPage',
components: {
SubscriptionsList,
SignInButton,
},
inject: ['usersPath'],
props: {
hasSubscriptions: {
type: Boolean,
required: true,
},
},
i18n: {
signinButtonTextWithSubscriptions: s__('Integrations|Sign in to add namespaces'),
signInText: s__('JiraService|Sign in to GitLab.com to get started.'),
},
};
</script>
<template>
<div v-if="hasSubscriptions">
<div class="gl-display-flex gl-justify-content-end">
<sign-in-button :users-path="usersPath">
{{ $options.i18n.signinButtonTextWithSubscriptions }}
</sign-in-button>
</div>
<subscriptions-list />
</div>
<div v-else class="gl-text-center">
<p class="gl-mb-7">{{ $options.i18n.signInText }}</p>
<sign-in-button class="gl-mb-7" :users-path="usersPath" />
</div>
</template>
<script>
import { GlEmptyState } from '@gitlab/ui';
import SubscriptionsList from '../components/subscriptions_list.vue';
import AddNamespaceButton from '../components/add_namespace_button.vue';
export default {
name: 'SubscriptionsPage',
components: {
GlEmptyState,
SubscriptionsList,
AddNamespaceButton,
},
props: {
hasSubscriptions: {
type: Boolean,
required: true,
},
},
};
</script>
<template>
<div v-if="hasSubscriptions">
<div class="gl-display-flex gl-justify-content-end">
<add-namespace-button />
</div>
<subscriptions-list />
</div>
<gl-empty-state
v-else
:title="s__('Integrations|No linked namespaces')"
:description="
s__(
'Integrations|Namespaces are the GitLab groups and subgroups you link to this Jira instance.',
)
"
>
<template #actions>
<add-namespace-button />
</template>
</gl-empty-state>
</template>
...@@ -40,10 +40,6 @@ $header-height: 40px; ...@@ -40,10 +40,6 @@ $header-height: 40px;
max-width: 1000px; max-width: 1000px;
} }
.jira-connect-app-body {
max-width: 768px;
}
// needed for external_link // needed for external_link
svg.s16 { svg.s16 {
width: 16px; width: 16px;
......
...@@ -92,6 +92,8 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -92,6 +92,8 @@ class Projects::CommitController < Projects::ApplicationController
end end
def branches def branches
return git_not_found! unless commit
# branch_names_contains/tag_names_contains can take a long time when there are thousands of # branch_names_contains/tag_names_contains can take a long time when there are thousands of
# branches/tags - each `git branch --contains xxx` request can consume a cpu core. # branches/tags - each `git branch --contains xxx` request can consume a cpu core.
# so only do the query when there are a manageable number of branches/tags # so only do the query when there are a manageable number of branches/tags
......
...@@ -178,6 +178,10 @@ class Blob < SimpleDelegator ...@@ -178,6 +178,10 @@ class Blob < SimpleDelegator
end end
end end
def symlink?
mode == MODE_SYMLINK
end
def extension def extension
@extension ||= extname.downcase.delete('.') @extension ||= extname.downcase.delete('.')
end end
......
...@@ -21,7 +21,7 @@ module Branches ...@@ -21,7 +21,7 @@ module Branches
error("Failed to create branch '#{branch_name}': invalid reference name '#{ref}'") error("Failed to create branch '#{branch_name}': invalid reference name '#{ref}'")
end end
rescue Gitlab::Git::PreReceiveError => e rescue Gitlab::Git::PreReceiveError => e
Gitlab::ErrorTracking.track_exception(e, pre_receive_message: e.raw_message, branch_name: branch_name, ref: ref) Gitlab::ErrorTracking.log_exception(e, pre_receive_message: e.raw_message, branch_name: branch_name, ref: ref)
error(e.message) error(e.message)
end end
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
= copy_file_path_button(blob.path) = copy_file_path_button(blob.path)
%small.mr-1 %small.mr-1
- if blob.mode == Blob::MODE_SYMLINK - if blob.symlink?
= _('Symbolic link') << ' ·' = _('Symbolic link') << ' ·'
= number_to_human_size(blob.raw_size) = number_to_human_size(blob.raw_size)
......
...@@ -316,6 +316,11 @@ Instead of: ...@@ -316,6 +316,11 @@ Instead of:
Use **expand** instead of **open** when you are talking about expanding or collapsing a section in the UI. Use **expand** instead of **open** when you are talking about expanding or collapsing a section in the UI.
## FAQ
We want users to find information quickly, and they rarely search for the term **FAQ**.
Information in FAQs belongs with other similar information, under an easily searchable topic title.
## field ## field
Use **box** instead of **field** or **text box**. Use **box** instead of **field** or **text box**.
...@@ -371,6 +376,11 @@ Use title case for **GitLab Runner**. This is the product you install. See also ...@@ -371,6 +376,11 @@ Use title case for **GitLab Runner**. This is the product you install. See also
Use **GitLab self-managed** to refer to the product license for GitLab instances managed by customers themselves. Use **GitLab self-managed** to refer to the product license for GitLab instances managed by customers themselves.
## guide
We want to speak directly to users. On `docs.gitlab.com`, do not use **guide** as part of a page title.
For example, **Snowplow Guide**. Instead, speak about the feature itself, and how to use it. For example, **Use Snowplow to do xyz**.
## Guest ## Guest
When writing about the Guest role: When writing about the Guest role:
......
<script> <script>
import { GlLink, GlButton, GlLoadingIcon, GlModal, GlSprintf } from '@gitlab/ui'; import { GlLink, GlButton, GlLoadingIcon, GlModal, GlSprintf } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { GEO_INFO_URL, REMOVE_NODE_MODAL_ID } from '../constants'; import { GEO_INFO_URL, REMOVE_NODE_MODAL_ID } from '../constants';
import GeoNodesFilters from './geo_nodes_filters.vue';
import GeoNodes from './geo_nodes.vue'; import GeoNodes from './geo_nodes.vue';
import GeoNodesEmptyState from './geo_nodes_empty_state.vue'; import GeoNodesEmptyState from './geo_nodes_empty_state.vue';
...@@ -25,6 +26,7 @@ export default { ...@@ -25,6 +26,7 @@ export default {
GlLink, GlLink,
GlButton, GlButton,
GlLoadingIcon, GlLoadingIcon,
GeoNodesFilters,
GeoNodes, GeoNodes,
GeoNodesEmptyState, GeoNodesEmptyState,
GlModal, GlModal,
...@@ -42,14 +44,15 @@ export default { ...@@ -42,14 +44,15 @@ export default {
}, },
computed: { computed: {
...mapState(['nodes', 'isLoading']), ...mapState(['nodes', 'isLoading']),
...mapGetters(['filteredNodes']),
noNodes() { noNodes() {
return !this.nodes || this.nodes.length === 0; return !this.nodes || this.nodes.length === 0;
}, },
primaryNodes() { primaryNodes() {
return this.nodes.filter((n) => n.primary); return this.filteredNodes.filter((n) => n.primary);
}, },
secondaryNodes() { secondaryNodes() {
return this.nodes.filter((n) => !n.primary); return this.filteredNodes.filter((n) => !n.primary);
}, },
}, },
created() { created() {
...@@ -100,14 +103,19 @@ export default { ...@@ -100,14 +103,19 @@ export default {
<gl-loading-icon v-if="isLoading" size="xl" class="gl-mt-5" /> <gl-loading-icon v-if="isLoading" size="xl" class="gl-mt-5" />
<template v-if="!isLoading"> <template v-if="!isLoading">
<div v-if="!noNodes"> <div v-if="!noNodes">
<h4 class="gl-font-lg gl-my-5">{{ $options.i18n.primarySite }}</h4> <geo-nodes-filters :total-nodes="nodes.length" />
<h4 v-if="primaryNodes.length" class="gl-font-lg gl-my-5">
{{ $options.i18n.primarySite }}
</h4>
<geo-nodes <geo-nodes
v-for="node in primaryNodes" v-for="node in primaryNodes"
:key="node.id" :key="node.id"
:node="node" :node="node"
data-testid="primary-nodes" data-testid="primary-nodes"
/> />
<h4 class="gl-font-lg gl-my-5">{{ $options.i18n.secondarySite }}</h4> <h4 v-if="secondaryNodes.length" class="gl-font-lg gl-my-5">
{{ $options.i18n.secondarySite }}
</h4>
<geo-nodes <geo-nodes
v-for="node in secondaryNodes" v-for="node in secondaryNodes"
:key="node.id" :key="node.id"
......
<script>
import { GlTabs, GlTab, GlBadge } from '@gitlab/ui';
import { mapGetters, mapActions } from 'vuex';
import { s__ } from '~/locale';
import { HEALTH_STATUS_UI, STATUS_FILTER_QUERY_PARAM } from 'ee/geo_nodes/constants';
export default {
name: 'GeoNodesFilters',
i18n: {
allTab: s__('Geo|All'),
},
components: {
GlTabs,
GlTab,
GlBadge,
},
props: {
totalNodes: {
type: Number,
required: false,
default: 0,
},
},
computed: {
...mapGetters(['countNodesForStatus']),
tabs() {
const ALL_TAB = { text: this.$options.i18n.allTab, count: this.totalNodes, status: null };
const tabs = [ALL_TAB];
Object.entries(HEALTH_STATUS_UI).forEach(([status, tab]) => {
const count = this.countNodesForStatus(status);
if (count) {
tabs.push({ ...tab, count, status });
}
});
return tabs;
},
},
methods: {
...mapActions(['setStatusFilter']),
tabChange(tabIndex) {
this.setStatusFilter(this.tabs[tabIndex]?.status);
},
},
STATUS_FILTER_QUERY_PARAM,
};
</script>
<template>
<gl-tabs
sync-active-tab-with-query-params
:query-param-name="$options.STATUS_FILTER_QUERY_PARAM"
data-testid="geo-sites-filter"
@input="tabChange"
>
<gl-tab v-for="tab in tabs" :key="tab.text" :query-param-value="tab.status">
<template #title>
<span>{{ tab.text }}</span>
<gl-badge size="sm" class="gl-tab-counter-badge">{{ tab.count }}</gl-badge>
</template>
</gl-tab>
</gl-tabs>
</template>
...@@ -78,3 +78,5 @@ export const REPOSITORY = 'repository'; ...@@ -78,3 +78,5 @@ export const REPOSITORY = 'repository';
export const BLOB = 'blob'; export const BLOB = 'blob';
export const REMOVE_NODE_MODAL_ID = 'remove-node-modal'; export const REMOVE_NODE_MODAL_ID = 'remove-node-modal';
export const STATUS_FILTER_QUERY_PARAM = 'status';
...@@ -46,3 +46,7 @@ export const removeNode = ({ commit, state }) => { ...@@ -46,3 +46,7 @@ export const removeNode = ({ commit, state }) => {
commit(types.RECEIVE_NODE_REMOVAL_ERROR); commit(types.RECEIVE_NODE_REMOVAL_ERROR);
}); });
}; };
export const setStatusFilter = ({ commit }, status) => {
commit(types.SET_STATUS_FILTER, status);
};
...@@ -59,3 +59,21 @@ export const canRemoveNode = (state) => (id) => { ...@@ -59,3 +59,21 @@ export const canRemoveNode = (state) => (id) => {
return !node.primary || state.nodes.length === 1; return !node.primary || state.nodes.length === 1;
}; };
export const filteredNodes = (state) => {
if (!state.statusFilter) {
return state.nodes;
}
return state.nodes.filter((n) =>
n.healthStatus
? n.healthStatus.toLowerCase() === state.statusFilter
: state.statusFilter === 'unknown',
);
};
export const countNodesForStatus = (state) => (status) => {
return state.nodes.filter((n) =>
n.healthStatus ? n.healthStatus.toLowerCase() === status : status === 'unknown',
).length;
};
...@@ -8,3 +8,5 @@ export const UNSTAGE_NODE_REMOVAL = 'UNSTAGE_NODE_REMOVAL'; ...@@ -8,3 +8,5 @@ export const UNSTAGE_NODE_REMOVAL = 'UNSTAGE_NODE_REMOVAL';
export const REQUEST_NODE_REMOVAL = 'REQUEST_NODE_REMOVAL'; export const REQUEST_NODE_REMOVAL = 'REQUEST_NODE_REMOVAL';
export const RECEIVE_NODE_REMOVAL_SUCCESS = 'RECEIVE_NODE_REMOVAL_SUCCESS'; export const RECEIVE_NODE_REMOVAL_SUCCESS = 'RECEIVE_NODE_REMOVAL_SUCCESS';
export const RECEIVE_NODE_REMOVAL_ERROR = 'RECEIVE_NODE_REMOVAL_ERROR'; export const RECEIVE_NODE_REMOVAL_ERROR = 'RECEIVE_NODE_REMOVAL_ERROR';
export const SET_STATUS_FILTER = 'SET_STATUS_FILTER';
...@@ -33,4 +33,7 @@ export default { ...@@ -33,4 +33,7 @@ export default {
state.isLoading = false; state.isLoading = false;
state.nodeToBeRemoved = null; state.nodeToBeRemoved = null;
}, },
[types.SET_STATUS_FILTER](state, status) {
state.statusFilter = status;
},
}; };
...@@ -5,5 +5,6 @@ const createState = ({ primaryVersion, primaryRevision, replicableTypes }) => ({ ...@@ -5,5 +5,6 @@ const createState = ({ primaryVersion, primaryRevision, replicableTypes }) => ({
nodes: [], nodes: [],
isLoading: false, isLoading: false,
nodeToBeRemoved: null, nodeToBeRemoved: null,
statusFilter: null,
}); });
export default createState; export default createState;
...@@ -22,14 +22,14 @@ query getCorpuses( ...@@ -22,14 +22,14 @@ query getCorpuses(
id id
name name
updatedAt updatedAt
packageFiles(last: 1) { packageFiles(first: 1) {
nodes { nodes {
id id
size size
downloadPath downloadPath
} }
} }
pipelines(last: 1) { pipelines(first: 1) {
nodes { nodes {
id id
createdAt createdAt
......
...@@ -3,7 +3,7 @@ import { GlIntersperse } from '@gitlab/ui'; ...@@ -3,7 +3,7 @@ import { GlIntersperse } from '@gitlab/ui';
import { n__, s__ } from '~/locale'; import { n__, s__ } from '~/locale';
import { removeUnnecessaryDashes } from '../../utils'; import { removeUnnecessaryDashes } from '../../utils';
import { fromYaml, humanizeNetworkPolicy } from '../policy_editor/network_policy/lib'; import { fromYaml, humanizeNetworkPolicy } from '../policy_editor/network_policy/lib';
import PolicyPreview from '../policy_editor/policy_preview.vue'; import PolicyPreviewHuman from '../policy_editor/policy_preview_human.vue';
import BasePolicy from './base_policy.vue'; import BasePolicy from './base_policy.vue';
import PolicyInfoRow from './policy_info_row.vue'; import PolicyInfoRow from './policy_info_row.vue';
...@@ -12,11 +12,12 @@ export default { ...@@ -12,11 +12,12 @@ export default {
description: s__('SecurityOrchestration|Description'), description: s__('SecurityOrchestration|Description'),
network: s__('NetworkPolicies|Network'), network: s__('NetworkPolicies|Network'),
status: s__('SecurityOrchestration|Status'), status: s__('SecurityOrchestration|Status'),
summary: s__('SecurityOrchestration|Summary'),
}, },
components: { components: {
GlIntersperse, GlIntersperse,
BasePolicy, BasePolicy,
PolicyPreview, PolicyPreviewHuman,
PolicyInfoRow, PolicyInfoRow,
}, },
props: { props: {
...@@ -58,6 +59,10 @@ export default { ...@@ -58,6 +59,10 @@ export default {
<template #type>{{ $options.i18n.network }}</template> <template #type>{{ $options.i18n.network }}</template>
<template #default="{ statusLabel }"> <template #default="{ statusLabel }">
<policy-info-row :label="$options.i18n.summary">
<policy-preview-human :policy-description="humanizedPolicy" />
</policy-info-row>
<div v-if="parsedYaml"> <div v-if="parsedYaml">
<policy-info-row <policy-info-row
v-if="parsedYaml.description" v-if="parsedYaml.description"
...@@ -81,13 +86,6 @@ export default { ...@@ -81,13 +86,6 @@ export default {
</gl-intersperse> </gl-intersperse>
</policy-info-row> </policy-info-row>
</div> </div>
<policy-preview
class="gl-mt-4"
:initial-tab="initialTab"
:policy-yaml="policyYaml"
:policy-description="humanizedPolicy"
/>
</template> </template>
</base-policy> </base-policy>
</template> </template>
<script> <script>
import { GlButton, GlDrawer } from '@gitlab/ui'; import { GlButton, GlDrawer, GlTabs, GlTab } from '@gitlab/ui';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants'; import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
import { getContentWrapperHeight, removeUnnecessaryDashes } from '../../utils'; import { getContentWrapperHeight, removeUnnecessaryDashes } from '../../utils';
import { POLICIES_LIST_CONTAINER_CLASS, POLICY_TYPE_COMPONENT_OPTIONS } from '../constants'; import { POLICIES_LIST_CONTAINER_CLASS, POLICY_TYPE_COMPONENT_OPTIONS } from '../constants';
...@@ -17,6 +17,8 @@ export default { ...@@ -17,6 +17,8 @@ export default {
components: { components: {
GlButton, GlButton,
GlDrawer, GlDrawer,
GlTab,
GlTabs,
PolicyYamlEditor: () => PolicyYamlEditor: () =>
import(/* webpackChunkName: 'policy_yaml_editor' */ '../policy_yaml_editor.vue'), import(/* webpackChunkName: 'policy_yaml_editor' */ '../policy_yaml_editor.vue'),
CiliumNetworkPolicy, CiliumNetworkPolicy,
...@@ -70,7 +72,7 @@ export default { ...@@ -70,7 +72,7 @@ export default {
v-on="$listeners" v-on="$listeners"
> >
<template v-if="policy" #title> <template v-if="policy" #title>
<h3 class="gl-my-0">{{ policy.name }}</h3> <h4 class="gl-my-0 gl-mr-3">{{ policy.name }}</h4>
</template> </template>
<template v-if="policy" #header> <template v-if="policy" #header>
<gl-button <gl-button
...@@ -82,15 +84,27 @@ export default { ...@@ -82,15 +84,27 @@ export default {
>{{ s__('NetworkPolicies|Edit policy') }}</gl-button >{{ s__('NetworkPolicies|Edit policy') }}</gl-button
> >
</template> </template>
<div v-if="policy"> <gl-tabs v-if="policy" class="gl-p-0!" justified content-class="gl-py-0" lazy>
<gl-tab title="Details" class="gl-mt-5 gl-ml-6 gl-mr-3">
<component :is="policyComponent" v-if="policyComponent" :policy="policy" /> <component :is="policyComponent" v-if="policyComponent" :policy="policy" />
<div v-else> <div v-else>
<h5>{{ s__('NetworkPolicies|Policy definition') }}</h5> <h5>{{ s__('NetworkPolicies|Policy definition') }}</h5>
<p> <p>
{{ s__("NetworkPolicies|Define this policy's location, conditions and actions.") }} {{ s__("NetworkPolicies|Define this policy's location, conditions and actions.") }}
</p> </p>
<policy-yaml-editor :value="policyYaml" data-testid="policy-yaml-editor" /> <policy-yaml-editor
</div> :value="policyYaml"
data-testid="policy-yaml-editor-default-component"
/>
</div> </div>
</gl-tab>
<gl-tab v-if="policyComponent" title="Yaml">
<policy-yaml-editor
class="gl-h-100vh"
:value="policyYaml"
data-testid="policy-yaml-editor-tab-content"
/>
</gl-tab>
</gl-tabs>
</gl-drawer> </gl-drawer>
</template> </template>
...@@ -328,7 +328,10 @@ export default { ...@@ -328,7 +328,10 @@ export default {
:id="$options.policyPreviewHumanCollapseId" :id="$options.policyPreviewHumanCollapseId"
v-model="isPolicyPreviewHumanVisible" v-model="isPolicyPreviewHumanVisible"
> >
<policy-preview-human :policy-description="humanizedPolicy" /> <policy-preview-human
class="gl-bg-gray-10 gl-py-3 gl-px-4"
:policy-description="humanizedPolicy"
/>
</gl-collapse> </gl-collapse>
</div> </div>
</template> </template>
......
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
import { PARSING_ERROR_MESSAGE } from './constants';
import PolicyPreviewHuman from './policy_preview_human.vue';
export default {
i18n: {
PARSING_ERROR_MESSAGE,
},
components: {
GlTabs,
GlTab,
PolicyPreviewHuman,
},
props: {
policyYaml: {
type: String,
required: true,
},
policyDescription: {
type: String,
required: false,
default: '',
},
initialTab: {
type: Number,
required: false,
default: 0,
},
},
data() {
return { selectedTab: this.initialTab };
},
};
</script>
<template>
<gl-tabs v-model="selectedTab" content-class="gl-pt-0">
<gl-tab :title="s__('NetworkPolicies|Rule')">
<policy-preview-human
:class="{
'gl-border-t-none! gl-rounded-top-left-none gl-rounded-top-right-none': Boolean(
policyDescription,
),
}"
:policy-description="policyDescription"
/>
</gl-tab>
<gl-tab :title="s__('NetworkPolicies|.yaml')">
<pre class="gl-bg-white gl-rounded-top-left-none gl-rounded-top-right-none gl-border-t-none"
>{{ policyYaml }}
</pre>
</gl-tab>
</gl-tabs>
</template>
...@@ -24,11 +24,7 @@ export default { ...@@ -24,11 +24,7 @@ export default {
</script> </script>
<template> <template>
<div <div v-if="policyDescription" v-safe-html:[$options.safeHtmlConfig]="policyDescription"></div>
v-if="policyDescription"
v-safe-html:[$options.safeHtmlConfig]="policyDescription"
class="gl-bg-gray-10 gl-py-3 gl-px-4"
></div>
<div v-else> <div v-else>
<gl-alert variant="info" :dismissible="false"> <gl-alert variant="info" :dismissible="false">
{{ $options.i18n.PARSING_ERROR_MESSAGE }} {{ $options.i18n.PARSING_ERROR_MESSAGE }}
......
...@@ -68,6 +68,7 @@ module MergeRequests ...@@ -68,6 +68,7 @@ module MergeRequests
# This currently applies only the first diff # This currently applies only the first diff
render_template( render_template(
file: 'vulnerabilities/remediation', file: 'vulnerabilities/remediation',
formats: :patch,
locals: { locals: {
diff: diffs.first, diff: diffs.first,
head_commit: head_commit, head_commit: head_commit,
...@@ -80,12 +81,13 @@ module MergeRequests ...@@ -80,12 +81,13 @@ module MergeRequests
def render_description(vulnerability) def render_description(vulnerability)
render_template( render_template(
file: 'vulnerabilities/merge_request_description', file: 'vulnerabilities/merge_request_description',
formats: :md,
locals: { vulnerability: vulnerability } locals: { vulnerability: vulnerability }
) )
end end
def render_template(file:, locals:) def render_template(file:, locals:, formats:)
ActionController::Base.new.render_to_string(template: file, locals: locals) ApplicationController.render(template: file, locals: locals, formats: formats)
end end
def can_create_merge_request?(source_branch) def can_create_merge_request?(source_branch)
......
...@@ -58,6 +58,47 @@ RSpec.describe 'GEO Nodes', :geo do ...@@ -58,6 +58,47 @@ RSpec.describe 'GEO Nodes', :geo do
expect(all('.geo-node-details-grid-columns').last).to have_link('Open replications', href: expected_url) expect(all('.geo-node-details-grid-columns').last).to have_link('Open replications', href: expected_url)
end end
context 'Status Filters', :js do
it 'defaults to the All tab when a status query is not already set' do
visit admin_geo_nodes_path
tab_count = find('[data-testid="geo-sites-filter"] .active .badge').text.to_i
results_count = page.all('[data-testid="primary-nodes"]').length + page.all('[data-testid="secondary-nodes"]').length
expect(find('[data-testid="geo-sites-filter"] .active')).to have_content('All')
expect(results_count).to be(tab_count)
end
it 'sets the correct tab when a status query is already set' do
visit admin_geo_nodes_path(status: 'unknown')
tab_count = find('[data-testid="geo-sites-filter"] .active .badge').text.to_i
results_count = page.all('[data-testid="primary-nodes"]').length + page.all('[data-testid="secondary-nodes"]').length
expect(find('[data-testid="geo-sites-filter"] .active')).not_to have_content('All')
expect(find('[data-testid="geo-sites-filter"] .active')).to have_content('Unknown')
expect(results_count).to be(tab_count)
end
it 'properly updates the query and sets the tab when a new one is clicked' do
visit admin_geo_nodes_path
tab_count = find('[data-testid="geo-sites-filter"] .active .badge').text.to_i
results_count = page.all('[data-testid="primary-nodes"]').length + page.all('[data-testid="secondary-nodes"]').length
expect(find('[data-testid="geo-sites-filter"] .active')).to have_content('All')
expect(results_count).to be(tab_count)
click_link 'Unknown'
wait_for_requests
tab_count = find('[data-testid="geo-sites-filter"] .active .badge').text.to_i
results_count = page.all('[data-testid="primary-nodes"]').length + page.all('[data-testid="secondary-nodes"]').length
expect(find('[data-testid="geo-sites-filter"] .active')).not_to have_content('All')
expect(find('[data-testid="geo-sites-filter"] .active')).to have_content('Unknown')
expect(page).to have_current_path(admin_geo_nodes_path(status: 'unknown'))
expect(results_count).to be(tab_count)
end
end
end end
end end
end end
import { GlLink, GlButton, GlLoadingIcon, GlModal, GlSprintf } from '@gitlab/ui'; import { GlLink, GlButton, GlLoadingIcon, GlModal, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import GeoNodesApp from 'ee/geo_nodes/components/app.vue'; import GeoNodesApp from 'ee/geo_nodes/components/app.vue';
import GeoNodes from 'ee/geo_nodes/components/geo_nodes.vue'; import GeoNodes from 'ee/geo_nodes/components/geo_nodes.vue';
import GeoNodesEmptyState from 'ee/geo_nodes/components/geo_nodes_empty_state.vue'; import GeoNodesEmptyState from 'ee/geo_nodes/components/geo_nodes_empty_state.vue';
import { GEO_INFO_URL } from 'ee/geo_nodes/constants'; import { GEO_INFO_URL } from 'ee/geo_nodes/constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { MOCK_NODES, MOCK_NEW_NODE_URL, MOCK_EMPTY_STATE_SVG } from '../mock_data'; import { MOCK_NODES, MOCK_NEW_NODE_URL, MOCK_EMPTY_STATE_SVG } from '../mock_data';
Vue.use(Vuex); Vue.use(Vuex);
...@@ -25,24 +24,26 @@ describe('GeoNodesApp', () => { ...@@ -25,24 +24,26 @@ describe('GeoNodesApp', () => {
geoNodesEmptyStateSvg: MOCK_EMPTY_STATE_SVG, geoNodesEmptyStateSvg: MOCK_EMPTY_STATE_SVG,
}; };
const createComponent = (initialState, props) => { const createComponent = (initialState, props, getters) => {
const store = new Vuex.Store({ const store = new Vuex.Store({
state: { state: {
...initialState, ...initialState,
}, },
actions: actionSpies, actions: actionSpies,
getters: {
filteredNodes: () => [],
...getters,
},
}); });
wrapper = extendedWrapper( wrapper = shallowMountExtended(GeoNodesApp, {
shallowMount(GeoNodesApp, {
store, store,
propsData: { propsData: {
...defaultProps, ...defaultProps,
...props, ...props,
}, },
stubs: { GlSprintf }, stubs: { GlSprintf },
}), });
);
}; };
afterEach(() => { afterEach(() => {
...@@ -58,6 +59,8 @@ describe('GeoNodesApp', () => { ...@@ -58,6 +59,8 @@ describe('GeoNodesApp', () => {
const findPrimaryGeoNodes = () => wrapper.findAllByTestId('primary-nodes'); const findPrimaryGeoNodes = () => wrapper.findAllByTestId('primary-nodes');
const findSecondaryGeoNodes = () => wrapper.findAllByTestId('secondary-nodes'); const findSecondaryGeoNodes = () => wrapper.findAllByTestId('secondary-nodes');
const findGlModal = () => wrapper.findComponent(GlModal); const findGlModal = () => wrapper.findComponent(GlModal);
const findPrimarySiteTitle = () => wrapper.findByText('Primary site');
const findSecondarySiteTitle = () => wrapper.findByText('Secondary site');
describe('template', () => { describe('template', () => {
describe('always', () => { describe('always', () => {
...@@ -89,7 +92,7 @@ describe('GeoNodesApp', () => { ...@@ -89,7 +92,7 @@ describe('GeoNodesApp', () => {
`conditionally`, `conditionally`,
({ isLoading, nodes, showLoadingIcon, showNodes, showEmptyState, showAddButton }) => { ({ isLoading, nodes, showLoadingIcon, showNodes, showEmptyState, showAddButton }) => {
beforeEach(() => { beforeEach(() => {
createComponent({ isLoading, nodes }); createComponent({ isLoading, nodes }, null, { filteredNodes: () => nodes });
}); });
describe(`when isLoading is ${isLoading} & nodes length ${nodes.length}`, () => { describe(`when isLoading is ${isLoading} & nodes length ${nodes.length}`, () => {
...@@ -117,7 +120,7 @@ describe('GeoNodesApp', () => { ...@@ -117,7 +120,7 @@ describe('GeoNodesApp', () => {
const secondaryNodes = MOCK_NODES.filter((n) => !n.primary); const secondaryNodes = MOCK_NODES.filter((n) => !n.primary);
beforeEach(() => { beforeEach(() => {
createComponent({ nodes: MOCK_NODES }); createComponent({ nodes: MOCK_NODES }, null, { filteredNodes: () => MOCK_NODES });
}); });
it('renders the correct Geo Node component for each node', () => { it('renders the correct Geo Node component for each node', () => {
...@@ -125,6 +128,28 @@ describe('GeoNodesApp', () => { ...@@ -125,6 +128,28 @@ describe('GeoNodesApp', () => {
expect(findSecondaryGeoNodes()).toHaveLength(secondaryNodes.length); expect(findSecondaryGeoNodes()).toHaveLength(secondaryNodes.length);
}); });
}); });
describe.each`
description | nodes | primaryTitle | secondaryTitle
${'with both primary and secondary nodes'} | ${MOCK_NODES} | ${true} | ${true}
${'with only primary nodes'} | ${MOCK_NODES.filter((n) => n.primary)} | ${true} | ${false}
${'with only secondary nodes'} | ${MOCK_NODES.filter((n) => !n.primary)} | ${false} | ${true}
${'with no nodes'} | ${[]} | ${false} | ${false}
`('Site Titles', ({ description, nodes, primaryTitle, secondaryTitle }) => {
describe(`${description}`, () => {
beforeEach(() => {
createComponent({ nodes }, null, { filteredNodes: () => nodes });
});
it(`should ${primaryTitle ? '' : 'not '}render the Primary Site Title`, () => {
expect(findPrimarySiteTitle().exists()).toBe(primaryTitle);
});
it(`should ${secondaryTitle ? '' : 'not '}render the Secondary Site Title`, () => {
expect(findSecondarySiteTitle().exists()).toBe(secondaryTitle);
});
});
});
}); });
describe('onCreate', () => { describe('onCreate', () => {
......
import { GlTabs, GlTab } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import GeoNodesFilters from 'ee/geo_nodes/components/geo_nodes_filters.vue';
import { HEALTH_STATUS_UI, STATUS_FILTER_QUERY_PARAM } from 'ee/geo_nodes/constants';
Vue.use(Vuex);
const MOCK_TAB_COUNT = 5;
describe('GeoNodesFilters', () => {
let wrapper;
const defaultProps = {
totalNodes: MOCK_TAB_COUNT,
};
const actionSpies = {
setStatusFilter: jest.fn(),
};
const createComponent = (initialState, props, getters) => {
const store = new Vuex.Store({
state: {
...initialState,
},
actions: actionSpies,
getters: {
countNodesForStatus: () => () => 0,
...getters,
},
});
wrapper = shallowMountExtended(GeoNodesFilters, {
store,
propsData: {
...defaultProps,
...props,
},
stubs: { GlTab },
});
};
afterEach(() => {
wrapper.destroy();
});
const findGlTabs = () => wrapper.findComponent(GlTabs);
const findAllGlTabs = () => wrapper.findAllComponents(GlTab);
const findAllGlTabTitles = () => wrapper.findAllComponents(GlTab).wrappers.map((w) => w.text());
const findAllTab = () => findAllGlTabs().at(0);
describe('template', () => {
describe('always', () => {
beforeEach(() => {
createComponent();
});
it('renders the GlTabs', () => {
expect(findGlTabs().exists()).toBe(true);
});
it('allows GlTabs to manage the query param', () => {
expect(findGlTabs().attributes('syncactivetabwithqueryparams')).toBe('true');
expect(findGlTabs().attributes('queryparamname')).toBe(STATUS_FILTER_QUERY_PARAM);
});
it('renders the All tab with the totalNodes count', () => {
expect(findAllTab().exists()).toBe(true);
expect(findAllTab().text()).toBe(`All ${MOCK_TAB_COUNT}`);
});
});
describe('conditional tabs', () => {
describe('when every status has counts', () => {
beforeEach(() => {
createComponent(
null,
{ totalNodes: MOCK_TAB_COUNT },
{ countNodesForStatus: () => () => MOCK_TAB_COUNT },
);
});
it('renders every status tab', () => {
const expectedTabTitles = [
`All ${MOCK_TAB_COUNT}`,
...Object.values(HEALTH_STATUS_UI).map((tab) => `${tab.text} ${MOCK_TAB_COUNT}`),
];
expect(findAllGlTabTitles()).toStrictEqual(expectedTabTitles);
});
});
describe('when only certain statuses have counts', () => {
const MOCK_COUNTER_GETTER = () => (status) => {
if (status === 'healthy' || status === 'unhealthy') {
return MOCK_TAB_COUNT;
}
return 0;
};
beforeEach(() => {
createComponent(
null,
{ totalNodes: MOCK_TAB_COUNT },
{ countNodesForStatus: MOCK_COUNTER_GETTER },
);
});
it('renders only those status tabs', () => {
const expectedTabTitles = [
`All ${MOCK_TAB_COUNT}`,
`Healthy ${MOCK_TAB_COUNT}`,
`Unhealthy ${MOCK_TAB_COUNT}`,
];
expect(findAllGlTabTitles()).toStrictEqual(expectedTabTitles);
});
});
});
});
describe('methods', () => {
describe('when clicking each tab', () => {
const expectedTabs = [
{ status: null },
...Object.keys(HEALTH_STATUS_UI).map((status) => {
return { status };
}),
];
beforeEach(() => {
createComponent(
null,
{ totalNodes: MOCK_TAB_COUNT },
{ countNodesForStatus: () => () => MOCK_TAB_COUNT },
);
});
it('calls setStatusFilter with the correct status', () => {
for (let i = 0; i < findAllGlTabs().length; i += 1) {
findGlTabs().vm.$emit('input', i);
expect(actionSpies.setStatusFilter).toHaveBeenCalledWith(
expect.any(Object),
expectedTabs[i].status,
);
}
});
});
});
});
...@@ -241,3 +241,24 @@ export const MOCK_NODE_STATUSES_RES = [ ...@@ -241,3 +241,24 @@ export const MOCK_NODE_STATUSES_RES = [
web_geo_projects_url: 'http://127.0.0.1:3002/replication/projects', web_geo_projects_url: 'http://127.0.0.1:3002/replication/projects',
}, },
]; ];
export const MOCK_HEALTH_STATUS_NODES = [
{
healthStatus: 'Healthy',
},
{
healthStatus: 'Healthy',
},
{
healthStatus: 'Unhealthy',
},
{
healthStatus: 'Disabled',
},
{
healthStatus: 'Offline',
},
{
healthStatus: null,
},
];
...@@ -135,4 +135,15 @@ describe('GeoNodes Store Actions', () => { ...@@ -135,4 +135,15 @@ describe('GeoNodes Store Actions', () => {
}); });
}); });
}); });
describe('setStatusFilter', () => {
it('should dispatch the correct mutations', () => {
return testAction({
action: actions.setStatusFilter,
payload: 'healthy',
state,
expectedMutations: [{ type: types.SET_STATUS_FILTER, payload: 'healthy' }],
});
});
});
}); });
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
MOCK_PRIMARY_VERIFICATION_INFO, MOCK_PRIMARY_VERIFICATION_INFO,
MOCK_SECONDARY_VERIFICATION_INFO, MOCK_SECONDARY_VERIFICATION_INFO,
MOCK_SECONDARY_SYNC_INFO, MOCK_SECONDARY_SYNC_INFO,
MOCK_HEALTH_STATUS_NODES,
} from '../mock_data'; } from '../mock_data';
describe('GeoNodes Store Getters', () => { describe('GeoNodes Store Getters', () => {
...@@ -69,4 +70,44 @@ describe('GeoNodes Store Getters', () => { ...@@ -69,4 +70,44 @@ describe('GeoNodes Store Getters', () => {
}); });
}); });
}); });
describe.each`
status | expectedNodes
${null} | ${MOCK_HEALTH_STATUS_NODES}
${'healthy'} | ${[{ healthStatus: 'Healthy' }, { healthStatus: 'Healthy' }]}
${'unhealthy'} | ${[{ healthStatus: 'Unhealthy' }]}
${'offline'} | ${[{ healthStatus: 'Offline' }]}
${'disabled'} | ${[{ healthStatus: 'Disabled' }]}
${'unknown'} | ${[{ healthStatus: null }]}
`('filteredNodes', ({ status, expectedNodes }) => {
describe(`when status is ${status}`, () => {
beforeEach(() => {
state.nodes = MOCK_HEALTH_STATUS_NODES;
state.statusFilter = status;
});
it('should return the correct filtered array', () => {
expect(getters.filteredNodes(state)).toStrictEqual(expectedNodes);
});
});
});
describe.each`
status | expectedCount
${'healthy'} | ${2}
${'unhealthy'} | ${1}
${'offline'} | ${1}
${'disabled'} | ${1}
${'unknown'} | ${1}
`('countNodesForStatus', ({ status, expectedCount }) => {
describe(`when status is ${status}`, () => {
beforeEach(() => {
state.nodes = MOCK_HEALTH_STATUS_NODES;
});
it(`should return ${expectedCount}`, () => {
expect(getters.countNodesForStatus(state)(status)).toBe(expectedCount);
});
});
});
}); });
...@@ -105,4 +105,12 @@ describe('GeoNodes Store Mutations', () => { ...@@ -105,4 +105,12 @@ describe('GeoNodes Store Mutations', () => {
expect(state.nodeToBeRemoved).toEqual(null); expect(state.nodeToBeRemoved).toEqual(null);
}); });
}); });
describe('SET_STATUS_FILTER', () => {
it('sets statusFilter', () => {
mutations[types.SET_STATUS_FILTER](state, 'healthy');
expect(state.statusFilter).toBe('healthy');
});
});
}); });
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CiliumNetworkPolicy component supported YAML renders policy preview tabs 1`] = ` exports[`CiliumNetworkPolicy component supported YAML renders policy preview 1`] = `
<div> <div>
<h5 <h5
class="gl-mt-3" class="gl-mt-3"
...@@ -14,6 +14,14 @@ exports[`CiliumNetworkPolicy component supported YAML renders policy preview tab ...@@ -14,6 +14,14 @@ exports[`CiliumNetworkPolicy component supported YAML renders policy preview tab
Network Network
</p> </p>
<policy-info-row-stub
label="Summary"
>
<policy-preview-human-stub
policydescription="Deny all traffic"
/>
</policy-info-row-stub>
<div> <div>
<policy-info-row-stub <policy-info-row-stub
data-testid="description" data-testid="description"
...@@ -32,26 +40,10 @@ exports[`CiliumNetworkPolicy component supported YAML renders policy preview tab ...@@ -32,26 +40,10 @@ exports[`CiliumNetworkPolicy component supported YAML renders policy preview tab
<!----> <!---->
</div> </div>
<policy-preview-stub
class="gl-mt-4"
initialtab="0"
policydescription="Deny all traffic"
policyyaml="apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
description: test description
metadata:
name: test-policy
spec:
endpointSelector:
matchLabels:
network-policy.gitlab.com/disabled_by: gitlab
"
/>
</div> </div>
`; `;
exports[`CiliumNetworkPolicy component unsupported YAML renders policy preview tabs 1`] = ` exports[`CiliumNetworkPolicy component unsupported YAML renders policy preview 1`] = `
<div> <div>
<h5 <h5
class="gl-mt-3" class="gl-mt-3"
...@@ -65,12 +57,12 @@ exports[`CiliumNetworkPolicy component unsupported YAML renders policy preview t ...@@ -65,12 +57,12 @@ exports[`CiliumNetworkPolicy component unsupported YAML renders policy preview t
Network Network
</p> </p>
<!----> <policy-info-row-stub
label="Summary"
>
<policy-preview-human-stub />
</policy-info-row-stub>
<policy-preview-stub <!---->
class="gl-mt-4"
initialtab="1"
policyyaml="unsupportedPrimaryKey: test"
/>
</div> </div>
`; `;
...@@ -2,7 +2,7 @@ import { GlIntersperse } from '@gitlab/ui'; ...@@ -2,7 +2,7 @@ import { GlIntersperse } from '@gitlab/ui';
import BasePolicy from 'ee/threat_monitoring/components/policy_drawer/base_policy.vue'; import BasePolicy from 'ee/threat_monitoring/components/policy_drawer/base_policy.vue';
import CiliumNetworkPolicy from 'ee/threat_monitoring/components/policy_drawer/cilium_network_policy.vue'; import CiliumNetworkPolicy from 'ee/threat_monitoring/components/policy_drawer/cilium_network_policy.vue';
import { toYaml } from 'ee/threat_monitoring/components/policy_editor/network_policy/lib'; import { toYaml } from 'ee/threat_monitoring/components/policy_editor/network_policy/lib';
import PolicyPreview from 'ee/threat_monitoring/components/policy_editor/policy_preview.vue'; import PolicyPreviewHuman from 'ee/threat_monitoring/components/policy_editor/policy_preview_human.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('CiliumNetworkPolicy component', () => { describe('CiliumNetworkPolicy component', () => {
...@@ -15,7 +15,7 @@ describe('CiliumNetworkPolicy component', () => { ...@@ -15,7 +15,7 @@ describe('CiliumNetworkPolicy component', () => {
}); });
const unsupportedYaml = 'unsupportedPrimaryKey: test'; const unsupportedYaml = 'unsupportedPrimaryKey: test';
const findPolicyPreview = () => wrapper.findComponent(PolicyPreview); const findPolicyPreview = () => wrapper.findComponent(PolicyPreviewHuman);
const findDescription = () => wrapper.findByTestId('description'); const findDescription = () => wrapper.findByTestId('description');
const findEnvironments = () => wrapper.findByTestId('environments'); const findEnvironments = () => wrapper.findByTestId('environments');
...@@ -40,7 +40,7 @@ describe('CiliumNetworkPolicy component', () => { ...@@ -40,7 +40,7 @@ describe('CiliumNetworkPolicy component', () => {
factory({ propsData: { policy: { yaml: supportedYaml } } }); factory({ propsData: { policy: { yaml: supportedYaml } } });
}); });
it('renders policy preview tabs', () => { it('renders policy preview', () => {
expect(wrapper.find('div').element).toMatchSnapshot(); expect(wrapper.find('div').element).toMatchSnapshot();
}); });
...@@ -50,12 +50,7 @@ describe('CiliumNetworkPolicy component', () => { ...@@ -50,12 +50,7 @@ describe('CiliumNetworkPolicy component', () => {
}); });
it('does render the policy preview', () => { it('does render the policy preview', () => {
expect(findPolicyPreview().exists()).toBe(true); expect(findPolicyPreview().props('policyDescription')).toBe('Deny all traffic');
expect(findPolicyPreview().props()).toStrictEqual({
initialTab: 0,
policyDescription: 'Deny all traffic',
policyYaml: supportedYaml,
});
}); });
}); });
...@@ -64,7 +59,7 @@ describe('CiliumNetworkPolicy component', () => { ...@@ -64,7 +59,7 @@ describe('CiliumNetworkPolicy component', () => {
factory({ propsData: { policy: { yaml: unsupportedYaml } } }); factory({ propsData: { policy: { yaml: unsupportedYaml } } });
}); });
it('renders policy preview tabs', () => { it('renders policy preview', () => {
expect(wrapper.find('div').element).toMatchSnapshot(); expect(wrapper.find('div').element).toMatchSnapshot();
}); });
...@@ -74,11 +69,7 @@ describe('CiliumNetworkPolicy component', () => { ...@@ -74,11 +69,7 @@ describe('CiliumNetworkPolicy component', () => {
it('does render the policy preview', () => { it('does render the policy preview', () => {
expect(findPolicyPreview().exists()).toBe(true); expect(findPolicyPreview().exists()).toBe(true);
expect(findPolicyPreview().props()).toStrictEqual({ expect(findPolicyPreview().props('policyDescription')).toBe(null);
initialTab: 1,
policyDescription: null,
policyYaml: unsupportedYaml,
});
}); });
}); });
......
import { GlButton, GlDrawer, GlTabs, GlTab } from '@gitlab/ui';
import { POLICY_TYPE_COMPONENT_OPTIONS } from 'ee/threat_monitoring/components/constants'; import { POLICY_TYPE_COMPONENT_OPTIONS } from 'ee/threat_monitoring/components/constants';
import CiliumNetworkPolicy from 'ee/threat_monitoring/components/policy_drawer/cilium_network_policy.vue'; import CiliumNetworkPolicy from 'ee/threat_monitoring/components/policy_drawer/cilium_network_policy.vue';
import PolicyDrawer from 'ee/threat_monitoring/components/policy_drawer/policy_drawer.vue'; import PolicyDrawer from 'ee/threat_monitoring/components/policy_drawer/policy_drawer.vue';
import ScanExecutionPolicy from 'ee/threat_monitoring/components/policy_drawer/scan_execution_policy.vue'; import ScanExecutionPolicy from 'ee/threat_monitoring/components/policy_drawer/scan_execution_policy.vue';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { import {
mockNetworkPoliciesResponse, mockNetworkPoliciesResponse,
mockCiliumPolicy, mockCiliumPolicy,
...@@ -14,22 +15,25 @@ const [mockGenericPolicy] = mockNetworkPoliciesResponse; ...@@ -14,22 +15,25 @@ const [mockGenericPolicy] = mockNetworkPoliciesResponse;
describe('PolicyDrawer component', () => { describe('PolicyDrawer component', () => {
let wrapper; let wrapper;
const factory = ({ propsData } = {}) => { const factory = ({ mountFn = shallowMountExtended, propsData, stubs = {} } = {}) => {
wrapper = mountExtended(PolicyDrawer, { wrapper = mountFn(PolicyDrawer, {
propsData: { propsData: {
editPolicyPath: '/policies/policy/edit?environment_id=-1', editPolicyPath: '/policies/policy/edit?environment_id=-1',
open: true, open: true,
...propsData, ...propsData,
}, },
stubs: { PolicyYamlEditor: true }, stubs: { PolicyYamlEditor: true, ...stubs },
}); });
}; };
// Finders // Finders
const findEditButton = () => wrapper.findByTestId('edit-button'); const findEditButton = () => wrapper.findByTestId('edit-button');
const findPolicyEditor = () => wrapper.findByTestId('policy-yaml-editor'); const findAllTabs = () => wrapper.findAllComponents(GlTab);
const findCiliumNetworkPolicy = () => wrapper.findComponent(CiliumNetworkPolicy); const findCiliumNetworkPolicy = () => wrapper.findComponent(CiliumNetworkPolicy);
const findScanExecutionPolicy = () => wrapper.findComponent(ScanExecutionPolicy); const findScanExecutionPolicy = () => wrapper.findComponent(ScanExecutionPolicy);
const findDefaultComponentPolicyEditor = () =>
wrapper.findByTestId('policy-yaml-editor-default-component');
const findTabPolicyEditor = () => wrapper.findByTestId('policy-yaml-editor-tab-content');
// Shared assertions // Shared assertions
const itRendersEditButton = () => { const itRendersEditButton = () => {
...@@ -47,7 +51,7 @@ describe('PolicyDrawer component', () => { ...@@ -47,7 +51,7 @@ describe('PolicyDrawer component', () => {
describe('by default', () => { describe('by default', () => {
beforeEach(() => { beforeEach(() => {
factory(); factory({ stubs: { GlDrawer } });
}); });
it('does not render edit button', () => { it('does not render edit button', () => {
...@@ -58,6 +62,7 @@ describe('PolicyDrawer component', () => { ...@@ -58,6 +62,7 @@ describe('PolicyDrawer component', () => {
describe('given a generic network policy', () => { describe('given a generic network policy', () => {
beforeEach(() => { beforeEach(() => {
factory({ factory({
mountFn: mountExtended,
propsData: { propsData: {
policy: mockGenericPolicy, policy: mockGenericPolicy,
}, },
...@@ -65,9 +70,7 @@ describe('PolicyDrawer component', () => { ...@@ -65,9 +70,7 @@ describe('PolicyDrawer component', () => {
}); });
it('renders network policy editor with manifest', () => { it('renders network policy editor with manifest', () => {
const policyEditor = findPolicyEditor(); expect(findDefaultComponentPolicyEditor().attributes('value')).toBe(mockGenericPolicy.yaml);
expect(policyEditor.exists()).toBe(true);
expect(policyEditor.attributes('value')).toBe(mockGenericPolicy.yaml);
}); });
itRendersEditButton(); itRendersEditButton();
...@@ -84,6 +87,11 @@ describe('PolicyDrawer component', () => { ...@@ -84,6 +87,11 @@ describe('PolicyDrawer component', () => {
policy: mock, policy: mock,
policyType, policyType,
}, },
stubs: {
GlButton,
GlDrawer,
GlTabs,
},
}); });
}); });
...@@ -91,6 +99,14 @@ describe('PolicyDrawer component', () => { ...@@ -91,6 +99,14 @@ describe('PolicyDrawer component', () => {
expect(finder().exists()).toBe(true); expect(finder().exists()).toBe(true);
}); });
it('renders the tabs', () => {
expect(findAllTabs()).toHaveLength(2);
});
it('renders the policy editor', () => {
expect(findTabPolicyEditor().attributes('value')).toBe(mock.yaml);
});
itRendersEditButton(); itRendersEditButton();
}); });
}); });
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PolicyPreview component with policy description renders policy preview tabs 1`] = `
<gl-tabs-stub
contentclass="gl-pt-0"
queryparamname="tab"
theme="indigo"
value="0"
>
<gl-tab-stub
title="Rule"
titlelinkclass=""
>
<policy-preview-human-stub
class="gl-border-t-none! gl-rounded-top-left-none gl-rounded-top-right-none"
policydescription="<strong>bar</strong><br><div>test</div><script></script>"
/>
</gl-tab-stub>
<gl-tab-stub
title=".yaml"
titlelinkclass=""
>
<pre
class="gl-bg-white gl-rounded-top-left-none gl-rounded-top-right-none gl-border-t-none"
>
foo
</pre>
</gl-tab-stub>
</gl-tabs-stub>
`;
import { GlTabs } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import PolicyPreviewHuman from 'ee/threat_monitoring/components/policy_editor/policy_preview_human.vue';
import PolicyPreview from 'ee/threat_monitoring/components/policy_editor/policy_preview.vue';
describe('PolicyPreview component', () => {
let wrapper;
const findTabs = () => wrapper.findComponent(GlTabs);
const findPolicyPreviewHuman = () => wrapper.findComponent(PolicyPreviewHuman);
const factory = ({ propsData } = {}) => {
wrapper = shallowMount(PolicyPreview, {
propsData: {
...propsData,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('with policy description', () => {
const policyDescription = '<strong>bar</strong><br><div>test</div><script></script>';
beforeEach(() => {
factory({
propsData: {
policyYaml: 'foo',
policyDescription,
},
});
});
it('renders policy preview tabs', () => {
expect(findTabs().element).toMatchSnapshot();
});
it('renders the policy preview human', () => {
expect(findPolicyPreviewHuman().props('policyDescription')).toBe(policyDescription);
});
it('renders the first tab', () => {
expect(findTabs().attributes().value).toEqual('0');
});
describe('initial tab', () => {
it('selects initial tab', () => {
factory({
propsData: {
policyYaml: 'foo',
policyDescription: 'bar',
initialTab: 1,
},
});
expect(findTabs().attributes().value).toEqual('1');
});
});
});
describe('without policy description', () => {
beforeEach(() => {
factory({
propsData: {
policyYaml: 'foo',
},
});
});
});
});
...@@ -15797,6 +15797,9 @@ msgstr "" ...@@ -15797,6 +15797,9 @@ msgstr ""
msgid "Geo|Adjust your filters/search criteria above. If you believe this may be an error, please refer to the %{linkStart}Geo Troubleshooting%{linkEnd} documentation for more information." msgid "Geo|Adjust your filters/search criteria above. If you believe this may be an error, please refer to the %{linkStart}Geo Troubleshooting%{linkEnd} documentation for more information."
msgstr "" msgstr ""
msgid "Geo|All"
msgstr ""
msgid "Geo|All %{replicable_name}" msgid "Geo|All %{replicable_name}"
msgstr "" msgstr ""
...@@ -19343,9 +19346,6 @@ msgstr "" ...@@ -19343,9 +19346,6 @@ msgstr ""
msgid "Integrations|No linked namespaces" msgid "Integrations|No linked namespaces"
msgstr "" msgstr ""
msgid "Integrations|Note: this integration only works with accounts on GitLab.com (SaaS)."
msgstr ""
msgid "Integrations|Projects using custom settings" msgid "Integrations|Projects using custom settings"
msgstr "" msgstr ""
...@@ -19385,6 +19385,9 @@ msgstr "" ...@@ -19385,6 +19385,9 @@ msgstr ""
msgid "Integrations|Send notifications about project events to a Unify Circuit conversation. %{docs_link}" msgid "Integrations|Send notifications about project events to a Unify Circuit conversation. %{docs_link}"
msgstr "" msgstr ""
msgid "Integrations|Sign in to GitLab"
msgstr ""
msgid "Integrations|Sign in to add namespaces" msgid "Integrations|Sign in to add namespaces"
msgstr "" msgstr ""
...@@ -23625,9 +23628,6 @@ msgstr "" ...@@ -23625,9 +23628,6 @@ msgstr ""
msgid "NetworkPolicies|%{strongOpen}any%{strongClose} port" msgid "NetworkPolicies|%{strongOpen}any%{strongClose} port"
msgstr "" msgstr ""
msgid "NetworkPolicies|.yaml"
msgstr ""
msgid "NetworkPolicies|.yaml mode" msgid "NetworkPolicies|.yaml mode"
msgstr "" msgstr ""
...@@ -23706,9 +23706,6 @@ msgstr "" ...@@ -23706,9 +23706,6 @@ msgstr ""
msgid "NetworkPolicies|Policy definition" msgid "NetworkPolicies|Policy definition"
msgstr "" msgstr ""
msgid "NetworkPolicies|Rule"
msgstr ""
msgid "NetworkPolicies|Rule mode" msgid "NetworkPolicies|Rule mode"
msgstr "" msgstr ""
...@@ -31953,6 +31950,9 @@ msgstr "" ...@@ -31953,6 +31950,9 @@ msgstr ""
msgid "SecurityOrchestration|Status" msgid "SecurityOrchestration|Status"
msgstr "" msgstr ""
msgid "SecurityOrchestration|Summary"
msgstr ""
msgid "SecurityOrchestration|The %{scanners} %{severities} in an open merge request targeting %{branches}." msgid "SecurityOrchestration|The %{scanners} %{severities} in an open merge request targeting %{branches}."
msgstr "" msgstr ""
......
...@@ -183,6 +183,18 @@ RSpec.describe Projects::CommitController do ...@@ -183,6 +183,18 @@ RSpec.describe Projects::CommitController do
expect(assigns(:tags)).to eq([]) expect(assigns(:tags)).to eq([])
expect(assigns(:tags_limit_exceeded)).to be_truthy expect(assigns(:tags_limit_exceeded)).to be_truthy
end end
context 'when commit is not found' do
it 'responds with 404' do
get(:branches, params: {
namespace_id: project.namespace,
project_id: project,
id: '11111111111111111111111111111111111111'
})
expect(response).to be_not_found
end
end
end end
describe 'POST revert' do describe 'POST revert' do
......
import { GlLink, GlEmptyState } from '@gitlab/ui'; import { GlLink } from '@gitlab/ui';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import JiraConnectApp from '~/jira_connect/subscriptions/components/app.vue'; import JiraConnectApp from '~/jira_connect/subscriptions/components/app.vue';
import AddNamespaceButton from '~/jira_connect/subscriptions/components/add_namespace_button.vue'; import SignInPage from '~/jira_connect/subscriptions/pages/sign_in.vue';
import SignInButton from '~/jira_connect/subscriptions/components/sign_in_button.vue'; import SubscriptionsPage from '~/jira_connect/subscriptions/pages/subscriptions.vue';
import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue';
import UserLink from '~/jira_connect/subscriptions/components/user_link.vue'; import UserLink from '~/jira_connect/subscriptions/components/user_link.vue';
import createStore from '~/jira_connect/subscriptions/store'; import createStore from '~/jira_connect/subscriptions/store';
import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types'; import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types';
...@@ -23,10 +22,8 @@ describe('JiraConnectApp', () => { ...@@ -23,10 +22,8 @@ describe('JiraConnectApp', () => {
const findAlert = () => wrapper.findByTestId('jira-connect-persisted-alert'); const findAlert = () => wrapper.findByTestId('jira-connect-persisted-alert');
const findAlertLink = () => findAlert().findComponent(GlLink); const findAlertLink = () => findAlert().findComponent(GlLink);
const findSignInButton = () => wrapper.findComponent(SignInButton); const findSignInPage = () => wrapper.findComponent(SignInPage);
const findAddNamespaceButton = () => wrapper.findComponent(AddNamespaceButton); const findSubscriptionsPage = () => wrapper.findComponent(SubscriptionsPage);
const findSubscriptionsList = () => wrapper.findComponent(SubscriptionsList);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const createComponent = ({ provide, mountFn = shallowMountExtended } = {}) => { const createComponent = ({ provide, mountFn = shallowMountExtended } = {}) => {
store = createStore(); store = createStore();
...@@ -43,49 +40,35 @@ describe('JiraConnectApp', () => { ...@@ -43,49 +40,35 @@ describe('JiraConnectApp', () => {
describe('template', () => { describe('template', () => {
describe.each` describe.each`
scenario | usersPath | subscriptions | expectSignInButton | expectEmptyState | expectNamespaceButton | expectSubscriptionsList scenario | usersPath | shouldRenderSignInPage | shouldRenderSubscriptionsPage
${'user is not signed in with subscriptions'} | ${'/users'} | ${[mockSubscription]} | ${true} | ${false} | ${false} | ${true} ${'user is not signed in'} | ${'/users'} | ${true} | ${false}
${'user is not signed in without subscriptions'} | ${'/users'} | ${undefined} | ${true} | ${false} | ${false} | ${false} ${'user is signed in'} | ${undefined} | ${false} | ${true}
${'user is signed in with subscriptions'} | ${undefined} | ${[mockSubscription]} | ${false} | ${false} | ${true} | ${true} `('when $scenario', ({ usersPath, shouldRenderSignInPage, shouldRenderSubscriptionsPage }) => {
${'user is signed in without subscriptions'} | ${undefined} | ${undefined} | ${false} | ${true} | ${false} | ${false}
`(
'when $scenario',
({
usersPath,
expectSignInButton,
subscriptions,
expectEmptyState,
expectNamespaceButton,
expectSubscriptionsList,
}) => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
provide: { provide: {
usersPath, usersPath,
subscriptions, subscriptions: [mockSubscription],
}, },
}); });
}); });
it(`${expectSignInButton ? 'renders' : 'does not render'} sign in button`, () => { it(`${shouldRenderSignInPage ? 'renders' : 'does not render'} sign in page`, () => {
expect(findSignInButton().exists()).toBe(expectSignInButton); expect(findSignInPage().exists()).toBe(shouldRenderSignInPage);
}); if (shouldRenderSignInPage) {
expect(findSignInPage().props('hasSubscriptions')).toBe(true);
it(`${expectEmptyState ? 'renders' : 'does not render'} empty state`, () => { }
expect(findEmptyState().exists()).toBe(expectEmptyState);
}); });
it(`${ it(`${
expectNamespaceButton ? 'renders' : 'does not render' shouldRenderSubscriptionsPage ? 'renders' : 'does not render'
} button to add namespace`, () => { } subscriptions page`, () => {
expect(findAddNamespaceButton().exists()).toBe(expectNamespaceButton); expect(findSubscriptionsPage().exists()).toBe(shouldRenderSubscriptionsPage);
if (shouldRenderSubscriptionsPage) {
expect(findSubscriptionsPage().props('hasSubscriptions')).toBe(true);
}
}); });
it(`${expectSubscriptionsList ? 'renders' : 'does not render'} subscriptions list`, () => {
expect(findSubscriptionsList().exists()).toBe(expectSubscriptionsList);
}); });
},
);
it('renders UserLink component', () => { it('renders UserLink component', () => {
createComponent({ createComponent({
......
...@@ -11,11 +11,12 @@ jest.mock('~/jira_connect/subscriptions/utils'); ...@@ -11,11 +11,12 @@ jest.mock('~/jira_connect/subscriptions/utils');
describe('SignInButton', () => { describe('SignInButton', () => {
let wrapper; let wrapper;
const createComponent = () => { const createComponent = ({ slots } = {}) => {
wrapper = shallowMount(SignInButton, { wrapper = shallowMount(SignInButton, {
propsData: { propsData: {
usersPath: MOCK_USERS_PATH, usersPath: MOCK_USERS_PATH,
}, },
slots,
}); });
}; };
...@@ -29,6 +30,7 @@ describe('SignInButton', () => { ...@@ -29,6 +30,7 @@ describe('SignInButton', () => {
createComponent(); createComponent();
expect(findButton().exists()).toBe(true); expect(findButton().exists()).toBe(true);
expect(findButton().text()).toBe(SignInButton.i18n.defaultButtonText);
}); });
describe.each` describe.each`
...@@ -45,4 +47,12 @@ describe('SignInButton', () => { ...@@ -45,4 +47,12 @@ describe('SignInButton', () => {
expect(findButton().attributes('href')).toBe(expectedHref); expect(findButton().attributes('href')).toBe(expectedHref);
}); });
}); });
describe('with slot', () => {
const mockSlotContent = 'custom button content!';
it('renders slot content in button', () => {
createComponent({ slots: { default: mockSlotContent } });
expect(wrapper.text()).toMatchInterpolatedText(mockSlotContent);
});
});
}); });
import { mount } from '@vue/test-utils';
import SignInPage from '~/jira_connect/subscriptions/pages/sign_in.vue';
import SignInButton from '~/jira_connect/subscriptions/components/sign_in_button.vue';
import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue';
import createStore from '~/jira_connect/subscriptions/store';
jest.mock('~/jira_connect/subscriptions/utils');
describe('SignInPage', () => {
let wrapper;
let store;
const findSignInButton = () => wrapper.findComponent(SignInButton);
const findSubscriptionsList = () => wrapper.findComponent(SubscriptionsList);
const createComponent = ({ provide, props } = {}) => {
store = createStore();
wrapper = mount(SignInPage, {
store,
provide,
propsData: props,
});
};
afterEach(() => {
wrapper.destroy();
});
describe('template', () => {
const mockUsersPath = '/test';
describe.each`
scenario | expectSubscriptionsList | signInButtonText
${'with subscriptions'} | ${true} | ${SignInPage.i18n.signinButtonTextWithSubscriptions}
${'without subscriptions'} | ${false} | ${SignInButton.i18n.defaultButtonText}
`('$scenario', ({ expectSubscriptionsList, signInButtonText }) => {
beforeEach(() => {
createComponent({
provide: {
usersPath: mockUsersPath,
},
props: {
hasSubscriptions: expectSubscriptionsList,
},
});
});
it(`renders sign in button with text ${signInButtonText}`, () => {
expect(findSignInButton().text()).toMatchInterpolatedText(signInButtonText);
});
it('renders sign in button with `usersPath` prop', () => {
expect(findSignInButton().props('usersPath')).toBe(mockUsersPath);
});
it(`${expectSubscriptionsList ? 'renders' : 'does not render'} subscriptions list`, () => {
expect(findSubscriptionsList().exists()).toBe(expectSubscriptionsList);
});
});
});
});
import { GlEmptyState } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import SubscriptionsPage from '~/jira_connect/subscriptions/pages/subscriptions.vue';
import AddNamespaceButton from '~/jira_connect/subscriptions/components/add_namespace_button.vue';
import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue';
import createStore from '~/jira_connect/subscriptions/store';
describe('SubscriptionsPage', () => {
let wrapper;
let store;
const findAddNamespaceButton = () => wrapper.findComponent(AddNamespaceButton);
const findSubscriptionsList = () => wrapper.findComponent(SubscriptionsList);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const createComponent = ({ props } = {}) => {
store = createStore();
wrapper = shallowMount(SubscriptionsPage, {
store,
propsData: props,
});
};
afterEach(() => {
wrapper.destroy();
});
describe('template', () => {
describe.each`
scenario | expectSubscriptionsList | expectEmptyState
${'with subscriptions'} | ${true} | ${false}
${'without subscriptions'} | ${false} | ${true}
`('$scenario', ({ expectEmptyState, expectSubscriptionsList }) => {
beforeEach(() => {
createComponent({
props: {
hasSubscriptions: expectSubscriptionsList,
},
});
});
it('renders button to add namespace', () => {
expect(findAddNamespaceButton().exists()).toBe(true);
});
it(`${expectEmptyState ? 'renders' : 'does not render'} empty state`, () => {
expect(findEmptyState().exists()).toBe(expectEmptyState);
});
it(`${expectSubscriptionsList ? 'renders' : 'does not render'} subscriptions list`, () => {
expect(findSubscriptionsList().exists()).toBe(expectSubscriptionsList);
});
});
});
});
...@@ -215,6 +215,20 @@ RSpec.describe Blob do ...@@ -215,6 +215,20 @@ RSpec.describe Blob do
end end
end end
describe '#symlink?' do
it 'is true for symlinks' do
symlink_blob = fake_blob(path: 'file', mode: '120000')
expect(symlink_blob.symlink?).to eq true
end
it 'is false for non-symlinks' do
non_symlink_blob = fake_blob(path: 'file', mode: '100755')
expect(non_symlink_blob.symlink?).to eq false
end
end
describe '#extension' do describe '#extension' do
it 'returns the extension' do it 'returns the extension' do
blob = fake_blob(path: 'file.md') blob = fake_blob(path: 'file.md')
......
...@@ -65,7 +65,7 @@ RSpec.describe Branches::CreateService do ...@@ -65,7 +65,7 @@ RSpec.describe Branches::CreateService do
allow(project.repository).to receive(:add_branch).and_raise(pre_receive_error) allow(project.repository).to receive(:add_branch).and_raise(pre_receive_error)
expect(Gitlab::ErrorTracking).to receive(:track_exception).with( expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
pre_receive_error, pre_receive_error,
pre_receive_message: raw_message, pre_receive_message: raw_message,
branch_name: 'new-feature', branch_name: 'new-feature',
......
...@@ -4,13 +4,14 @@ module FakeBlobHelpers ...@@ -4,13 +4,14 @@ module FakeBlobHelpers
class FakeBlob class FakeBlob
include BlobLike include BlobLike
attr_reader :path, :size, :data, :lfs_oid, :lfs_size attr_reader :path, :size, :data, :lfs_oid, :lfs_size, :mode
def initialize(path: 'file.txt', size: 1.kilobyte, data: 'foo', binary: false, lfs: nil) def initialize(path: 'file.txt', size: 1.kilobyte, data: 'foo', binary: false, lfs: nil, mode: nil)
@path = path @path = path
@size = size @size = size
@data = data @data = data
@binary = binary @binary = binary
@mode = mode
@lfs_pointer = lfs.present? @lfs_pointer = lfs.present?
if @lfs_pointer if @lfs_pointer
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment