- Plese type the following to confirm:
+ Please type the following to confirm:
delete
diff --git a/spec/frontend/ci/catalog/components/list/catalog_search_spec.js b/spec/frontend/ci/catalog/components/list/catalog_search_spec.js
index a3175372b76..feeffed0919 100644
--- a/spec/frontend/ci/catalog/components/list/catalog_search_spec.js
+++ b/spec/frontend/ci/catalog/components/list/catalog_search_spec.js
@@ -16,20 +16,22 @@ describe('CatalogSearch', () => {
const findSorting = () => wrapper.findComponent(GlSorting);
const findAllSortingItems = () => findSorting().props('sortOptions');
- const createComponent = () => {
- wrapper = shallowMountExtended(CatalogSearch, {});
+ const createComponent = ({ withCatalogPopularity = false } = {}) => {
+ wrapper = shallowMountExtended(CatalogSearch, {
+ provide: { glFeatures: { ciCatalogPopularity: withCatalogPopularity } },
+ });
};
- beforeEach(() => {
- createComponent();
- });
-
describe('default UI', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
it('renders the search bar', () => {
expect(findSearchBar().exists()).toBe(true);
});
- it('sets sorting options', () => {
+ it('adds sorting options', () => {
const sortOptionsProp = findAllSortingItems();
expect(sortOptionsProp).toHaveLength(3);
expect(sortOptionsProp[0].text).toBe('Released at');
@@ -40,7 +42,23 @@ describe('CatalogSearch', () => {
});
});
+ describe('with `ci_catalog_popularity` ff turned on', () => {
+ beforeEach(() => {
+ createComponent({ withCatalogPopularity: true });
+ });
+
+ it('adds the popularity option', () => {
+ const sortOptionsProp = findAllSortingItems();
+ expect(sortOptionsProp).toHaveLength(4);
+ expect(sortOptionsProp[3].text).toBe('Popularity');
+ });
+ });
+
describe('search', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
it('passes down the search value to the search component', async () => {
const newSearchTerm = 'cat';
@@ -85,6 +103,10 @@ describe('CatalogSearch', () => {
});
describe('sort', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
describe('when changing sort order', () => {
it('changes the `isAscending` prop to the sorting component', async () => {
expect(findSorting().props().isAscending).toBe(false);
diff --git a/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js b/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js
index b7b84d51559..669f26c5373 100644
--- a/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js
+++ b/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js
@@ -29,13 +29,18 @@ describe('CiResourcesListItem', () => {
resource,
};
- const createComponent = ({ props = {} } = {}) => {
+ const createComponent = ({ props = {}, withCatalogPopularity = false } = {}) => {
wrapper = shallowMountExtended(CiResourcesListItem, {
router,
propsData: {
...defaultProps,
...props,
},
+ provide: {
+ glFeatures: {
+ ciCatalogPopularity: withCatalogPopularity,
+ },
+ },
stubs: {
GlSprintf,
GlTruncate,
@@ -51,6 +56,7 @@ describe('CiResourcesListItem', () => {
const findVerificationBadge = () => wrapper.findComponent(CiVerificationBadge);
const findTimeAgoMessage = () => wrapper.findComponent(GlSprintf);
const findFavorites = () => wrapper.findByTestId('stats-favorites');
+ const findUsage = () => wrapper.findByTestId('stats-usage');
const findMarkdown = () => wrapper.findComponent(Markdown);
beforeEach(() => {
@@ -286,8 +292,29 @@ describe('CiResourcesListItem', () => {
});
});
+ describe('with FF `ci_catalog_popularity` turned on', () => {
+ beforeEach(() => {
+ createComponent({ withCatalogPopularity: true });
+ });
+
+ it('renders the statistics', () => {
+ expect(findUsage().exists()).toBe(true);
+ expect(findUsage().text()).toBe('4');
+ });
+ });
+
+ describe('with FF `ci_catalog_popularity` disabled', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the statistics', () => {
+ expect(findUsage().exists()).toBe(false);
+ });
+ });
+
describe('when there are no statistics', () => {
- it('render favorites as 0', () => {
+ it('render favorites and usage as 0', () => {
createComponent({
props: {
resource: {
diff --git a/spec/frontend/ci/catalog/mock.js b/spec/frontend/ci/catalog/mock.js
index eb41566592e..2066a7e5d74 100644
--- a/spec/frontend/ci/catalog/mock.js
+++ b/spec/frontend/ci/catalog/mock.js
@@ -95,6 +95,7 @@ export const catalogResponseBody = {
name: 'Project-42 Name',
description: 'A simple component',
starCount: 0,
+ last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-42/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@@ -111,6 +112,7 @@ export const catalogResponseBody = {
name: 'Project-41 Name',
description: 'A simple component',
starCount: 0,
+ last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-41/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@@ -127,6 +129,7 @@ export const catalogResponseBody = {
name: 'Project-40 Name',
description: 'A simple component',
starCount: 0,
+ last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-40/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@@ -143,6 +146,7 @@ export const catalogResponseBody = {
name: 'Project-39 Name',
description: 'A simple component',
starCount: 0,
+ last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-39/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@@ -159,6 +163,7 @@ export const catalogResponseBody = {
name: 'Project-38 Name',
description: 'A simple component',
starCount: 0,
+ last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-38/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@@ -175,6 +180,7 @@ export const catalogResponseBody = {
name: 'Project-37 Name',
description: 'A simple component',
starCount: 0,
+ last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-37/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@@ -191,6 +197,7 @@ export const catalogResponseBody = {
name: 'Project-36 Name',
description: 'A simple component',
starCount: 0,
+ last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-36/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@@ -207,6 +214,7 @@ export const catalogResponseBody = {
name: 'Project-35 Name',
description: 'A simple component',
starCount: 0,
+ last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-35/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@@ -223,6 +231,7 @@ export const catalogResponseBody = {
name: 'Project-34 Name',
description: 'A simple component',
starCount: 0,
+ last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-34/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@@ -239,6 +248,7 @@ export const catalogResponseBody = {
name: 'Project-33 Name',
description: 'A simple component',
starCount: 0,
+ last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-33/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@@ -255,6 +265,7 @@ export const catalogResponseBody = {
name: 'Project-32 Name',
description: 'A simple component',
starCount: 0,
+ last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-32/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@@ -271,6 +282,7 @@ export const catalogResponseBody = {
name: 'Project-31 Name',
description: 'A simple component',
starCount: 0,
+ last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-31/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@@ -287,6 +299,7 @@ export const catalogResponseBody = {
name: 'Project-30 Name',
description: 'A simple component',
starCount: 0,
+ last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-30/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@@ -303,6 +316,7 @@ export const catalogResponseBody = {
name: 'Project-29 Name',
description: 'A simple component',
starCount: 0,
+ last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-29/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@@ -319,6 +333,7 @@ export const catalogResponseBody = {
name: 'Project-28 Name',
description: 'A simple component',
starCount: 0,
+ last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-28/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@@ -335,6 +350,7 @@ export const catalogResponseBody = {
name: 'Project-27 Name',
description: 'A simple component',
starCount: 0,
+ last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-27/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@@ -351,6 +367,7 @@ export const catalogResponseBody = {
name: 'Project-26 Name',
description: 'A simple component',
starCount: 0,
+ last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-26/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@@ -367,6 +384,7 @@ export const catalogResponseBody = {
name: 'Project-25 Name',
description: 'A simple component',
starCount: 0,
+ last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-25/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@@ -383,6 +401,7 @@ export const catalogResponseBody = {
name: 'Project-24 Name',
description: 'A simple component',
starCount: 0,
+ last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-24/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@@ -399,6 +418,7 @@ export const catalogResponseBody = {
name: 'Project-23 Name',
description: 'A simple component',
starCount: 0,
+ last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-23/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@@ -434,6 +454,7 @@ export const catalogSinglePageResponse = {
name: 'Project-45 Name',
description: 'A simple component',
starCount: 0,
+ last30DayUsageCount: 4,
verificationLevel: 'UNVERIFIED',
versions: {
__typename: 'CiCatalogResourceVersionConnection',
@@ -505,6 +526,7 @@ export const catalogSharedDataMock = {
description: 'This is the description of the repo',
name: 'Ruby',
starCount: 1,
+ last30DayUsageCount: 4,
starrersPath: '/path/to/project/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@@ -543,6 +565,7 @@ const generateResourcesNodes = (count = 20, startId = 0) => {
icon: 'my-icon',
name: `My component #${i}`,
starCount: 10,
+ last30DayUsageCount: 4,
versions: {
__typename: 'CiCatalogResourceVersionConnection',
nodes: [
diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
index cf952b2930e..5dd6dec66b5 100644
--- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
@@ -365,7 +365,6 @@ describe('Pipeline schedules app', () => {
last: null,
nextPageCursor: '',
prevPageCursor: '',
- sortValue: 'ID_DESC',
});
});
});
@@ -427,7 +426,6 @@ describe('Pipeline schedules app', () => {
last: null,
nextPageCursor: '',
prevPageCursor: '',
- sortValue: 'ID_DESC',
});
});
@@ -441,7 +439,6 @@ describe('Pipeline schedules app', () => {
last: null,
prevPageCursor: '',
nextPageCursor: pageInfo.endCursor,
- sortValue: 'ID_DESC',
});
expect(findPagination().props('value')).toEqual(2);
});
@@ -459,50 +456,6 @@ describe('Pipeline schedules app', () => {
});
});
- describe('when sorting changes', () => {
- const newSort = 'DESCRIPTION_ASC';
-
- beforeEach(async () => {
- createComponent([[getPipelineSchedulesQuery, successHandler]]);
-
- await waitForPromises();
- await findTable().vm.$emit('update-sorting', newSort, 'description', false);
- });
-
- it('passes it to the graphql query', () => {
- expect(successHandler).toHaveBeenCalledTimes(2);
- expect(successHandler.mock.calls[1][0]).toEqual({
- projectPath: 'gitlab-org/gitlab',
- ids: null,
- first: SCHEDULES_PER_PAGE,
- last: null,
- nextPageCursor: '',
- prevPageCursor: '',
- sortValue: newSort,
- });
- });
- });
-
- describe('when update-sorting event is emitted', () => {
- beforeEach(async () => {
- createComponent([[getPipelineSchedulesQuery, successHandlerWithPagination]]);
- await waitForPromises();
- });
-
- it('resets the page count', async () => {
- expect(findPagination().props('value')).toEqual(1);
-
- await setPage(2);
-
- expect(findPagination().props('value')).toEqual(2);
-
- await findTable().vm.$emit('update-sorting', 'DESCRIPTION_DESC', 'description', true);
- await waitForPromises();
-
- expect(findPagination().props('value')).toEqual(1);
- });
- });
-
it.each`
description | handler | buttonDisabled | alertExists
${'limit reached'} | ${planLimitReachedHandler} | ${true} | ${true}
diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_table_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_table_spec.js
deleted file mode 100644
index 99c005c639c..00000000000
--- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_table_spec.js
+++ /dev/null
@@ -1,89 +0,0 @@
-import { GlTable } from '@gitlab/ui';
-import waitForPromises from 'helpers/wait_for_promises';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import PipelineSchedulesTable from '~/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue';
-import {
- TH_DESCRIPTION_TEST_ID,
- TH_TARGET_TEST_ID,
- TH_NEXT_TEST_ID,
-} from '~/ci/pipeline_schedules/constants';
-import { mockPipelineScheduleNodes, mockPipelineScheduleCurrentUser } from '../mock_data';
-
-describe('Pipeline schedules table', () => {
- let wrapper;
-
- const createComponent = () => {
- wrapper = mountExtended(PipelineSchedulesTable, {
- propsData: {
- schedules: mockPipelineScheduleNodes,
- currentUser: mockPipelineScheduleCurrentUser,
- sortBy: 'ID',
- sortDesc: true,
- },
- });
- };
-
- const findTable = () => wrapper.findComponent(GlTable);
-
- describe('sorting', () => {
- beforeEach(async () => {
- createComponent();
-
- await waitForPromises();
- });
-
- it.each`
- sortValue | sortBy | sortDesc
- ${'DESCRIPTION_ASC'} | ${'description'} | ${false}
- ${'DESCRIPTION_DESC'} | ${'description'} | ${true}
- ${'REF_ASC'} | ${'target'} | ${false}
- ${'REF_DESC'} | ${'target'} | ${true}
- ${'NEXT_RUN_AT_ASC'} | ${'next'} | ${false}
- ${'NEXT_RUN_AT_DESC'} | ${'next'} | ${true}
- `(
- 'emits sort data in expected format for sortValue $sortValue',
- ({ sortValue, sortBy, sortDesc }) => {
- findTable().vm.$emit('sort-changed', { sortBy, sortDesc });
-
- expect(wrapper.emitted('update-sorting')[0]).toEqual([sortValue, sortBy, sortDesc]);
- },
- );
-
- it('emits no update-sorting event when called with unsortable column', () => {
- findTable().vm.$emit('sort-changed', { sortBy: 'actions', sortDesc: false });
-
- expect(wrapper.emitted('update-sorting')).toBeUndefined();
- });
-
- it('emits no update-sorting event when called with unknown column', () => {
- findTable().vm.$emit('sort-changed', { sortBy: 'not-defined-never', sortDesc: false });
-
- expect(wrapper.emitted('update-sorting')).toBeUndefined();
- });
- });
-
- describe('sorting the pipeline schedules table by column', () => {
- beforeEach(async () => {
- createComponent();
-
- await waitForPromises();
- });
-
- it.each`
- description | selector
- ${'description'} | ${TH_DESCRIPTION_TEST_ID}
- ${'target'} | ${TH_TARGET_TEST_ID}
- ${'next'} | ${TH_NEXT_TEST_ID}
- `('updates sort with new direction when sorting by $description', async ({ selector }) => {
- const [[attr, value]] = Object.entries(selector);
- const columnHeader = () => wrapper.find(`[${attr}="${value}"]`);
- expect(columnHeader().attributes('aria-sort')).toBe('none');
- columnHeader().trigger('click');
- await waitForPromises();
- expect(columnHeader().attributes('aria-sort')).toBe('ascending');
- columnHeader().trigger('click');
- await waitForPromises();
- expect(columnHeader().attributes('aria-sort')).toBe('descending');
- });
- });
-});
diff --git a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_logs_spec.js b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_logs_spec.js
index b51e08a0c33..15a4e683940 100644
--- a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_logs_spec.js
+++ b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_logs_spec.js
@@ -135,98 +135,128 @@ describe('kubernetes_logs', () => {
});
describe('when environment data is ready', () => {
- describe('when logs data is empty', () => {
- beforeEach(async () => {
- k8sLogsQueryMock = jest.fn().mockResolvedValue({});
- mountComponent();
- await waitForPromises();
+ describe('when no container is specified for the logs', () => {
+ describe('when logs data is empty', () => {
+ beforeEach(async () => {
+ k8sLogsQueryMock = jest.fn().mockResolvedValue({});
+ mountComponent();
+ await waitForPromises();
+ });
+
+ it('should not render loading state', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ it('should not render error state', () => {
+ expect(findAlert().exists()).toBe(false);
+ });
+ it('should not render logs viewer', () => {
+ expect(findLogsViewer().exists()).toBe(false);
+ });
+ it('should render empty state with pod name', () => {
+ expect(findEmptyState().text()).toBe('No logs available for pod test-pod');
+ });
});
- it('should not render loading state', () => {
- expect(findLoadingIcon().exists()).toBe(false);
+ describe('when logs data fetched successfully', () => {
+ beforeEach(async () => {
+ mountComponent();
+ await waitForPromises();
+ });
+
+ it('should not render loading state', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ it('should not render error state', () => {
+ expect(findAlert().exists()).toBe(false);
+ });
+ it('should not render empty state', () => {
+ expect(findEmptyState().exists()).toBe(false);
+ });
+
+ it('should query logs', () => {
+ expect(k8sLogsQueryMock).toHaveBeenCalledWith(
+ expect.anything(),
+ {
+ configuration,
+ namespace: defaultProps.namespace,
+ podName: defaultProps.podName,
+ containerName: '',
+ },
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+ it('should render logs viewer component with correct parameters', () => {
+ const expectedLogLines = [
+ {
+ content: [{ text: logsMockData[0].content }],
+ lineNumber: 1,
+ lineId: 'L1',
+ },
+ {
+ content: [{ text: logsMockData[1].content }],
+ lineNumber: 2,
+ lineId: 'L2',
+ },
+ ];
+ expect(findLogsViewer().props()).toMatchObject({
+ logLines: expectedLogLines,
+ highlightedLine: 'L2',
+ });
+ });
});
- it('should not render error state', () => {
- expect(findAlert().exists()).toBe(false);
- });
- it('should not render logs viewer', () => {
- expect(findLogsViewer().exists()).toBe(false);
- });
- it('should render empty state', () => {
- expect(findEmptyState().text()).toBe('No logs available for pod test-pod');
+
+ describe('when logs data fetch failed', () => {
+ const errorMessage = 'Error while fetching logs';
+
+ beforeEach(async () => {
+ k8sLogsQueryMock = jest.fn().mockResolvedValue({
+ error: { message: errorMessage },
+ });
+ mountComponent();
+ await waitForPromises();
+ });
+
+ it('should not render loading state', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ it('should render error state', () => {
+ expect(findAlert().text()).toBe(errorMessage);
+ });
+ it('should render empty state', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
+ it('should not render logs viewer', () => {
+ expect(findLogsViewer().exists()).toBe(false);
+ });
});
});
-
- describe('when logs data fetched successfully', () => {
+ describe('when a container is specified for the logs', () => {
beforeEach(async () => {
- mountComponent();
+ k8sLogsQueryMock = jest.fn().mockResolvedValue({});
+ mountComponent({ containerName: 'my-container' });
await waitForPromises();
});
- it('should not render loading state', () => {
- expect(findLoadingIcon().exists()).toBe(false);
- });
- it('should not render error state', () => {
- expect(findAlert().exists()).toBe(false);
- });
- it('should not render empty state', () => {
- expect(findEmptyState().exists()).toBe(false);
+ it('should render empty state with pod and container name', () => {
+ expect(findEmptyState().text()).toBe(
+ 'No logs available for container my-container of pod test-pod',
+ );
});
- it('should query logs', () => {
+ it('should query logs with the container name included', () => {
expect(k8sLogsQueryMock).toHaveBeenCalledWith(
expect.anything(),
{
configuration,
namespace: defaultProps.namespace,
podName: defaultProps.podName,
+ containerName: 'my-container',
},
expect.anything(),
expect.anything(),
);
});
- it('should render logs viewer component with correct parameters', () => {
- const expectedLogLines = [
- {
- content: [{ text: logsMockData[0].content }],
- lineNumber: 1,
- lineId: 'L1',
- },
- {
- content: [{ text: logsMockData[1].content }],
- lineNumber: 2,
- lineId: 'L2',
- },
- ];
- expect(findLogsViewer().props()).toMatchObject({
- logLines: expectedLogLines,
- highlightedLine: 'L2',
- });
- });
- });
-
- describe('when logs data fetch failed', () => {
- const errorMessage = 'Error while fetching logs';
-
- beforeEach(async () => {
- k8sLogsQueryMock = jest.fn().mockResolvedValue({
- error: { message: errorMessage },
- });
- mountComponent();
- await waitForPromises();
- });
-
- it('should not render loading state', () => {
- expect(findLoadingIcon().exists()).toBe(false);
- });
- it('should render error state', () => {
- expect(findAlert().text()).toBe(errorMessage);
- });
- it('should render empty state', () => {
- expect(findEmptyState().exists()).toBe(true);
- });
- it('should not render logs viewer', () => {
- expect(findLogsViewer().exists()).toBe(false);
- });
});
});
});
diff --git a/spec/frontend/environments/graphql/resolvers/k8s_logs_spec.js b/spec/frontend/environments/graphql/resolvers/k8s_logs_spec.js
index 5f10d34cca2..9fd4ab1cf14 100644
--- a/spec/frontend/environments/graphql/resolvers/k8s_logs_spec.js
+++ b/spec/frontend/environments/graphql/resolvers/k8s_logs_spec.js
@@ -30,6 +30,27 @@ describe('k8sLogs', () => {
watchStream = bootstrapWatcherMock();
});
+ it('should request pods logs if no container is specified', async () => {
+ await k8sLogs(null, { configuration, namespace, podName }, { client });
+
+ expect(
+ watchStream.subscribeToStreamMock,
+ ).toHaveBeenCalledWith('/api/v1/namespaces/default/pods/test-pod/log', { follow: true });
+ });
+
+ it('should request specific container logs if container is specified', async () => {
+ const containerName = 'my-container';
+ await k8sLogs(null, { configuration, namespace, podName, containerName }, { client });
+
+ expect(watchStream.subscribeToStreamMock).toHaveBeenCalledWith(
+ '/api/v1/namespaces/default/pods/test-pod/log',
+ {
+ follow: true,
+ container: containerName,
+ },
+ );
+ });
+
const errorMessage = 'event error message';
const logContent = 'Plain text log data';
it.each([
diff --git a/spec/frontend/ide/components/web_ide_error_spec.js b/spec/frontend/ide/components/web_ide_error_spec.js
new file mode 100644
index 00000000000..85fb5040336
--- /dev/null
+++ b/spec/frontend/ide/components/web_ide_error_spec.js
@@ -0,0 +1,51 @@
+import { GlAlert, GlButton } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import WebIdeError from '~/ide/components/web_ide_error.vue';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+
+const findButtons = (wrapper) => wrapper.findAllComponents(GlButton);
+
+describe('WebIdeError', () => {
+ const MOCK_SIGN_OUT_PATH = '/users/sign_out';
+
+ let wrapper;
+
+ useMockLocationHelper();
+ function createWrapper() {
+ wrapper = mount(WebIdeError, {
+ propsData: {
+ signOutPath: MOCK_SIGN_OUT_PATH,
+ },
+ });
+ }
+
+ it('renders alert component', () => {
+ createWrapper();
+ const alert = wrapper.findComponent(GlAlert);
+
+ expect(alert.text()).toMatchInterpolatedText(
+ 'Failed to load the Web IDE For more information, see the developer console. Try to reload the page or sign out and in again. If the issue persists, report a problem. Reload Sign out',
+ );
+ });
+
+ it('renders reload page button', () => {
+ createWrapper();
+ const reloadButton = findButtons(wrapper).at(0);
+
+ expect(reloadButton.text()).toEqual('Reload');
+
+ reloadButton.vm.$emit('click');
+ expect(window.location.reload).toHaveBeenCalled();
+ });
+
+ it('renders sign out button', () => {
+ createWrapper();
+ const signOutButton = findButtons(wrapper).at(1);
+
+ expect(signOutButton.text()).toEqual('Sign out');
+ expect(signOutButton.attributes()).toMatchObject({
+ 'data-method': 'post',
+ href: MOCK_SIGN_OUT_PATH,
+ });
+ });
+});
diff --git a/spec/frontend/ide/init_gitlab_web_ide_spec.js b/spec/frontend/ide/init_gitlab_web_ide_spec.js
index 453bad21e4a..5f7e4caed19 100644
--- a/spec/frontend/ide/init_gitlab_web_ide_spec.js
+++ b/spec/frontend/ide/init_gitlab_web_ide_spec.js
@@ -8,6 +8,7 @@ import Tracking from '~/tracking';
import { TEST_HOST } from 'helpers/test_constants';
import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { renderWebIdeError } from '~/ide/render_web_ide_error';
import { getMockCallbackUrl } from './helpers';
jest.mock('@gitlab/web-ide');
@@ -18,6 +19,7 @@ jest.mock('~/lib/utils/csrf', () => ({
headerKey: 'mock-csrf-header',
}));
jest.mock('~/tracking');
+jest.mock('~/ide/render_web_ide_error');
const ROOT_ELEMENT_ID = 'ide';
const TEST_NONCE = 'test123nonce';
@@ -30,6 +32,7 @@ const TEST_FILE_PATH = 'foo/README.md';
const TEST_MR_ID = '7';
const TEST_MR_TARGET_PROJECT = 'gitlab-org/the-real-gitlab';
const TEST_SIGN_IN_PATH = 'sign-in';
+const TEST_SIGN_OUT_PATH = 'sign-out';
const TEST_FORK_INFO = { fork_path: '/forky' };
const TEST_IDE_REMOTE_PATH = '/-/ide/remote/:remote_host/:remote_path';
const TEST_START_REMOTE_PARAMS = {
@@ -82,6 +85,7 @@ describe('ide/init_gitlab_web_ide', () => {
],
});
el.dataset.signInPath = TEST_SIGN_IN_PATH;
+ el.dataset.signOutPath = TEST_SIGN_OUT_PATH;
document.body.append(el);
};
@@ -272,6 +276,27 @@ describe('ide/init_gitlab_web_ide', () => {
});
});
+ describe('on start error', () => {
+ const mockError = new Error('error');
+
+ beforeEach(() => {
+ jest.mocked(start).mockImplementationOnce(() => {
+ throw mockError;
+ });
+
+ createSubject();
+ });
+
+ it('shows alert', () => {
+ expect(start).toHaveBeenCalledTimes(1);
+ expect(renderWebIdeError).toHaveBeenCalledTimes(1);
+ expect(renderWebIdeError).toHaveBeenCalledWith({
+ error: mockError,
+ signOutPath: TEST_SIGN_OUT_PATH,
+ });
+ });
+ });
+
describe('when extensionsGallerySettings is in dataset', () => {
function setMockExtensionGallerySettingsDataset(
mockSettings = TEST_EXTENSIONS_GALLERY_SETTINGS,
diff --git a/spec/frontend/ide/render_web_ide_error_spec.js b/spec/frontend/ide/render_web_ide_error_spec.js
new file mode 100644
index 00000000000..46d2451b36a
--- /dev/null
+++ b/spec/frontend/ide/render_web_ide_error_spec.js
@@ -0,0 +1,55 @@
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import { renderWebIdeError } from '~/ide/render_web_ide_error';
+import { logError } from '~/lib/logger';
+import { resetHTMLFixture } from 'helpers/fixtures';
+
+jest.mock('~/sentry/sentry_browser_wrapper');
+jest.mock('~/lib/logger');
+
+describe('render web IDE error', () => {
+ const MOCK_ERROR = new Error('error');
+ const MOCK_SIGNOUT_PATH = '/signout';
+
+ const setupFlashContainer = () => {
+ const flashContainer = document.createElement('div');
+ flashContainer.classList.add('flash-container');
+
+ document.body.appendChild(flashContainer);
+ };
+
+ const findAlert = () => document.querySelector('.flash-container .gl-alert');
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ describe('with flash-container', () => {
+ beforeEach(() => {
+ setupFlashContainer();
+
+ renderWebIdeError({ error: MOCK_ERROR, signOutPath: MOCK_SIGNOUT_PATH });
+ });
+
+ it('logs error to Sentry', () => {
+ expect(Sentry.captureException).toHaveBeenCalledWith(MOCK_ERROR);
+ });
+
+ it('logs error to console', () => {
+ expect(logError).toHaveBeenCalledWith('Failed to load Web IDE', MOCK_ERROR);
+ });
+
+ it('should render alert', () => {
+ expect(findAlert()).toBeInstanceOf(HTMLElement);
+ });
+ });
+
+ describe('no .flash-container', () => {
+ beforeEach(() => {
+ renderWebIdeError({ error: MOCK_ERROR, signOutPath: MOCK_SIGNOUT_PATH });
+ });
+
+ it('does not render alert', () => {
+ expect(findAlert()).toBeNull();
+ });
+ });
+});
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index b85663e2ef2..3c82a7d5ef9 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -57,6 +57,18 @@ RSpec.describe Repository, feature_category: :source_code_management do
it { is_expected.to match_array(["'test'"]) }
end
+ context 'when exclude_refs is provided' do
+ let(:opts) { { exclude_refs: ['master'] } }
+
+ it { is_expected.not_to include('master') }
+ end
+
+ context 'with limit + exclude_refs options' do
+ let(:opts) { { limit: 1, exclude_refs: ["'test'"] } }
+
+ it { is_expected.to match_array(["2-mb-file"]) }
+ end
+
describe 'when storage is broken', :broken_storage do
it 'raises a storage error' do
expect_to_raise_storage_error do
@@ -79,6 +91,18 @@ RSpec.describe Repository, feature_category: :source_code_management do
it { is_expected.to match_array(['v1.1.0']) }
end
+
+ context 'when exclude_refs is provided' do
+ let(:opts) { { exclude_refs: ['v1.1.0'] } }
+
+ it { is_expected.not_to include('v1.1.0') }
+ end
+
+ context 'with limit + exclude_refs options' do
+ let(:opts) { { limit: 1, exclude_refs: ["v1.1.0"] } }
+
+ it { is_expected.to match_array(["v1.1.1"]) }
+ end
end
describe '#tags_sorted_by' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index cb47e6c0a15..469721d8cf8 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1377,37 +1377,6 @@ RSpec.describe User, feature_category: :user_profile do
end
end
- describe '.with_expiring_and_not_notified_personal_access_tokens' do
- let_it_be(:user1) { create(:user) }
- let_it_be(:user2) { create(:user) }
- let_it_be(:user3) { create(:user) }
-
- let_it_be(:expired_token) { create(:personal_access_token, user: user1, expires_at: 2.days.ago) }
- let_it_be(:revoked_token) { create(:personal_access_token, user: user1, revoked: true) }
- let_it_be(:impersonation_token) { create(:personal_access_token, :impersonation, user: user1, expires_at: 2.days.from_now) }
- let_it_be(:valid_token_and_notified) { create(:personal_access_token, user: user2, expires_at: 2.days.from_now, expire_notification_delivered: true) }
- let_it_be(:valid_token1) { create(:personal_access_token, user: user2, expires_at: 2.days.from_now) }
- let_it_be(:valid_token2) { create(:personal_access_token, user: user2, expires_at: 2.days.from_now) }
-
- let(:users) { described_class.with_expiring_and_not_notified_personal_access_tokens(from) }
-
- context 'in one day' do
- let(:from) { 1.day.from_now }
-
- it "doesn't include an user" do
- expect(users).to be_empty
- end
- end
-
- context 'in three days' do
- let(:from) { 3.days.from_now }
-
- it 'only includes user2' do
- expect(users).to contain_exactly(user2)
- end
- end
- end
-
describe '.with_personal_access_tokens_expired_today' do
let_it_be(:user1) { create(:user) }
let_it_be(:expired_today) { create(:personal_access_token, user: user1, expires_at: Date.current) }
diff --git a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
index d87564b9a8c..a458ff108f0 100644
--- a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
@@ -896,6 +896,22 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
describe 'GET /api/v4/jobs/:id/artifacts' do
let(:token) { job.token }
+ def expect_use_primary
+ lb_session = ::Gitlab::Database::LoadBalancing::Session.current
+
+ expect(lb_session).to receive(:use_primary).and_call_original
+
+ allow(::Gitlab::Database::LoadBalancing::Session).to receive(:current).and_return(lb_session)
+ end
+
+ def expect_no_use_primary
+ lb_session = ::Gitlab::Database::LoadBalancing::Session.current
+
+ expect(lb_session).not_to receive(:use_primary)
+
+ allow(::Gitlab::Database::LoadBalancing::Session).to receive(:current).and_return(lb_session)
+ end
+
it_behaves_like 'API::CI::Runner application context metadata', 'GET /api/:version/jobs/:id/artifacts' do
let(:send_request) { download_artifact }
end
@@ -927,13 +943,32 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
allow(Gitlab::ApplicationContext).to receive(:push).and_call_original
end
- it 'downloads artifacts' do
- expect(Gitlab::ApplicationContext).to receive(:push).with(artifact: an_instance_of(Ci::JobArtifact)).once.and_call_original
+ context 'when ci_job_artifacts_use_primary_to_authenticate feature flag is on' do
+ it 'downloads artifacts' do
+ expect_use_primary
+ expect(Gitlab::ApplicationContext).to receive(:push).with(artifact: an_instance_of(Ci::JobArtifact)).once.and_call_original
- download_artifact
+ download_artifact
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers.to_h).to include download_headers
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers.to_h).to include download_headers
+ end
+ end
+
+ context 'when ci_job_artifacts_use_primary_to_authenticate feature flag is off' do
+ before do
+ stub_feature_flags(ci_job_artifacts_use_primary_to_authenticate: false)
+ end
+
+ it 'downloads artifacts' do
+ expect_no_use_primary
+ expect(Gitlab::ApplicationContext).to receive(:push).with(artifact: an_instance_of(Ci::JobArtifact)).once.and_call_original
+
+ download_artifact
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers.to_h).to include download_headers
+ end
end
end
diff --git a/spec/requests/groups/packages_controller_spec.rb b/spec/requests/groups/packages_controller_spec.rb
new file mode 100644
index 00000000000..8a7edbd5c0b
--- /dev/null
+++ b/spec/requests/groups/packages_controller_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::PackagesController, feature_category: :package_registry do
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:project) { create(:project, :public, group: group) }
+
+ describe 'GET #show' do
+ let_it_be(:package) { create(:package, project: project) }
+
+ subject do
+ get group_package_path(group_id: group.full_path, id: package.id)
+ response
+ end
+
+ it { is_expected.to have_gitlab_http_status(:ok) }
+
+ it { is_expected.to have_attributes(body: have_pushed_frontend_feature_flags(packagesProtectedPackages: true)) }
+
+ context 'when feature flag "packages_protected_packages" is disabled' do
+ before do
+ stub_feature_flags(packages_protected_packages: false)
+ end
+
+ it { is_expected.to have_gitlab_http_status(:ok) }
+
+ it { is_expected.to have_attributes(body: have_pushed_frontend_feature_flags(packagesProtectedPackages: false)) }
+ end
+ end
+end
diff --git a/spec/support/capybara_wait_for_all_requests.rb b/spec/support/capybara_wait_for_requests.rb
similarity index 88%
rename from spec/support/capybara_wait_for_all_requests.rb
rename to spec/support/capybara_wait_for_requests.rb
index 6f272474cf6..8deaeb8e45b 100644
--- a/spec/support/capybara_wait_for_all_requests.rb
+++ b/spec/support/capybara_wait_for_requests.rb
@@ -5,7 +5,7 @@ require_relative 'helpers/wait_for_requests'
module Capybara
class Session
- module WaitForAllRequestsAfterVisitPage
+ module WaitForRequestsAfterVisitPage
include CapybaraHelpers
include WaitForRequests
@@ -14,11 +14,11 @@ module Capybara
yield if block
- wait_for_all_requests
+ wait_for_requests
end
end
- prepend WaitForAllRequestsAfterVisitPage
+ prepend WaitForRequestsAfterVisitPage
end
module Node
diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb
index 8de581f1b2d..f8127e51155 100644
--- a/spec/support/shared_examples/features/work_items_shared_examples.rb
+++ b/spec/support/shared_examples/features/work_items_shared_examples.rb
@@ -181,8 +181,6 @@ RSpec.shared_examples 'work items assignees' do
end
it 'updates the assignee in real-time' do
- Capybara::Session.new(:other_session)
-
using_session :other_session do
visit work_items_path
expect(work_item.reload.assignees).not_to include(user)
@@ -240,8 +238,6 @@ RSpec.shared_examples 'work items labels' do
end
it 'updates the assignee in real-time' do
- Capybara::Session.new(:other_session)
-
using_session :other_session do
visit work_items_path
expect(work_item.reload.labels).not_to include(label)
diff --git a/spec/support_specs/capybara_wait_for_all_requests_spec.rb b/spec/support_specs/capybara_wait_for_requests_spec.rb
similarity index 73%
rename from spec/support_specs/capybara_wait_for_all_requests_spec.rb
rename to spec/support_specs/capybara_wait_for_requests_spec.rb
index 64b89ba0b56..1027dc22704 100644
--- a/spec/support_specs/capybara_wait_for_all_requests_spec.rb
+++ b/spec/support_specs/capybara_wait_for_requests_spec.rb
@@ -2,22 +2,22 @@
require 'fast_spec_helper'
require 'capybara'
-require 'support/capybara_wait_for_all_requests'
+require 'support/capybara_wait_for_requests'
-RSpec.describe 'capybara_wait_for_all_requests', feature_category: :tooling do # rubocop:disable RSpec/FilePath
- context 'for Capybara::Session::WaitForAllRequestsAfterVisitPage' do
+RSpec.describe 'capybara_wait_for_requests', feature_category: :tooling do
+ context 'for Capybara::Session::WaitForRequestsAfterVisitPage' do
let(:page_visitor) do
Class.new do
def visit(visit_uri)
visit_uri
end
- prepend Capybara::Session::WaitForAllRequestsAfterVisitPage
+ prepend Capybara::Session::WaitForRequestsAfterVisitPage
end.new
end
- it 'waits for all requests after a page visit' do
- expect(page_visitor).to receive(:wait_for_all_requests)
+ it 'waits for requests after a page visit' do
+ expect(page_visitor).to receive(:wait_for_requests)
page_visitor.visit('http://test.com')
end
diff --git a/spec/workers/personal_access_tokens/expiring_worker_spec.rb b/spec/workers/personal_access_tokens/expiring_worker_spec.rb
index 6b021da623d..a8d3781af50 100644
--- a/spec/workers/personal_access_tokens/expiring_worker_spec.rb
+++ b/spec/workers/personal_access_tokens/expiring_worker_spec.rb
@@ -130,14 +130,87 @@ RSpec.describe PersonalAccessTokens::ExpiringWorker, type: :worker, feature_cate
context 'when a token is owned by a group bot' do
let_it_be(:project_bot) { create(:user, :project_bot) }
- let_it_be(:group) { create(:group) }
let_it_be(:expiring_token) { create(:personal_access_token, user: project_bot, expires_at: 5.days.from_now) }
- before_all do
- group.add_developer(project_bot)
+ context 'when the group of the resource bot exists' do
+ let_it_be(:group) { create(:group) }
+
+ before_all do
+ group.add_maintainer(project_bot)
+ end
+
+ it_behaves_like 'sends notification about expiry of bot user tokens'
+
+ it 'updates expire notification delivered attribute of the token' do
+ expect { worker.perform }.to change { expiring_token.reload.expire_notification_delivered }.from(false).to(true)
+ end
+
+ context 'when exception is raised during processing' do
+ context 'with a single resource access token' do
+ before do
+ allow_next_instance_of(NotificationService) do |service|
+ allow(service).to(
+ receive(:bot_resource_access_token_about_to_expire)
+ .with(project_bot, expiring_token.name)
+ .and_raise('boom!')
+ )
+ end
+ end
+
+ it 'logs error' do
+ expect(Gitlab::AppLogger).to(
+ receive(:error)
+ .with({ message: 'Failed to send notification about expiring resource access tokens',
+ class: described_class,
+ "exception.class": "RuntimeError",
+ "exception.message": "boom!",
+ user_id: project_bot.id })
+ )
+
+ worker.perform
+ end
+
+ it 'does not update token with failed delivery' do
+ expect { worker.perform }.not_to change { expiring_token.reload.expire_notification_delivered }
+ end
+ end
+
+ context 'with multiple resource access tokens' do
+ let_it_be(:another_project_bot) { create(:user, :project_bot) }
+ let_it_be(:another_expiring_token) { create(:personal_access_token, user: another_project_bot, expires_at: 5.days.from_now) }
+
+ before_all do
+ group.add_maintainer(another_project_bot)
+ end
+
+ it 'continues sending email' do
+ expect_next_instance_of(NotificationService) do |service|
+ expect(service).to(
+ receive(:bot_resource_access_token_about_to_expire)
+ .with(project_bot, expiring_token.name)
+ .and_raise('boom!')
+ )
+ expect(service).to(
+ receive(:bot_resource_access_token_about_to_expire)
+ .with(another_project_bot, another_expiring_token.name)
+ .and_call_original
+ )
+ end
+
+ worker.perform
+ end
+ end
+ end
end
- it_behaves_like 'sends notification about expiry of bot user tokens'
+ context 'when the group of the resource bot has been deleted' do
+ it 'does not update token with no delivery' do
+ expect(Group).to be_none
+ expect(Project).to be_none
+
+ expect { worker.perform }.not_to change { expiring_token.reload.expire_notification_delivered }
+ end
+ end
end
end
end