Skip to content
Snippets Groups Projects
Commit 95766868 authored by root's avatar root
Browse files

Update from merge request

parent 977cb753
No related branches found
No related tags found
No related merge requests found
Pipeline #1555768798 passed
Showing
with 1029 additions and 168 deletions
......@@ -3,7 +3,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapState, mapGetters } from 'vuex';
import { getCookie, setCookie } from '~/lib/utils/common_utils';
import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue';
import LegacyValueStreamMetrics from '~/analytics/shared/components/legacy_value_stream_metrics.vue';
import { VSA_METRICS_GROUPS } from '~/analytics/shared/constants';
import { toYmd, generateValueStreamsDashboardLink } from '~/analytics/shared/utils';
import PathNavigation from '~/analytics/cycle_analytics/components/path_navigation.vue';
......@@ -24,7 +24,7 @@ export default {
PathNavigation,
StageTable,
ValueStreamFilters,
ValueStreamMetrics,
LegacyValueStreamMetrics,
UrlSync,
},
props: {
......@@ -188,7 +188,7 @@ export default {
@selected="onSelectStage"
/>
</div>
<value-stream-metrics
<legacy-value-stream-metrics
:request-path="namespace.fullPath"
:request-params="filterParams"
:requests="metricsRequests"
......
<script>
// NOTE: the API requests for this component are being migrated to graphql
// related issue: https://gitlab.com/gitlab-org/gitlab/-/issues/498179
import { GlSkeletonLoader } from '@gitlab/ui';
import { isEqual, keyBy } from 'lodash';
import { createAlert } from '~/alert';
import { sprintf, s__ } from '~/locale';
import { fetchMetricsData, removeFlash } from '../utils';
import ValueStreamsDashboardLink from './value_streams_dashboard_link.vue';
import MetricTile from './metric_tile.vue';
const extractMetricsGroupData = (keyList = [], data = []) => {
if (!keyList.length || !data.length) return [];
const kv = keyBy(data, 'identifier');
return keyList.map((id) => kv[id] || null).filter((obj) => Boolean(obj));
};
const groupRawMetrics = (groups = [], rawData = []) => {
return groups.map((curr) => {
const { keys, ...rest } = curr;
return {
data: extractMetricsGroupData(keys, rawData),
keys,
...rest,
};
});
};
export default {
name: 'LegacyValueStreamMetrics',
components: {
GlSkeletonLoader,
MetricTile,
ValueStreamsDashboardLink,
},
props: {
requestPath: {
type: String,
required: true,
},
requestParams: {
type: Object,
required: true,
},
requests: {
type: Array,
required: true,
},
filterFn: {
type: Function,
required: false,
default: null,
},
groupBy: {
type: Array,
required: false,
default: () => [],
},
dashboardsPath: {
type: String,
required: false,
default: null,
},
},
data() {
return {
metrics: [],
groupedMetrics: [],
isLoading: false,
};
},
computed: {
hasGroupedMetrics() {
return Boolean(this.groupBy.length);
},
},
watch: {
requestParams(newVal, oldVal) {
if (!isEqual(newVal, oldVal)) {
this.fetchData();
}
},
},
mounted() {
this.fetchData();
},
methods: {
shouldDisplayDashboardLink(index) {
// When we have groups of metrics, we should only display the link for the first group
return index === 0 && this.dashboardsPath;
},
fetchData() {
removeFlash();
this.isLoading = true;
return fetchMetricsData(this.requests, this.requestPath, this.requestParams)
.then((data) => {
this.metrics = this.filterFn ? this.filterFn(data) : data;
if (this.hasGroupedMetrics) {
this.groupedMetrics = groupRawMetrics(this.groupBy, this.metrics);
}
this.isLoading = false;
})
.catch((err) => {
const message = sprintf(
s__(
'ValueStreamAnalytics|There was an error while fetching value stream analytics %{requestTypeName} data.',
),
{ requestTypeName: err.message },
);
createAlert({ message });
this.isLoading = false;
});
},
},
};
</script>
<template>
<div class="gl-flex" data-testid="vsa-metrics" :class="isLoading ? 'gl-my-6' : 'gl-mt-6'">
<gl-skeleton-loader v-if="isLoading" />
<template v-else>
<div v-if="hasGroupedMetrics" class="gl-flex-col">
<div
v-for="(group, groupIndex) in groupedMetrics"
:key="group.key"
class="gl-mb-7"
data-testid="vsa-metrics-group"
>
<h4 class="gl-my-0">{{ group.title }}</h4>
<div class="gl-flex gl-flex-wrap">
<metric-tile
v-for="metric in group.data"
:key="metric.identifier"
:metric="metric"
class="gl-mt-5 gl-pr-10"
/>
<value-streams-dashboard-link
v-if="shouldDisplayDashboardLink(groupIndex)"
class="gl-mt-5"
:request-path="dashboardsPath"
/>
</div>
</div>
</div>
<div v-else class="gl-mb-7 gl-flex gl-flex-wrap">
<metric-tile
v-for="metric in metrics"
:key="metric.identifier"
:metric="metric"
class="gl-mt-5 gl-pr-10"
/>
</div>
</template>
</div>
</template>
<script>
import { GlSkeletonLoader } from '@gitlab/ui';
import { isEqual, keyBy } from 'lodash';
import { isEqual } from 'lodash';
import { createAlert } from '~/alert';
import { sprintf, s__ } from '~/locale';
import { fetchMetricsData, removeFlash } from '../utils';
import { s__ } from '~/locale';
import {
DORA_METRICS_QUERY_TYPE,
FLOW_METRICS_QUERY_TYPE,
ALL_METRICS_QUERY_TYPE,
VALUE_STREAM_METRIC_TILE_METADATA,
} from '../constants';
import { rawMetricToMetricTile, extractQueryResponseFromNamespace } from '../utils';
import { BUCKETING_INTERVAL_ALL } from '../graphql/constants';
import FlowMetricsQuery from '../graphql/flow_metrics.query.graphql';
import DoraMetricsQuery from '../graphql/dora_metrics.query.graphql';
import ValueStreamsDashboardLink from './value_streams_dashboard_link.vue';
import MetricTile from './metric_tile.vue';
const extractMetricsGroupData = (keyList = [], data = []) => {
if (!keyList.length || !data.length) return [];
const kv = keyBy(data, 'identifier');
return keyList.map((id) => kv[id] || null).filter((obj) => Boolean(obj));
const dataKeys = data.map(({ identifier }) => identifier);
if (!keyList.length || !dataKeys.some((key) => keyList.includes(key))) return [];
return keyList.reduce((acc, curr) => {
const metric = data.find((item) => item.identifier === curr);
return metric ? [...acc, metric] : acc;
}, []);
};
const groupRawMetrics = (groups = [], rawData = []) => {
......@@ -40,9 +53,10 @@ export default {
type: Object,
required: true,
},
requests: {
type: Array,
required: true,
queryType: {
type: String,
required: false,
default: ALL_METRICS_QUERY_TYPE,
},
filterFn: {
type: Function,
......@@ -62,56 +76,111 @@ export default {
},
data() {
return {
metrics: [],
groupedMetrics: [],
isLoading: false,
flowMetrics: [],
doraMetrics: [],
};
},
computed: {
hasGroupedMetrics() {
return Boolean(this.groupBy.length);
},
isLoading() {
return Boolean(
this.$apollo.queries.doraMetrics.loading || this.$apollo.queries.flowMetrics.loading,
);
},
groupedMetrics() {
return groupRawMetrics(this.groupBy, this.metrics);
},
isFlowMetricsQuery() {
return [ALL_METRICS_QUERY_TYPE, FLOW_METRICS_QUERY_TYPE].includes(this.queryType);
},
isDoraMetricsQuery() {
return [ALL_METRICS_QUERY_TYPE, DORA_METRICS_QUERY_TYPE].includes(this.queryType);
},
displayableMetrics() {
// NOTE: workaround while the flowMetrics/doraMetrics dont support including/excluding unwanted metrics from the response
return Object.keys(VALUE_STREAM_METRIC_TILE_METADATA);
},
metrics() {
const combined = [...this.flowMetrics, ...this.doraMetrics].filter(({ identifier }) =>
this.displayableMetrics.includes(identifier),
);
const filtered = this.filterFn ? this.filterFn(combined) : combined;
return filtered.map((metric) => rawMetricToMetricTile(metric));
},
},
watch: {
requestParams(newVal, oldVal) {
async requestParams(newVal, oldVal) {
if (!isEqual(newVal, oldVal)) {
this.fetchData();
await Promise.all([
this.$apollo.queries.doraMetrics.refetch(),
this.$apollo.queries.flowMetrics.refetch(),
]);
}
},
},
mounted() {
this.fetchData();
apollo: {
flowMetrics: {
query: FlowMetricsQuery,
variables() {
const { created_after: startDate, created_before: endDate } = this.requestParams;
return { startDate, endDate, fullPath: this.requestPath };
},
skip() {
return !this.isFlowMetricsQuery;
},
update(data) {
const metrics = extractQueryResponseFromNamespace({
result: { data },
resultKey: 'flowMetrics',
});
return Object.values(metrics).filter((metric) => metric?.identifier);
},
error() {
createAlert({
message: s__('ValueStreamAnalytics|There was an error while fetching flow metrics data.'),
});
},
},
doraMetrics: {
query: DoraMetricsQuery,
variables() {
const { created_after: startDate, created_before: endDate } = this.requestParams;
return {
fullPath: this.requestPath,
interval: BUCKETING_INTERVAL_ALL,
startDate,
endDate,
};
},
skip() {
return !this.isDoraMetricsQuery;
},
update(data) {
const responseData = extractQueryResponseFromNamespace({
result: { data },
resultKey: 'dora',
});
const [rawMetrics] = responseData.metrics;
return Object.entries(rawMetrics).reduce((acc, [identifier, value]) => {
return [...acc, { identifier, value }];
}, []);
},
error() {
createAlert({
message: s__('ValueStreamAnalytics|There was an error while fetching DORA metrics data.'),
});
},
},
},
methods: {
shouldDisplayDashboardLink(index) {
// When we have groups of metrics, we should only display the link for the first group
return index === 0 && this.dashboardsPath;
},
fetchData() {
removeFlash();
this.isLoading = true;
return fetchMetricsData(this.requests, this.requestPath, this.requestParams)
.then((data) => {
this.metrics = this.filterFn ? this.filterFn(data) : data;
if (this.hasGroupedMetrics) {
this.groupedMetrics = groupRawMetrics(this.groupBy, this.metrics);
}
this.isLoading = false;
})
.catch((err) => {
const message = sprintf(
s__(
'ValueStreamAnalytics|There was an error while fetching value stream analytics %{requestTypeName} data.',
),
{ requestTypeName: err.message },
);
createAlert({ message });
this.isLoading = false;
});
},
},
};
</script>
......
......@@ -93,6 +93,10 @@ export const METRIC_POPOVER_LABEL = s__('ValueStreamAnalytics|View details');
export const ISSUES_COMPLETED_TYPE = 'issues_completed';
export const ALL_METRICS_QUERY_TYPE = 'ALL_METRICS_QUERY_TYPE';
export const DORA_METRICS_QUERY_TYPE = 'DORA_METRICS_QUERY_TYPE';
export const FLOW_METRICS_QUERY_TYPE = 'FLOW_METRICS_QUERY_TYPE';
export const FLOW_METRICS = {
LEAD_TIME: 'lead_time',
CYCLE_TIME: 'cycle_time',
......@@ -110,13 +114,19 @@ export const DORA_METRICS = {
CHANGE_FAILURE_RATE: 'change_failure_rate',
};
const VSA_FLOW_METRICS_GROUP = {
key: 'lifecycle_metrics',
title: s__('ValueStreamAnalytics|Lifecycle metrics'),
keys: Object.values(FLOW_METRICS),
};
export const VSA_METRICS_GROUPS = [VSA_FLOW_METRICS_GROUP];
export const VSA_METRICS_GROUPS = [
{
key: 'lifecycle_metrics',
title: s__('ValueStreamAnalytics|Lifecycle metrics'),
keys: [
FLOW_METRICS.LEAD_TIME,
FLOW_METRICS.CYCLE_TIME,
FLOW_METRICS.ISSUES,
FLOW_METRICS.COMMITS,
FLOW_METRICS.DEPLOYS,
],
},
];
export const VULNERABILITY_CRITICAL_TYPE = 'vulnerability_critical';
export const VULNERABILITY_HIGH_TYPE = 'vulnerability_high';
......@@ -151,6 +161,9 @@ export const VALUE_STREAM_METRIC_DISPLAY_UNITS = {
[UNITS.PERCENT]: '%',
};
// NOTE: ideally we would return these fields in the metrics queries
// the flow metrics query returns some but not all fields we need
// while the DORA query do not return any.
export const VALUE_STREAM_METRIC_TILE_METADATA = {
[DORA_METRICS.DEPLOYMENT_FREQUENCY]: {
label: s__('DORA4Metrics|Deployment frequency'),
......@@ -215,7 +228,7 @@ export const VALUE_STREAM_METRIC_TILE_METADATA = {
unit: UNITS.DAYS,
},
[FLOW_METRICS.ISSUES]: {
label: s__('DORA4Metrics|Issues created'),
label: s__('DORA4Metrics|New issues'),
unit: UNITS.COUNT,
description: s__('ValueStreamAnalytics|Number of new issues created.'),
groupLink: '-/issues_analytics',
......
import { flatten } from 'lodash';
import dateFormat from '~/lib/dateformat';
import { SECONDS_IN_DAY } from '~/lib/utils/datetime_utility';
import { slugify } from '~/lib/utils/text_utility';
import { joinPaths } from '~/lib/utils/url_utility';
import { urlQueryToFilter } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import { dateFormats, VALUE_STREAM_METRIC_METADATA } from './constants';
import {
dateFormats,
FLOW_METRICS,
MAX_METRIC_PRECISION,
UNITS,
VALUE_STREAM_METRIC_DISPLAY_UNITS,
VALUE_STREAM_METRIC_TILE_METADATA,
} from './constants';
export const filterBySearchTerm = (data = [], searchTerm = '', filterByKey = 'name') => {
if (!searchTerm?.length) return data;
......@@ -117,10 +125,76 @@ const requestData = ({ request, endpoint, requestPath, params, name }) => {
export const fetchMetricsData = (requests = [], requestPath, params) => {
const promises = requests.map((r) => requestData({ ...r, requestPath, params }));
return Promise.all(promises).then((responses) =>
prepareTimeMetricsData(flatten(responses), VALUE_STREAM_METRIC_METADATA),
prepareTimeMetricsData(flatten(responses), VALUE_STREAM_METRIC_TILE_METADATA),
);
};
/**
* Formats any valid number as percentage
*
* @param {number|string} decimalValue Decimal value between 0 and 1 to be converted to a percentage
* @param {number} precision The number of decimal places to round to
*
* @returns {string} Returns a formatted string multiplied by 100
*/
export const formatAsPercentageWithoutSymbol = (decimalValue = 0, precision = 1) => {
const parsed = Number.isNaN(Number(decimalValue)) ? 0 : decimalValue;
return (parsed * 100).toFixed(precision);
};
/**
* Converts a time in seconds to number of days, with variable precision
*
* @param {Number} seconds Time in seconds
* @param {Number} precision Specifies the number of digits after the decimal
*
* @returns {Float} The number of days
*/
export const secondsToDays = (seconds, precision = 1) =>
(seconds / SECONDS_IN_DAY).toFixed(precision);
export const scaledValueForDisplay = (value, units, precision = MAX_METRIC_PRECISION) => {
switch (units) {
case UNITS.PERCENT:
return formatAsPercentageWithoutSymbol(value);
case UNITS.DAYS:
return secondsToDays(value, precision);
default:
return value;
}
};
const prepareMetricValue = ({ identifier, value, unit }) => {
// NOTE: the flow metrics graphql endpoint returns values already scaled for display
if (!value) {
// ensures we return `-` for 0/null etc
return '-';
}
return Object.values(FLOW_METRICS).includes(identifier)
? value
: scaledValueForDisplay(value, unit);
};
/**
* Prepares metric data to be rendered in the metric_tile component
*
* @param {MetricData[]} data - The metric data to be rendered
* @returns {TransformedMetricData[]} An array of metrics ready to render in the metric_tile
*/
export const rawMetricToMetricTile = (metric) => {
const { identifier, value, ...metricRest } = metric;
const { unit, label, ...metadataRest } = VALUE_STREAM_METRIC_TILE_METADATA[identifier];
return {
...metadataRest,
...metricRest,
title: label,
identifier,
label,
unit: VALUE_STREAM_METRIC_DISPLAY_UNITS[unit],
value: prepareMetricValue({ value, unit, identifier }),
};
};
/**
* Generates a URL link to the VSD dashboard based on the group
* and project paths passed into the method.
......@@ -149,3 +223,24 @@ export const extractVSAFeaturesFromGON = () => ({
cycleAnalyticsForProjects: Boolean(gon?.licensed_features?.cycleAnalyticsForProjects),
groupLevelAnalyticsDashboard: Boolean(gon?.licensed_features?.groupLevelAnalyticsDashboard),
});
/**
* Takes a raw GraphQL response which could contain data for a group or project namespace,
* and returns the data for the namespace which is present in the response.
*
* @param {Object} params
* @param {string} params.resultKey - The data to be extracted from the namespace.
* @param {Object} params.result
* @param {Object} params.result.data
* @param {Object} params.result.data.group - The group GraphQL response.
* @param {Object} params.result.data.project - The project GraphQL response.
* @returns {Object} The data extracted from either group[resultKey] or project[resultKey].
*/
export const extractQueryResponseFromNamespace = ({ result, resultKey }) => {
const { group = null, project = null } = result.data;
if (group || project) {
const namespace = group ?? project;
return namespace[resultKey] || {};
}
return {};
};
......@@ -17459,6 +17459,9 @@ msgstr ""
msgid "DORA4Metrics|Month to date"
msgstr ""
 
msgid "DORA4Metrics|New issues"
msgstr ""
msgid "DORA4Metrics|No data available for Group: %{fullPath}"
msgstr ""
 
......@@ -60910,6 +60913,12 @@ msgstr ""
msgid "ValueStreamAnalytics|The time to successfully deliver a commit into production. This metric reflects the efficiency of CI/CD pipelines."
msgstr ""
 
msgid "ValueStreamAnalytics|There was an error while fetching DORA metrics data."
msgstr ""
msgid "ValueStreamAnalytics|There was an error while fetching flow metrics data."
msgstr ""
msgid "ValueStreamAnalytics|There was an error while fetching value stream analytics %{requestTypeName} data."
msgstr ""
 
......@@ -4,7 +4,7 @@ import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue';
import LegacyValueStreamMetrics from '~/analytics/shared/components/legacy_value_stream_metrics.vue';
import BaseComponent from '~/analytics/cycle_analytics/components/base.vue';
import PathNavigation from '~/analytics/cycle_analytics/components/path_navigation.vue';
import StageTable from '~/analytics/cycle_analytics/components/stage_table.vue';
......@@ -79,7 +79,7 @@ function createComponent({ initialState, initialGetters } = {}) {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPathNavigation = () => wrapper.findComponent(PathNavigation);
const findFilters = () => wrapper.findComponent(ValueStreamFilters);
const findOverviewMetrics = () => wrapper.findComponent(ValueStreamMetrics);
const findOverviewMetrics = () => wrapper.findComponent(LegacyValueStreamMetrics);
const findStageTable = () => wrapper.findComponent(StageTable);
const findStageEvents = () => findStageTable().props('stageEvents');
const findEmptyStageTitle = () => wrapper.findComponent(GlEmptyState).props('title');
......
import { GlSkeletonLoader } from '@gitlab/ui';
import { nextTick } from 'vue';
import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics/summary.json';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import LegacyValueStreamMetrics from '~/analytics/shared/components/legacy_value_stream_metrics.vue';
import { METRIC_TYPE_SUMMARY } from '~/api/analytics_api';
import { VSA_METRICS_GROUPS, VALUE_STREAM_METRIC_METADATA } from '~/analytics/shared/constants';
import { prepareTimeMetricsData } from '~/analytics/shared/utils';
import MetricTile from '~/analytics/shared/components/metric_tile.vue';
import ValueStreamsDashboardLink from '~/analytics/shared/components/value_streams_dashboard_link.vue';
import { createAlert } from '~/alert';
import { group } from '../mock_data';
jest.mock('~/alert');
describe('LegacyValueStreamMetrics', () => {
let wrapper;
let mockGetValueStreamSummaryMetrics;
let mockFilterFn;
const { full_path: requestPath } = group;
const fakeReqName = 'Mock metrics';
const metricsRequestFactory = () => ({
request: mockGetValueStreamSummaryMetrics,
endpoint: METRIC_TYPE_SUMMARY,
name: fakeReqName,
});
const createComponent = (props = {}) => {
return shallowMountExtended(LegacyValueStreamMetrics, {
propsData: {
requestPath,
requestParams: {},
requests: [metricsRequestFactory()],
...props,
},
});
};
const findVSDLink = () => wrapper.findComponent(ValueStreamsDashboardLink);
const findMetrics = () => wrapper.findAllComponents(MetricTile);
const findMetricsGroups = () => wrapper.findAllByTestId('vsa-metrics-group');
const expectToHaveRequest = (fields) => {
expect(mockGetValueStreamSummaryMetrics).toHaveBeenCalledWith({
endpoint: METRIC_TYPE_SUMMARY,
requestPath,
...fields,
});
};
describe('with successful requests', () => {
beforeEach(() => {
mockGetValueStreamSummaryMetrics = jest.fn().mockResolvedValue({ data: metricsData });
});
it('will display a loader with pending requests', async () => {
wrapper = createComponent();
await nextTick();
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
});
describe('with data loaded', () => {
beforeEach(async () => {
wrapper = createComponent();
await waitForPromises();
});
it('fetches data from the value stream analytics endpoint', () => {
expectToHaveRequest({ params: {} });
});
describe.each`
index | identifier | value | label
${0} | ${metricsData[0].identifier} | ${metricsData[0].value} | ${metricsData[0].title}
${1} | ${metricsData[1].identifier} | ${metricsData[1].value} | ${metricsData[1].title}
${2} | ${metricsData[2].identifier} | ${metricsData[2].value} | ${metricsData[2].title}
${3} | ${metricsData[3].identifier} | ${metricsData[3].value} | ${metricsData[3].title}
`('metric tiles', ({ identifier, index, value, label }) => {
it(`renders a metric tile component for "${label}"`, () => {
const metric = findMetrics().at(index);
expect(metric.props('metric')).toMatchObject({ identifier, value, label });
expect(metric.isVisible()).toBe(true);
});
});
it('will not display a loading icon', () => {
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false);
});
describe('filterFn', () => {
const transferredMetricsData = prepareTimeMetricsData(
metricsData,
VALUE_STREAM_METRIC_METADATA,
);
it('with a filter function, will call the function with the metrics data', async () => {
const filteredData = [
{ identifier: 'issues', value: '3', title: 'New issues', description: 'foo' },
];
mockFilterFn = jest.fn(() => filteredData);
wrapper = createComponent({
filterFn: mockFilterFn,
});
await waitForPromises();
expect(mockFilterFn).toHaveBeenCalledWith(transferredMetricsData);
expect(findMetrics().at(0).props('metric')).toEqual(filteredData[0]);
});
it('without a filter function, it will only update the metrics', async () => {
wrapper = createComponent();
await waitForPromises();
expect(mockFilterFn).not.toHaveBeenCalled();
expect(findMetrics().at(0).props('metric')).toEqual(transferredMetricsData[0]);
});
});
describe('with additional params', () => {
beforeEach(async () => {
wrapper = createComponent({
requestParams: {
'project_ids[]': [1],
created_after: '2020-01-01',
created_before: '2020-02-01',
},
});
await waitForPromises();
});
it('fetches data for the `getValueStreamSummaryMetrics` request', () => {
expectToHaveRequest({
params: {
'project_ids[]': [1],
created_after: '2020-01-01',
created_before: '2020-02-01',
},
});
});
});
describe('groupBy', () => {
beforeEach(async () => {
mockGetValueStreamSummaryMetrics = jest.fn().mockResolvedValue({ data: metricsData });
wrapper = createComponent({ groupBy: VSA_METRICS_GROUPS });
await waitForPromises();
});
it('renders the metrics as separate groups', () => {
const groups = findMetricsGroups();
expect(groups).toHaveLength(VSA_METRICS_GROUPS.length);
});
it('renders titles for each group', () => {
const groups = findMetricsGroups();
groups.wrappers.forEach((g, index) => {
const { title } = VSA_METRICS_GROUPS[index];
expect(g.html()).toContain(title);
});
});
});
});
});
describe('Value Streams Dashboard Link', () => {
it('will render when a dashboardsPath is set', async () => {
wrapper = createComponent({
groupBy: VSA_METRICS_GROUPS,
dashboardsPath: 'fake-group-path',
});
await waitForPromises();
const vsdLink = findVSDLink();
expect(vsdLink.exists()).toBe(true);
expect(vsdLink.props()).toEqual({ requestPath: 'fake-group-path' });
});
it('does not render without a dashboardsPath', async () => {
wrapper = createComponent({ groupBy: VSA_METRICS_GROUPS });
await waitForPromises();
expect(findVSDLink().exists()).toBe(false);
});
});
describe('with a request failing', () => {
beforeEach(async () => {
mockGetValueStreamSummaryMetrics = jest.fn().mockRejectedValue();
wrapper = createComponent();
await waitForPromises();
});
it('should render an error message', () => {
expect(createAlert).toHaveBeenCalledWith({
message: `There was an error while fetching value stream analytics ${fakeReqName} data.`,
});
});
});
});
import { GlSkeletonLoader } from '@gitlab/ui';
import { nextTick } from 'vue';
import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics/summary.json';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue';
import { METRIC_TYPE_SUMMARY } from '~/api/analytics_api';
import { VSA_METRICS_GROUPS, VALUE_STREAM_METRIC_METADATA } from '~/analytics/shared/constants';
import { prepareTimeMetricsData } from '~/analytics/shared/utils';
import FlowMetricsQuery from '~/analytics/shared/graphql/flow_metrics.query.graphql';
import DoraMetricsQuery from '~/analytics/shared/graphql/dora_metrics.query.graphql';
import { FLOW_METRICS, DORA_METRICS, VSA_METRICS_GROUPS } from '~/analytics/shared/constants';
import MetricTile from '~/analytics/shared/components/metric_tile.vue';
import ValueStreamsDashboardLink from '~/analytics/shared/components/value_streams_dashboard_link.vue';
import { createAlert } from '~/alert';
import { group } from '../mock_data';
import {
mockGraphqlFlowMetricsResponse,
mockGraphqlDoraMetricsResponse,
} from '../../shared/helpers';
import {
mockDoraMetricsResponseData,
mockFlowMetricsResponseData,
mockMetricTilesData,
} from '../../shared/mock_data';
const mockTypePolicy = {
Query: { fields: { project: { merge: false }, group: { merge: false } } },
};
jest.mock('~/alert');
Vue.use(VueApollo);
describe('ValueStreamMetrics', () => {
let wrapper;
let mockGetValueStreamSummaryMetrics;
let mockApolloProvider;
let mockFilterFn;
let flowMetricsRequestHandler = null;
let doraMetricsRequestHandler = null;
const setGraphqlQueryHandlerResponses = ({
doraMetricsResponse = mockDoraMetricsResponseData,
flowMetricsResponse = mockFlowMetricsResponseData,
} = {}) => {
flowMetricsRequestHandler = mockGraphqlFlowMetricsResponse(flowMetricsResponse);
doraMetricsRequestHandler = mockGraphqlDoraMetricsResponse(doraMetricsResponse);
};
const { full_path: requestPath } = group;
const fakeReqName = 'Mock metrics';
const metricsRequestFactory = () => ({
request: mockGetValueStreamSummaryMetrics,
endpoint: METRIC_TYPE_SUMMARY,
name: fakeReqName,
});
const createMockApolloProvider = ({
flowMetricsRequest = flowMetricsRequestHandler,
doraMetricsRequest = doraMetricsRequestHandler,
} = {}) => {
return createMockApollo(
[
[FlowMetricsQuery, flowMetricsRequest],
[DoraMetricsQuery, doraMetricsRequest],
],
{},
{
typePolicies: mockTypePolicy,
},
);
};
const { path: requestPath } = group;
const createComponent = (props = {}) => {
return shallowMountExtended(ValueStreamMetrics, {
const createComponent = async ({ props = {}, apolloProvider = null } = {}) => {
wrapper = shallowMountExtended(ValueStreamMetrics, {
apolloProvider,
propsData: {
requestPath,
requestParams: {},
requests: [metricsRequestFactory()],
...props,
},
});
await waitForPromises();
};
const findVSDLink = () => wrapper.findComponent(ValueStreamsDashboardLink);
const findMetrics = () => wrapper.findAllComponents(MetricTile);
const findMetricsGroups = () => wrapper.findAllByTestId('vsa-metrics-group');
const expectToHaveRequest = (fields) => {
expect(mockGetValueStreamSummaryMetrics).toHaveBeenCalledWith({
endpoint: METRIC_TYPE_SUMMARY,
requestPath,
...fields,
const expectDoraMetricsRequests = ({ fullPath = requestPath, startDate, endDate } = {}) =>
expect(doraMetricsRequestHandler).toHaveBeenCalledWith({
fullPath,
startDate,
endDate,
interval: 'ALL',
});
};
describe('with successful requests', () => {
beforeEach(() => {
mockGetValueStreamSummaryMetrics = jest.fn().mockResolvedValue({ data: metricsData });
const expectFlowMetricsRequests = ({
fullPath = requestPath,
labelNames,
startDate,
endDate,
} = {}) =>
expect(flowMetricsRequestHandler).toHaveBeenCalledWith({
fullPath,
startDate,
endDate,
labelNames,
});
it('will display a loader with pending requests', async () => {
wrapper = createComponent();
await nextTick();
afterEach(() => {
mockApolloProvider = null;
});
describe('loading requests', () => {
beforeEach(() => {
setGraphqlQueryHandlerResponses();
createComponent({ apolloProvider: createMockApolloProvider() });
});
it('will display a loader with pending requests', () => {
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
});
});
describe('with data loaded', () => {
describe('with data loaded', () => {
describe('default', () => {
beforeEach(async () => {
wrapper = createComponent();
await waitForPromises();
setGraphqlQueryHandlerResponses();
mockApolloProvider = createMockApolloProvider();
await createComponent({ apolloProvider: mockApolloProvider });
});
it('fetches dora metrics data', () => {
expectDoraMetricsRequests();
});
it('fetches flow metrics data', () => {
expectFlowMetricsRequests();
});
it('fetches data from the value stream analytics endpoint', () => {
expectToHaveRequest({ params: {} });
it('will not display a loading icon', () => {
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false);
});
describe.each`
index | identifier | value | label
${0} | ${metricsData[0].identifier} | ${metricsData[0].value} | ${metricsData[0].title}
${1} | ${metricsData[1].identifier} | ${metricsData[1].value} | ${metricsData[1].title}
${2} | ${metricsData[2].identifier} | ${metricsData[2].value} | ${metricsData[2].title}
${3} | ${metricsData[3].identifier} | ${metricsData[3].value} | ${metricsData[3].title}
index | identifier | value | label
${0} | ${FLOW_METRICS.ISSUES} | ${10} | ${'New issues'}
${1} | ${FLOW_METRICS.CYCLE_TIME} | ${'-'} | ${'Cycle time'}
${2} | ${FLOW_METRICS.LEAD_TIME} | ${10} | ${'Lead time'}
${3} | ${FLOW_METRICS.DEPLOYS} | ${751} | ${'Deploys'}
${4} | ${DORA_METRICS.DEPLOYMENT_FREQUENCY} | ${23.75} | ${'Deployment frequency'}
${5} | ${DORA_METRICS.CHANGE_FAILURE_RATE} | ${'5.7'} | ${'Change failure rate'}
${6} | ${DORA_METRICS.LEAD_TIME_FOR_CHANGES} | ${'0.2721'} | ${'Lead time for changes'}
${7} | ${DORA_METRICS.TIME_TO_RESTORE_SERVICE} | ${'0.8343'} | ${'Time to restore service'}
`('metric tiles', ({ identifier, index, value, label }) => {
it(`renders a metric tile component for "${label}"`, () => {
const metric = findMetrics().at(index);
......@@ -85,85 +155,92 @@ describe('ValueStreamMetrics', () => {
expect(metric.isVisible()).toBe(true);
});
});
});
it('will not display a loading icon', () => {
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false);
describe('with filterFn', () => {
beforeEach(() => {
setGraphqlQueryHandlerResponses();
mockApolloProvider = createMockApolloProvider();
});
describe('filterFn', () => {
const transferredMetricsData = prepareTimeMetricsData(
metricsData,
VALUE_STREAM_METRIC_METADATA,
);
it('with a filter function, will call the function with the metrics data', async () => {
const filteredData = mockMetricTilesData[0];
it('with a filter function, will call the function with the metrics data', async () => {
const filteredData = [
{ identifier: 'issues', value: '3', title: 'New issues', description: 'foo' },
];
mockFilterFn = jest.fn(() => filteredData);
mockFilterFn = jest.fn(() => [filteredData]);
wrapper = createComponent({
await createComponent({
apolloProvider: mockApolloProvider,
props: {
filterFn: mockFilterFn,
});
await waitForPromises();
expect(mockFilterFn).toHaveBeenCalledWith(transferredMetricsData);
expect(findMetrics().at(0).props('metric')).toEqual(filteredData[0]);
},
});
it('without a filter function, it will only update the metrics', async () => {
wrapper = createComponent();
expect(mockFilterFn).toHaveBeenCalled();
expect(findMetrics().at(0).props('metric').identifier).toEqual(filteredData.identifier);
});
await waitForPromises();
it('without a filter function, it will only update the metrics', async () => {
await createComponent({ apolloProvider: mockApolloProvider });
expect(mockFilterFn).not.toHaveBeenCalled();
expect(findMetrics().at(0).props('metric')).toEqual(transferredMetricsData[0]);
});
expect(mockFilterFn).not.toHaveBeenCalled();
});
});
describe('with additional params', () => {
beforeEach(async () => {
setGraphqlQueryHandlerResponses();
describe('with additional params', () => {
beforeEach(async () => {
wrapper = createComponent({
await createComponent({
apolloProvider: createMockApolloProvider(),
props: {
requestParams: {
'project_ids[]': [1],
created_after: '2020-01-01',
created_before: '2020-02-01',
'labelNames[]': ['some', 'fake', 'label'],
},
});
await waitForPromises();
},
});
});
it('fetches data for the `getValueStreamSummaryMetrics` request', () => {
expectToHaveRequest({
params: {
'project_ids[]': [1],
created_after: '2020-01-01',
created_before: '2020-02-01',
},
});
it('fetches the flowMetrics data', () => {
expectFlowMetricsRequests({
'project_ids[]': [1],
startDate: '2020-01-01',
endDate: '2020-02-01',
});
});
describe('groupBy', () => {
beforeEach(async () => {
mockGetValueStreamSummaryMetrics = jest.fn().mockResolvedValue({ data: metricsData });
wrapper = createComponent({ groupBy: VSA_METRICS_GROUPS });
await waitForPromises();
it('fetches the doraMetrics data', () => {
expectDoraMetricsRequests({
'project_ids[]': [1],
startDate: '2020-01-01',
endDate: '2020-02-01',
});
});
});
it('renders the metrics as separate groups', () => {
const groups = findMetricsGroups();
expect(groups).toHaveLength(VSA_METRICS_GROUPS.length);
describe('with groupBy', () => {
beforeEach(async () => {
setGraphqlQueryHandlerResponses();
await createComponent({
apolloProvider: createMockApolloProvider(),
props: { groupBy: VSA_METRICS_GROUPS },
});
});
it('renders titles for each group', () => {
const groups = findMetricsGroups();
groups.wrappers.forEach((g, index) => {
const { title } = VSA_METRICS_GROUPS[index];
expect(g.html()).toContain(title);
});
it('renders the metrics as separate groups', () => {
const groups = findMetricsGroups();
expect(groups).toHaveLength(VSA_METRICS_GROUPS.length);
});
it('renders titles for each group', () => {
const groups = findMetricsGroups();
groups.wrappers.forEach((g, index) => {
const { title } = VSA_METRICS_GROUPS[index];
expect(g.html()).toContain(title);
});
});
});
......@@ -171,11 +248,15 @@ describe('ValueStreamMetrics', () => {
describe('Value Streams Dashboard Link', () => {
it('will render when a dashboardsPath is set', async () => {
wrapper = createComponent({
groupBy: VSA_METRICS_GROUPS,
dashboardsPath: 'fake-group-path',
setGraphqlQueryHandlerResponses();
await createComponent({
apolloProvider: createMockApolloProvider(),
props: {
groupBy: VSA_METRICS_GROUPS,
dashboardsPath: 'fake-group-path',
},
});
await waitForPromises();
const vsdLink = findVSDLink();
......@@ -184,24 +265,45 @@ describe('ValueStreamMetrics', () => {
});
it('does not render without a dashboardsPath', async () => {
wrapper = createComponent({ groupBy: VSA_METRICS_GROUPS });
await waitForPromises();
await createComponent({
apolloProvider: createMockApolloProvider(),
props: { groupBy: VSA_METRICS_GROUPS },
});
expect(findVSDLink().exists()).toBe(false);
});
});
describe('with a request failing', () => {
beforeEach(async () => {
mockGetValueStreamSummaryMetrics = jest.fn().mockRejectedValue();
wrapper = createComponent();
describe('failing DORA metrics request', () => {
beforeEach(async () => {
doraMetricsRequestHandler = jest.fn().mockRejectedValue({});
await waitForPromises();
await createComponent({
apolloProvider: createMockApolloProvider(),
});
});
it('should render an error message', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'There was an error while fetching DORA metrics data.',
});
});
});
it('should render an error message', () => {
expect(createAlert).toHaveBeenCalledWith({
message: `There was an error while fetching value stream analytics ${fakeReqName} data.`,
describe('failing flow metrics request', () => {
beforeEach(async () => {
flowMetricsRequestHandler = jest.fn().mockRejectedValue({});
await createComponent({
apolloProvider: createMockApolloProvider(),
});
});
it('should render an error message', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'There was an error while fetching flow metrics data.',
});
});
});
});
......
import { mockDoraMetricsResponseData, mockFlowMetricsResponseData } from './mock_data';
export const mockGraphqlFlowMetricsResponse = (mockDataResponse = mockFlowMetricsResponseData) =>
jest.fn().mockResolvedValue({
data: {
project: null,
group: { id: 'fake-flow-metrics-request', flowMetrics: mockDataResponse },
},
});
export const mockGraphqlDoraMetricsResponse = (mockDataResponse = mockDoraMetricsResponseData) =>
jest.fn().mockResolvedValue({
data: {
project: null,
group: { id: 'fake-dora-metrics-request', dora: mockDataResponse },
},
});
import { DORA_METRICS, VALUE_STREAM_METRIC_TILE_METADATA } from '~/analytics/shared/constants';
export const mockLastVulnerabilityCountData = {
date: '2020-05-20',
critical: 7,
high: 6,
medium: 5,
low: 4,
};
const deploymentFrequency = {
...VALUE_STREAM_METRIC_TILE_METADATA[DORA_METRICS.DEPLOYMENT_FREQUENCY],
label: 'Deployment frequency',
identifier: DORA_METRICS.DEPLOYMENT_FREQUENCY,
value: 23.75,
};
const changeFailureRate = {
...VALUE_STREAM_METRIC_TILE_METADATA[DORA_METRICS.CHANGE_FAILURE_RATE],
label: 'Change failure rate',
identifier: DORA_METRICS.CHANGE_FAILURE_RATE,
value: 0.056578947368421055,
};
const leadTimeForChanges = {
...VALUE_STREAM_METRIC_TILE_METADATA[DORA_METRICS.LEAD_TIME_FOR_CHANGES],
label: 'Lead time for changes',
identifier: DORA_METRICS.LEAD_TIME_FOR_CHANGES,
value: 23508,
};
const timeToRestoreService = {
...VALUE_STREAM_METRIC_TILE_METADATA[DORA_METRICS.TIME_TO_RESTORE_SERVICE],
label: 'Time to restore service',
identifier: DORA_METRICS.TIME_TO_RESTORE_SERVICE,
value: 72080,
};
export const mockDoraMetricsResponseData = {
metrics: [
{
date: null,
deployment_frequency: deploymentFrequency.value,
change_failure_rate: changeFailureRate.value,
lead_time_for_changes: leadTimeForChanges.value,
time_to_restore_service: timeToRestoreService.value,
__typename: 'DoraMetric',
},
],
__typename: 'Dora',
};
const issues = {
unit: null,
value: 10,
identifier: 'issues',
links: [],
title: 'New issues',
__typename: 'ValueStreamAnalyticsMetric',
};
const cycleTime = {
unit: 'days',
value: null,
identifier: 'cycle_time',
links: [],
title: 'Cycle time',
__typename: 'ValueStreamAnalyticsMetric',
};
const leadTime = {
unit: 'days',
value: 10,
identifier: 'lead_time',
links: [
{
label: 'Dashboard',
name: 'Lead time',
docsLink: null,
url: '/groups/test-graphql-dora/-/issues_analytics',
__typename: 'ValueStreamMetricLinkType',
},
{
label: 'Go to docs',
name: 'Lead time',
docsLink: true,
url: '/help/user/analytics/index#definitions',
__typename: 'ValueStreamMetricLinkType',
},
],
title: 'Lead time',
__typename: 'ValueStreamAnalyticsMetric',
};
const deploys = {
unit: null,
value: 751,
identifier: 'deploys',
links: [],
title: 'Deploys',
__typename: 'ValueStreamAnalyticsMetric',
};
export const rawMetricData = [
issues,
cycleTime,
leadTime,
deploys,
deploymentFrequency,
changeFailureRate,
leadTimeForChanges,
timeToRestoreService,
];
export const mockMetricTilesData = rawMetricData.map(({ value, ...rest }) => ({
...rest,
value: !value ? '-' : value,
}));
export const mockFlowMetricsResponseData = {
issues,
issues_completed: {
unit: 'issues',
value: 109,
identifier: 'issues_completed',
links: [
{
label: 'Dashboard',
name: 'Issues Completed',
docsLink: null,
url: '/groups/toolbox/-/issues_analytics',
__typename: 'ValueStreamMetricLinkType',
},
{
label: 'Go to docs',
name: 'Issues Completed',
docsLink: true,
url: '/help/user/analytics/index#definitions',
__typename: 'ValueStreamMetricLinkType',
},
],
title: 'Issues Completed',
__typename: 'ValueStreamAnalyticsMetric',
},
cycle_time: cycleTime,
lead_time: leadTime,
deploys,
median_time_to_merge: {
unit: 'days',
value: '0.3',
identifier: 'median_time_to_merge',
links: [],
title: 'Time to Merge',
__typename: 'ValueStreamAnalyticsMetric',
},
__typename: 'GroupValueStreamAnalyticsFlowMetrics',
};
......@@ -18,8 +18,8 @@ cmd/gitlab-zip-metadata/main.go:17:5: exported: exported var Version should have
cmd/gitlab-zip-metadata/main.go:66:9: superfluous-else: if block ends with call to os.Exit function, so drop this else and outdent its block (revive)
internal/api/channel_settings.go:57:28: G402: TLS MinVersion too low. (gosec)
internal/channel/channel.go:128:31: response body must be closed (bodyclose)
internal/config/config.go:246:18: G204: Subprocess launched with variable (gosec)
internal/config/config.go:328:8: G101: Potential hardcoded credentials (gosec)
internal/config/config.go:247:18: G204: Subprocess launched with variable (gosec)
internal/config/config.go:339:8: G101: Potential hardcoded credentials (gosec)
internal/dependencyproxy/dependencyproxy.go:114: Function 'Inject' is too long (61 > 60) (funlen)
internal/dependencyproxy/dependencyproxy_test.go:510: internal/dependencyproxy/dependencyproxy_test.go:510: Line contains TODO/BUG/FIXME/NOTE/OPTIMIZE/HACK: "note that the timeout duration here is s..." (godox)
internal/git/archive.go:35:2: var-naming: struct field CommitId should be CommitID (revive)
......
......@@ -5,6 +5,7 @@ go 1.22
toolchain go1.22.6
require (
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1
github.com/BurntSushi/toml v1.4.0
github.com/alecthomas/chroma/v2 v2.14.0
......@@ -51,7 +52,6 @@ require (
cloud.google.com/go/trace v1.10.12 // indirect
contrib.go.opencensus.io/exporter/stackdriver v0.13.14 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
......
......@@ -16,6 +16,7 @@ import (
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
"github.com/BurntSushi/toml"
......@@ -266,7 +267,7 @@ func (c *Config) RegisterGoCloudURLOpeners() error {
c.ObjectStorageConfig.URLMux = new(blob.URLMux)
creds := c.ObjectStorageCredentials
if strings.EqualFold(creds.Provider, "AzureRM") && creds.AzureCredentials.AccountName != "" && creds.AzureCredentials.AccountKey != "" {
if strings.EqualFold(creds.Provider, "AzureRM") && creds.AzureCredentials.AccountName != "" {
urlOpener := creds.AzureCredentials.getURLOpener()
c.ObjectStorageConfig.URLMux.RegisterBucket(azureblob.Scheme, urlOpener)
}
......@@ -288,12 +289,22 @@ func (creds *AzureCredentials) getURLOpener() *azureblob.URLOpener {
}
clientFunc := func(svcURL azureblob.ServiceURL, containerName azureblob.ContainerName) (*container.Client, error) {
sharedKeyCred, err := azblob.NewSharedKeyCredential(creds.AccountName, creds.AccountKey)
containerURL := fmt.Sprintf("%s/%s", svcURL, containerName)
if creds.AccountKey != "" {
sharedKeyCred, err := azblob.NewSharedKeyCredential(creds.AccountName, creds.AccountKey)
if err != nil {
return nil, fmt.Errorf("error creating Azure credentials: %w", err)
}
return container.NewClientWithSharedKeyCredential(containerURL, sharedKeyCred, &container.ClientOptions{})
}
creds, err := azidentity.NewDefaultAzureCredential(nil)
if err != nil {
return nil, fmt.Errorf("error creating Azure credentials: %w", err)
return nil, fmt.Errorf("error creating default Azure credentials: %w", err)
}
containerURL := fmt.Sprintf("%s/%s", svcURL, containerName)
return container.NewClientWithSharedKeyCredential(containerURL, sharedKeyCred, &container.ClientOptions{})
return container.NewClient(containerURL, creds, nil)
}
return &azureblob.URLOpener{
......
......@@ -17,6 +17,14 @@ azure_storage_account_name = "azuretester"
azure_storage_access_key = "deadbeef"
`
const azureConfigWithManagedIdentity = `
[object_storage]
provider = "AzureRM"
[object_storage.azurerm]
azure_storage_account_name = "azuretester"
`
const googleConfigWithKeyLocation = `
[object_storage]
provider = "Google"
......@@ -101,6 +109,21 @@ func TestRegisterGoCloudAzureURLOpeners(t *testing.T) {
testRegisterGoCloudURLOpener(t, cfg, "azblob")
}
func TestRegisterGoCloudAzureURLOpenersWithManagedIdentity(t *testing.T) {
cfg, err := LoadConfig(azureConfigWithManagedIdentity)
require.NoError(t, err)
expected := ObjectStorageCredentials{
Provider: "AzureRM",
AzureCredentials: AzureCredentials{
AccountName: "azuretester",
},
}
require.Equal(t, expected, cfg.ObjectStorageCredentials)
testRegisterGoCloudURLOpener(t, cfg, "azblob")
}
func TestRegisterGoCloudGoogleURLOpenersWithJSONKeyLocation(t *testing.T) {
cfg, err := LoadConfig(googleConfigWithKeyLocation)
require.NoError(t, err)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment