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

Automatic merge of gitlab-org/gitlab-ce master

parents 41469e6c c48f29c1
......@@ -80,11 +80,6 @@ Lint/InterpolationCheck:
Lint/MissingCopEnableDirective:
Enabled: false
# Offense count: 1
Lint/ReturnInVoidContext:
Exclude:
- 'app/models/project.rb'
# Offense count: 9
Lint/UriEscapeUnescape:
Exclude:
......
......@@ -202,7 +202,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
dispatch(
'setErrorMessage',
{
text: __('An error accured whilst committing your changes.'),
text: __('An error occurred whilst committing your changes.'),
action: () =>
dispatch('commitChanges').then(() =>
dispatch('setErrorMessage', null, { root: true }),
......
......@@ -35,13 +35,7 @@ export default {
computed: {
chartData() {
return this.graphData.queries.reduce((accumulator, query) => {
const xLabel = `${query.unit}`;
accumulator[xLabel] = {};
query.result.forEach(res =>
res.values.forEach(v => {
accumulator[xLabel][v.time.toISOString()] = v.value;
}),
);
accumulator[query.unit] = query.result.reduce((acc, res) => acc.concat(res.values), []);
return accumulator;
}, {});
},
......@@ -51,14 +45,17 @@ export default {
name: 'Time',
type: 'time',
axisLabel: {
formatter: date => dateFormat(date, 'h:MMtt'),
formatter: date => dateFormat(date, 'h:MM TT'),
},
axisPointer: {
snap: true,
},
nameTextStyle: {
padding: [18, 0, 0, 0],
},
},
yAxis: {
name: this.graphData.y_label,
name: this.yAxisLabel,
axisLabel: {
formatter: value => value.toFixed(3),
},
......@@ -74,6 +71,10 @@ export default {
xAxisLabel() {
return this.graphData.queries.map(query => query.label).join(', ');
},
yAxisLabel() {
const [query] = this.graphData.queries;
return `${this.graphData.y_label} (${query.unit})`;
},
},
methods: {
formatTooltipText(params) {
......@@ -85,7 +86,7 @@ export default {
</script>
<template>
<div class="prometheus-graph">
<div class="prometheus-graph col-12 col-lg-6">
<div class="prometheus-graph-header">
<h5 class="prometheus-graph-title">{{ graphData.title }}</h5>
<div class="prometheus-graph-widgets"><slot></slot></div>
......
......@@ -13,7 +13,7 @@ function checkQueryEmptyData(query) {
result: query.result.filter(timeSeries => {
const newTimeSeries = timeSeries;
const hasValue = series =>
!Number.isNaN(series.value) && (series.value !== null || series.value !== undefined);
!Number.isNaN(series[1]) && (series[1] !== null || series[1] !== undefined);
const hasNonNullValue = timeSeries.values.find(hasValue);
newTimeSeries.values = hasNonNullValue ? newTimeSeries.values : [];
......@@ -33,10 +33,10 @@ function normalizeMetrics(metrics) {
...query,
result: query.result.map(result => ({
...result,
values: result.values.map(([timestamp, value]) => ({
time: new Date(timestamp * 1000),
value: Number(value),
})),
values: result.values.map(([timestamp, value]) => [
new Date(timestamp * 1000).toISOString(),
Number(value),
]),
})),
}));
......
import $ from 'jquery';
import createFlash from '~/flash';
import GfmAutoComplete from '~/gfm_auto_complete';
import emojiRegex from 'emoji-regex';
import EmojiMenu from './emoji_menu';
const defaultStatusEmoji = 'speech_balloon';
......@@ -42,6 +43,17 @@ document.addEventListener('DOMContentLoaded', () => {
const emojiAutocomplete = new GfmAutoComplete();
emojiAutocomplete.setup($(statusMessageField), { emojis: true });
const userNameInput = document.getElementById('user_name');
userNameInput.addEventListener('input', () => {
const EMOJI_REGEX = emojiRegex();
if (EMOJI_REGEX.test(userNameInput.value)) {
// set field to invalid so it gets detected by GlFieldErrors
userNameInput.setCustomValidity('Invalid field');
} else {
userNameInput.setCustomValidity('');
}
});
import(/* webpackChunkName: 'emoji' */ '~/emoji')
.then(Emoji => {
const emojiMenu = new EmojiMenu(
......
......@@ -400,18 +400,7 @@
}
.prometheus-graph {
flex: 1 0 auto;
min-width: 450px;
max-width: 100%;
padding: $gl-padding / 2;
h5 {
font-size: 16px;
}
@include media-breakpoint-down(sm) {
min-width: 100%;
}
}
.prometheus-graph-header {
......@@ -421,6 +410,7 @@
margin-bottom: $gl-padding-8;
h5 {
font-size: $gl-font-size-large;
margin: 0;
}
}
......
......@@ -182,11 +182,11 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
def serialize_environments(request, response, nested = false)
serializer = EnvironmentSerializer
EnvironmentSerializer
.new(project: @project, current_user: @current_user)
.tap { |serializer| serializer.within_folders if nested }
.with_pagination(request, response)
serializer = serializer.within_folders if nested
serializer.represent(@environments)
.represent(@environments)
end
def authorize_stop_environment!
......
......@@ -8,9 +8,13 @@ module ErrorTracking
belongs_to :project
validates :api_url, length: { maximum: 255 }, public_url: true, url: { enforce_sanitization: true }
validates :api_url, length: { maximum: 255 }, public_url: true, url: { enforce_sanitization: true, ascii_only: true }, allow_nil: true
validate :validate_api_url_path
validates :api_url, presence: true, if: :enabled
validate :validate_api_url_path, if: :enabled
validates :token, presence: true, if: :enabled
attr_encrypted :token,
mode: :per_attribute_iv,
......@@ -19,6 +23,31 @@ module ErrorTracking
after_save :clear_reactive_cache!
def project_name
super || project_name_from_slug
end
def organization_name
super || organization_name_from_slug
end
def project_slug
project_slug_from_api_url
end
def organization_slug
organization_slug_from_api_url
end
def self.build_api_url_from(api_host:, project_slug:, organization_slug:)
uri = Addressable::URI.parse("#{api_host}/api/0/projects/#{organization_slug}/#{project_slug}/")
uri.path = uri.path.squeeze('/')
uri.to_s
rescue Addressable::URI::InvalidURIError
api_host
end
def sentry_client
Sentry::Client.new(api_url, token)
end
......@@ -33,6 +62,10 @@ module ErrorTracking
end
end
def list_sentry_projects
{ projects: sentry_client.list_projects }
end
def calculate_reactive_cache(request, opts)
case request
when 'list_issues'
......@@ -47,13 +80,53 @@ module ErrorTracking
url.sub('api/0/projects/', '')
end
def api_host
return if api_url.blank?
# This returns http://example.com/
Addressable::URI.join(api_url, '/').to_s
end
private
def project_name_from_slug
@project_name_from_slug ||= project_slug_from_api_url&.titleize
end
def organization_name_from_slug
@organization_name_from_slug ||= organization_slug_from_api_url&.titleize
end
def project_slug_from_api_url
extract_slug(:project)
end
def organization_slug_from_api_url
extract_slug(:organization)
end
def extract_slug(capture)
return if api_url.blank?
begin
url = Addressable::URI.parse(api_url)
rescue Addressable::URI::InvalidURIError
return nil
end
@slug_match ||= url.path.match(%r{^/api/0/projects/+(?<organization>[^/]+)/+(?<project>[^/|$]+)}) || {}
@slug_match[capture]
end
def validate_api_url_path
unless URI(api_url).path.starts_with?('/api/0/projects')
errors.add(:api_url, 'path needs to start with /api/0/projects')
return if api_url.blank?
begin
unless Addressable::URI.parse(api_url).path.starts_with?('/api/0/projects')
errors.add(:api_url, 'path needs to start with /api/0/projects')
end
rescue Addressable::URI::InvalidURIError
end
rescue URI::InvalidURIError
end
end
end
......@@ -738,11 +738,13 @@ class Project < ActiveRecord::Base
end
def import_url=(value)
return super(value) unless Gitlab::UrlSanitizer.valid?(value)
import_url = Gitlab::UrlSanitizer.new(value)
super(import_url.sanitized_url)
create_or_update_import_data(credentials: import_url.credentials)
if Gitlab::UrlSanitizer.valid?(value)
import_url = Gitlab::UrlSanitizer.new(value)
super(import_url.sanitized_url)
create_or_update_import_data(credentials: import_url.credentials)
else
super(value)
end
end
def import_url
......@@ -1066,7 +1068,7 @@ class Project < ActiveRecord::Base
# rubocop: disable CodeReuse/ServiceClass
def create_labels
Label.templates.each do |label|
params = label.attributes.except('id', 'template', 'created_at', 'updated_at')
params = label.attributes.except('id', 'template', 'created_at', 'updated_at', 'type')
Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true)
end
end
......
......@@ -19,9 +19,19 @@ module Members
current_user: current_user
)
members.each { |member| after_execute(member: member) }
errors = []
success
members.each do |member|
if member.errors.any?
errors << "#{member.user.username}: #{member.errors.full_messages.to_sentence}"
else
after_execute(member: member)
end
end
return success unless errors.any?
error(errors.to_sentence)
end
private
......
......@@ -2,7 +2,7 @@
- @content_class = "limit-container-width" unless fluid_layout
- gravatar_link = link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host
= bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default js-quick-submit' }, authenticity_token: true do |f|
= bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default js-quick-submit gl-show-field-errors' }, authenticity_token: true do |f|
= form_errors(@user)
.row
......@@ -77,10 +77,10 @@
.col-lg-8
.row
- if @user.read_only_attribute?(:name)
= f.text_field :name, required: true, readonly: true, wrapper: { class: 'col-md-9' },
= f.text_field :name, required: true, readonly: true, wrapper: { class: 'col-md-9 qa-full-name' },
help: s_("Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you") % { provider_label: attribute_provider_label(:name) }
- else
= f.text_field :name, label: 'Full name', required: true, wrapper: { class: 'col-md-9' }, help: s_("Profiles|Enter your name, so people you know can recognize you")
= f.text_field :name, label: 'Full name', required: true, title: s_("Profiles|Using emojis in names seems fun, but please try to set a status message instead"), wrapper: { class: 'col-md-9 qa-full-name' }, help: s_("Profiles|Enter your name, so people you know can recognize you")
= f.text_field :id, readonly: true, label: 'User ID', wrapper: { class: 'col-md-3' }
- if @user.read_only_attribute?(:email)
......
---
title: Block emojis and symbol characters from users full names
merge_request: 24523
author:
type: other
---
title: Resolve UI bug adding group members with lower permissions
merge_request: 24820
author:
type: fixed
---
title: Fix template labels not being created on new projects
merge_request: 24803
author:
type: fixed
# frozen_string_literal: true
class AddColumnsProjectErrorTrackingSettings < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :project_error_tracking_settings, :project_name, :string
add_column :project_error_tracking_settings, :organization_name, :string
change_column_default :project_error_tracking_settings, :enabled, from: true, to: false
change_column_null :project_error_tracking_settings, :api_url, true
end
end
# frozen_string_literal: true
class FixNullTypeLabels < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
update_column_in_batches(:labels, :type, 'ProjectLabel') do |table, query|
query.where(
table[:project_id].not_eq(nil)
.and(table[:template].eq(false))
.and(table[:type].eq(nil))
)
end
end
def down
# no action
end
end
......@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20190124200344) do
ActiveRecord::Schema.define(version: 20190131122559) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -2214,10 +2214,12 @@ ActiveRecord::Schema.define(version: 20190124200344) do
end
create_table "project_error_tracking_settings", primary_key: "project_id", id: :integer, force: :cascade do |t|
t.boolean "enabled", default: true, null: false
t.string "api_url", null: false
t.boolean "enabled", default: false, null: false
t.string "api_url"
t.string "encrypted_token"
t.string "encrypted_token_iv"
t.string "project_name"
t.string "organization_name"
end
create_table "project_feature_usages", primary_key: "project_id", id: :integer, force: :cascade do |t|
......
......@@ -27,7 +27,7 @@ The source of the documentation is maintained in the following repository locati
| Project | Path |
| --- | --- |
| [GitLab Community Edition](https://gitlab.com/gitlab-org/gitlab-ce/) | [`/doc`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc) |
| [GitLab Enterprise Edition](https://gitlab.com/gitlab-org/gitlab-ce/) | [`/doc`](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/doc) |
| [GitLab Enterprise Edition](https://gitlab.com/gitlab-org/gitlab-ee/) | [`/doc`](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/doc) |
| [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner/) | [`/docs`](https://gitlab.com/gitlab-org/gitlab-runner/tree/master/docs) |
| [Omnibus GitLab](https://gitlab.com/gitlab-org/omnibus-gitlab/) | [`/doc`](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/doc) |
......
......@@ -264,7 +264,7 @@ your Pages project are the same.
1. A PEM certificate
1. An intermediate certificate
1. A public key
1. A private key
![Pages project - adding certificates](img/add_certificate_to_pages.png)
......@@ -280,7 +280,7 @@ Usually it's combined with the PEM certificate, but there are
some cases in which you need to add them manually.
[CloudFlare certs](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/)
are one of these cases.
- A public key is an encrypted key which validates
- A private key is an encrypted key which validates
your PEM against your domain.
### Now what?
......@@ -293,7 +293,7 @@ of this, it's simple:
and paste the root certificate (usually available from your CA website)
and paste it in the [same field as your PEM certificate](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/),
just jumping a line between them.
- Copy your public key and paste it in the last field
- Copy your private key and paste it in the last field
>**Note:**
**Do not** open certificates or encryption keys in
......
......@@ -16,11 +16,18 @@ module Gitlab
# re-raises the exception.
#
# steal_class - The name of the class for which to steal jobs.
def self.steal(steal_class)
enqueued = Sidekiq::Queue.new(self.queue)
scheduled = Sidekiq::ScheduledSet.new
def self.steal(steal_class, retry_dead_jobs: false)
queues = [
Sidekiq::ScheduledSet.new,
Sidekiq::Queue.new(self.queue)
]
if retry_dead_jobs
queues << Sidekiq::RetrySet.new
queues << Sidekiq::DeadSet.new
end
[scheduled, enqueued].each do |queue|
queues.each do |queue|
queue.each do |job|
migration_class, migration_args = job.args
......
......@@ -179,6 +179,7 @@ excluded_attributes:
error_tracking_setting:
- :encrypted_token
- :encrypted_token_iv
- :enabled
methods:
labels:
......
......@@ -676,9 +676,6 @@ msgstr ""
msgid "An empty GitLab User field will add the FogBugz user's full name (e.g. \"By John Smith\") in the description of all issues and comments. It will also associate and/or assign these issues and comments with the project creator."
msgstr ""
msgid "An error accured whilst committing your changes."
msgstr ""
msgid "An error has occurred"
msgstr ""
......@@ -811,6 +808,9 @@ msgstr ""
msgid "An error occurred while validating username"
msgstr ""
msgid "An error occurred whilst committing your changes."
msgstr ""
msgid "An error occurred whilst fetching the job trace."
msgstr ""
......@@ -7091,6 +7091,9 @@ msgstr ""
msgid "Profiles|Username successfully changed"
msgstr ""
msgid "Profiles|Using emojis in names seems fun, but please try to set a status message instead"
msgstr ""
msgid "Profiles|What's your status?"
msgstr ""
......
......@@ -6,5 +6,7 @@ FactoryBot.define do
api_url 'https://gitlab.com/api/0/projects/sentry-org/sentry-project'
enabled true
token 'access_token_123'
project_name 'Sentry Project'
organization_name 'Sentry Org'
end
end
require_relative '../support/helpers/test_env'
FactoryBot.define do
PAGES_ACCESS_LEVEL_SCHEMA_VERSION = 20180423204600
PAGES_ACCESS_LEVEL_SCHEMA_VERSION ||= 20180423204600
# Project without repository
#
......
......@@ -10,6 +10,7 @@ describe 'User edit profile' do
def submit_settings
click_button 'Update profile settings'
wait_for_requests if respond_to?(:wait_for_requests)
end
it 'changes user profile' do
......@@ -35,6 +36,17 @@ describe 'User edit profile' do
expect(page).to have_content('Profile was successfully updated')
end
it 'shows an error if the full name contains an emoji', :js do
simulate_input('#user_name', 'Martin 😀')
submit_settings
page.within('.qa-full-name') do
expect(page).to have_css '.gl-field-error-outline'
expect(find('.gl-field-error')).not_to have_selector('.hidden')
expect(find('.gl-field-error')).to have_content('Using emojis in names seems fun, but please try to set a status message instead')
end
end
context 'user avatar' do
before do
attach_file(:user_avatar, Rails.root.join('spec', 'fixtures', 'banana_sample.gif'))
......@@ -61,6 +73,11 @@ describe 'User edit profile' do
end
context 'user status', :js do
def visit_user
visit user_path(user)
wait_for_requests
end
def select_emoji(emoji_name, is_modal = false)
emoji_menu_class = is_modal ? '.js-modal-status-emoji-menu' : '.js-status-emoji-menu'
toggle_button = find('.js-toggle-emoji-menu')
......@@ -71,18 +88,16 @@ describe 'User edit profile' do
context 'profile edit form' do
it 'shows the user status form' do
visit(profile_path)
expect(page).to have_content('Current status')
end
it 'adds emoji to user status' do
emoji = 'biohazard'
visit(profile_path)
select_emoji(emoji)
submit_settings
visit user_path(user)
visit_user
within('.cover-status') do
expect(page).to have_emoji(emoji)
end
......@@ -90,11 +105,11 @@ describe 'User edit profile' do
it 'adds message to user status' do
message = 'I have something to say'
visit(profile_path)
fill_in 'js-status-message-field', with: message
submit_settings
visit user_path(user)
visit_user
within('.cover-status') do
expect(page).to have_emoji('speech_balloon')
expect(page).to have_content message
......@@ -104,12 +119,12 @@ describe 'User edit profile' do
it 'adds message and emoji to user status' do
emoji = 'tanabata_tree'
message = 'Playing outside'
visit(profile_path)
select_emoji(emoji)
fill_in 'js-status-message-field', with: message
submit_settings
visit user_path(user)
visit_user
within('.cover-status') do
expect(page).to have_emoji(emoji)
expect(page).to have_content message
......@@ -119,7 +134,8 @@ describe 'User edit profile' do
it 'clears the user status' do
user_status = create(:user_status, user: user, message: 'Eating bread', emoji: 'stuffed_flatbread')
visit user_path(user)
visit_user
within('.cover-status') do
expect(page).to have_emoji(user_status.emoji)
expect(page).to have_content user_status.message
......@@ -129,15 +145,13 @@ describe 'User edit profile' do
click_button 'js-clear-user-status-button'
submit_settings
wait_for_requests
visit_user
visit user_path(user)
expect(page).not_to have_selector '.cover-status'
end
it 'displays a default emoji if only message is entered' do
message = 'a status without emoji'
visit(profile_path)
fill_in 'js-status-message-field', with: message
within('.js-toggle-emoji-menu') do
......@@ -162,6 +176,7 @@ describe 'User edit profile' do
page.within "#set-user-status-modal" do
click_button 'Set status'
end
wait_for_requests
end
before do
......@@ -202,7 +217,8 @@ describe 'User edit profile' do
select_emoji(emoji, true)
set_user_status_in_modal
visit user_path(user)
visit_user
within('.cover-status') do
expect(page).to have_emoji(emoji)
end
......@@ -225,7 +241,8 @@ describe 'User edit profile' do
find('.js-status-message-field').native.send_keys(message)
set_user_status_in_modal
visit user_path(user)
visit_user
within('.cover-status') do
expect(page).to have_emoji('speech_balloon')
expect(page).to have_content message
......@@ -240,7 +257,8 @@ describe 'User edit profile' do
find('.js-status-message-field').native.send_keys(message)
set_user_status_in_modal
visit user_path(user)
visit_user
within('.cover-status') do
expect(page).to have_emoji(emoji)
expect(page).to have_content message
......@@ -250,7 +268,9 @@ describe 'User edit profile' do
it 'clears the user status with the "X" button' do
user_status = create(:user_status, user: user, message: 'Eating bread', emoji: 'stuffed_flatbread')
visit user_path(user)
visit_user
wait_for_requests
within('.cover-status') do
expect(page).to have_emoji(user_status.emoji)
expect(page).to have_content user_status.message
......@@ -265,14 +285,18 @@ describe 'User edit profile' do
find('.js-clear-user-status-button').click
set_user_status_in_modal
visit user_path(user)
visit_user
wait_for_requests
expect(page).not_to have_selector '.cover-status'
end
it 'clears the user status with the "Remove status" button' do
user_status = create(:user_status, user: user, message: 'Eating bread', emoji: 'stuffed_flatbread')
visit user_path(user)
visit_user
wait_for_requests
within('.cover-status') do
expect(page).to have_emoji(user_status.emoji)
expect(page).to have_content user_status.message
......@@ -288,7 +312,8 @@ describe 'User edit profile' do
click_button 'Remove status'
end
visit user_path(user)
visit_user
expect(page).not_to have_selector '.cover-status'
end
......
......@@ -104,6 +104,38 @@ describe Gitlab::BackgroundMigration do
end
end
end
context 'when retry_dead_jobs is true', :sidekiq, :redis do
let(:retry_queue) do
[double(args: ['Object', [3]], queue: described_class.queue, delete: true)]
end
let(:dead_queue) do
[double(args: ['Object', [4]], queue: described_class.queue, delete: true)]
end
before do
allow(Sidekiq::RetrySet).to receive(:new).and_return(retry_queue)
allow(Sidekiq::DeadSet).to receive(:new).and_return(dead_queue)
end
it 'steals from the dead and retry queue' do
Sidekiq::Testing.disable! do
expect(described_class).to receive(:perform)
.with('Object', [1]).ordered
expect(described_class).to receive(:perform)
.with('Object', [2]).ordered
expect(described_class).to receive(:perform)
.with('Object', [3]).ordered
expect(described_class).to receive(:perform)
.with('Object', [4]).ordered
BackgroundMigrationWorker.perform_async('Object', [2])
BackgroundMigrationWorker.perform_in(10.minutes, 'Object', [1])
described_class.steal('Object', retry_dead_jobs: true)
end
end
end
end
describe '.perform' do
......
......@@ -651,5 +651,6 @@ ResourceLabelEvent:
ErrorTracking::ProjectErrorTrackingSetting:
- id
- api_url
- enabled
- project_id
- project_name
- organization_name
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20190131122559_fix_null_type_labels')
describe FixNullTypeLabels, :migration do
let(:migration) { described_class.new }
let(:projects) { table(:projects) }
let(:namespaces) { table(:namespaces) }
let(:labels) { table(:labels) }
before do
group = namespaces.create(name: 'labels-test-project', path: 'labels-test-project', type: 'Group')
project = projects.create!(namespace_id: group.id, name: 'labels-test-group', path: 'labels-test-group')
@template_label = labels.create(title: 'template', template: true)
@project_label = labels.create(title: 'project label', project_id: project.id, type: 'ProjectLabel')
@group_label = labels.create(title: 'group_label', group_id: group.id, type: 'GroupLabel')
@broken_label_1 = labels.create(title: 'broken 1', project_id: project.id)
@broken_label_2 = labels.create(title: 'broken 2', project_id: project.id)
end
describe '#up' do
it 'fix labels with type missing' do
migration.up
# Labels that requires type change
expect(@broken_label_1.reload.type).to eq('ProjectLabel')
expect(@broken_label_2.reload.type).to eq('ProjectLabel')
# Labels out of scope
expect(@template_label.reload.type).to be_nil
expect(@project_label.reload.type).to eq('ProjectLabel')
expect(@group_label.reload.type).to eq('GroupLabel')
end
end
end
......@@ -15,9 +15,11 @@ describe ErrorTracking::ProjectErrorTrackingSetting do
describe 'Validations' do
context 'when api_url is over 255 chars' do
it 'fails validation' do
before do
subject.api_url = 'https://' + 'a' * 250
end
it 'fails validation' do
expect(subject).not_to be_valid
expect(subject.errors.messages[:api_url]).to include('is too long (maximum is 255 characters)')
end
......@@ -31,6 +33,34 @@ describe ErrorTracking::ProjectErrorTrackingSetting do
end
end
context 'presence validations' do
using RSpec::Parameterized::TableSyntax
valid_api_url = 'http://example.com/api/0/projects/org-slug/proj-slug/'
valid_token = 'token'
where(:enabled, :token, :api_url, :valid?) do
true | nil | nil | false
true | nil | valid_api_url | false
true | valid_token | nil | false
true | valid_token | valid_api_url | true
false | nil | nil | true
false | nil | valid_api_url | true
false | valid_token | nil | true
false | valid_token | valid_api_url | true
end
with_them do
before do
subject.enabled = enabled
subject.token = token
subject.api_url = api_url
end
it { expect(subject.valid?).to eq(valid?) }
end
end
context 'URL path' do
it 'fails validation with wrong path' do
subject.api_url = 'http://gitlab.com/project1/something'
......@@ -45,6 +75,16 @@ describe ErrorTracking::ProjectErrorTrackingSetting do
expect(subject).to be_valid
end
end
context 'non ascii chars in api_url' do
before do
subject.api_url = 'http://gitlab.com/api/0/projects/project1/something€'
end
it 'fails validation' do
expect(subject).not_to be_valid
end
end
end
describe '#sentry_external_url' do
......@@ -106,4 +146,138 @@ describe ErrorTracking::ProjectErrorTrackingSetting do
end
end
end
describe '#list_sentry_projects' do
let(:projects) { [:list, :of, :projects] }
let(:sentry_client) { spy(:sentry_client) }
it 'calls sentry client' do
expect(subject).to receive(:sentry_client).and_return(sentry_client)
expect(sentry_client).to receive(:list_projects).and_return(projects)
result = subject.list_sentry_projects
expect(result).to eq(projects: projects)
end
end
context 'slugs' do
shared_examples_for 'slug from api_url' do |method, slug|
context 'when api_url is correct' do
before do
subject.api_url = 'http://gitlab.com/api/0/projects/org-slug/project-slug/'
end
it 'returns slug' do
expect(subject.public_send(method)).to eq(slug)
end
end
context 'when api_url is blank' do
before do
subject.api_url = nil
end
it 'returns nil' do
expect(subject.public_send(method)).to be_nil
end
end
end
it_behaves_like 'slug from api_url', :project_slug, 'project-slug'
it_behaves_like 'slug from api_url', :organization_slug, 'org-slug'
end
context 'names from api_url' do
shared_examples_for 'name from api_url' do |name, titleized_slug|
context 'name is present in DB' do
it 'returns name from DB' do
subject[name] = 'Sentry name'
subject.api_url = 'http://gitlab.com/api/0/projects/org-slug/project-slug'
expect(subject.public_send(name)).to eq('Sentry name')
end
end
context 'name is null in DB' do
it 'titleizes and returns slug from api_url' do
subject[name] = nil
subject.api_url = 'http://gitlab.com/api/0/projects/org-slug/project-slug'
expect(subject.public_send(name)).to eq(titleized_slug)
end
it 'returns nil when api_url is incorrect' do
subject[name] = nil
subject.api_url = 'http://gitlab.com/api/0/projects/'
expect(subject.public_send(name)).to be_nil
end
it 'returns nil when api_url is blank' do
subject[name] = nil
subject.api_url = nil
expect(subject.public_send(name)).to be_nil
end
end
end
it_behaves_like 'name from api_url', :organization_name, 'Org Slug'
it_behaves_like 'name from api_url', :project_name, 'Project Slug'
end
describe '.build_api_url_from' do
it 'correctly builds api_url with slugs' do
api_url = described_class.build_api_url_from(
api_host: 'http://sentry.com/',
organization_slug: 'org-slug',
project_slug: 'proj-slug'
)
expect(api_url).to eq('http://sentry.com/api/0/projects/org-slug/proj-slug/')
end
it 'correctly builds api_url without slugs' do
api_url = described_class.build_api_url_from(
api_host: 'http://sentry.com/',
organization_slug: nil,
project_slug: nil
)
expect(api_url).to eq('http://sentry.com/api/0/projects/')
end
it 'does not raise exception with invalid url' do
api_url = described_class.build_api_url_from(
api_host: ':::',
organization_slug: 'org-slug',
project_slug: 'proj-slug'
)
expect(api_url).to eq(':::')
end
end
describe '#api_host' do
context 'when api_url exists' do
before do
subject.api_url = 'https://example.com/api/0/projects/org-slug/proj-slug/'
end
it 'extracts the api_host from api_url' do
expect(subject.api_host).to eq('https://example.com/')
end
end
context 'when api_url is nil' do
before do
subject.api_url = nil
end
it 'returns nil' do
expect(subject.api_url).to eq(nil)
end
end
end
end
......@@ -36,4 +36,13 @@ describe Members::CreateService do
expect(result[:message]).to be_present
expect(project.users).not_to include project_user
end
it 'does not add an invalid member' do
params = { user_ids: project_user.id.to_s, access_level: -1 }
result = described_class.new(user, params).execute(project)
expect(result[:status]).to eq(:error)
expect(result[:message]).to include(project_user.username)
expect(project.users).not_to include project_user
end
end
......@@ -16,7 +16,11 @@ describe Projects::CreateService, '#execute' do
Label.create(title: "bug", template: true)
project = create_project(user, opts)
expect(project.labels).not_to be_empty
created_label = project.reload.labels.last
expect(created_label.type).to eq('ProjectLabel')
expect(created_label.project_id).to eq(project.id)
expect(created_label.title).to eq('bug')
end
context 'user namespace' do
......
......@@ -3416,6 +3416,11 @@ elliptic@^6.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.0"
emoji-regex@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
emoji-unicode-version@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/emoji-unicode-version/-/emoji-unicode-version-0.2.1.tgz#0ebf3666b5414097971d34994e299fce75cdbafc"
......
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