Commit 9bcd6ccb authored by James Lopez's avatar James Lopez

Merge branch 'ce-to-ee-2018-04-06' into 'master'

CE upstream - 2018-04-06 12:25 UTC

See merge request gitlab-org/gitlab-ee!5267
parents bca6c280 226db913
......@@ -4,7 +4,8 @@ import $ from 'jquery';
import _ from 'underscore';
import Cookies from 'js-cookie';
import { __ } from './locale';
import { isInIssuePage, isInMRPage, isInEpicPage, hasVueMRDiscussionsCookie, updateTooltipTitle } from './lib/utils/common_utils';
import { updateTooltipTitle } from './lib/utils/common_utils';
import { isInVueNoteablePage } from './lib/utils/dom_utils';
import flash from './flash';
import axios from './lib/utils/axios_utils';
......@@ -243,7 +244,7 @@ class AwardsHandler {
addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
const isMainAwardsBlock = votesBlock.closest('.js-noteable-awards').length;
if (this.isInVueNoteablePage() && !isMainAwardsBlock) {
if (isInVueNoteablePage() && !isMainAwardsBlock) {
const id = votesBlock.attr('id').replace('note_', '');
this.hideMenuElement($('.emoji-menu'));
......@@ -295,16 +296,8 @@ class AwardsHandler {
}
}
isVueMRDiscussions() {
return isInMRPage() && hasVueMRDiscussionsCookie() && !$('#diffs').is(':visible');
}
isInVueNoteablePage() {
return isInIssuePage() || isInEpicPage() || this.isVueMRDiscussions();
}
getVotesBlock() {
if (this.isInVueNoteablePage()) {
if (isInVueNoteablePage()) {
const $el = $('.js-add-award.is-active').closest('.note.timeline-entry');
if ($el.length) {
......
/* eslint-disable import/prefer-default-export */
import $ from 'jquery';
import { isInIssuePage, isInMRPage, isInEpicPage, hasVueMRDiscussionsCookie } from './common_utils';
const isVueMRDiscussions = () => isInMRPage() && hasVueMRDiscussionsCookie() && !$('#diffs').is(':visible');
export const addClassIfElementExists = (element, className) => {
if (element) {
element.classList.add(className);
}
};
export const isInVueNoteablePage = () => isInIssuePage() || isInEpicPage() || isVueMRDiscussions();
......@@ -13,8 +13,11 @@ export default function initMrNotes() {
data() {
const notesDataset = document.getElementById('js-vue-mr-discussions')
.dataset;
const noteableData = JSON.parse(notesDataset.noteableData);
noteableData.noteableType = notesDataset.noteableType;
return {
noteableData: JSON.parse(notesDataset.noteableData),
noteableData,
currentUserData: JSON.parse(notesDataset.currentUserData),
notesData: JSON.parse(notesDataset.notesData),
};
......
......@@ -49,16 +49,7 @@ export default {
computed: {
...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']),
noteableType() {
// FIXME -- @fatihacet Get this from JSON data.
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants;
if (this.noteableData.noteableType === EPIC_NOTEABLE_TYPE) {
return EPIC_NOTEABLE_TYPE;
}
return this.noteableData.merge_params
? MERGE_REQUEST_NOTEABLE_TYPE
: ISSUE_NOTEABLE_TYPE;
return this.noteableData.noteableType;
},
allNotes() {
if (this.isLoading) {
......
......@@ -14,3 +14,9 @@ export const EPIC_NOTEABLE_TYPE = 'epic';
export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request';
export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
export const RESOLVE_NOTE_METHOD_NAME = 'post';
export const NOTEABLE_TYPE_MAPPING = {
Issue: ISSUE_NOTEABLE_TYPE,
MergeRequest: MERGE_REQUEST_NOTEABLE_TYPE,
Epic: EPIC_NOTEABLE_TYPE,
};
......@@ -9,16 +9,7 @@ export default {
},
computed: {
noteableType() {
switch (this.note.noteable_type) {
case 'MergeRequest':
return constants.MERGE_REQUEST_NOTEABLE_TYPE;
case 'Issue':
return constants.ISSUE_NOTEABLE_TYPE;
case 'Epic':
return constants.EPIC_NOTEABLE_TYPE;
default:
return '';
}
return constants.NOTEABLE_TYPE_MAPPING[this.note.noteable_type];
},
},
};
<script>
import _ from 'underscore';
import axios from '~/lib/utils/axios_utils';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
import Flash from '~/flash';
export default {
components: {
GlModal,
},
props: {
actionUrl: {
type: String,
required: true,
},
rootUrl: {
type: String,
required: true,
},
initialUsername: {
type: String,
required: true,
},
},
data() {
return {
isRequestPending: false,
username: this.initialUsername,
newUsername: this.initialUsername,
};
},
computed: {
path() {
return sprintf(s__('Profiles|Current path: %{path}'), {
path: `${this.rootUrl}${this.username}`,
});
},
modalText() {
return sprintf(
s__(`Profiles|
You are going to change the username %{currentUsernameBold} to %{newUsernameBold}.
Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group.
Please update your Git repository remotes as soon as possible.`),
{
currentUsernameBold: `<strong>${_.escape(this.username)}</strong>`,
newUsernameBold: `<strong>${_.escape(this.newUsername)}</strong>`,
currentUsername: _.escape(this.username),
newUsername: _.escape(this.newUsername),
},
false,
);
},
},
methods: {
onConfirm() {
this.isRequestPending = true;
const username = this.newUsername;
const putData = {
user: {
username,
},
};
return axios
.put(this.actionUrl, putData)
.then(result => {
Flash(result.data.message, 'notice');
this.username = username;
this.isRequestPending = false;
})
.catch(error => {
Flash(error.response.data.message);
this.isRequestPending = false;
throw error;
});
},
},
modalId: 'username-change-confirmation-modal',
inputId: 'username-change-input',
buttonText: s__('Profiles|Update username'),
};
</script>
<template>
<div>
<div class="form-group">
<label :for="$options.inputId">{{ s__('Profiles|Path') }}</label>
<div class="input-group">
<div class="input-group-addon">{{ rootUrl }}</div>
<input
:id="$options.inputId"
class="form-control"
required="required"
v-model="newUsername"
:disabled="isRequestPending"
/>
</div>
<p class="help-block">
{{ path }}
</p>
</div>
<button
:data-target="`#${$options.modalId}`"
class="btn btn-warning"
type="button"
data-toggle="modal"
:disabled="isRequestPending || newUsername === username"
>
{{ $options.buttonText }}
</button>
<gl-modal
:id="$options.modalId"
:header-title-text="s__('Profiles|Change username') + '?'"
footer-primary-button-variant="warning"
:footer-primary-button-text="$options.buttonText"
@submit="onConfirm"
>
<span v-html="modalText"></span>
</gl-modal>
</div>
</template>
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import UpdateUsername from './components/update_username.vue';
import deleteAccountModal from './components/delete_account_modal.vue';
export default () => {
Vue.use(Translate);
const updateUsernameElement = document.getElementById('update-username');
// eslint-disable-next-line no-new
new Vue({
el: updateUsernameElement,
components: {
UpdateUsername,
},
render(createElement) {
return createElement('update-username', {
props: { ...updateUsernameElement.dataset },
});
},
});
const deleteAccountButton = document.getElementById('delete-account-button');
const deleteAccountModalEl = document.getElementById('delete-account-modal');
// eslint-disable-next-line no-new
......
<script>
const buttonVariants = [
'danger',
'primary',
'success',
'warning',
];
const buttonVariants = ['danger', 'primary', 'success', 'warning'];
export default {
export default {
name: 'GlModal',
props: {
......@@ -24,7 +19,7 @@
type: String,
required: false,
default: 'primary',
validator: value => buttonVariants.indexOf(value) !== -1,
validator: value => buttonVariants.includes(value),
},
footerPrimaryButtonText: {
type: String,
......@@ -41,7 +36,7 @@
this.$emit('submit', event);
},
},
};
};
</script>
<template>
......@@ -60,7 +55,7 @@
<slot name="header">
<button
type="button"
class="close"
class="close js-modal-close-action"
data-dismiss="modal"
:aria-label="s__('Modal|Close')"
@click="emitCancel($event)"
......@@ -83,7 +78,7 @@
<slot name="footer">
<button
type="button"
class="btn"
class="btn js-modal-cancel-action"
data-dismiss="modal"
@click="emitCancel($event)"
>
......@@ -91,7 +86,7 @@
</button>
<button
type="button"
class="btn"
class="btn js-modal-primary-action"
:class="`btn-${footerPrimaryButtonVariant}`"
data-dismiss="modal"
@click="emitSubmit($event)"
......
......@@ -53,13 +53,19 @@ class ProfilesController < Profiles::ApplicationController
def update_username
result = Users::UpdateService.new(current_user, user: @user, username: username_param).execute
options = if result[:status] == :success
{ notice: "Username successfully changed" }
respond_to do |format|
if result[:status] == :success
message = s_("Profiles|Username successfully changed")
format.html { redirect_back_or_default(default: { action: 'show' }, options: { notice: message }) }
format.json { render json: { message: message }, status: :ok }
else
{ alert: "Username change failed - #{result[:message]}" }
end
message = s_("Profiles|Username change failed - %{message}") % { message: result[:message] }
redirect_back_or_default(default: { action: 'show' }, options: options)
format.html { redirect_back_or_default(default: { action: 'show' }, options: { alert: message }) }
format.json { render json: { message: message }, status: :unprocessable_entity }
end
end
end
private
......
......@@ -17,7 +17,7 @@ class MergeRequestDiffCommit < ActiveRecord::Base
commit_hash.merge(
merge_request_diff_id: merge_request_diff_id,
relative_order: index,
sha: sha_attribute.type_cast_for_database(sha),
sha: sha_attribute.serialize(sha), # rubocop:disable Cop/ActiveRecordSerialize
authored_date: Gitlab::Database.sanitize_timestamp(commit_hash[:authored_date]),
committed_date: Gitlab::Database.sanitize_timestamp(commit_hash[:committed_date])
)
......
......@@ -718,10 +718,6 @@ class User < ActiveRecord::Base
projects_limit - personal_projects_count
end
def personal_projects_count
@personal_projects_count ||= personal_projects.count
end
def recent_push(project = nil)
service = Users::LastPushEventService.new(self)
......@@ -1068,6 +1064,12 @@ class User < ActiveRecord::Base
end
end
def personal_projects_count(force: false)
Rails.cache.fetch(['users', id, 'personal_projects_count'], force: force, expires_in: 24.hours, raw: true) do
personal_projects.count
end.to_i
end
def update_todos_count_cache
todos_done_count(force: true)
todos_pending_count(force: true)
......@@ -1078,6 +1080,7 @@ class User < ActiveRecord::Base
invalidate_merge_request_cache_counts
invalidate_todos_done_count
invalidate_todos_pending_count
invalidate_personal_projects_count
end
def invalidate_issue_cache_counts
......@@ -1096,6 +1099,10 @@ class User < ActiveRecord::Base
Rails.cache.delete(['users', id, 'todos_pending_count'])
end
def invalidate_personal_projects_count
Rails.cache.delete(['users', id, 'personal_projects_count'])
end
# This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth
# flow means we don't call that automatically (and can't conveniently do so).
#
......
......@@ -98,6 +98,8 @@ module Projects
system_hook_service.execute_hooks_for(@project, :create)
setup_authorizations
current_user.invalidate_personal_projects_count
end
# Refresh the current user's authorizations inline (so they can access the
......
......@@ -35,6 +35,8 @@ module Projects
system_hook_service.execute_hooks_for(project, :destroy)
log_info("Project \"#{project.full_path}\" was removed")
current_user.invalidate_personal_projects_count
true
rescue => error
attempt_rollback(project, error.message)
......
......@@ -26,6 +26,8 @@ module Projects
transfer(project)
current_user.invalidate_personal_projects_count
true
rescue Projects::TransferService::TransferError => ex
project.reload
......
......@@ -57,20 +57,8 @@
= succeed '.' do
= link_to 'Learn more', help_page_path('user/profile/index', anchor: 'changing-your-username'), target: '_blank'
.col-lg-8
= form_for @user, url: update_username_profile_path, method: :put, html: {class: "update-username"} do |f|
.form-group
= f.label :username, "Path", class: "label-light"
.input-group
.input-group-addon
= root_url
= f.text_field :username, required: true, class: 'form-control'
.help-block
Current path:
#{root_url}#{current_user.username}
.prepend-top-default
= f.button class: "btn btn-warning", type: "submit" do
= icon "spinner spin", class: "hidden loading-username"
Update username
- data = { initial_username: current_user.username, root_url: root_url, action_url: update_username_profile_path(format: :json) }
#update-username{ data: data }
%hr
.row.prepend-top-default
......
......@@ -8,4 +8,5 @@
%section.js-vue-notes-event
#js-vue-notes{ data: { notes_data: notes_data(@issue),
noteable_data: serialize_issuable(@issue),
noteable_type: 'issue',
current_user_data: UserSerializer.new.represent(current_user, only_path: true).to_json } }
......@@ -92,6 +92,7 @@
- if has_vue_discussions_cookie?
#js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request),
noteable_data: serialize_issuable(@merge_request),
noteable_type: 'merge_request',
current_user_data: UserSerializer.new.represent(current_user).to_json} }
#commits.commits.tab-pane
......
---
title: Added confirmation modal for changing username
merge_request: 17405
author:
type: added
---
title: Cache personal projects count.
merge_request: 18197
author:
type: performance
module ActiveRecord
# Remove this initializer when upgraded to Rails 5.0
unless Gitlab.rails5?
module ActiveRecord
class PredicateBuilder
class ArrayHandler
module TypeCasting
......@@ -17,4 +19,5 @@ module ActiveRecord
prepend TypeCasting
end
end
end
end
......@@ -44,6 +44,7 @@ the `author` field. GitLab team members **should not**.
- _Any_ contribution from a community member, no matter how small, **may** have
a changelog entry regardless of these guidelines if the contributor wants one.
Example: "Fixed a typo on the search results page. (Jane Smith)"
- Performance improvements **should** have a changelog entry.
## Writing good changelog entries
......
......@@ -72,15 +72,23 @@ The maximum size your Git repository is allowed to be including LFS.
## Shared Runners
Shared Runners on GitLab.com run in [autoscale mode] and powered by
DigitalOcean. Autoscaling means reduced waiting times to spin up builds,
and isolated VMs for each project, thus maximizing security.
Google Cloud Platform and DigitalOcean. Autoscaling means reduced
waiting times to spin up CI/CD jobs, and isolated VMs for each project,
thus maximizing security.
They're free to use for public open source projects and limited to 2000 CI
minutes per month per group for private projects. Read about all
[GitLab.com plans](https://about.gitlab.com/pricing/).
All your builds run on 2GB (RAM) ephemeral instances, with CoreOS and the latest
Docker Engine installed. The default region of the VMs is NYC.
In case of DigitalOcean based Runners, all your CI/CD jobs run on ephemeral
instances with 2GB of RAM, CoreOS and the latest Docker Engine installed.
Instances provide 2 vCPUs and 60GB of SSD disk space. The default region of the
VMs is NYC1.
In case of Google Cloud Platform based Runners, all your CI/CD jobs run on
ephemeral instances with 3.75GB of RAM, CoreOS and the latest Docker Engine
installed. Instances provide 1 vCPU and 25GB of HDD disk space. The default
region of the VMs is US East1.
Below are the shared Runners settings.
......@@ -88,52 +96,116 @@ Below are the shared Runners settings.
| ----------- | ----------------- | ---------- |
| [GitLab Runner] | [Runner versions dashboard][ci_version_dashboard] | - |
| Executor | `docker+machine` | - |
| Default Docker image | `ruby:2.1` | - |
| Default Docker image | `ruby:2.5` | - |
| `privileged` (run [Docker in Docker]) | `true` | `false` |
[ci_version_dashboard]: https://monitor.gitlab.net/dashboard/db/ci?refresh=5m&orgId=1&panelId=12&fullscreen&from=now-1h&to=now&var-runner_type=All&var-cache_server=All&var-gl_monitor_fqdn=postgres-01.db.prd.gitlab.com&var-has_minutes=yes&var-hanging_droplets_cleaner=All&var-droplet_zero_machines_cleaner=All&var-runner_job_failure_reason=All&theme=light
[ci_version_dashboard]: https://monitor.gitlab.net/dashboard/db/ci?from=now-1h&to=now&refresh=5m&orgId=1&panelId=12&fullscreen&theme=light
### `config.toml`
The full contents of our `config.toml` are:
**DigitalOcean**
```toml
concurrent = X
check_interval = 1
metrics_server = "X"
sentry_dsn = "X"
[[runners]]
name = "docker-auto-scale"
limit = X
request_concurrency = X
url = "https://gitlab.com/ci"
url = "https://gitlab.com/"
token = "SHARED_RUNNER_TOKEN"
executor = "docker+machine"
environment = [
"DOCKER_DRIVER=overlay2"
]
limit = X
[runners.docker]
image = "ruby:2.1"
image = "ruby:2.5"
privileged = true
[runners.machine]
IdleCount = 40
IdleCount = 20
IdleTime = 1800
OffPeakPeriods = ["* * * * * sat,sun *"]
OffPeakTimezone = "UTC"
OffPeakIdleCount = 5
OffPeakIdleTime = 1800
MaxBuilds = 1
MachineName = "srm-%s"
MachineDriver = "digitalocean"
MachineName = "machine-%s-digital-ocean-2gb"
MachineOptions = [
"digitalocean-image=coreos-stable",
"digitalocean-image=X",
"digitalocean-ssh-user=core",
"digitalocean-access-token=DIGITAL_OCEAN_ACCESS_TOKEN",
"digitalocean-region=nyc1",
"digitalocean-size=2gb",
"digitalocean-size=s-2vcpu-2gb",
"digitalocean-private-networking",
"digitalocean-userdata=/etc/gitlab-runner/cloudinit.sh",
"engine-registry-mirror=http://IP_TO_OUR_REGISTRY_MIRROR"
"digitalocean-tags=shared_runners,gitlab_com",
"engine-registry-mirror=http://INTERNAL_IP_OF_OUR_REGISTRY_MIRROR",
"digitalocean-access-token=DIGITAL_OCEAN_ACCESS_TOKEN",
]
[runners.cache]
Type = "s3"
ServerAddress = "IP_TO_OUR_CACHE_SERVER"
BucketName = "runner"
Insecure = true
Shared = true
ServerAddress = "INTERNAL_IP_OF_OUR_CACHE_SERVER"
AccessKey = "ACCESS_KEY"
SecretKey = "ACCESS_SECRET_KEY"
```
**Google Cloud Platform**
```toml
concurrent = X
check_interval = 1
metrics_server = "X"
sentry_dsn = "X"
[[runners]]
name = "docker-auto-scale"
request_concurrency = X
url = "https://gitlab.com/"
token = "SHARED_RUNNER_TOKEN"
executor = "docker+machine"
environment = [
"DOCKER_DRIVER=overlay2"
]
limit = X
[runners.docker]
image = "ruby:2.5"
privileged = true
[runners.machine]
IdleCount = 20
IdleTime = 1800
OffPeakPeriods = ["* * * * * sat,sun *"]
OffPeakTimezone = "UTC"
OffPeakIdleCount = 5
OffPeakIdleTime = 1800
MaxBuilds = 1
MachineName = "srm-%s"
MachineDriver = "google"
MachineOptions = [
"google-project=PROJECT",
"google-disk-size=25",
"google-machine-type=n1-standard-1",
"google-username=core",
"google-tags=gitlab-com,srm",
"google-use-internal-ip",
"google-zone=us-east1-d",
"google-machine-image=PROJECT/global/images/IMAGE",
"engine-registry-mirror=http://INTERNAL_IP_OF_OUR_REGISTRY_MIRROR"
]
[runners.cache]
Type = "s3"
BucketName = "runner"
Insecure = true
Shared = true
ServerAddress = "INTERNAL_IP_OF_OUR_CACHE_SERVER"
AccessKey = "ACCESS_KEY"
SecretKey = "ACCESS_SECRET_KEY"
```
## Sidekiq
......
......@@ -96,7 +96,7 @@ module Gitlab
commit_hash.merge(
merge_request_diff_id: merge_request_diff.id,
relative_order: index,
sha: sha_attribute.type_cast_for_database(sha)
sha: sha_attribute.serialize(sha)
)
end
......
module Gitlab
module Database
BINARY_TYPE = if Gitlab::Database.postgresql?
BINARY_TYPE =
if Gitlab::Database.postgresql?
# PostgreSQL defines its own class with slightly different
# behaviour from the default Binary type.
ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Bytea
else
# In Rails 5.0 `Type` has been moved from `ActiveRecord` to `ActiveModel`
# https://github.com/rails/rails/commit/9cc8c6f3730df3d94c81a55be9ee1b7b4ffd29f6#diff-f8ba7983a51d687976e115adcd95822b
# Remove this method and leave just `ActiveModel::Type::Binary` when removing Gitlab.rails5? code.
if Gitlab.rails5?
ActiveModel::Type::Binary
else
ActiveRecord::Type::Binary
end
end
# Class for casting binary data to hexadecimal SHA1 hashes (and vice-versa).
#
......@@ -16,18 +24,39 @@ module Gitlab
class ShaAttribute < BINARY_TYPE
PACK_FORMAT = 'H*'.freeze
# Casts binary data to a SHA1 in hexadecimal.
# It is called from activerecord-4.2.10/lib/active_record internal methods.
# Remove this method when removing Gitlab.rails5? code.
def type_cast_from_database(value)
value = super
unpack_sha(super)
end
# It is called from activerecord-4.2.10/lib/active_record internal methods.
# Remove this method when removing Gitlab.rails5? code.
def type_cast_for_database(value)
serialize(value)
end
# It is called from activerecord-5.0.6/lib/active_record/attribute.rb
# Remove this method when removing Gitlab.rails5? code..
def deserialize(value)
value = Gitlab.rails5? ? super : method(:type_cast_from_database).super_method.call(value)
unpack_sha(value)
end
# Rename this method to `deserialize(value)` removing Gitlab.rails5? code.
# Casts binary data to a SHA1 in hexadecimal.
def unpack_sha(value)
# Uncomment this line when removing Gitlab.rails5? code.
# value = super
value ? value.unpack(PACK_FORMAT)[0] : nil
end
# Casts a SHA1 in hexadecimal to the proper binary format.
def type_cast_for_database(value)
def serialize(value)
arg = value ? [value].pack(PACK_FORMAT) : nil
super(arg)
Gitlab.rails5? ? super(arg) : method(:type_cast_for_database).super_method.call(arg)
end
end
end
......
......@@ -84,6 +84,28 @@ describe ProfilesController, :request_store do
expect(user.username).to eq(new_username)
end
it 'updates a username using JSON request' do
sign_in(user)
put :update_username,
user: { username: new_username },
format: :json
expect(response.status).to eq(200)
expect(json_response['message']).to eq('Username successfully changed')
end
it 'renders an error message when the username was not updated' do
sign_in(user)
put :update_username,
user: { username: 'invalid username.git' },
format: :json
expect(response.status).to eq(422)
expect(json_response['message']).to match(/Username change failed/)
end
it 'raises a correct error when the username is missing' do
sign_in(user)
......
......@@ -97,9 +97,13 @@ describe 'Profile account page', :js do
end
it 'changes my username' do
fill_in 'user_username', with: 'new-username'
fill_in 'username-change-input', with: 'new-username'
click_button('Update username')
page.find('[data-target="#username-change-confirmation-modal"]').click
page.within('.modal') do
find('.js-modal-primary-action').click
end
expect(page).to have_content('new-username')
end
......
require 'rails_helper'
feature 'Profile > Account' do
feature 'Profile > Account', :js do
given(:user) { create(:user, username: 'foo') }
before do
......@@ -59,6 +59,12 @@ end
def update_username(new_username)
allow(user.namespace).to receive(:move_dir)
visit profile_account_path
fill_in 'user_username', with: new_username
click_button 'Update username'
fill_in 'username-change-input', with: new_username
page.find('[data-target="#username-change-confirmation-modal"]').click
page.within('.modal') do
find('.js-modal-primary-action').click
end
end
......@@ -52,6 +52,7 @@ export const noteableDataMock = {
updated_at: '2017-08-04T09:53:01.226Z',
updated_by_id: 1,
web_url: '/gitlab-org/gitlab-ce/issues/26',
noteableType: 'issue',
};
export const lastFetchedAt = '1501862675';
......
import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
import updateUsername from '~/profile/account/components/update_username.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('UpdateUsername component', () => {
const rootUrl = gl.TEST_HOST;
const actionUrl = `${gl.TEST_HOST}/update/username`;
const username = 'hasnoname';
const newUsername = 'new_username';
let Component;
let vm;
let axiosMock;
beforeEach(() => {
axiosMock = new MockAdapter(axios);
Component = Vue.extend(updateUsername);
vm = mountComponent(Component, {
actionUrl,
rootUrl,
initialUsername: username,
});
});
afterEach(() => {
vm.$destroy();
axiosMock.restore();
});
const findElements = () => {
const modalSelector = `#${vm.$options.modalId}`;
return {
input: vm.$el.querySelector(`#${vm.$options.inputId}`),
openModalBtn: vm.$el.querySelector(`[data-target="${modalSelector}"]`),
modal: vm.$el.querySelector(modalSelector),
modalBody: vm.$el.querySelector(`${modalSelector} .modal-body`),
modalHeader: vm.$el.querySelector(`${modalSelector} .modal-title`),
confirmModalBtn: vm.$el.querySelector(`${modalSelector} .btn-warning`),
};
};
it('has a disabled button if the username was not changed', done => {
const { input, openModalBtn } = findElements();
input.dispatchEvent(new Event('input'));
Vue.nextTick()
.then(() => {
expect(vm.username).toBe(username);
expect(vm.newUsername).toBe(username);
expect(openModalBtn).toBeDisabled();
})
.then(done)
.catch(done.fail);
});
it('has an enabled button which if the username was changed', done => {
const { input, openModalBtn } = findElements();
input.value = newUsername;
input.dispatchEvent(new Event('input'));
Vue.nextTick()
.then(() => {
expect(vm.username).toBe(username);
expect(vm.newUsername).toBe(newUsername);
expect(openModalBtn).not.toBeDisabled();
})
.then(done)
.catch(done.fail);
});
it('confirmation modal contains proper header and body', done => {
const { modalBody, modalHeader } = findElements();
vm.newUsername = newUsername;
Vue.nextTick()
.then(() => {
expect(modalHeader.textContent).toContain('Change username?');
expect(modalBody.textContent).toContain(
`You are going to change the username ${username} to ${newUsername}`,
);
})
.then(done)
.catch(done.fail);
});
it('confirmation modal should escape usernames properly', done => {
const { modalBody } = findElements();
vm.username = vm.newUsername = '<i>Italic</i>';
Vue.nextTick()
.then(() => {
expect(modalBody.innerHTML).toContain('&lt;i&gt;Italic&lt;/i&gt;');
expect(modalBody.innerHTML).not.toContain(vm.username);
})
.then(done)
.catch(done.fail);
});
it('executes API call on confirmation button click', done => {
const { confirmModalBtn } = findElements();
axiosMock.onPut(actionUrl).replyOnce(() => [200, { message: 'Username changed' }]);
spyOn(axios, 'put').and.callThrough();
vm.newUsername = newUsername;
Vue.nextTick()
.then(() => {
confirmModalBtn.click();
expect(axios.put).toHaveBeenCalledWith(actionUrl, { user: { username: newUsername } });
})
.then(done)
.catch(done.fail);
});
it('sets the username after a successful update', done => {
const { input, openModalBtn } = findElements();
axiosMock.onPut(actionUrl).replyOnce(() => {
expect(input).toBeDisabled();
expect(openModalBtn).toBeDisabled();
return [200, { message: 'Username changed' }];
});
vm.newUsername = newUsername;
vm
.onConfirm()
.then(() => {
expect(vm.username).toBe(newUsername);
expect(vm.newUsername).toBe(newUsername);
expect(input).not.toBeDisabled();
expect(input.value).toBe(newUsername);
expect(openModalBtn).toBeDisabled();
})
.then(done)
.catch(done.fail);
});
it('does not set the username after a erroneous update', done => {
const { input, openModalBtn } = findElements();
axiosMock.onPut(actionUrl).replyOnce(() => {
expect(input).toBeDisabled();
expect(openModalBtn).toBeDisabled();
return [400, { message: 'Invalid username' }];
});
const invalidUsername = 'anything.git';
vm.newUsername = invalidUsername;
vm
.onConfirm()
.then(() => done.fail('Expected onConfirm to throw!'))
.catch(() => {
expect(vm.username).toBe(username);
expect(vm.newUsername).toBe(invalidUsername);
expect(input).not.toBeDisabled();
expect(input.value).toBe(invalidUsername);
expect(openModalBtn).not.toBeDisabled();
})
.then(done)
.catch(done.fail);
});
});
......@@ -19,15 +19,15 @@ describe Gitlab::Database::ShaAttribute do
let(:attribute) { described_class.new }
describe '#type_cast_from_database' do
describe '#deserialize' do
it 'converts the binary SHA to a String' do
expect(attribute.type_cast_from_database(binary_from_db)).to eq(sha)
expect(attribute.deserialize(binary_from_db)).to eq(sha)
end
end
describe '#type_cast_for_database' do
describe '#serialize' do
it 'converts a SHA String to binary data' do
expect(attribute.type_cast_for_database(sha).to_s).to eq(binary_sha)
expect(attribute.serialize(sha).to_s).to eq(binary_sha)
end
end
end
......@@ -36,7 +36,7 @@ describe MergeRequestDiffCommit do
"committer_email": "dmitriy.zaporozhets@gmail.com",
"merge_request_diff_id": merge_request_diff_id,
"relative_order": 0,
"sha": sha_attribute.type_cast_for_database('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
"sha": sha_attribute.serialize("5937ac0a7beb003549fc5fd26fc247adbce4a52e")
},
{
"message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
......@@ -48,7 +48,7 @@ describe MergeRequestDiffCommit do
"committer_email": "dmitriy.zaporozhets@gmail.com",
"merge_request_diff_id": merge_request_diff_id,
"relative_order": 1,
"sha": sha_attribute.type_cast_for_database('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
"sha": sha_attribute.serialize("570e7b2abdd848b95f2f578043fc23bd6f6fd24d")
}
]
end
......@@ -79,7 +79,7 @@ describe MergeRequestDiffCommit do
"committer_email": "alejorro70@gmail.com",
"merge_request_diff_id": merge_request_diff_id,
"relative_order": 0,
"sha": sha_attribute.type_cast_for_database('ba3343bc4fa403a8dfbfcab7fc1a8c29ee34bd69')
"sha": sha_attribute.serialize("ba3343bc4fa403a8dfbfcab7fc1a8c29ee34bd69")
}]
end
......
......@@ -2337,6 +2337,20 @@ describe User do
end
end
context '#invalidate_personal_projects_count' do
let(:user) { build_stubbed(:user) }
it 'invalidates cache for personal projects counter' do
cache_mock = double
expect(cache_mock).to receive(:delete).with(['users', user.id, 'personal_projects_count'])
allow(Rails).to receive(:cache).and_return(cache_mock)
user.invalidate_personal_projects_count
end
end
describe '#allow_password_authentication_for_web?' do
context 'regular user' do
let(:user) { build(:user) }
......@@ -2386,13 +2400,11 @@ describe User do
user = build(:user)
projects = double(:projects, count: 1)
expect(user).to receive(:personal_projects).once.and_return(projects)
expect(user).to receive(:personal_projects).and_return(projects)
2.times do
expect(user.personal_projects_count).to eq(1)
end
end
end
describe '#projects_limit_left' do
it 'returns the number of projects that can be created by the user' do
......
......@@ -28,6 +28,14 @@ describe Projects::CreateService, '#execute' do
end
end
describe 'after create actions' do
it 'invalidate personal_projects_count caches' do
expect(user).to receive(:invalidate_personal_projects_count)
create_project(user, opts)
end
end
context "admin creates project with other user's namespace_id" do
it 'sets the correct permissions' do
admin = create(:admin)
......
......@@ -79,6 +79,12 @@ describe Projects::DestroyService do
end
it_behaves_like 'deleting the project'
it 'invalidates personal_project_count cache' do
expect(user).to receive(:invalidate_personal_projects_count)
destroy_project(project, user)
end
end
context 'Sidekiq fake' do
......
......@@ -37,6 +37,12 @@ describe Projects::TransferService do
transfer_project(project, user, group)
end
it 'invalidates the user\'s personal_project_count cache' do
expect(user).to receive(:invalidate_personal_projects_count)
transfer_project(project, user, group)
end
it 'executes system hooks' do
transfer_project(project, user, group) do |service|
expect(service).to receive(:execute_system_hooks)
......
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