Commit c353fdc4 authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-06-01

# Conflicts:
#	spec/models/concerns/has_variable_spec.rb
#	spec/requests/api/runner_spec.rb

[ci skip]
parents 3883a936 81e899ea
......@@ -569,7 +569,7 @@ GEM
omniauth-github (1.3.0)
omniauth (~> 1.5)
omniauth-oauth2 (>= 1.4.0, < 2.0)
omniauth-gitlab (1.0.2)
omniauth-gitlab (1.0.3)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0)
omniauth-google-oauth2 (0.5.3)
......
......@@ -11,6 +11,7 @@ const Api = {
projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels',
mergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
mergeRequestsPath: '/api/:version/merge_requests',
mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
groupLabelsPath: '/groups/:namespace_path/-/labels',
......@@ -109,6 +110,12 @@ const Api = {
return axios.get(url);
},
mergeRequests(params = {}) {
const url = Api.buildUrl(Api.mergeRequestsPath);
return axios.get(url, { params });
},
mergeRequestChanges(projectPath, mergeRequestId) {
const url = Api.buildUrl(Api.mergeRequestChangesPath)
.replace(':id', encodeURIComponent(projectPath))
......
......@@ -6,6 +6,7 @@ import * as getters from './getters';
import mutations from './mutations';
import commitModule from './modules/commit';
import pipelines from './modules/pipelines';
import mergeRequests from './modules/merge_requests';
Vue.use(Vuex);
......@@ -18,6 +19,7 @@ export const createStore = () =>
modules: {
commit: commitModule,
pipelines,
mergeRequests,
},
});
......
import { __ } from '../../../../locale';
import Api from '../../../../api';
import flash from '../../../../flash';
import * as types from './mutation_types';
export const requestMergeRequests = ({ commit }) => commit(types.REQUEST_MERGE_REQUESTS);
export const receiveMergeRequestsError = ({ commit }) => {
flash(__('Error loading merge requests.'));
commit(types.RECEIVE_MERGE_REQUESTS_ERROR);
};
export const receiveMergeRequestsSuccess = ({ commit }, data) =>
commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, data);
export const fetchMergeRequests = ({ dispatch, state: { scope, state } }, search = '') => {
dispatch('requestMergeRequests');
dispatch('resetMergeRequests');
Api.mergeRequests({ scope, state, search })
.then(({ data }) => dispatch('receiveMergeRequestsSuccess', data))
.catch(() => dispatch('receiveMergeRequestsError'));
};
export const resetMergeRequests = ({ commit }) => commit(types.RESET_MERGE_REQUESTS);
export default () => {};
export const scopes = {
assignedToMe: 'assigned-to-me',
createdByMe: 'created-by-me',
};
export const states = {
opened: 'opened',
closed: 'closed',
merged: 'merged',
};
import state from './state';
import * as actions from './actions';
import mutations from './mutations';
export default {
namespaced: true,
state: state(),
actions,
mutations,
};
export const REQUEST_MERGE_REQUESTS = 'REQUEST_MERGE_REQUESTS';
export const RECEIVE_MERGE_REQUESTS_ERROR = 'RECEIVE_MERGE_REQUESTS_ERROR';
export const RECEIVE_MERGE_REQUESTS_SUCCESS = 'RECEIVE_MERGE_REQUESTS_SUCCESS';
export const RESET_MERGE_REQUESTS = 'RESET_MERGE_REQUESTS';
/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
export default {
[types.REQUEST_MERGE_REQUESTS](state) {
state.isLoading = true;
},
[types.RECEIVE_MERGE_REQUESTS_ERROR](state) {
state.isLoading = false;
},
[types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, data) {
state.isLoading = false;
state.mergeRequests = data.map(mergeRequest => ({
id: mergeRequest.id,
iid: mergeRequest.iid,
title: mergeRequest.title,
projectId: mergeRequest.project_id,
projectPathWithNamespace: mergeRequest.web_url
.replace(`${gon.gitlab_url}/`, '')
.replace(`/merge_requests/${mergeRequest.iid}`, ''),
}));
},
[types.RESET_MERGE_REQUESTS](state) {
state.mergeRequests = [];
},
};
import { scopes, states } from './constants';
export default () => ({
isLoading: false,
mergeRequests: [],
scope: scopes.assignedToMe,
state: states.opened,
});
......@@ -36,6 +36,11 @@ html [type="button"],
cursor: pointer;
}
b,
strong {
font-weight: bold;
}
a {
color: $gl-link-color;
}
......@@ -48,6 +53,12 @@ a {
}
}
code {
padding: 2px 4px;
background-color: $red-100;
border-radius: 3px;
}
table {
// Remove any table border lines
border-spacing: 0;
......
.modal-header {
background-color: $modal-body-bg;
padding: #{3 * $grid-size} #{2 * $grid-size};
.page-title,
.modal-title {
......
......@@ -340,10 +340,6 @@ code {
}
}
a > code {
color: $link-color;
}
.monospace {
font-family: $monospace_font;
}
......
......@@ -6,15 +6,16 @@ module CacheableAttributes
end
class_methods do
def cache_key
"#{name}:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:#{Rails.version}".freeze
end
# Can be overriden
def current_without_cache
last
end
def cache_key
"#{name}:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:json".freeze
end
# Can be overriden
def defaults
{}
end
......@@ -24,10 +25,18 @@ module CacheableAttributes
end
def cached
json_attributes = Rails.cache.read(cache_key)
return nil unless json_attributes.present?
if RequestStore.active?
RequestStore[:"#{name}_cached_attributes"] ||= retrieve_from_cache
else
retrieve_from_cache
end
end
def retrieve_from_cache
record = Rails.cache.read(cache_key)
ensure_cache_setup if record.present?
build_from_defaults(JSON.parse(json_attributes))
record
end
def current
......@@ -35,7 +44,12 @@ module CacheableAttributes
return cached_record if cached_record.present?
current_without_cache.tap { |current_record| current_record&.cache! }
rescue
rescue => e
if Rails.env.production?
Rails.logger.warn("Cached record for #{name} couldn't be loaded, falling back to uncached record: #{e}")
else
raise e
end
# Fall back to an uncached value if there are any problems (e.g. Redis down)
current_without_cache
end
......@@ -46,9 +60,15 @@ module CacheableAttributes
# Gracefully handle when Redis is not available. For example,
# omnibus may fail here during gitlab:assets:compile.
end
def ensure_cache_setup
# This is a workaround for a Rails bug that causes attribute methods not
# to be loaded when read from cache: https://github.com/rails/rails/issues/27348
define_attribute_methods
end
end
def cache!
Rails.cache.write(self.class.cache_key, attributes.to_json)
Rails.cache.write(self.class.cache_key, self)
end
end
......@@ -77,6 +77,7 @@ module ReactiveCaching
def clear_reactive_cache!(*args)
Rails.cache.delete(full_reactive_cache_key(*args))
Rails.cache.delete(alive_reactive_cache_key(*args))
end
def exclusively_update_reactive_cache!(*args)
......
......@@ -30,8 +30,6 @@ module TimeTrackable
return if @time_spent == 0
touch if touchable?
if @time_spent == :reset
reset_spent_time
else
......@@ -59,10 +57,6 @@ module TimeTrackable
private
def touchable?
valid? && persisted?
end
def reset_spent_time
timelogs.new(time_spent: total_time_spent * -1, user: @time_spent_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
......
......@@ -2,8 +2,8 @@ class Timelog < ActiveRecord::Base
validates :time_spent, :user, presence: true
validate :issuable_id_is_present
belongs_to :issue
belongs_to :merge_request
belongs_to :issue, touch: true
belongs_to :merge_request, touch: true
belongs_to :user
def issuable
......
......@@ -185,7 +185,10 @@ class IssuableBaseService < BaseService
old_associations = associations_before_update(issuable)
label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids)
if labels_changing?(issuable.label_ids, label_ids)
params[:label_ids] = label_ids
issuable.touch
end
if issuable.changed? || params.present?
issuable.assign_attributes(params.merge(updated_by: current_user))
......
......@@ -19,7 +19,7 @@
.form-text.text-muted
Choose any color.
%br
Or you can choose one of suggested colors below
Or you can choose one of the suggested colors below
.suggest-colors
- suggested_colors.each do |color|
......
......@@ -4,8 +4,8 @@
%div{ class: container_class }
%ul.nav-links.log-tabs.nav.nav-tabs
- @loggers.each do |klass|
%li{ class: active_when(klass == @loggers.first) }>
= link_to klass.file_name, "##{klass.file_name_noext}", data: { toggle: 'tab' }
%li.nav-item
= link_to klass.file_name, "##{klass.file_name_noext}", data: { toggle: 'tab' }, class: "#{active_when(klass == @loggers.first)} nav-link"
.row-content-block
To prevent performance issues admin logs output the last 2000 lines
.tab-content
......
......@@ -2,11 +2,12 @@
.modal-dialog.modal-lg
.modal-content
.modal-header
%a.close{ href: "#", "data-dismiss" => "modal" } ×
%h4
%h4.modal-title
Keyboard Shortcuts
%small
= link_to '(Show all)', '#', class: 'js-more-help-button'
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
.row
.col-lg-4
......
......@@ -77,11 +77,10 @@
.modal-dialog
.modal-content
.modal-header
%button.close{ type: 'button', 'data-dismiss': 'modal' }
%span
&times;
%h4.modal-title
Position and size your new avatar
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
.profile-crop-image-container
%img.modal-profile-crop-image{ alt: 'Avatar cropper' }
......
......@@ -2,8 +2,9 @@
.modal-dialog
.modal-content
.modal-header
%a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3 Import projects from Bitbucket
%h3.modal-title Import projects from Bitbucket
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
To enable importing projects from Bitbucket,
- if current_user.admin?
......
......@@ -2,8 +2,9 @@
.modal-dialog
.modal-content
.modal-header
%a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3 Import projects from GitLab.com
%h3.modal-title Import projects from GitLab.com
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
To enable importing projects from GitLab.com,
- if current_user.admin?
......
......@@ -8,10 +8,10 @@
.modal-dialog{ role: "document" }
.modal-content
.modal-header
%button.close{ type: "button", data: { dismiss: "modal" }, aria: { label: "close" } }
%span{ aria: { hidden: "true" } }= icon("times")
%h4.modal-title
Create new #{name} by email
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
%p
You can create a new #{name} inside this project by sending an email to the following email address:
......
......@@ -2,8 +2,9 @@
.modal-dialog.modal-lg
.modal-content
.modal-header
%a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title= _('Create New Directory')
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
= form_tag project_create_dir_path(@project, @id), method: :post, remote: false, class: 'js-create-dir-form js-quick-submit js-requires-input' do
.form-group.row
......
......@@ -2,8 +2,9 @@
.modal-dialog
.modal-content
.modal-header
%a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title Delete #{@blob.name}
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
= form_tag project_blob_path(@project, @id), method: :delete, class: 'js-delete-blob-form js-quick-submit js-requires-input' do
......
......@@ -2,8 +2,9 @@
.modal-dialog.modal-lg
.modal-content
.modal-header
%a.close{ href: "#", "data-dismiss" => "modal" } &times;
%h3.page-title= title
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
= form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form', data: { method: method } do
.dropzone
......
......@@ -2,11 +2,12 @@
.modal-dialog
.modal-content
.modal-header
%button.close{ data: { dismiss: 'modal' } } ×
%h3.page-title
- title_branch_name = capture do
%span.js-branch-name.ref-name>[branch name]
= s_("Branches|Delete protected branch '%{branch_name}'?").html_safe % { branch_name: title_branch_name }
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
%p
......
......@@ -15,8 +15,9 @@
.modal-dialog
.modal-content
.modal-header
%a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title= title
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
- if description
%p.append-bottom-20= description
......
......@@ -2,11 +2,11 @@
.modal-dialog
.modal-content
.modal-header
%h4.modal-title.float-left
%h4.modal-title
= s_('DeployTokens|Revoke')
%b #{token.name}?
%button.close{ 'aria-label' => _('Close'), 'data-dismiss' => 'modal', type: 'button' }
%span{ 'aria-hidden' => 'true' } &times;
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
%p
= s_('DeployTokens|You are about to revoke')
......
......@@ -2,8 +2,9 @@
.modal-dialog
.modal-content
.modal-header
%h3 Check out, review, and merge locally
%a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.modal-title Check out, review, and merge locally
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
%p
%strong Step 1.
......
......@@ -2,8 +2,9 @@
.modal-dialog
.modal-content
.modal-header
%a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title= s_("WikiNewPageTitle|New Wiki Page")
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
%form.new-wiki-page
.form-group
......
......@@ -4,7 +4,8 @@
.modal-header
%h3.page-title
Confirmation required
%a.close{ href: "#", "data-dismiss" => "modal" } ×
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
%p.text-danger.js-confirm-text
......
......@@ -2,8 +2,9 @@
.modal-dialog
.modal-content
.modal-header
%button.close{ data: {dismiss: 'modal' } } &times;
%h3.page-title Delete #{render_colored_label(label, tooltip: false)} ?
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
%p
......
......@@ -19,7 +19,7 @@
.form-text.text-muted
Choose any color.
%br
Or you can choose one of suggested colors below
Or you can choose one of the suggested colors below
.suggest-colors
- suggested_colors.each do |color|
......
......@@ -2,10 +2,10 @@
.modal-dialog
.modal-content
.modal-header
%button.close{ type: "button", "aria-label": "close", data: { dismiss: "modal" } }
%span{ "aria-hidden": "true" } ×
%h4#custom-notifications-title.modal-title
#{ _('Custom notification events') }
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
.container-fluid
......
---
title: Updates updated_at on label changes
merge_request: 19065
author: Jacopo Beschi @jacopo-beschi
type: fixed
---
title: Fixes the styling on the modal headers
merge_request: 19312
author: samdbeckham
type: fixed
---
title: Fixed HTTP_PROXY environment not honored when reading remote traces.
merge_request: 19282
author: NLR
type: fixed
---
title: Fixes a spelling error on the new label page
merge_request: 19316
author: samdbeckham
type: fixed
---
title: Updates ReactiveCaching clear_reactive_caching method to clear both data and
alive caching
merge_request: 19311
author:
type: fixed
---
title: Bump omniauth-gitlab to 1.0.3
merge_request:
author:
type: changed
# Dependencies
> TODO: Add Dependencies
\ No newline at end of file
## Adding Dependencies.
GitLab uses `yarn` to manage dependencies. These dependencies are defined in
two groups within `package.json`, `dependencies` and `devDependencies`. For
our purposes, we consider anything that is required to compile our production
assets a "production" dependency. That is, anything required to run the
`webpack` script with `NODE_ENV=production`. Tools like `eslint`, `karma`, and
various plugins and tools used in development are considered `devDependencies`.
This distinction is used by omnibus to determine which dependencies it requires
when building GitLab.
Exceptions are made for some tools that we require in the
`gitlab:assets:compile` CI job such as `webpack-bundle-analyzer` to analyze our
production assets post-compile.
---
> TODO: Add Dependencies
......@@ -148,7 +148,7 @@ module Gitlab
def get_chunk
unless in_range?
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
response = Net::HTTP.start(uri.hostname, uri.port, proxy_from_env: true, use_ssl: uri.scheme == 'https') do |http|
http.request(request)
end
......
......@@ -3,6 +3,7 @@ FactoryBot.define do
title { generate(:title) }
project
author { project.creator }
updated_by { author }
trait :confidential do
confidential true
......
......@@ -28,7 +28,7 @@
"due_date": { "type": "date" },
"confidential": { "type": "boolean" },
"discussion_locked": { "type": ["boolean", "null"] },
"updated_by_id": { "type": ["string", "null"] },
"updated_by_id": { "type": ["integer", "null"] },
"time_estimate": { "type": "integer" },
"total_time_spent": { "type": "integer" },
"human_time_estimate": { "type": ["integer", "null"] },
......
import { decorateData } from '~/ide/stores/utils';
import state from '~/ide/stores/state';
import commitState from '~/ide/stores/modules/commit/state';
import mergeRequestsState from '~/ide/stores/modules/merge_requests/state';
import pipelinesState from '~/ide/stores/modules/pipelines/state';
export const resetStore = store => {
const newState = {
...state(),
commit: commitState(),
mergeRequests: mergeRequestsState(),
pipelines: pipelinesState(),
};
store.replaceState(newState);
......
......@@ -147,3 +147,13 @@ export const fullPipelinesResponse = {
],
},
};
export const mergeRequests = [
{
id: 1,
iid: 1,
title: 'Test merge request',
project_id: 1,
web_url: `${gl.TEST_HOST}/namespace/project-path/merge_requests/1`,
},
];
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import state from '~/ide/stores/modules/merge_requests/state';
import * as types from '~/ide/stores/modules/merge_requests/mutation_types';
import actions, {
requestMergeRequests,
receiveMergeRequestsError,
receiveMergeRequestsSuccess,
fetchMergeRequests,
resetMergeRequests,
} from '~/ide/stores/modules/merge_requests/actions';
import { mergeRequests } from '../../../mock_data';
import testAction from '../../../../helpers/vuex_action_helper';
describe('IDE merge requests actions', () => {
let mockedState;
let mock;
beforeEach(() => {
mockedState = state();
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('requestMergeRequests', () => {
it('should should commit request', done => {
testAction(
requestMergeRequests,
null,
mockedState,
[{ type: types.REQUEST_MERGE_REQUESTS }],
[],
done,
);
});
});
describe('receiveMergeRequestsError', () => {
let flashSpy;
beforeEach(() => {
flashSpy = spyOnDependency(actions, 'flash');
});
it('should should commit error', done => {
testAction(
receiveMergeRequestsError,
null,
mockedState,
[{ type: types.RECEIVE_MERGE_REQUESTS_ERROR }],
[],
done,
);
});
it('creates flash message', () => {
receiveMergeRequestsError({ commit() {} });
expect(flashSpy).toHaveBeenCalled();
});
});
describe('receiveMergeRequestsSuccess', () => {
it('should commit received data', done => {
testAction(
receiveMergeRequestsSuccess,
'data',
mockedState,
[{ type: types.RECEIVE_MERGE_REQUESTS_SUCCESS, payload: 'data' }],
[],
done,
);
});
});
describe('fetchMergeRequests', () => {
beforeEach(() => {
gon.api_version = 'v4';
});
describe('success', () => {
beforeEach(() => {
mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(200, mergeRequests);
});
it('calls API with params from state', () => {
const apiSpy = spyOn(axios, 'get').and.callThrough();
fetchMergeRequests({ dispatch() {}, state: mockedState });
expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), {
params: {
scope: 'assigned-to-me',
state: 'opened',
search: '',
},
});
});
it('calls API with search', () => {
const apiSpy = spyOn(axios, 'get').and.callThrough();
fetchMergeRequests({ dispatch() {}, state: mockedState }, 'testing search');
expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), {
params: {
scope: 'assigned-to-me',
state: 'opened',
search: 'testing search',
},
});
});
it('dispatches request', done => {
testAction(
fetchMergeRequests,
null,
mockedState,
[],
[
{ type: 'requestMergeRequests' },
{ type: 'resetMergeRequests' },
{ type: 'receiveMergeRequestsSuccess' },
],
done,
);
});
it('dispatches success with received data', done => {
testAction(
fetchMergeRequests,
null,
mockedState,
[],
[
{ type: 'requestMergeRequests' },
{ type: 'resetMergeRequests' },
{ type: 'receiveMergeRequestsSuccess', payload: mergeRequests },
],
done,
);
});
});
describe('error', () => {
beforeEach(() => {
mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(500);
});
it('dispatches error', done => {
testAction(
fetchMergeRequests,
null,
mockedState,
[],
[
{ type: 'requestMergeRequests' },
{ type: 'resetMergeRequests' },
{ type: 'receiveMergeRequestsError' },
],
done,
);
});
});
});
describe('resetMergeRequests', () => {
it('commits reset', done => {
testAction(
resetMergeRequests,
null,
mockedState,
[{ type: types.RESET_MERGE_REQUESTS }],
[],
done,
);
});
});
});
import state from '~/ide/stores/modules/merge_requests/state';
import mutations from '~/ide/stores/modules/merge_requests/mutations';
import * as types from '~/ide/stores/modules/merge_requests/mutation_types';
import { mergeRequests } from '../../../mock_data';
describe('IDE merge requests mutations', () => {
let mockedState;
beforeEach(() => {
mockedState = state();
});
describe(types.REQUEST_MERGE_REQUESTS, () => {
it('sets loading to true', () => {
mutations[types.REQUEST_MERGE_REQUESTS](mockedState);
expect(mockedState.isLoading).toBe(true);
});
});
describe(types.RECEIVE_MERGE_REQUESTS_ERROR, () => {
it('sets loading to false', () => {
mutations[types.RECEIVE_MERGE_REQUESTS_ERROR](mockedState);
expect(mockedState.isLoading).toBe(false);
});
});
describe(types.RECEIVE_MERGE_REQUESTS_SUCCESS, () => {
it('sets merge requests', () => {
gon.gitlab_url = gl.TEST_HOST;
mutations[types.RECEIVE_MERGE_REQUESTS_SUCCESS](mockedState, mergeRequests);
expect(mockedState.mergeRequests).toEqual([
{
id: 1,
iid: 1,
title: 'Test merge request',
projectId: 1,
projectPathWithNamespace: 'namespace/project-path',
},
]);
});
});
describe(types.RESET_MERGE_REQUESTS, () => {
it('clears merge request array', () => {
mockedState.mergeRequests = ['test'];
mutations[types.RESET_MERGE_REQUESTS](mockedState);
expect(mockedState.mergeRequests).toEqual([]);
});
});
});
......@@ -92,7 +92,7 @@ describe Backup::Repository do
end
def list_repositories
Dir[SEED_STORAGE_PATH + '/*.git']
Dir[File.join(SEED_STORAGE_PATH, '*.git')]
end
end
......
......@@ -22,7 +22,7 @@ describe CacheableAttributes do
attr_accessor :attributes
def initialize(attrs = {})
def initialize(attrs = {}, *)
@attributes = attrs
end
end
......@@ -52,7 +52,7 @@ describe CacheableAttributes do
describe '.cache_key' do
it 'excludes cache attributes' do
expect(minimal_test_class.cache_key).to eq("TestClass:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:json")
expect(minimal_test_class.cache_key).to eq("TestClass:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:#{Rails.version}")
end
end
......@@ -75,49 +75,117 @@ describe CacheableAttributes do
context 'without any attributes given' do
it 'intializes a new object with the defaults' do
expect(minimal_test_class.build_from_defaults).not_to be_persisted
expect(minimal_test_class.build_from_defaults.attributes).to eq(minimal_test_class.defaults)
end
end
context 'without attributes given' do
context 'with attributes given' do
it 'intializes a new object with the given attributes merged into the defaults' do
expect(minimal_test_class.build_from_defaults(foo: 'd').attributes[:foo]).to eq('d')
end
end
describe 'edge cases on concrete implementations' do
describe '.build_from_defaults' do
context 'without any attributes given' do
it 'intializes all attributes even if they are nil' do
record = ApplicationSetting.build_from_defaults
expect(record).not_to be_persisted
expect(record.sign_in_text).to be_nil
end
end
end
end
end
describe '.current', :use_clean_rails_memory_store_caching do
context 'redis unavailable' do
it 'returns an uncached record' do
before do
allow(minimal_test_class).to receive(:last).and_return(:last)
expect(Rails.cache).to receive(:read).and_raise(Redis::BaseError)
expect(Rails.cache).to receive(:read).with(minimal_test_class.cache_key).and_raise(Redis::BaseError)
end
context 'in production environment' do
before do
expect(Rails.env).to receive(:production?).and_return(true)
end
it 'returns an uncached record and logs a warning' do
expect(Rails.logger).to receive(:warn).with("Cached record for TestClass couldn't be loaded, falling back to uncached record: Redis::BaseError")
expect(minimal_test_class.current).to eq(:last)
expect(minimal_test_class.current).to eq(:last)
end
end
context 'in other environments' do
before do
expect(Rails.env).to receive(:production?).and_return(false)
end
it 'returns an uncached record and logs a warning' do
expect(Rails.logger).not_to receive(:warn)
expect { minimal_test_class.current }.to raise_error(Redis::BaseError)
end
end
end
context 'when a record is not yet present' do
it 'does not cache nil object' do
# when missing settings a nil object is returned, but not cached
allow(minimal_test_class).to receive(:last).twice.and_return(nil)
allow(ApplicationSetting).to receive(:current_without_cache).twice.and_return(nil)
expect(minimal_test_class.current).to be_nil
expect(Rails.cache.exist?(minimal_test_class.cache_key)).to be(false)
expect(ApplicationSetting.current).to be_nil
expect(Rails.cache.exist?(ApplicationSetting.cache_key)).to be(false)
end
it 'cache non-nil object' do
# when the settings are set the method returns a valid object
allow(minimal_test_class).to receive(:last).and_call_original
it 'caches non-nil object' do
create(:application_setting)
expect(minimal_test_class.current).to eq(minimal_test_class.last)
expect(Rails.cache.exist?(minimal_test_class.cache_key)).to be(true)
expect(ApplicationSetting.current).to eq(ApplicationSetting.last)
expect(Rails.cache.exist?(ApplicationSetting.cache_key)).to be(true)
# subsequent calls retrieve the record from the cache
last_record = minimal_test_class.last
expect(minimal_test_class).not_to receive(:last)
expect(minimal_test_class.current.attributes).to eq(last_record.attributes)
last_record = ApplicationSetting.last
expect(ApplicationSetting).not_to receive(:current_without_cache)
expect(ApplicationSetting.current.attributes).to eq(last_record.attributes)
end
end
describe 'edge cases' do
describe 'caching behavior', :use_clean_rails_memory_store_caching do
it 'retrieves upload fields properly' do
ar_record = create(:appearance, :with_logo)
ar_record.cache!
cache_record = Appearance.current
expect(cache_record).to be_persisted
expect(cache_record.logo).to be_an(AttachmentUploader)
expect(cache_record.logo.url).to end_with('/dk.png')
end
it 'retrieves markdown fields properly' do
ar_record = create(:appearance, description: '**Hello**')
ar_record.cache!
cache_record = Appearance.current
expect(cache_record.description).to eq('**Hello**')
expect(cache_record.description_html).to eq('<p dir="auto"><strong>Hello</strong></p>')
end
end
end
it 'uses RequestStore in addition to Rails.cache', :request_store do
# Warm up the cache
create(:application_setting).cache!
expect(Rails.cache).to receive(:read).with(ApplicationSetting.cache_key).once.and_call_original
2.times { ApplicationSetting.current }
end
end
describe '.cached', :use_clean_rails_memory_store_caching do
......@@ -127,27 +195,36 @@ describe CacheableAttributes do
end
end
context 'when cached settings do not include the latest defaults' do
context 'when cached is warm' do
before do
Rails.cache.write(minimal_test_class.cache_key, { bar: 'b', baz: 'c' }.to_json)
minimal_test_class.define_singleton_method(:defaults) do
{ foo: 'a', bar: 'b', baz: 'c' }
end
# Warm up the cache
create(:appearance).cache!
end
it 'includes attributes from defaults' do
expect(minimal_test_class.cached.attributes[:foo]).to eq(minimal_test_class.defaults[:foo])
it 'retrieves the record from cache' do
expect(ActiveRecord::QueryRecorder.new { Appearance.cached }.count).to eq(0)
expect(Appearance.cached).to eq(Appearance.current_without_cache)
end
end
end
describe '#cache!', :use_clean_rails_memory_store_caching do
let(:appearance_record) { create(:appearance) }
let(:record) { create(:appearance) }
it 'caches the attributes' do
appearance_record.cache!
record.cache!
expect(Rails.cache.read(Appearance.cache_key)).to eq(appearance_record.attributes.to_json)
expect(Rails.cache.read(Appearance.cache_key)).to eq(record)
end
describe 'edge cases' do
let(:record) { create(:appearance) }
it 'caches the attributes' do
record.cache!
expect(Rails.cache.read(Appearance.cache_key)).to eq(record)
end
end
end
end
......@@ -45,7 +45,11 @@ describe HasVariable do
end
it 'fails to decrypt if iv is incorrect' do
<<<<<<< HEAD
# attr_encrypted expects the IV to be 16-bytes and base64-encoded
=======
# attr_encrypted expects the IV to be 16 bytes and base64-encoded
>>>>>>> upstream/master
subject.encrypted_value_iv = [SecureRandom.hex(8)].pack('m')
subject.instance_variable_set(:@value, nil)
......
......@@ -12,6 +12,7 @@ describe Issuable do
it { is_expected.to belong_to(:author) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) }
it { is_expected.to have_many(:labels) }
context 'Notes' do
let!(:note) { create(:note, noteable: issue, project: issue.project) }
......@@ -274,8 +275,8 @@ describe Issuable do
it 'skips coercion for not Integer values' do
expect { issue.time_estimate = nil }.to change { issue.time_estimate }.to(nil)
expect { issue.time_estimate = 'invalid time' }.not_to raise_error(StandardError)
expect { issue.time_estimate = 22.33 }.not_to raise_error(StandardError)
expect { issue.time_estimate = 'invalid time' }.not_to raise_error
expect { issue.time_estimate = 22.33 }.not_to raise_error
end
end
......
......@@ -94,6 +94,7 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
end
it { expect(instance.result).to be_nil }
it { expect(reactive_cache_alive?(instance)).to be_falsy }
end
describe '#exclusively_update_reactive_cache!' do
......
......@@ -5,6 +5,9 @@ RSpec.describe Timelog do
let(:issue) { create(:issue) }
let(:merge_request) { create(:merge_request) }
it { is_expected.to belong_to(:issue).touch(true) }
it { is_expected.to belong_to(:merge_request).touch(true) }
it { is_expected.to be_valid }
it { is_expected.to validate_presence_of(:time_spent) }
......
......@@ -1355,19 +1355,25 @@ describe API::Issues do
expect(json_response['labels']).to eq([label.title])
end
it 'removes all labels' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), labels: ''
it 'removes all labels and touches the record' do
Timecop.travel(1.minute.from_now) do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), labels: ''
end
expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to eq([])
expect(json_response['updated_at']).to be > Time.now
end
it 'updates labels' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
it 'updates labels and touches the record' do
Timecop.travel(1.minute.from_now) do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
labels: 'foo,bar'
end
expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to include 'foo'
expect(json_response['labels']).to include 'bar'
expect(json_response['updated_at']).to be > Time.now
end
it 'allows special label names' do
......
......@@ -270,11 +270,14 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, commands: "ls\ndate")
end
<<<<<<< HEAD
before do
stub_artifacts_object_storage
job
end
=======
>>>>>>> upstream/master
describe 'POST /api/v4/jobs/request' do
let!(:last_update) {}
let!(:new_update) { }
......
......@@ -337,12 +337,18 @@ describe Issues::UpdateService, :mailer do
context 'when the labels change' do
before do
update_issue(label_ids: [label.id])
Timecop.freeze(1.minute.from_now) do
update_issue(label_ids: [label.id])
end
end
it 'marks todos as done' do
expect(todo.reload.done?).to eq true
end
it 'updates updated_at' do
expect(issue.reload.updated_at).to be > Time.now
end
end
end
......
......@@ -355,12 +355,18 @@ describe MergeRequests::UpdateService, :mailer do
context 'when the labels change' do
before do
update_merge_request({ label_ids: [label.id] })
Timecop.freeze(1.minute.from_now) do
update_merge_request({ label_ids: [label.id] })
end
end
it 'marks pending todos as done' do
expect(pending_todo.reload).to be_done
end
it 'updates updated_at' do
expect(merge_request.reload.updated_at).to be > Time.now
end
end
context 'when the assignee changes' do
......
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