Commit 6f9edd1a authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 94e614c9
We’re closing our issue tracker on GitHub so we can focus on the GitLab.com project and respond to issues more quickly.
We encourage you to open an issue on the [GitLab.com issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues). You can log into GitLab.com using your GitHub account.
We encourage you to open an issue on the [GitLab.com issue tracker](https://gitlab.com/gitlab-org/gitlab/issues). You can log into GitLab.com using your GitHub account.
Thank you for taking the time to contribute back to GitLab!
Please open a merge request [on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests), we look forward to reviewing your contribution! You can log into GitLab.com using your GitHub account.
Please open a merge request [on GitLab.com](https://gitlab.com/gitlab-org/gitlab/merge_requests), we look forward to reviewing your contribution! You can log into GitLab.com using your GitHub account.
......@@ -24,17 +24,6 @@ package-and-qa-manual:
when: manual
needs: ["build-qa-image", "gitlab:assets:compile pull-cache"]
package-and-qa-manual:master:
extends:
- .package-and-qa-base
- .only-code-qa-changes
only:
refs:
- master@gitlab-org/gitlab-foss
- master@gitlab-org/gitlab
when: manual
needs: ["build-qa-image", "gitlab:assets:compile"]
package-and-qa:
extends:
- .package-and-qa-base
......@@ -44,3 +33,14 @@ package-and-qa:
- master
needs: ["build-qa-image", "gitlab:assets:compile pull-cache"]
allow_failure: true
schedule:package-and-qa:
extends:
- .package-and-qa-base
- .only-code-qa-changes
only:
refs:
- schedules@gitlab-org/gitlab
- schedules@gitlab-org/gitlab-foss
needs: ["build-qa-image", "gitlab:assets:compile"]
allow_failure: true
......@@ -2,17 +2,10 @@
Please read this!
Before opening a new issue, make sure to search for keywords in the issues
filtered by the "regression" or "bug" label.
filtered by the "regression" or "bug" label:
For the Community Edition issue tracker:
- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=regression
- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=bug
For the Enterprise Edition issue tracker:
- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=regression
- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=bug
- https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=regression
- https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=bug
and verify the issue you're about to submit isn't a duplicate.
--->
......
......@@ -24,7 +24,7 @@ Remove the `:feature_name` feature flag ...
If applicable, any groups/projects that are happy to have this feature turned on early. Some organizations may wish to test big changes they are interested in with a small subset of users ahead of time for example.
- `gitlab-org/gitlab-ce`/`gitlab-org/gitlab-ee` projects
- `gitlab-org/gitlab` project
- `gitlab-org`/`gitlab-com` groups
- ...
......
......@@ -26,7 +26,7 @@
## Confidence
<!-- How do we know this is a problem? Please provide and link to any supporting information (e.g. data, customer verbatims) and use this basis to provide a numerical assessment on our confidence level in this problem's severity:
<!-- How do we know this is a problem? Please provide and link to any supporting information (e.g. data, customer verbatims) and use this basis to provide a numerical assessment on our confidence level in this problem's severity:
100% = High confidence
80% = Medium confidence
......@@ -34,7 +34,7 @@
## Effort
<!-- How much effort do we think it will be to solve this problem? Please include all counterparts (Product, UX, Engineering, etc) in your assessment and quantify the number of person-months needed to dedicate to the effort.
<!-- How much effort do we think it will be to solve this problem? Please include all counterparts (Product, UX, Engineering, etc) in your assessment and quantify the number of person-months needed to dedicate to the effort.
For example, if the solution will take a product manager, designer, and engineer two weeks of effort - you may quantify this as 1.5 (based on 0.5 months x 3 people). -->
......
......@@ -18,13 +18,7 @@ Set the title to: `Security Release: 12.2.X, 12.1.X, and 12.0.X`
## Security Issues:
### CE
* {https://gitlab.com/gitlab-org/gitlab-ce/issues link}
### EE
* {https://gitlab.com/gitlab-org/gitlab-ee/issues link}
* {https://gitlab.com/gitlab-org/gitlab/issues link}
## Security Issues in dev.gitlab.org:
......
......@@ -2,7 +2,7 @@
<!-- This issue outlines testing activities related to a particular issue or epic.
[Here is an example test plan](https://gitlab.com/gitlab-org/gitlab-ce/issues/50353)
[Here is an example test plan](https://gitlab.com/gitlab-org/gitlab-foss/issues/50353)
This and other comments should be removed as you write the plan -->
......@@ -63,7 +63,7 @@ intersection of Components and Attributes.
Some features might be simple enough that they only involve one Component, while
more complex features could involve multiple or even all.
Example (from https://gitlab.com/gitlab-org/gitlab-ce/issues/50353):
Example (from https://gitlab.com/gitlab-org/gitlab-foss/issues/50353):
* Repository is
* Intuitive
* It's easy to select the desired file template
......
......@@ -47,7 +47,7 @@ export default {
dockerConnectionErrorText() {
return sprintf(
s__(`ContainerRegistry|We are having trouble connecting to Docker, which could be due to an
issue with your project name or path.
issue with your project name or path.
%{docLinkStart}More Information%{docLinkEnd}`),
{
docLinkStart: `<a href="${this.helpPagePath}#docker-connection-error" target="_blank">`,
......@@ -58,8 +58,8 @@ export default {
},
introText() {
return sprintf(
s__(`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every
project can have its own space to store its Docker images.
s__(`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every
project can have its own space to store its Docker images.
%{docLinkStart}More Information%{docLinkEnd}`),
{
docLinkStart: `<a href="${this.helpPagePath}" target="_blank">`,
......@@ -109,7 +109,7 @@ export default {
:svg-path="containersErrorImage"
>
<template #description>
<p v-html="dockerConnectionErrorText"></p>
<p class="js-character-error-text" v-html="dockerConnectionErrorText"></p>
</template>
</gl-empty-state>
......
......@@ -49,7 +49,7 @@ export default {
}
},
handleDeleteRepository() {
this.deleteItem(this.repo)
return this.deleteItem(this.repo)
.then(() => {
createFlash(__('This container registry has been scheduled for deletion.'), 'notice');
this.fetchRepos();
......@@ -67,7 +67,8 @@ export default {
<div class="container-image">
<div class="container-image-head">
<gl-button class="js-toggle-repo btn-link align-baseline" @click="toggleRepo">
<icon :name="iconName" /> {{ repo.name }}
<icon :name="iconName" />
{{ repo.name }}
</gl-button>
<clipboard-button
......
......@@ -198,8 +198,9 @@ export default {
:title="s__('ContainerRegistry|Remove selected images')"
:aria-label="s__('ContainerRegistry|Remove selected images')"
@click="deleteMultipleItems()"
><icon name="remove"
/></gl-button>
>
<icon name="remove" />
</gl-button>
</th>
</tr>
</thead>
......@@ -223,9 +224,9 @@ export default {
/>
</td>
<td>
<span v-gl-tooltip.bottom class="monospace" :title="item.revision">
{{ item.shortRevision }}
</span>
<span v-gl-tooltip.bottom class="monospace" :title="item.revision">{{
item.shortRevision
}}</span>
</td>
<td>
{{ formatSize(item.size) }}
......@@ -236,9 +237,9 @@ export default {
</td>
<td>
<span v-gl-tooltip.bottom :title="tooltipTitle(item.createdAt)">
{{ timeFormated(item.createdAt) }}
</span>
<span v-gl-tooltip.bottom :title="tooltipTitle(item.createdAt)">{{
timeFormated(item.createdAt)
}}</span>
</td>
<td class="content action-buttons">
......@@ -262,6 +263,7 @@ export default {
v-if="shouldRenderPagination"
:change="onPageChange"
:page-info="repo.pagination"
class="js-registry-pagination"
/>
<gl-modal ref="deleteModal" :modal-id="modalId" ok-variant="danger">
......
......@@ -2,12 +2,12 @@
module Clusters
class KubernetesNamespaceFinder
attr_reader :cluster, :project, :environment_slug
attr_reader :cluster, :project, :environment_name
def initialize(cluster, project:, environment_slug:, allow_blank_token: false)
def initialize(cluster, project:, environment_name:, allow_blank_token: false)
@cluster = cluster
@project = project
@environment_slug = environment_slug
@environment_name = environment_name
@allow_blank_token = allow_blank_token
end
......@@ -20,7 +20,11 @@ module Clusters
attr_reader :allow_blank_token
def find_namespace(with_environment:)
relation = with_environment ? namespaces.with_environment_slug(environment_slug) : namespaces
relation = if with_environment
namespaces.with_environment_name(environment_name)
else
namespaces
end
relation.find_by_project_id(project.id)
end
......
......@@ -172,7 +172,7 @@ module Clusters
persisted_namespace = Clusters::KubernetesNamespaceFinder.new(
self,
project: project,
environment_slug: environment.slug
environment_name: environment.name
).execute
persisted_namespace&.namespace || Gitlab::Kubernetes::DefaultNamespace.new(self, project: project).from_environment_slug(environment.slug)
......
......@@ -27,7 +27,7 @@ module Clusters
algorithm: 'aes-256-cbc'
scope :has_service_account_token, -> { where.not(encrypted_service_account_token: nil) }
scope :with_environment_slug, -> (slug) { joins(:environment).where(environments: { slug: slug }) }
scope :with_environment_name, -> (name) { joins(:environment).where(environments: { name: name }) }
def token_name
"#{namespace}-token"
......
......@@ -105,19 +105,11 @@ module Clusters
private
##
# Environment slug can be predicted given an environment
# name, so even if the environment isn't persisted yet we
# still know what to look for.
def environment_slug(name)
Gitlab::Slug::Environment.new(name).generate
end
def find_persisted_namespace(project, environment_name:)
Clusters::KubernetesNamespaceFinder.new(
cluster,
project: project,
environment_slug: environment_slug(environment_name)
environment_name: environment_name
).execute
end
......
---
title: Fix bug that caused a merge to show an error message
merge_request: 17466
author:
type: fixed
......@@ -134,7 +134,7 @@ graph RL;
M[coverage];
N[pages];
O[static-analysis];
P["package-and-qa-manual:master<br/>(master schedule only)"];
P["schedule:package-and-qa<br/>(master schedule only)"];
Q[package-and-qa];
R[package-and-qa-manual];
......
......@@ -36,7 +36,7 @@ module Gitlab
Clusters::KubernetesNamespaceFinder.new(
deployment_cluster,
project: environment.project,
environment_slug: environment.slug,
environment_name: environment.name,
allow_blank_token: true
).execute
end
......
......@@ -7,7 +7,7 @@ RSpec.describe Clusters::KubernetesNamespaceFinder do
described_class.new(
cluster,
project: project,
environment_slug: 'production',
environment_name: 'production',
allow_blank_token: allow_blank_token
)
end
......@@ -22,8 +22,8 @@ RSpec.describe Clusters::KubernetesNamespaceFinder do
end
describe '#execute' do
let(:production) { create(:environment, project: project, slug: 'production') }
let(:staging) { create(:environment, project: project, slug: 'staging') }
let(:production) { create(:environment, project: project, name: 'production') }
let(:staging) { create(:environment, project: project, name: 'staging') }
let(:cluster) { create(:cluster, :group, :provided_by_user) }
let(:project) { create(:project) }
......
import registry from '~/registry/components/app.vue';
import { mount } from '@vue/test-utils';
import { TEST_HOST } from '../../helpers/test_constants';
import { reposServerResponse, parsedReposServerResponse } from '../mock_data';
describe('Registry List', () => {
let wrapper;
const findCollapsibleContainer = w => w.findAll({ name: 'CollapsibeContainerRegisty' });
const findNoContainerImagesText = w => w.find('.js-no-container-images-text');
const findSpinner = w => w.find('.gl-spinner');
const findCharacterErrorText = w => w.find('.js-character-error-text');
const propsData = {
endpoint: `${TEST_HOST}/foo`,
helpPagePath: 'foo',
noContainersImage: 'foo',
containersErrorImage: 'foo',
repositoryUrl: 'foo',
};
const setMainEndpoint = jest.fn();
const fetchRepos = jest.fn();
const methods = {
setMainEndpoint,
fetchRepos,
};
beforeEach(() => {
wrapper = mount(registry, {
propsData,
computed: {
repos() {
return parsedReposServerResponse;
},
},
methods,
});
});
describe('with data', () => {
it('should render a list of CollapsibeContainerRegisty', () => {
const containers = findCollapsibleContainer(wrapper);
expect(wrapper.vm.repos.length).toEqual(reposServerResponse.length);
expect(containers.length).toEqual(reposServerResponse.length);
});
});
describe('without data', () => {
let localWrapper;
beforeEach(() => {
localWrapper = mount(registry, {
propsData,
computed: {
repos() {
return [];
},
},
methods,
});
});
it('should render empty message', () => {
const noContainerImagesText = findNoContainerImagesText(localWrapper);
expect(noContainerImagesText.text()).toEqual(
'With the Container Registry, every project can have its own space to store its Docker images. More Information',
);
});
});
describe('while loading data', () => {
let localWrapper;
beforeEach(() => {
localWrapper = mount(registry, {
propsData,
computed: {
repos() {
return [];
},
isLoading() {
return true;
},
},
methods,
});
});
it('should render a loading spinner', () => {
const spinner = findSpinner(localWrapper);
expect(spinner.exists()).toBe(true);
});
});
describe('invalid characters in path', () => {
let localWrapper;
beforeEach(() => {
localWrapper = mount(registry, {
propsData: {
...propsData,
characterError: true,
},
computed: {
repos() {
return [];
},
},
methods,
});
});
it('should render invalid characters error message', () => {
const characterErrorText = findCharacterErrorText(localWrapper);
expect(characterErrorText.text()).toEqual(
'We are having trouble connecting to Docker, which could be due to an issue with your project name or path. More Information',
);
});
});
});
import Vue from 'vue';
import { mount } from '@vue/test-utils';
import collapsibleComponent from '~/registry/components/collapsible_container.vue';
import { repoPropsData } from '../mock_data';
import createFlash from '~/flash';
jest.mock('~/flash.js');
describe('collapsible registry container', () => {
let wrapper;
const findDeleteBtn = w => w.find('.js-remove-repo');
const findContainerImageTags = w => w.find('.container-image-tags');
const findToggleRepos = w => w.findAll('.js-toggle-repo');
beforeEach(() => {
createFlash.mockClear();
// This is needed due to console.error called by vue to emit a warning that stop the tests
// see https://github.com/vuejs/vue-test-utils/issues/532
Vue.config.silent = true;
wrapper = mount(collapsibleComponent, {
propsData: {
repo: repoPropsData,
},
});
});
afterEach(() => {
Vue.config.silent = false;
});
describe('toggle', () => {
beforeEach(() => {
const fetchList = jest.fn();
wrapper.setMethods({ fetchList });
});
const expectIsClosed = () => {
const container = findContainerImageTags(wrapper);
expect(container.exists()).toBe(false);
expect(wrapper.vm.iconName).toEqual('angle-right');
};
it('should be closed by default', () => {
expectIsClosed();
});
it('should be open when user clicks on closed repo', () => {
const toggleRepos = findToggleRepos(wrapper);
toggleRepos.at(0).trigger('click');
const container = findContainerImageTags(wrapper);
expect(container.exists()).toBe(true);
expect(wrapper.vm.fetchList).toHaveBeenCalled();
});
it('should be closed when the user clicks on an opened repo', done => {
const toggleRepos = findToggleRepos(wrapper);
toggleRepos.at(0).trigger('click');
Vue.nextTick(() => {
toggleRepos.at(0).trigger('click');
Vue.nextTick(() => {
expectIsClosed();
done();
});
});
});
});
describe('delete repo', () => {
it('should be possible to delete a repo', () => {
const deleteBtn = findDeleteBtn(wrapper);
expect(deleteBtn.exists()).toBe(true);
});
it('should call deleteItem when confirming deletion', () => {
const deleteItem = jest.fn().mockResolvedValue();
const fetchRepos = jest.fn().mockResolvedValue();
wrapper.setMethods({ deleteItem, fetchRepos });
wrapper.vm.handleDeleteRepository();
expect(wrapper.vm.deleteItem).toHaveBeenCalledWith(wrapper.vm.repo);
});
it('should show an error when there is API error', () => {
const deleteItem = jest.fn().mockRejectedValue('error');
wrapper.setMethods({ deleteItem });
return wrapper.vm.handleDeleteRepository().then(() => {
expect(createFlash).toHaveBeenCalled();
});
});
});
});
import Vue from 'vue';
import tableRegistry from '~/registry/components/table_registry.vue';
import { mount } from '@vue/test-utils';
import { repoPropsData } from '../mock_data';
const [firstImage, secondImage] = repoPropsData.list;
describe('table registry', () => {
let wrapper;
const findSelectAllCheckbox = w => w.find('.js-select-all-checkbox > input');
const findSelectCheckboxes = w => w.findAll('.js-select-checkbox > input');
const findDeleteButton = w => w.find('.js-delete-registry');
const findDeleteButtonsRow = w => w.findAll('.js-delete-registry-row');
const findPagination = w => w.find('.js-registry-pagination');
const bulkDeletePath = 'path';
beforeEach(() => {
// This is needed due to console.error called by vue to emit a warning that stop the tests
// see https://github.com/vuejs/vue-test-utils/issues/532
Vue.config.silent = true;
wrapper = mount(tableRegistry, {
propsData: {
repo: repoPropsData,
},
});
});
afterEach(() => {
Vue.config.silent = false;
});
describe('rendering', () => {
it('should render a table with the registry list', () => {
expect(wrapper.findAll('.registry-image-row').length).toEqual(repoPropsData.list.length);
});
it('should render registry tag', () => {
const tds = wrapper.findAll('.registry-image-row td');
expect(tds.at(0).classes()).toContain('check');
expect(tds.at(1).html()).toContain(repoPropsData.list[0].tag);
expect(tds.at(2).html()).toContain(repoPropsData.list[0].shortRevision);
expect(tds.at(3).html()).toContain(repoPropsData.list[0].layers);
expect(tds.at(3).html()).toContain(repoPropsData.list[0].size);
expect(tds.at(4).html()).toContain(wrapper.vm.timeFormated(repoPropsData.list[0].createdAt));
});
});
describe('multi select', () => {
it('selecting a row should enable delete button', done => {
const deleteBtn = findDeleteButton(wrapper);
const checkboxes = findSelectCheckboxes(wrapper);
expect(deleteBtn.attributes('disabled')).toBe('disabled');
checkboxes.at(0).trigger('click');
Vue.nextTick(() => {
expect(deleteBtn.attributes('disabled')).toEqual(undefined);
done();
});
});
it('selecting all checkbox should select all rows and enable delete button', done => {
const selectAll = findSelectAllCheckbox(wrapper);
const checkboxes = findSelectCheckboxes(wrapper);
selectAll.trigger('click');
Vue.nextTick(() => {
const checked = checkboxes.filter(w => w.element.checked);
expect(checked.length).toBe(checkboxes.length);
done();
});
});
it('deselecting select all checkbox should deselect all rows and disable delete button', done => {
const checkboxes = findSelectCheckboxes(wrapper);
const selectAll = findSelectAllCheckbox(wrapper);
selectAll.trigger('click');
selectAll.trigger('click');
Vue.nextTick(() => {
const checked = checkboxes.filter(w => !w.element.checked);
expect(checked.length).toBe(checkboxes.length);
done();
});
});
it('should delete multiple items when multiple items are selected', done => {
const multiDeleteItems = jest.fn().mockResolvedValue();
wrapper.setMethods({ multiDeleteItems });
const selectAll = findSelectAllCheckbox(wrapper);
selectAll.trigger('click');
Vue.nextTick(() => {
const deleteBtn = findDeleteButton(wrapper);
expect(wrapper.vm.itemsToBeDeleted).toEqual([0, 1]);
expect(deleteBtn.attributes('disabled')).toEqual(undefined);
wrapper.vm.handleMultipleDelete();
Vue.nextTick(() => {
expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
expect(wrapper.vm.multiDeleteItems).toHaveBeenCalledWith({
path: bulkDeletePath,
items: [firstImage.tag, secondImage.tag],
});
done();
});
});
});
it('should show an error message if bulkDeletePath is not set', () => {
const showError = jest.fn();
wrapper.setMethods({ showError });
wrapper.setProps({
repo: {
...repoPropsData,
tagsPath: null,
},
});
wrapper.vm.handleMultipleDelete();
expect(wrapper.vm.showError).toHaveBeenCalled();
});
});
describe('delete registry', () => {
beforeEach(() => {
wrapper.setData({ itemsToBeDeleted: [0] });
});
it('should be possible to delete a registry', () => {
const deleteBtn = findDeleteButton(wrapper);
const deleteBtns = findDeleteButtonsRow(wrapper);
expect(wrapper.vm.itemsToBeDeleted).toEqual([0]);
expect(deleteBtn).toBeDefined();
expect(deleteBtn.attributes('disable')).toBe(undefined);
expect(deleteBtns.is('button')).toBe(true);
});
it('should allow deletion row by row', () => {
const deleteBtns = findDeleteButtonsRow(wrapper);
const deleteSingleItem = jest.fn();
const deleteItem = jest.fn().mockResolvedValue();
wrapper.setMethods({ deleteSingleItem, deleteItem });
deleteBtns.at(0).trigger('click');
expect(wrapper.vm.deleteSingleItem).toHaveBeenCalledWith(0);
wrapper.vm.handleSingleDelete(1);
expect(wrapper.vm.deleteItem).toHaveBeenCalledWith(1);
});
});
describe('pagination', () => {
let localWrapper = null;
const repo = {
repoPropsData,
pagination: {
total: 20,
perPage: 2,
nextPage: 2,
},
};
beforeEach(() => {
localWrapper = mount(tableRegistry, {
propsData: {
repo,
},
});
});
it('should exist', () => {
const pagination = findPagination(localWrapper);
expect(pagination.exists()).toBe(true);
});
it('should be visible when pagination is needed', () => {
const pagination = findPagination(localWrapper);
expect(pagination.isVisible()).toBe(true);
localWrapper.setProps({
repo: {
pagination: {
total: 0,
perPage: 10,
},
},
});
expect(localWrapper.vm.shouldRenderPagination).toBe(false);
});
it('should have a change function that update the list when run', () => {
const fetchList = jest.fn().mockResolvedValue();
localWrapper.setMethods({ fetchList });
localWrapper.vm.onPageChange(1);
expect(localWrapper.vm.fetchList).toHaveBeenCalledWith({ repo, page: 1 });
});
});
describe('modal content', () => {
it('should show the singular title and image name when deleting a single image', () => {
wrapper.setData({ itemsToBeDeleted: [1] });
wrapper.vm.setModalDescription(0);
expect(wrapper.vm.modalTitle).toBe('Remove image');
expect(wrapper.vm.modalDescription).toContain(firstImage.tag);
});
it('should show the plural title and image count when deleting more than one image', () => {
wrapper.setData({ itemsToBeDeleted: [1, 2] });
wrapper.vm.setModalDescription();
expect(wrapper.vm.modalTitle).toBe('Remove images');
expect(wrapper.vm.modalDescription).toContain('<b>2</b> images');
});
});
});
......@@ -2,81 +2,121 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import * as actions from '~/registry/stores/actions';
import * as types from '~/registry/stores/mutation_types';
import state from '~/registry/stores/state';
import { TEST_HOST } from 'spec/test_constants';
import { TEST_HOST } from '../../helpers/test_constants';
import testAction from '../../helpers/vuex_action_helper';
import createFlash from '~/flash';
import {
reposServerResponse,
registryServerResponse,
parsedReposServerResponse,
} from '../mock_data';
jest.mock('~/flash.js');
describe('Actions Registry Store', () => {
let mockedState;
let mock;
let state;
beforeEach(() => {
mockedState = state();
mockedState.endpoint = `${TEST_HOST}/endpoint.json`;
mock = new MockAdapter(axios);
state = {
endpoint: `${TEST_HOST}/endpoint.json`,
};
});
afterEach(() => {
mock.restore();
});
describe('server requests', () => {
describe('fetchRepos', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, reposServerResponse, {});
});
describe('fetchRepos', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, reposServerResponse, {});
});
it('should set receveived repos', done => {
testAction(
actions.fetchRepos,
null,
mockedState,
[
{ type: types.TOGGLE_MAIN_LOADING },
{ type: types.TOGGLE_MAIN_LOADING },
{ type: types.SET_REPOS_LIST, payload: reposServerResponse },
],
[],
done,
);
});
it('should set receveived repos', done => {
testAction(
actions.fetchRepos,
null,
state,
[
{ type: types.TOGGLE_MAIN_LOADING },
{ type: types.TOGGLE_MAIN_LOADING },
{ type: types.SET_REPOS_LIST, payload: reposServerResponse },
],
[],
done,
);
});
describe('fetchList', () => {
let repo;
beforeEach(() => {
mockedState.repos = parsedReposServerResponse;
[, repo] = mockedState.repos;
it('should create flash on API error', done => {
testAction(
actions.fetchRepos,
null,
{
endpoint: null,
},
[{ type: types.TOGGLE_MAIN_LOADING }, { type: types.TOGGLE_MAIN_LOADING }],
[],
() => {
expect(createFlash).toHaveBeenCalled();
done();
},
);
});
});
mock.onGet(repo.tagsPath).replyOnce(200, registryServerResponse, {});
});
describe('fetchList', () => {
let repo;
beforeEach(() => {
state.repos = parsedReposServerResponse;
[, repo] = state.repos;
mock.onGet(repo.tagsPath).replyOnce(200, registryServerResponse, {});
});
it('should set received list', done => {
testAction(
actions.fetchList,
{ repo },
mockedState,
[
{ type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: repo },
{ type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: repo },
{
type: types.SET_REGISTRY_LIST,
payload: {
repo,
resp: registryServerResponse,
headers: jasmine.anything(),
},
it('should set received list', done => {
testAction(
actions.fetchList,
{ repo },
state,
[
{ type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: repo },
{ type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: repo },
{
type: types.SET_REGISTRY_LIST,
payload: {
repo,
resp: registryServerResponse,
headers: expect.anything(),
},
],
[],
done,
);
});
},
],
[],
done,
);
});
it('should create flash on API error', done => {
const updatedRepo = {
...repo,
tagsPath: null,
};
testAction(
actions.fetchList,
{
repo: updatedRepo,
},
state,
[
{ type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: updatedRepo },
{ type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: updatedRepo },
],
[],
() => {
expect(createFlash).toHaveBeenCalled();
done();
},
);
});
});
......@@ -85,7 +125,7 @@ describe('Actions Registry Store', () => {
testAction(
actions.setMainEndpoint,
'endpoint',
mockedState,
state,
[{ type: types.SET_MAIN_ENDPOINT, payload: 'endpoint' }],
[],
done,
......@@ -98,7 +138,7 @@ describe('Actions Registry Store', () => {
testAction(
actions.toggleLoading,
null,
mockedState,
state,
[{ type: types.TOGGLE_MAIN_LOADING }],
[],
done,
......@@ -106,25 +146,42 @@ describe('Actions Registry Store', () => {
});
});
describe('deleteItem', () => {
it('should perform DELETE request on destroyPath', done => {
const destroyPath = `${TEST_HOST}/mygroup/myproject/container_registry/1.json`;
let deleted = false;
describe('deleteItem and multiDeleteItems', () => {
let deleted;
const destroyPath = `${TEST_HOST}/mygroup/myproject/container_registry/1.json`;
const expectDelete = done => {
expect(mock.history.delete.length).toBe(1);
expect(deleted).toBe(true);
done();
};
beforeEach(() => {
deleted = false;
mock.onDelete(destroyPath).replyOnce(() => {
deleted = true;
return [200];
});
});
it('deleteItem should perform DELETE request on destroyPath', done => {
testAction(
actions.deleteItem,
{
destroyPath,
},
mockedState,
state,
)
.then(() => {
expect(mock.history.delete.length).toBe(1);
expect(deleted).toBe(true);
done();
expectDelete(done);
})
.catch(done.fail);
});
it('multiDeleteItems should perform DELETE request on path', done => {
testAction(actions.multiDeleteItems, { path: destroyPath, items: [1] }, state)
.then(() => {
expectDelete(done);
})
.catch(done.fail);
});
......
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import Vue from 'vue';
import registry from '~/registry/components/app.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { TEST_HOST } from 'spec/test_constants';
import { reposServerResponse } from '../mock_data';
describe('Registry List', () => {
const Component = Vue.extend(registry);
const props = {
endpoint: `${TEST_HOST}/foo`,
helpPagePath: 'foo',
noContainersImage: 'foo',
containersErrorImage: 'foo',
repositoryUrl: 'foo',
};
let vm;
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
vm.$destroy();
});
describe('with data', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, reposServerResponse);
vm = mountComponent(Component, { ...props });
});
it('should render a list of repos', done => {
setTimeout(() => {
expect(vm.$store.state.repos.length).toEqual(reposServerResponse.length);
Vue.nextTick(() => {
expect(vm.$el.querySelectorAll('.container-image').length).toEqual(
reposServerResponse.length,
);
done();
});
}, 0);
});
describe('delete repository', () => {
it('should be possible to delete a repo', done => {
setTimeout(() => {
Vue.nextTick(() => {
expect(vm.$el.querySelector('.container-image-head .js-remove-repo')).toBeDefined();
done();
});
}, 0);
});
});
describe('toggle repository', () => {
it('should open the container', done => {
setTimeout(() => {
Vue.nextTick(() => {
vm.$el.querySelector('.js-toggle-repo').click();
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.js-toggle-repo use').getAttribute('xlink:href'),
).toContain('angle-up');
done();
});
});
}, 0);
});
});
});
describe('without data', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []);
vm = mountComponent(Component, { ...props });
});
it('should render empty message', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.js-no-container-images-text').textContent).toEqual(
'With the Container Registry, every project can have its own space to store its Docker images. More Information',
);
done();
}, 0);
});
});
describe('while loading data', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []);
vm = mountComponent(Component, { ...props });
});
it('should render a loading spinner', done => {
Vue.nextTick(() => {
expect(vm.$el.querySelector('.gl-spinner')).not.toBe(null);
done();
});
});
});
describe('invalid characters in path', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []);
vm = mountComponent(Component, {
...props,
characterError: true,
});
});
it('should render invalid characters error message', done => {
setTimeout(() => {
expect(vm.$el.querySelector('p')).not.toContain(
'We are having trouble connecting to Docker, which could be due to an issue with your project name or path. More information',
);
done();
});
});
});
});
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import Vue from 'vue';
import collapsibleComponent from '~/registry/components/collapsible_container.vue';
import store from '~/registry/stores';
import * as types from '~/registry/stores/mutation_types';
import { repoPropsData, registryServerResponse, reposServerResponse } from '../mock_data';
describe('collapsible registry container', () => {
let vm;
let mock;
const Component = Vue.extend(collapsibleComponent);
const findDeleteBtn = () => vm.$el.querySelector('.js-remove-repo');
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(repoPropsData.tagsPath).replyOnce(200, registryServerResponse, {});
store.commit(types.SET_REPOS_LIST, reposServerResponse);
vm = new Component({
store,
propsData: {
repo: repoPropsData,
},
}).$mount();
});
afterEach(() => {
mock.restore();
vm.$destroy();
});
describe('toggle', () => {
it('should be closed by default', () => {
expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
expect(vm.iconName).toEqual('angle-right');
});
it('should be open when user clicks on closed repo', done => {
vm.$el.querySelector('.js-toggle-repo').click();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.container-image-tags')).not.toBeNull();
expect(vm.iconName).toEqual('angle-up');
done();
});
});
it('should be closed when the user clicks on an opened repo', done => {
vm.$el.querySelector('.js-toggle-repo').click();
Vue.nextTick(() => {
vm.$el.querySelector('.js-toggle-repo').click();
setTimeout(() => {
Vue.nextTick(() => {
expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
expect(vm.iconName).toEqual('angle-right');
done();
});
});
});
});
});
describe('delete repo', () => {
it('should be possible to delete a repo', () => {
expect(findDeleteBtn()).not.toBeNull();
});
it('should call deleteItem when confirming deletion', done => {
findDeleteBtn().click();
spyOn(vm, 'deleteItem').and.returnValue(Promise.resolve());
Vue.nextTick(() => {
document.querySelector(`#${vm.modalId} .btn-danger`).click();
expect(vm.deleteItem).toHaveBeenCalledWith(vm.repo);
done();
});
});
});
});
import Vue from 'vue';
import tableRegistry from '~/registry/components/table_registry.vue';
import store from '~/registry/stores';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { repoPropsData } from '../mock_data';
const [firstImage, secondImage] = repoPropsData.list;
describe('table registry', () => {
let vm;
const Component = Vue.extend(tableRegistry);
const bulkDeletePath = 'path';
const findDeleteBtn = () => vm.$el.querySelector('.js-delete-registry');
const findDeleteBtnRow = () => vm.$el.querySelector('.js-delete-registry-row');
const findSelectAllCheckbox = () => vm.$el.querySelector('.js-select-all-checkbox > input');
const findAllRowCheckboxes = () =>
Array.from(vm.$el.querySelectorAll('.js-select-checkbox input'));
const confirmationModal = (child = '') => document.querySelector(`#${vm.modalId} ${child}`);
const createComponent = () => {
vm = mountComponentWithStore(Component, {
store,
props: {
repo: repoPropsData,
},
});
};
const selectAllCheckboxes = () => vm.selectAll();
const deselectAllCheckboxes = () => vm.deselectAll();
beforeEach(() => {
createComponent();
});
afterEach(() => {
vm.$destroy();
});
describe('rendering', () => {
it('should render a table with the registry list', () => {
expect(vm.$el.querySelectorAll('table tbody tr').length).toEqual(repoPropsData.list.length);
});
it('should render registry tag', () => {
const textRendered = vm.$el
.querySelector('.table tbody tr')
.textContent.trim()
// replace additional whitespace characters (e.g. new lines) with a single empty space
.replace(/\s\s+/g, ' ');
expect(textRendered).toContain(repoPropsData.list[0].tag);
expect(textRendered).toContain(repoPropsData.list[0].shortRevision);
expect(textRendered).toContain(repoPropsData.list[0].layers);
expect(textRendered).toContain(repoPropsData.list[0].size);
});
});
describe('multi select', () => {
it('should support multiselect and selecting a row should enable delete button', done => {
findSelectAllCheckbox().click();
selectAllCheckboxes();
expect(findSelectAllCheckbox().checked).toBe(true);
Vue.nextTick(() => {
expect(findDeleteBtn().disabled).toBe(false);
done();
});
});
it('selecting all checkbox should select all rows and enable delete button', done => {
selectAllCheckboxes();
Vue.nextTick(() => {
const checkedValues = findAllRowCheckboxes().filter(x => x.checked);
expect(checkedValues.length).toBe(repoPropsData.list.length);
done();
});
});
it('deselecting select all checkbox should deselect all rows and disable delete button', done => {
selectAllCheckboxes();
deselectAllCheckboxes();
Vue.nextTick(() => {
const checkedValues = findAllRowCheckboxes().filter(x => x.checked);
expect(checkedValues.length).toBe(0);
done();
});
});
it('should delete multiple items when multiple items are selected', done => {
selectAllCheckboxes();
Vue.nextTick(() => {
expect(vm.itemsToBeDeleted).toEqual([0, 1]);
expect(findDeleteBtn().disabled).toBe(false);
findDeleteBtn().click();
spyOn(vm, 'multiDeleteItems').and.returnValue(Promise.resolve());
Vue.nextTick(() => {
const modal = confirmationModal();
confirmationModal('.btn-danger').click();
expect(modal).toExist();
Vue.nextTick(() => {
expect(vm.itemsToBeDeleted).toEqual([]);
expect(vm.multiDeleteItems).toHaveBeenCalledWith({
path: bulkDeletePath,
items: [firstImage.tag, secondImage.tag],
});
done();
});
});
});
});
});
describe('delete registry', () => {
beforeEach(() => {
vm.itemsToBeDeleted = [0];
});
it('should be possible to delete a registry', done => {
Vue.nextTick(() => {
expect(vm.itemsToBeDeleted).toEqual([0]);
expect(findDeleteBtn()).toBeDefined();
expect(findDeleteBtn().disabled).toBe(false);
expect(findDeleteBtnRow()).toBeDefined();
done();
});
});
it('should call deleteItems and reset itemsToBeDeleted when confirming deletion', done => {
Vue.nextTick(() => {
expect(vm.itemsToBeDeleted).toEqual([0]);
expect(findDeleteBtn().disabled).toBe(false);
findDeleteBtn().click();
spyOn(vm, 'multiDeleteItems').and.returnValue(Promise.resolve());
Vue.nextTick(() => {
confirmationModal('.btn-danger').click();
expect(vm.itemsToBeDeleted).toEqual([]);
expect(vm.multiDeleteItems).toHaveBeenCalledWith({
path: bulkDeletePath,
items: [firstImage.tag],
});
done();
});
});
});
});
describe('pagination', () => {
it('should be possible to change the page', () => {
expect(vm.$el.querySelector('.gl-pagination')).toBeDefined();
});
});
describe('modal content', () => {
it('should show the singular title and image name when deleting a single image', done => {
findDeleteBtnRow().click();
Vue.nextTick(() => {
expect(vm.modalTitle).toBe('Remove image');
expect(vm.modalDescription).toContain(firstImage.tag);
done();
});
});
it('should show the plural title and image count when deleting more than one image', done => {
selectAllCheckboxes();
vm.setModalDescription();
Vue.nextTick(() => {
expect(vm.modalTitle).toBe('Remove images');
expect(vm.modalDescription).toContain('<b>2</b> images');
done();
});
});
});
});
......@@ -546,7 +546,7 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
before do
expect(Clusters::KubernetesNamespaceFinder).to receive(:new)
.with(cluster, project: environment.project, environment_slug: environment.slug)
.with(cluster, project: environment.project, environment_name: environment.name)
.and_return(double(execute: persisted_namespace))
end
......
......@@ -24,13 +24,13 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do
end
end
describe '.with_environment_slug' do
describe '.with_environment_name' do
let(:cluster) { create(:cluster, :group) }
let(:environment) { create(:environment, slug: slug) }
let(:environment) { create(:environment, name: name) }
let(:slug) { 'production' }
let(:name) { 'production' }
subject { described_class.with_environment_slug(slug) }
subject { described_class.with_environment_name(name) }
context 'there is no associated environment' do
let!(:namespace) { create(:cluster_kubernetes_namespace, cluster: cluster, project: environment.project) }
......@@ -48,12 +48,12 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do
)
end
context 'with a matching slug' do
context 'with a matching name' do
it { is_expected.to eq [namespace] }
end
context 'without a matching slug' do
let(:environment) { create(:environment, slug: 'staging') }
context 'without a matching name' do
let(:environment) { create(:environment, name: 'staging') }
it { is_expected.to be_empty }
end
......
......@@ -218,7 +218,7 @@ describe Clusters::Platforms::Kubernetes do
before do
allow(Clusters::KubernetesNamespaceFinder).to receive(:new)
.with(cluster, project: project, environment_slug: environment_slug)
.with(cluster, project: project, environment_name: environment_name)
.and_return(double(execute: persisted_namespace))
end
......
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