Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTimothy Andrew <mail@timothyandrew.net>2017-05-26 09:12:36 +0300
committerTimothy Andrew <mail@timothyandrew.net>2017-05-26 09:12:36 +0300
commit985edcd066e3e03919ce866148c0ceea55191393 (patch)
treed259b47bc5eab37276756bcd6069349bff3963e5
parent2ef4828fdc13ee69ec38ef87535c65bf32d0620d (diff)
parent729c75f700b75ea7b67e61ab01694f9d12623af1 (diff)
Merge remote-tracking branch 'origin/9-1-stable' into security-9-1
-rw-r--r--CHANGELOG.md38
-rw-r--r--VERSION2
-rw-r--r--app/assets/javascripts/autosave.js44
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js17
-rw-r--r--app/assets/javascripts/dispatcher.js2
-rw-r--r--app/assets/javascripts/droplab/constants.js3
-rw-r--r--app/assets/javascripts/droplab/drop_down.js2
-rw-r--r--app/assets/javascripts/droplab/utils.js16
-rw-r--r--app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js12
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js11
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js3
-rw-r--r--app/assets/javascripts/filtered_search/recent_searches_root.js7
-rw-r--r--app/assets/javascripts/filtered_search/services/recent_searches_service.js14
-rw-r--r--app/assets/javascripts/filtered_search/services/recent_searches_service_error.js11
-rw-r--r--app/assets/javascripts/gl_dropdown.js7
-rw-r--r--app/assets/javascripts/issuable/auto_width_dropdown_select.js38
-rw-r--r--app/assets/javascripts/lib/utils/accessor.js47
-rw-r--r--app/assets/javascripts/signin_tabs_memoizer.js12
-rw-r--r--app/assets/stylesheets/pages/issues.scss17
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss4
-rw-r--r--app/controllers/dashboard/snippets_controller.rb7
-rw-r--r--app/controllers/explore/groups_controller.rb2
-rw-r--r--app/controllers/explore/snippets_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb2
-rw-r--r--app/controllers/projects/snippets_controller.rb5
-rw-r--r--app/controllers/snippets_controller.rb26
-rw-r--r--app/controllers/users_controller.rb7
-rw-r--r--app/finders/groups_finder.rb20
-rw-r--r--app/finders/notes_finder.rb2
-rw-r--r--app/finders/snippets_finder.rb102
-rw-r--r--app/helpers/submodule_helper.rb46
-rw-r--r--app/models/concerns/mentionable.rb13
-rw-r--r--app/models/project.rb15
-rw-r--r--app/models/snippet.rb13
-rw-r--r--app/policies/project_snippet_policy.rb2
-rw-r--r--app/services/search/snippet_service.rb2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml2
-rw-r--r--app/views/shared/issuable/form/_branch_chooser.html.haml10
-rw-r--r--changelogs/unreleased/2246-uuid-is-nil-for-new-installation.yml4
-rw-r--r--changelogs/unreleased/30645-show-pipeline-events-description.yml4
-rw-r--r--changelogs/unreleased/30973-fix-network-graph-ordering.yml4
-rw-r--r--changelogs/unreleased/31292-milestone-sidebar-display-incorect-number-of-mr-when-minimized.yml4
-rw-r--r--changelogs/unreleased/add_index_on_ci_runners_contacted_at.yml4
-rw-r--r--changelogs/unreleased/dm-fix-ghost-user-validation.yml4
-rw-r--r--changelogs/unreleased/pages-0-4-1.yml4
-rw-r--r--changelogs/unreleased/zj-accept-default-branch-param.yml4
-rw-r--r--lib/api/groups.rb2
-rw-r--r--lib/api/helpers.rb2
-rw-r--r--lib/api/project_snippets.rb3
-rw-r--r--lib/api/snippets.rb4
-rw-r--r--lib/api/v3/groups.rb2
-rw-r--r--lib/api/v3/project_snippets.rb3
-rw-r--r--lib/api/v3/snippets.rb4
-rw-r--r--lib/gitlab/ci/cron_parser.rb19
-rw-r--r--lib/gitlab/email/receiver.rb18
-rw-r--r--lib/gitlab/git/repository.rb29
-rw-r--r--lib/gitlab/import_export/import_export.yml33
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb4
-rw-r--r--lib/gitlab/import_export/reader.rb5
-rw-r--r--lib/gitlab/project_search_results.rb4
-rw-r--r--qa/qa/page/main/menu.rb2
-rw-r--r--spec/controllers/groups_controller_spec.rb35
-rw-r--r--spec/controllers/snippets_controller_spec.rb355
-rw-r--r--spec/features/dashboard/snippets_spec.rb47
-rw-r--r--spec/features/issues/notes_on_issues_spec.rb77
-rw-r--r--spec/features/projects/snippets_spec.rb24
-rw-r--r--spec/features/snippets/explore_spec.rb25
-rw-r--r--spec/features/snippets/internal_snippet_spec.rb23
-rw-r--r--spec/features/triggers_spec.rb18
-rw-r--r--spec/features/users/snippets_spec.rb46
-rw-r--r--spec/finders/groups_finder_spec.rb57
-rw-r--r--spec/finders/snippets_finder_spec.rb117
-rw-r--r--spec/fixtures/emails/forwarded_new_issue.eml25
-rw-r--r--spec/helpers/submodule_helper_spec.rb12
-rw-r--r--spec/javascripts/autosave_spec.js134
-rw-r--r--spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js47
-rw-r--r--spec/javascripts/droplab/constants_spec.js6
-rw-r--r--spec/javascripts/droplab/drop_down_spec.js4
-rw-r--r--spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js20
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js34
-rw-r--r--spec/javascripts/filtered_search/recent_searches_root_spec.js31
-rw-r--r--spec/javascripts/filtered_search/services/recent_searches_service_error_spec.js18
-rw-r--r--spec/javascripts/filtered_search/services/recent_searches_service_spec.js95
-rw-r--r--spec/javascripts/gl_dropdown_spec.js46
-rw-r--r--spec/javascripts/lib/utils/accessor_spec.js78
-rw-r--r--spec/javascripts/signin_tabs_memoizer_spec.js90
-rw-r--r--spec/lib/gitlab/ci/cron_parser_spec.rb88
-rw-r--r--spec/lib/gitlab/email/receiver_spec.rb28
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/fork_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project.json1
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb13
-rw-r--r--spec/lib/gitlab/import_export/reader_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml22
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb75
-rw-r--r--spec/models/ci/trigger_schedule_spec.rb32
-rw-r--r--spec/models/network/graph_spec.rb21
-rw-r--r--spec/models/project_spec.rb19
-rw-r--r--spec/models/snippet_spec.rb40
-rw-r--r--spec/policies/project_snippet_policy_spec.rb80
-rw-r--r--spec/support/import_export/import_export.yml8
103 files changed, 2065 insertions, 589 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 64c60119d60..2679ff7a18c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,44 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 9.1.4 (2017-05-12)
+
+- No changes.
+- No changes.
+- No changes.
+- Fix error on CI/CD Settings page related to invalid pipeline trigger. !10948 (dosuken123)
+- Sort the network graph both by commit date and topographically. !11057
+- Fix cross referencing for private and internal projects. !11243
+- Handle incoming emails from aliases correctly.
+- Gracefully handle failures for incoming emails which do not match on the To header, and have no References header.
+- Add missing project attributes to Import/Export.
+- Fixed search terms not correctly highlighting.
+- Fixed bug where merge request JSON would be displayed.
+
+## 9.1.3 (2017-05-05)
+
+- No changes.
+- Do not show private groups on subgroups page if user doesn't have access to.
+- Enforce project features when searching blobs and wikis.
+- Fixed branches dropdown rendering branch names as HTML.
+- Make Asciidoc & other markup go through pipeline to prevent XSS.
+- Validate URLs in markdown using URI to detect the host correctly.
+- Fix for XSS in project import view caused by Hamlit filter usage.
+- Sanitize submodule URLs before linking to them in the file tree view.
+- Refactor snippets finder & dont return internal snippets for external users.
+- Fix snippets visibility for show action - external users can not see internal snippets.
+
+## 9.1.2 (2017-05-01)
+
+- Add index on ci_runners.contacted_at. !10876 (blackst0ne)
+- Fix pipeline events description for Slack and Mattermost integration. !10908
+- Fixed milestone sidebar showing incorrect number of MRs when collapsed. !10933
+- Fix ordering of commits in the network graph. !10936
+- Ensure the chat notifications service properly saves the "Notify only default branch" setting. !10959
+- Lazily sets UUID in ApplicationSetting for new installations.
+- Skip validation when creating internal (ghost, service desk) users.
+- Use GitLab Pages v0.4.1.
+
## 9.1.1 (2017-04-26)
- Add a transaction around move_issues_to_ghost_user. !10465
diff --git a/VERSION b/VERSION
index 44931da2660..03ea4a83bb9 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-9.1.1
+9.1.4
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index 8630b18a73f..cfab6c40b34 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -1,8 +1,11 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, max-len */
+import AccessorUtilities from './lib/utils/accessor';
window.Autosave = (function() {
function Autosave(field, key) {
this.field = field;
+ this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
+
if (key.join != null) {
key = key.join("/");
}
@@ -17,16 +20,12 @@ window.Autosave = (function() {
}
Autosave.prototype.restore = function() {
- var e, text;
- if (window.localStorage == null) {
- return;
- }
- try {
- text = window.localStorage.getItem(this.key);
- } catch (error) {
- e = error;
- return;
- }
+ var text;
+
+ if (!this.isLocalStorageAvailable) return;
+
+ text = window.localStorage.getItem(this.key);
+
if ((text != null ? text.length : void 0) > 0) {
this.field.val(text);
}
@@ -35,27 +34,22 @@ window.Autosave = (function() {
Autosave.prototype.save = function() {
var text;
- if (window.localStorage == null) {
- return;
- }
text = this.field.val();
- if ((text != null ? text.length : void 0) > 0) {
- try {
- return window.localStorage.setItem(this.key, text);
- } catch (error) {}
- } else {
- return this.reset();
+
+ if (this.isLocalStorageAvailable && (text != null ? text.length : void 0) > 0) {
+ return window.localStorage.setItem(this.key, text);
}
+
+ return this.reset();
};
Autosave.prototype.reset = function() {
- if (window.localStorage == null) {
- return;
- }
- try {
- return window.localStorage.removeItem(this.key);
- } catch (error) {}
+ if (!this.isLocalStorageAvailable) return;
+
+ return window.localStorage.removeItem(this.key);
};
return Autosave;
})();
+
+export default window.Autosave;
diff --git a/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js b/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js
index aa522e20c36..257df55e54f 100644
--- a/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js
+++ b/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js
@@ -1,3 +1,5 @@
+import AccessorUtilities from '../../lib/utils/accessor';
+
const unicodeSupportTestMap = {
// man, student (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
// occupationZwj: '\u{1F468}\u{200D}\u{1F393}',
@@ -140,16 +142,25 @@ function generateUnicodeSupportMap(testMap) {
function getUnicodeSupportMap() {
let unicodeSupportMap;
- const userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
+ let userAgentFromCache;
+
+ const isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
+
+ if (isLocalStorageAvailable) userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
+
try {
unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map'));
} catch (err) {
// swallow
}
+
if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) {
unicodeSupportMap = generateUnicodeSupportMap(unicodeSupportTestMap);
- window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
- window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
+
+ if (isLocalStorageAvailable) {
+ window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
+ window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
+ }
}
return unicodeSupportMap;
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 2efa72b4cac..4bf02cce8ec 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -46,6 +46,7 @@ import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
+import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
const ShortcutsBlob = require('./shortcuts_blob');
@@ -180,6 +181,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
new LabelsSelect();
new MilestoneSelect();
new gl.IssuableTemplateSelectors();
+ new AutoWidthDropdownSelect($('.js-target-branch-select')).init();
break;
case 'projects:tags:new':
new ZenMode();
diff --git a/app/assets/javascripts/droplab/constants.js b/app/assets/javascripts/droplab/constants.js
index 8883ed9aa14..868d47e91b3 100644
--- a/app/assets/javascripts/droplab/constants.js
+++ b/app/assets/javascripts/droplab/constants.js
@@ -3,11 +3,14 @@ const DATA_DROPDOWN = 'data-dropdown';
const SELECTED_CLASS = 'droplab-item-selected';
const ACTIVE_CLASS = 'droplab-item-active';
const IGNORE_CLASS = 'droplab-item-ignore';
+// Matches `{{anything}}` and `{{ everything }}`.
+const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g;
export {
DATA_TRIGGER,
DATA_DROPDOWN,
SELECTED_CLASS,
ACTIVE_CLASS,
+ TEMPLATE_REGEX,
IGNORE_CLASS,
};
diff --git a/app/assets/javascripts/droplab/drop_down.js b/app/assets/javascripts/droplab/drop_down.js
index 1fb4d63923c..de3927d683c 100644
--- a/app/assets/javascripts/droplab/drop_down.js
+++ b/app/assets/javascripts/droplab/drop_down.js
@@ -94,7 +94,7 @@ Object.assign(DropDown.prototype, {
},
renderChildren: function(data) {
- var html = utils.t(this.templateString, data);
+ var html = utils.template(this.templateString, data);
var template = document.createElement('div');
template.innerHTML = html;
diff --git a/app/assets/javascripts/droplab/utils.js b/app/assets/javascripts/droplab/utils.js
index c149a33a1e9..4da7344604e 100644
--- a/app/assets/javascripts/droplab/utils.js
+++ b/app/assets/javascripts/droplab/utils.js
@@ -1,19 +1,19 @@
/* eslint-disable */
-import { DATA_TRIGGER, DATA_DROPDOWN } from './constants';
+import { template as _template } from 'underscore';
+import { DATA_TRIGGER, DATA_DROPDOWN, TEMPLATE_REGEX } from './constants';
const utils = {
toCamelCase(attr) {
return this.camelize(attr.split('-').slice(1).join(' '));
},
- t(s, d) {
- for (const p in d) {
- if (Object.prototype.hasOwnProperty.call(d, p)) {
- s = s.replace(new RegExp(`{{${p}}}`, 'g'), d[p]);
- }
- }
- return s;
+ template(templateString, data) {
+ const template = _template(templateString, {
+ escape: TEMPLATE_REGEX,
+ });
+
+ return template(data);
},
camelize(str) {
diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js
index 9126422b335..15052dbd362 100644
--- a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js
+++ b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js
@@ -8,6 +8,11 @@ export default {
type: Array,
required: true,
},
+ isLocalStorageAvailable: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
computed: {
@@ -47,7 +52,12 @@ export default {
template: `
<div>
- <ul v-if="hasItems">
+ <div
+ v-if="!isLocalStorageAvailable"
+ class="dropdown-info-note">
+ This feature requires local storage to be enabled
+ </div>
+ <ul v-else-if="hasItems">
<li
v-for="(item, index) in processedItems"
:key="index">
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js
index 381c40c03d8..5b7b059666a 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js
@@ -63,7 +63,7 @@ require('./filtered_search_dropdown');
Object.assign({
icon: `fa-${icon}`,
hint,
- tag: `&lt;${tag}&gt;`,
+ tag: `<${tag}>`,
}, type && { type }),
);
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index b93a8f1d322..b55020912b6 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -1,5 +1,3 @@
-/* global Flash */
-
import FilteredSearchContainer from './container';
import RecentSearchesRoot from './recent_searches_root';
import RecentSearchesStore from './stores/recent_searches_store';
@@ -16,7 +14,9 @@ import eventHub from './event_hub';
this.tokensContainer = this.container.querySelector('.tokens-container');
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
- this.recentSearchesStore = new RecentSearchesStore();
+ this.recentSearchesStore = new RecentSearchesStore({
+ isLocalStorageAvailable: RecentSearchesService.isAvailable(),
+ });
let recentSearchesKey = 'issue-recent-searches';
if (page === 'merge_requests') {
recentSearchesKey = 'merge-request-recent-searches';
@@ -25,9 +25,10 @@ import eventHub from './event_hub';
// Fetch recent searches from localStorage
this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch()
- .catch(() => {
+ .catch((error) => {
+ if (error.name === 'RecentSearchesServiceError') return undefined;
// eslint-disable-next-line no-new
- new Flash('An error occured while parsing recent searches');
+ new window.Flash('An error occured while parsing recent searches');
// Gracefully fail to empty array
return [];
})
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
index a5657fc8720..64c5b50ca0c 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -177,6 +177,9 @@ class FilteredSearchVisualTokens {
static moveInputToTheRight() {
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
+
+ if (!input) return;
+
const inputLi = input.parentElement;
const tokenContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
diff --git a/app/assets/javascripts/filtered_search/recent_searches_root.js b/app/assets/javascripts/filtered_search/recent_searches_root.js
index 4e38409e12a..b2e6f63aacf 100644
--- a/app/assets/javascripts/filtered_search/recent_searches_root.js
+++ b/app/assets/javascripts/filtered_search/recent_searches_root.js
@@ -29,12 +29,15 @@ class RecentSearchesRoot {
}
render() {
+ const state = this.store.state;
this.vm = new Vue({
el: this.wrapperElement,
- data: this.store.state,
+ data() { return state; },
template: `
<recent-searches-dropdown-content
- :items="recentSearches" />
+ :items="recentSearches"
+ :is-local-storage-available="isLocalStorageAvailable"
+ />
`,
components: {
'recent-searches-dropdown-content': RecentSearchesDropdownContent,
diff --git a/app/assets/javascripts/filtered_search/services/recent_searches_service.js b/app/assets/javascripts/filtered_search/services/recent_searches_service.js
index 3e402d5aed0..a056dea928d 100644
--- a/app/assets/javascripts/filtered_search/services/recent_searches_service.js
+++ b/app/assets/javascripts/filtered_search/services/recent_searches_service.js
@@ -1,9 +1,17 @@
+import RecentSearchesServiceError from './recent_searches_service_error';
+import AccessorUtilities from '../../lib/utils/accessor';
+
class RecentSearchesService {
constructor(localStorageKey = 'issuable-recent-searches') {
this.localStorageKey = localStorageKey;
}
fetch() {
+ if (!RecentSearchesService.isAvailable()) {
+ const error = new RecentSearchesServiceError();
+ return Promise.reject(error);
+ }
+
const input = window.localStorage.getItem(this.localStorageKey);
let searches = [];
@@ -19,8 +27,14 @@ class RecentSearchesService {
}
save(searches = []) {
+ if (!RecentSearchesService.isAvailable()) return;
+
window.localStorage.setItem(this.localStorageKey, JSON.stringify(searches));
}
+
+ static isAvailable() {
+ return AccessorUtilities.isLocalStorageAccessSafe();
+ }
}
export default RecentSearchesService;
diff --git a/app/assets/javascripts/filtered_search/services/recent_searches_service_error.js b/app/assets/javascripts/filtered_search/services/recent_searches_service_error.js
new file mode 100644
index 00000000000..5917b223d63
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/services/recent_searches_service_error.js
@@ -0,0 +1,11 @@
+class RecentSearchesServiceError {
+ constructor(message) {
+ this.name = 'RecentSearchesServiceError';
+ this.message = message || 'Recent Searches Service is unavailable';
+ }
+}
+
+// Can't use `extends` for builtin prototypes and get true inheritance yet
+RecentSearchesServiceError.prototype = Error.prototype;
+
+export default RecentSearchesServiceError;
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index a03f1202a6d..2306d7b38aa 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -584,7 +584,12 @@ GitLabDropdown = (function() {
var link = document.createElement('a');
link.href = url;
- link.innerHTML = text;
+
+ if (this.highlight) {
+ link.innerHTML = text;
+ } else {
+ link.textContent = text;
+ }
if (selected) {
link.className = 'is-active';
diff --git a/app/assets/javascripts/issuable/auto_width_dropdown_select.js b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
new file mode 100644
index 00000000000..2203a56315e
--- /dev/null
+++ b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
@@ -0,0 +1,38 @@
+let instanceCount = 0;
+
+class AutoWidthDropdownSelect {
+ constructor(selectElement) {
+ this.$selectElement = $(selectElement);
+ this.dropdownClass = `js-auto-width-select-dropdown-${instanceCount}`;
+ instanceCount += 1;
+ }
+
+ init() {
+ const dropdownClass = this.dropdownClass;
+ this.$selectElement.select2({
+ dropdownCssClass: dropdownClass,
+ dropdownCss() {
+ let resultantWidth = 'auto';
+ const $dropdown = $(`.${dropdownClass}`);
+
+ // We have to look at the parent because
+ // `offsetParent` on a `display: none;` is `null`
+ const offsetParentWidth = $(this).parent().offsetParent().width();
+ // Reset any width to let it naturally flow
+ $dropdown.css('width', 'auto');
+ if ($dropdown.outerWidth(false) > offsetParentWidth) {
+ resultantWidth = offsetParentWidth;
+ }
+
+ return {
+ width: resultantWidth,
+ maxWidth: offsetParentWidth,
+ };
+ },
+ });
+
+ return this;
+ }
+}
+
+export default AutoWidthDropdownSelect;
diff --git a/app/assets/javascripts/lib/utils/accessor.js b/app/assets/javascripts/lib/utils/accessor.js
new file mode 100644
index 00000000000..1d18992af63
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/accessor.js
@@ -0,0 +1,47 @@
+function isPropertyAccessSafe(base, property) {
+ let safe;
+
+ try {
+ safe = !!base[property];
+ } catch (error) {
+ safe = false;
+ }
+
+ return safe;
+}
+
+function isFunctionCallSafe(base, functionName, ...args) {
+ let safe = true;
+
+ try {
+ base[functionName](...args);
+ } catch (error) {
+ safe = false;
+ }
+
+ return safe;
+}
+
+function isLocalStorageAccessSafe() {
+ let safe;
+
+ const TEST_KEY = 'isLocalStorageAccessSafe';
+ const TEST_VALUE = 'true';
+
+ safe = isPropertyAccessSafe(window, 'localStorage');
+ if (!safe) return safe;
+
+ safe = isFunctionCallSafe(window.localStorage, 'setItem', TEST_KEY, TEST_VALUE);
+
+ if (safe) window.localStorage.removeItem(TEST_KEY);
+
+ return safe;
+}
+
+const AccessorUtilities = {
+ isPropertyAccessSafe,
+ isFunctionCallSafe,
+ isLocalStorageAccessSafe,
+};
+
+export default AccessorUtilities;
diff --git a/app/assets/javascripts/signin_tabs_memoizer.js b/app/assets/javascripts/signin_tabs_memoizer.js
index d811d1cd53a..2587facc582 100644
--- a/app/assets/javascripts/signin_tabs_memoizer.js
+++ b/app/assets/javascripts/signin_tabs_memoizer.js
@@ -1,5 +1,7 @@
/* eslint no-param-reassign: ["error", { "props": false }]*/
/* eslint no-new: "off" */
+import AccessorUtilities from './lib/utils/accessor';
+
((global) => {
/**
* Memorize the last selected tab after reloading a page.
@@ -9,6 +11,8 @@
constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) {
this.currentTabKey = currentTabKey;
this.tabSelector = tabSelector;
+ this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
+
this.bootstrap();
}
@@ -37,11 +41,15 @@
}
saveData(val) {
- localStorage.setItem(this.currentTabKey, val);
+ if (!this.isLocalStorageAvailable) return undefined;
+
+ return window.localStorage.setItem(this.currentTabKey, val);
}
readData() {
- return localStorage.getItem(this.currentTabKey);
+ if (!this.isLocalStorageAvailable) return null;
+
+ return window.localStorage.getItem(this.currentTabKey);
}
}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index b2f45625a2a..4eeb52c096b 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -42,6 +42,7 @@ ul.related-merge-requests > li {
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
+ align-items: center;
.merge-request-id {
flex-shrink: 0;
@@ -50,6 +51,14 @@ ul.related-merge-requests > li {
.merge-request-info {
margin-left: 5px;
}
+
+ .row_title {
+ vertical-align: bottom;
+ }
+
+ gl-emoji {
+ font-size: 1em;
+ }
}
.merge-requests-title,
@@ -101,7 +110,13 @@ ul.related-merge-requests > li {
}
}
-.merge-request-ci-status {
+.merge-request-ci-status,
+.related-merge-requests {
+ .ci-status-link {
+ display: block;
+ margin-right: 5px;
+ }
+
svg {
margin-right: 4px;
position: relative;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 2f946ab2f59..a4f14aa6475 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -482,6 +482,10 @@
}
}
+.target-branch-select-dropdown-container {
+ position: relative;
+}
+
.assign-to-me-link {
padding-left: 12px;
white-space: nowrap;
diff --git a/app/controllers/dashboard/snippets_controller.rb b/app/controllers/dashboard/snippets_controller.rb
index bcfdbe14be9..8dd91264451 100644
--- a/app/controllers/dashboard/snippets_controller.rb
+++ b/app/controllers/dashboard/snippets_controller.rb
@@ -1,11 +1,10 @@
class Dashboard::SnippetsController < Dashboard::ApplicationController
def index
- @snippets = SnippetsFinder.new.execute(
+ @snippets = SnippetsFinder.new(
current_user,
- filter: :by_user,
- user: current_user,
+ author: current_user,
scope: params[:scope]
- )
+ ).execute
@snippets = @snippets.page(params[:page])
end
end
diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb
index 68228c095da..81883c543ba 100644
--- a/app/controllers/explore/groups_controller.rb
+++ b/app/controllers/explore/groups_controller.rb
@@ -1,6 +1,6 @@
class Explore::GroupsController < Explore::ApplicationController
def index
- @groups = GroupsFinder.new.execute(current_user)
+ @groups = GroupsFinder.new(current_user).execute
@groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present?
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.page(params[:page])
diff --git a/app/controllers/explore/snippets_controller.rb b/app/controllers/explore/snippets_controller.rb
index 28760c3f84b..d3f0e033068 100644
--- a/app/controllers/explore/snippets_controller.rb
+++ b/app/controllers/explore/snippets_controller.rb
@@ -1,6 +1,6 @@
class Explore::SnippetsController < Explore::ApplicationController
def index
- @snippets = SnippetsFinder.new.execute(current_user, filter: :all)
+ @snippets = SnippetsFinder.new(current_user).execute
@snippets = @snippets.page(params[:page])
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 593001e6396..d695459fe87 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -64,7 +64,7 @@ class GroupsController < Groups::ApplicationController
end
def subgroups
- @nested_groups = group.children
+ @nested_groups = GroupsFinder.new(current_user, parent: group).execute
@nested_groups = @nested_groups.search(params[:filter_groups]) if params[:filter_groups].present?
end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 5c9e0d4d1a1..d046a3e96d5 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -22,12 +22,11 @@ class Projects::SnippetsController < Projects::ApplicationController
respond_to :html
def index
- @snippets = SnippetsFinder.new.execute(
+ @snippets = SnippetsFinder.new(
current_user,
- filter: :by_project,
project: @project,
scope: params[:scope]
- )
+ ).execute
@snippets = @snippets.page(params[:page])
if @snippets.out_of_range? && @snippets.total_pages != 0
redirect_to namespace_project_snippets_path(page: @snippets.total_pages)
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index f3fd3da8b20..ba02372ff4e 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -25,12 +25,8 @@ class SnippetsController < ApplicationController
return render_404 unless @user
- @snippets = SnippetsFinder.new.execute(current_user, {
- filter: :by_user,
- user: @user,
- scope: params[:scope]
- })
- .page(params[:page])
+ @snippets = SnippetsFinder.new(current_user, author: @user, scope: params[:scope])
+ .execute.page(params[:page])
render 'index'
else
@@ -80,20 +76,20 @@ class SnippetsController < ApplicationController
protected
def snippet
- @snippet ||= if current_user
- PersonalSnippet.where("author_id = ? OR visibility_level IN (?)",
- current_user.id,
- [Snippet::PUBLIC, Snippet::INTERNAL]).
- find(params[:id])
- else
- PersonalSnippet.find(params[:id])
- end
+ @snippet ||= PersonalSnippet.find_by(id: params[:id])
end
+
alias_method :awardable, :snippet
alias_method :spammable, :snippet
def authorize_read_snippet!
- authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet)
+ return if can?(current_user, :read_personal_snippet, @snippet)
+
+ if current_user
+ render_404
+ else
+ authenticate_user!
+ end
end
def authorize_update_snippet!
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index a452bbba422..eb131139787 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -131,12 +131,11 @@ class UsersController < ApplicationController
end
def load_snippets
- @snippets = SnippetsFinder.new.execute(
+ @snippets = SnippetsFinder.new(
current_user,
- filter: :by_user,
- user: user,
+ author: user,
scope: params[:scope]
- ).page(params[:page])
+ ).execute.page(params[:page])
end
def projects_for_current_user
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
index d932a17883f..f68610e197c 100644
--- a/app/finders/groups_finder.rb
+++ b/app/finders/groups_finder.rb
@@ -1,13 +1,19 @@
class GroupsFinder < UnionFinder
- def execute(current_user = nil)
- segments = all_groups(current_user)
+ def initialize(current_user = nil, params = {})
+ @current_user = current_user
+ @params = params
+ end
- find_union(segments, Group).with_route.order_id_desc
+ def execute
+ groups = find_union(all_groups, Group).with_route.order_id_desc
+ by_parent(groups)
end
private
- def all_groups(current_user)
+ attr_reader :current_user, :params
+
+ def all_groups
groups = []
groups << current_user.authorized_groups if current_user
@@ -15,4 +21,10 @@ class GroupsFinder < UnionFinder
groups
end
+
+ def by_parent(groups)
+ return groups unless params[:parent]
+
+ groups.where(parent: params[:parent])
+ end
end
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index 3c499184b41..c5fbf359c5b 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -67,7 +67,7 @@ class NotesFinder
when "merge_request"
MergeRequestsFinder.new(@current_user, project_id: @project.id).execute
when "snippet", "project_snippet"
- SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project)
+ SnippetsFinder.new(@current_user, project: @project).execute
else
raise 'invalid target_type'
end
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index da6e6e87a6f..c04f61de79c 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -1,66 +1,74 @@
-class SnippetsFinder
- def execute(current_user, params = {})
- filter = params[:filter]
- user = params.fetch(:user, current_user)
-
- case filter
- when :all then
- snippets(current_user).fresh
- when :public then
- Snippet.are_public.fresh
- when :by_user then
- by_user(current_user, user, params[:scope])
- when :by_project
- by_project(current_user, params[:project], params[:scope])
- end
+class SnippetsFinder < UnionFinder
+ attr_accessor :current_user, :params
+
+ def initialize(current_user, params = {})
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ items = init_collection
+ items = by_project(items)
+ items = by_author(items)
+ items = by_visibility(items)
+
+ items.fresh
end
private
- def snippets(current_user)
- if current_user
- Snippet.public_and_internal
- else
- # Not authenticated
- #
- # Return only:
- # public snippets
- Snippet.are_public
- end
+ def init_collection
+ items = Snippet.all
+
+ accessible(items)
end
- def by_user(current_user, user, scope)
- snippets = user.snippets.fresh
+ def accessible(items)
+ segments = []
+ segments << items.public_to_user(current_user)
+ segments << authorized_to_user(items) if current_user
- if current_user
- include_private = user == current_user
- by_scope(snippets, scope, include_private)
- else
- snippets.are_public
- end
+ find_union(segments, Snippet)
end
- def by_project(current_user, project, scope)
- snippets = project.snippets.fresh
+ def authorized_to_user(items)
+ items.where(
+ 'author_id = :author_id
+ OR project_id IN (:project_ids)',
+ author_id: current_user.id,
+ project_ids: current_user.authorized_projects.select(:id))
+ end
- if current_user
- include_private = project.team.member?(current_user) || current_user.admin?
- by_scope(snippets, scope, include_private)
- else
- snippets.are_public
- end
+ def by_visibility(items)
+ visibility = params[:visibility] || visibility_from_scope
+
+ return items unless visibility
+
+ items.where(visibility_level: visibility)
+ end
+
+ def by_author(items)
+ return items unless params[:author]
+
+ items.where(author_id: params[:author].id)
+ end
+
+ def by_project(items)
+ return items unless params[:project]
+
+ items.where(project_id: params[:project].id)
end
- def by_scope(snippets, scope = nil, include_private = false)
- case scope.to_s
+ def visibility_from_scope
+ case params[:scope].to_s
when 'are_private'
- include_private ? snippets.are_private : Snippet.none
+ Snippet::PRIVATE
when 'are_internal'
- snippets.are_internal
+ Snippet::INTERNAL
when 'are_public'
- snippets.are_public
+ Snippet::PUBLIC
else
- include_private ? snippets : snippets.public_and_internal
+ nil
end
end
end
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
index fb95f2b565e..b50b2578a8a 100644
--- a/app/helpers/submodule_helper.rb
+++ b/app/helpers/submodule_helper.rb
@@ -1,28 +1,30 @@
module SubmoduleHelper
include Gitlab::ShellAdapter
+ VALID_SUBMODULE_PROTOCOLS = %w[http https git ssh].freeze
+
# links to files listing for submodule if submodule is a project on this server
def submodule_links(submodule_item, ref = nil, repository = @repository)
url = repository.submodule_url_for(ref, submodule_item.path)
- return url, nil unless url =~ /([^\/:]+)\/([^\/]+\.git)\Z/
-
- namespace = $1
- project = $2
- project.chomp!('.git')
+ if url =~ /([^\/:]+)\/([^\/]+\.git)\Z/
+ namespace, project = $1, $2
+ project.sub!(/\.git\z/, '')
- if self_url?(url, namespace, project)
- return namespace_project_path(namespace, project),
- namespace_project_tree_path(namespace, project,
- submodule_item.id)
- elsif relative_self_url?(url)
- relative_self_links(url, submodule_item.id)
- elsif github_dot_com_url?(url)
- standard_links('github.com', namespace, project, submodule_item.id)
- elsif gitlab_dot_com_url?(url)
- standard_links('gitlab.com', namespace, project, submodule_item.id)
+ if self_url?(url, namespace, project)
+ [namespace_project_path(namespace, project),
+ namespace_project_tree_path(namespace, project, submodule_item.id)]
+ elsif relative_self_url?(url)
+ relative_self_links(url, submodule_item.id)
+ elsif github_dot_com_url?(url)
+ standard_links('github.com', namespace, project, submodule_item.id)
+ elsif gitlab_dot_com_url?(url)
+ standard_links('gitlab.com', namespace, project, submodule_item.id)
+ else
+ [sanitize_submodule_url(url), nil]
+ end
else
- return url, nil
+ [sanitize_submodule_url(url), nil]
end
end
@@ -71,4 +73,16 @@ module SubmoduleHelper
namespace_project_tree_path(namespace, base, commit)
]
end
+
+ def sanitize_submodule_url(url)
+ uri = URI.parse(url)
+
+ if uri.scheme.in?(VALID_SUBMODULE_PROTOCOLS)
+ uri.to_s
+ else
+ nil
+ end
+ rescue URI::InvalidURIError
+ nil
+ end
end
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 7e56e371b27..85505d235b7 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -44,14 +44,15 @@ module Mentionable
end
def all_references(current_user = nil, extractor: nil)
+ @extractors ||= {}
+
# Use custom extractor if it's passed in the function parameters.
if extractor
- @extractor = extractor
+ @extractors[current_user] = extractor
else
- @extractor ||= Gitlab::ReferenceExtractor.
- new(project, current_user)
+ extractor = @extractors[current_user] ||= Gitlab::ReferenceExtractor.new(project, current_user)
- @extractor.reset_memoized_values
+ extractor.reset_memoized_values
end
self.class.mentionable_attrs.each do |attr, options|
@@ -62,10 +63,10 @@ module Mentionable
skip_project_check: skip_project_check?
)
- @extractor.analyze(text, options)
+ extractor.analyze(text, options)
end
- @extractor
+ extractor
end
def mentioned_users(current_user = nil)
diff --git a/app/models/project.rb b/app/models/project.rb
index e25db4c22d4..ecea7063a7b 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1266,6 +1266,9 @@ class Project < ActiveRecord::Base
else
update_attribute(name, value)
end
+
+ rescue ActiveRecord::RecordNotSaved => e
+ handle_update_attribute_error(e, value)
end
def pushes_since_gc
@@ -1379,4 +1382,16 @@ class Project < ActiveRecord::Base
ContainerRepository.build_root_repository(self).has_tags?
end
+
+ def handle_update_attribute_error(ex, value)
+ if ex.message.start_with?('Failed to replace')
+ if value.respond_to?(:each)
+ invalid = value.detect(&:invalid?)
+
+ raise ex, ([ex.message] + invalid.errors.full_messages).join(' ') if invalid
+ end
+ end
+
+ raise ex
+ end
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 380835707e8..8b0b9eb006e 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -169,18 +169,5 @@ class Snippet < ActiveRecord::Base
where(table[:content].matches(pattern))
end
-
- def accessible_to(user)
- return are_public unless user.present?
- return all if user.admin?
-
- where(
- 'visibility_level IN (:visibility_levels)
- OR author_id = :author_id
- OR project_id IN (:project_ids)',
- visibility_levels: [Snippet::PUBLIC, Snippet::INTERNAL],
- author_id: user.id,
- project_ids: user.authorized_projects.select(:id))
- end
end
end
diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb
index 3a96836917e..cf8ff92617f 100644
--- a/app/policies/project_snippet_policy.rb
+++ b/app/policies/project_snippet_policy.rb
@@ -13,7 +13,7 @@ class ProjectSnippetPolicy < BasePolicy
can! :read_project_snippet
end
- if @subject.private? && @subject.project.team.member?(@user)
+ if @subject.project.team.member?(@user)
can! :read_project_snippet
end
end
diff --git a/app/services/search/snippet_service.rb b/app/services/search/snippet_service.rb
index 4f161beea4d..85da0be6fff 100644
--- a/app/services/search/snippet_service.rb
+++ b/app/services/search/snippet_service.rb
@@ -7,7 +7,7 @@ module Search
end
def execute
- snippets = Snippet.accessible_to(current_user)
+ snippets = SnippetsFinder.new(current_user).execute
Gitlab::SnippetSearchResults.new(snippets, params[:search])
end
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 881ee9fd596..9e306d4543c 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -6,7 +6,7 @@
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('diff_notes')
-.merge-request{ 'data-url' => merge_request_path(@merge_request), 'data-project-path' => project_path(@merge_request.project) }
+.merge-request{ 'data-url' => merge_request_path(@merge_request, format: :json), 'data-project-path' => project_path(@merge_request.project) }
= render "projects/merge_requests/show/mr_title"
.merge-request-details.issuable-details{ data: { id: @merge_request.project.id } }
diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml
index 2793e7bcff4..f57b4d899ce 100644
--- a/app/views/shared/issuable/form/_branch_chooser.html.haml
+++ b/app/views/shared/issuable/form/_branch_chooser.html.haml
@@ -10,12 +10,16 @@
= form.label :source_branch, class: 'control-label'
.col-sm-10
.issuable-form-select-holder
- = form.select(:source_branch, [issuable.source_branch], {}, { class: 'source_branch select2 span2', disabled: true })
+ = form.select(:source_branch, [issuable.source_branch], {}, { class: 'source_branch select2', disabled: true })
.form-group
= form.label :target_branch, class: 'control-label'
- .col-sm-10
+ .col-sm-10.target-branch-select-dropdown-container
.issuable-form-select-holder
- = form.select(:target_branch, issuable.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: issuable.new_record?, data: { placeholder: "Select branch" }})
+ = form.select(:target_branch, issuable.target_branches,
+ { include_blank: true },
+ { class: 'target_branch js-target-branch-select',
+ disabled: issuable.new_record?,
+ data: { placeholder: "Select branch" }})
- if issuable.new_record?
&nbsp;
= link_to 'Change branches', mr_change_branches_path(issuable)
diff --git a/changelogs/unreleased/2246-uuid-is-nil-for-new-installation.yml b/changelogs/unreleased/2246-uuid-is-nil-for-new-installation.yml
deleted file mode 100644
index 70d35f06af4..00000000000
--- a/changelogs/unreleased/2246-uuid-is-nil-for-new-installation.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Lazily sets UUID in ApplicationSetting for new installations
-merge_request:
-author:
diff --git a/changelogs/unreleased/30645-show-pipeline-events-description.yml b/changelogs/unreleased/30645-show-pipeline-events-description.yml
deleted file mode 100644
index fb75dde1d86..00000000000
--- a/changelogs/unreleased/30645-show-pipeline-events-description.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix pipeline events description for Slack and Mattermost integration
-merge_request: 10908
-author:
diff --git a/changelogs/unreleased/30973-fix-network-graph-ordering.yml b/changelogs/unreleased/30973-fix-network-graph-ordering.yml
deleted file mode 100644
index 420ec107842..00000000000
--- a/changelogs/unreleased/30973-fix-network-graph-ordering.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix ordering of commits in the network graph
-merge_request: 10936
-author:
diff --git a/changelogs/unreleased/31292-milestone-sidebar-display-incorect-number-of-mr-when-minimized.yml b/changelogs/unreleased/31292-milestone-sidebar-display-incorect-number-of-mr-when-minimized.yml
deleted file mode 100644
index dee831c668b..00000000000
--- a/changelogs/unreleased/31292-milestone-sidebar-display-incorect-number-of-mr-when-minimized.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed milestone sidebar showing incorrect number of MRs when collapsed
-merge_request: 10933
-author:
diff --git a/changelogs/unreleased/add_index_on_ci_runners_contacted_at.yml b/changelogs/unreleased/add_index_on_ci_runners_contacted_at.yml
deleted file mode 100644
index 10c3206c2ff..00000000000
--- a/changelogs/unreleased/add_index_on_ci_runners_contacted_at.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add index on ci_runners.contacted_at
-merge_request: 10876
-author: blackst0ne
diff --git a/changelogs/unreleased/dm-fix-ghost-user-validation.yml b/changelogs/unreleased/dm-fix-ghost-user-validation.yml
deleted file mode 100644
index 4214786cb5a..00000000000
--- a/changelogs/unreleased/dm-fix-ghost-user-validation.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Skip validation when creating internal (ghost, service desk) users
-merge_request:
-author:
diff --git a/changelogs/unreleased/pages-0-4-1.yml b/changelogs/unreleased/pages-0-4-1.yml
deleted file mode 100644
index fbc78a36cae..00000000000
--- a/changelogs/unreleased/pages-0-4-1.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Use GitLab Pages v0.4.1
-merge_request:
-author:
diff --git a/changelogs/unreleased/zj-accept-default-branch-param.yml b/changelogs/unreleased/zj-accept-default-branch-param.yml
deleted file mode 100644
index 8f6fa8a6386..00000000000
--- a/changelogs/unreleased/zj-accept-default-branch-param.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Ensure the chat notifications service properly saves the "Notify only default branch" setting
-merge_request: 10959
-author:
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 32bbf956d7f..d358e29c300 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -47,7 +47,7 @@ module API
elsif current_user.admin
Group.all
elsif params[:all_available]
- GroupsFinder.new.execute(current_user)
+ GroupsFinder.new(current_user).execute
else
current_user.groups
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index ddff3c8c1e8..7db64379141 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -92,7 +92,7 @@ module API
def find_project_snippet(id)
finder_params = { filter: :by_project, project: user_project }
- SnippetsFinder.new.execute(current_user, finder_params).find(id)
+ SnippetsFinder.new(current_user, finder_params).execute.find(id)
end
def find_merge_request_with_access(iid, access_level = :read_merge_request)
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index cfee38a9baf..98bc9c28527 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -17,8 +17,7 @@ module API
end
def snippets_for_current_user
- finder_params = { filter: :by_project, project: user_project }
- SnippetsFinder.new.execute(current_user, finder_params)
+ SnippetsFinder.new(current_user, project: user_project).execute
end
end
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index b93fdc62808..53f5953a8fb 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -8,11 +8,11 @@ module API
resource :snippets do
helpers do
def snippets_for_current_user
- SnippetsFinder.new.execute(current_user, filter: :by_user, user: current_user)
+ SnippetsFinder.new(current_user, author: current_user).execute
end
def public_snippets
- SnippetsFinder.new.execute(current_user, filter: :public)
+ SnippetsFinder.new(current_user, visibility: Snippet::PUBLIC).execute
end
end
diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb
index 63d464b926b..dbf7a3cf785 100644
--- a/lib/api/v3/groups.rb
+++ b/lib/api/v3/groups.rb
@@ -45,7 +45,7 @@ module API
groups = if current_user.admin
Group.all
elsif params[:all_available]
- GroupsFinder.new.execute(current_user)
+ GroupsFinder.new(current_user).execute
else
current_user.groups
end
diff --git a/lib/api/v3/project_snippets.rb b/lib/api/v3/project_snippets.rb
index fc065a22d74..c41fee32610 100644
--- a/lib/api/v3/project_snippets.rb
+++ b/lib/api/v3/project_snippets.rb
@@ -18,8 +18,7 @@ module API
end
def snippets_for_current_user
- finder_params = { filter: :by_project, project: user_project }
- SnippetsFinder.new.execute(current_user, finder_params)
+ SnippetsFinder.new(current_user, project: user_project).execute
end
end
diff --git a/lib/api/v3/snippets.rb b/lib/api/v3/snippets.rb
index 07dac7e9904..0762fc02d70 100644
--- a/lib/api/v3/snippets.rb
+++ b/lib/api/v3/snippets.rb
@@ -8,11 +8,11 @@ module API
resource :snippets do
helpers do
def snippets_for_current_user
- SnippetsFinder.new.execute(current_user, filter: :by_user, user: current_user)
+ SnippetsFinder.new(current_user, author: current_user).execute
end
def public_snippets
- SnippetsFinder.new.execute(current_user, filter: :public)
+ SnippetsFinder.new(current_user, visibility: Snippet::PUBLIC).execute
end
end
diff --git a/lib/gitlab/ci/cron_parser.rb b/lib/gitlab/ci/cron_parser.rb
index a3cc350ef22..dad8c3cdf5b 100644
--- a/lib/gitlab/ci/cron_parser.rb
+++ b/lib/gitlab/ci/cron_parser.rb
@@ -6,7 +6,7 @@ module Gitlab
def initialize(cron, cron_timezone = 'UTC')
@cron = cron
- @cron_timezone = cron_timezone
+ @cron_timezone = ActiveSupport::TimeZone.find_tzinfo(cron_timezone).name
end
def next_time_from(time)
@@ -24,8 +24,23 @@ module Gitlab
private
+ # NOTE:
+ # cron_timezone can only accept timezones listed in TZInfo::Timezone.
+ # Aliases of Timezones from ActiveSupport::TimeZone are NOT accepted,
+ # because Rufus::Scheduler only supports TZInfo::Timezone.
+ #
+ # For example, those codes have the same effect.
+ # Time.zone = 'Pacific Time (US & Canada)' (ActiveSupport::TimeZone)
+ # Time.zone = 'America/Los_Angeles' (TZInfo::Timezone)
+ #
+ # However, try_parse_cron only accepts the latter format.
+ # try_parse_cron('* * * * *', 'Pacific Time (US & Canada)') -> Doesn't work
+ # try_parse_cron('* * * * *', 'America/Los_Angeles') -> Works
+ # If you want to know more, please take a look
+ # https://github.com/rails/rails/blob/master/activesupport/lib/active_support/values/time_zone.rb
def try_parse_cron(cron, cron_timezone)
- Rufus::Scheduler.parse("#{cron} #{cron_timezone}")
+ cron_line = Rufus::Scheduler.parse("#{cron} #{cron_timezone}")
+ cron_line if cron_line.is_a?(Rufus::Scheduler::CronLine)
rescue
# noop
end
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index bb4fdd1f1f4..3da00422ed3 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -60,9 +60,8 @@ module Gitlab
end
def key_from_additional_headers(mail)
- references = ensure_references_array(mail.references)
-
- find_key_from_references(references)
+ find_key_from_references(mail) ||
+ find_key_from_delivered_to_header(mail)
end
def ensure_references_array(references)
@@ -73,15 +72,24 @@ module Gitlab
# Handle emails from clients which append with commas,
# example clients are Microsoft exchange and iOS app
Gitlab::IncomingEmail.scan_fallback_references(references)
+ when nil
+ []
end
end
- def find_key_from_references(references)
- references.find do |mail_id|
+ def find_key_from_references(mail)
+ ensure_references_array(mail.references).find do |mail_id|
key = Gitlab::IncomingEmail.key_from_fallback_message_id(mail_id)
break key if key
end
end
+
+ def find_key_from_delivered_to_header(mail)
+ Array(mail[:delivered_to]).find do |header|
+ key = Gitlab::IncomingEmail.key_from_address(header.value)
+ break key if key
+ end
+ end
end
end
end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index dd47e7166c4..a9c764adb18 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -486,8 +486,9 @@ module Gitlab
# :contains is the commit contained by the refs from which to begin (SHA1 or name)
# :max_count is the maximum number of commits to fetch
# :skip is the number of commits to skip
- # :order is the commits order and allowed value is :none (default), :date, or :topo
- # commit ordering types are documented here:
+ # :order is the commits order and allowed value is :none (default), :date,
+ # :topo, or any combination of them (in an array). Commit ordering types
+ # are documented here:
# http://www.rubydoc.info/github/libgit2/rugged/Rugged#SORT_NONE-constant)
#
def find_commits(options = {})
@@ -1265,16 +1266,30 @@ module Gitlab
@gitaly_ref_client ||= Gitlab::GitalyClient::Ref.new(self)
end
- # Returns the `Rugged` sorting type constant for a given
- # sort type key. Valid keys are `:none`, `:topo`, and `:date`
- def rugged_sort_type(key)
+ def gitaly_commit_client
+ @gitaly_commit_client ||= Gitlab::GitalyClient::Commit.new(self)
+ end
+
+ def gitaly_migrate(method, &block)
+ Gitlab::GitalyClient.migrate(method, &block)
+ rescue GRPC::NotFound => e
+ raise NoRepository.new(e)
+ rescue GRPC::BadStatus => e
+ raise CommandError.new(e)
+ end
+
+ # Returns the `Rugged` sorting type constant for one or more given
+ # sort types. Valid keys are `:none`, `:topo`, and `:date`, or an array
+ # containing more than one of them. `:date` uses a combination of date and
+ # topological sorting to closer mimic git's native ordering.
+ def rugged_sort_type(sort_type)
@rugged_sort_types ||= {
none: Rugged::SORT_NONE,
topo: Rugged::SORT_TOPO,
- date: Rugged::SORT_DATE
+ date: Rugged::SORT_DATE | Rugged::SORT_TOPO
}
- @rugged_sort_types.fetch(key, Rugged::SORT_NONE)
+ @rugged_sort_types.fetch(sort_type, Rugged::SORT_NONE)
end
end
end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 899a6567768..101d88cfa30 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -41,7 +41,6 @@ project_tree:
- :statuses
- triggers:
- :trigger_schedule
- - :deploy_keys
- :services
- :hooks
- protected_branches:
@@ -53,10 +52,6 @@ project_tree:
# Only include the following attributes for the models specified.
included_attributes:
- project:
- - :description
- - :visibility_level
- - :archived
user:
- :id
- :email
@@ -66,6 +61,32 @@ included_attributes:
# Do not include the following attributes for the models specified.
excluded_attributes:
+ project:
+ - :name
+ - :path
+ - :namespace_id
+ - :creator_id
+ - :import_url
+ - :import_status
+ - :avatar
+ - :import_type
+ - :import_source
+ - :import_error
+ - :mirror
+ - :runners_token
+ - :repository_storage
+ - :repository_read_only
+ - :lfs_enabled
+ - :import_jid
+ - :created_at
+ - :updated_at
+ - :import_jid
+ - :import_jid
+ - :id
+ - :star_count
+ - :last_repository_updated_at
+ - :last_repository_check_at
+ - :last_activity_at
snippets:
- :expired_at
merge_request_diff:
@@ -94,3 +115,5 @@ methods:
- :utf8_st_diffs
merge_requests:
- :diff_head_sha
+ project:
+ - :description_html
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index 2e349b5f9a9..84ab1977dfa 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -71,14 +71,14 @@ module Gitlab
def restore_project
return @project unless @tree_hash
- @project.update(project_params)
+ @project.update_columns(project_params)
@project
end
def project_params
@tree_hash.reject do |key, value|
# return params that are not 1 to many or 1 to 1 relations
- value.is_a?(Array) || key == key.singularize
+ value.respond_to?(:each) && !Project.column_names.include?(key)
end
end
diff --git a/lib/gitlab/import_export/reader.rb b/lib/gitlab/import_export/reader.rb
index a1e7159fe42..eb7f5120592 100644
--- a/lib/gitlab/import_export/reader.rb
+++ b/lib/gitlab/import_export/reader.rb
@@ -15,7 +15,10 @@ module Gitlab
# Outputs a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
# for outputting a project in JSON format, including its relations and sub relations.
def project_tree
- @attributes_finder.find_included(:project).merge(include: build_hash(@tree))
+ attributes = @attributes_finder.find(:project)
+ project_attributes = attributes.is_a?(Hash) ? attributes[:project] : {}
+
+ project_attributes.merge(include: build_hash(@tree))
rescue => e
@shared.error(e)
false
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 0b8959f2fb9..47cfe412715 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -82,6 +82,8 @@ module Gitlab
private
def blobs
+ return [] unless Ability.allowed?(@current_user, :download_code, @project)
+
@blobs ||= begin
blobs = project.repository.search_files_by_content(query, repository_ref).first(100)
found_file_names = Set.new
@@ -102,6 +104,8 @@ module Gitlab
end
def wiki_blobs
+ return [] unless Ability.allowed?(@current_user, :read_wiki, @project)
+
@wiki_blobs ||= begin
if project.wiki_enabled? && query.present?
project_wiki = ProjectWiki.new(project)
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb
index 45db7a92fa4..7ce4e9009f5 100644
--- a/qa/qa/page/main/menu.rb
+++ b/qa/qa/page/main/menu.rb
@@ -11,7 +11,7 @@ module QA
end
def go_to_admin_area
- within_user_menu { click_link 'Admin Area' }
+ within_user_menu { click_link 'Admin area' }
end
def sign_out
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index cad82a34fb0..42f0591600e 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -26,6 +26,41 @@ describe GroupsController do
end
end
+ describe 'GET #subgroups' do
+ let!(:public_subgroup) { create(:group, :public, parent: group) }
+ let!(:private_subgroup) { create(:group, :private, parent: group) }
+
+ context 'as a user' do
+ before do
+ sign_in(user)
+ end
+
+ it 'shows the public subgroups' do
+ get :subgroups, id: group.to_param
+
+ expect(assigns(:nested_groups)).to contain_exactly(public_subgroup)
+ end
+
+ context 'being member' do
+ it 'shows public and private subgroups the user is member of' do
+ private_subgroup.add_guest(user)
+
+ get :subgroups, id: group.to_param
+
+ expect(assigns(:nested_groups)).to contain_exactly(public_subgroup, private_subgroup)
+ end
+ end
+ end
+
+ context 'as a guest' do
+ it 'shows the public subgroups' do
+ get :subgroups, id: group.to_param
+
+ expect(assigns(:nested_groups)).to contain_exactly(public_subgroup)
+ end
+ end
+ end
+
describe 'GET #issues' do
let(:issue_1) { create(:issue, project: project) }
let(:issue_2) { create(:issue, project: project) }
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 5de3b9890ef..feca85971d4 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -3,137 +3,52 @@ require 'spec_helper'
describe SnippetsController do
let(:user) { create(:user) }
- describe 'GET #new' do
- context 'when signed in' do
- before do
- sign_in(user)
- end
+ describe 'GET #index' do
+ let(:user) { create(:user) }
- it 'responds with status 200' do
- get :new
+ context 'when username parameter is present' do
+ it 'renders snippets of a user when username is present' do
+ get :index, username: user.username
- expect(response).to have_http_status(200)
+ expect(response).to render_template(:index)
end
end
- context 'when not signed in' do
- it 'redirects to the sign in page' do
- get :new
+ context 'when username parameter is not present' do
+ it 'redirects to explore snippets page when user is not logged in' do
+ get :index
- expect(response).to redirect_to(new_user_session_path)
+ expect(response).to redirect_to(explore_snippets_path)
end
- end
- end
-
- describe 'GET #show' do
- context 'when the personal snippet is private' do
- let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
-
- context 'when signed in' do
- before do
- sign_in(user)
- end
-
- context 'when signed in user is not the author' do
- let(:other_author) { create(:author) }
- let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) }
-
- it 'responds with status 404' do
- get :show, id: other_personal_snippet.to_param
-
- expect(response).to have_http_status(404)
- end
- end
-
- context 'when signed in user is the author' do
- it 'renders the snippet' do
- get :show, id: personal_snippet.to_param
-
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
- end
- end
- end
-
- context 'when not signed in' do
- it 'redirects to the sign in page' do
- get :show, id: personal_snippet.to_param
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
- end
-
- context 'when the personal snippet is internal' do
- let(:personal_snippet) { create(:personal_snippet, :internal, author: user) }
-
- context 'when signed in' do
- before do
- sign_in(user)
- end
-
- it 'renders the snippet' do
- get :show, id: personal_snippet.to_param
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
- end
- end
+ it 'redirects to snippets dashboard page when user is logged in' do
+ sign_in(user)
- context 'when not signed in' do
- it 'redirects to the sign in page' do
- get :show, id: personal_snippet.to_param
+ get :index
- expect(response).to redirect_to(new_user_session_path)
- end
+ expect(response).to redirect_to(dashboard_snippets_path)
end
end
+ end
- context 'when the personal snippet is public' do
- let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
-
- context 'when signed in' do
- before do
- sign_in(user)
- end
-
- it 'renders the snippet' do
- get :show, id: personal_snippet.to_param
-
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
- end
+ describe 'GET #new' do
+ context 'when signed in' do
+ before do
+ sign_in(user)
end
- context 'when not signed in' do
- it 'renders the snippet' do
- get :show, id: personal_snippet.to_param
+ it 'responds with status 200' do
+ get :new
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
- end
+ expect(response).to have_http_status(200)
end
end
- context 'when the personal snippet does not exist' do
- context 'when signed in' do
- before do
- sign_in(user)
- end
-
- it 'responds with status 404' do
- get :show, id: 'doesntexist'
-
- expect(response).to have_http_status(404)
- end
- end
-
- context 'when not signed in' do
- it 'responds with status 404' do
- get :show, id: 'doesntexist'
+ context 'when not signed in' do
+ it 'redirects to the sign in page' do
+ get :new
- expect(response).to have_http_status(404)
- end
+ expect(response).to redirect_to(new_user_session_path)
end
end
end
@@ -350,149 +265,181 @@ describe SnippetsController do
end
end
- %w(raw download).each do |action|
- describe "GET #{action}" do
- context 'when the personal snippet is private' do
- let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
+ shared_examples "line endings" do |action|
+ context 'CRLF line ending' do
+ let(:personal_snippet) do
+ create(:personal_snippet, :public, author: user, content: "first line\r\nsecond line\r\nthird line")
+ end
- context 'when signed in' do
- before do
- sign_in(user)
- end
+ it 'returns LF line endings by default' do
+ get action, id: personal_snippet.to_param
- context 'when signed in user is not the author' do
- let(:other_author) { create(:author) }
- let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) }
+ expect(response.body).to eq("first line\nsecond line\nthird line")
+ end
- it 'responds with status 404' do
- get action, id: other_personal_snippet.to_param
+ it 'does not convert line endings when parameter present' do
+ get action, id: personal_snippet.to_param, line_ending: :raw
- expect(response).to have_http_status(404)
- end
+ expect(response.body).to eq("first line\r\nsecond line\r\nthird line")
+ end
+ end
+ end
+
+ shared_examples 'snippet response' do |action|
+ context 'when the personal snippet is private' do
+ let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ context 'when signed in user is not the author' do
+ let(:other_author) { create(:author) }
+ let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) }
+
+ it 'responds with status 404' do
+ get action, id: other_personal_snippet.to_param
+
+ expect(response).to have_http_status(404)
end
+ end
- context 'when signed in user is the author' do
- before { get action, id: personal_snippet.to_param }
+ context 'when signed in user is the author' do
+ before { get action, id: personal_snippet.to_param }
- it 'responds with status 200' do
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
- end
+ it 'responds with status 200' do
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response).to have_http_status(200)
+ end
- it 'has expected headers' do
+ it 'has expected headers' do
+ if action == :show
+ expect(response.header['Content-Type']).to eq('text/html; charset=utf-8')
+ else
expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
+ end
- if action == :download
- expect(response.header['Content-Disposition']).to match(/attachment/)
- elsif action == :raw
- expect(response.header['Content-Disposition']).to match(/inline/)
- end
+ if action == :download
+ expect(response.header['Content-Disposition']).to match(/attachment/)
+ elsif action == :raw
+ expect(response.header['Content-Disposition']).to match(/inline/)
end
end
end
+ end
- context 'when not signed in' do
- it 'redirects to the sign in page' do
- get action, id: personal_snippet.to_param
+ context 'when not signed in' do
+ it 'redirects to the sign in page' do
+ get action, id: personal_snippet.to_param
- expect(response).to redirect_to(new_user_session_path)
- end
+ expect(response).to redirect_to(new_user_session_path)
end
end
+ end
- context 'when the personal snippet is internal' do
- let(:personal_snippet) { create(:personal_snippet, :internal, author: user) }
+ context 'when the personal snippet is internal' do
+ let(:personal_snippet) { create(:personal_snippet, :internal, author: user) }
- context 'when signed in' do
- before do
- sign_in(user)
- end
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
- it 'responds with status 200' do
- get action, id: personal_snippet.to_param
+ it 'responds with status 200' do
+ get action, id: personal_snippet.to_param
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
- end
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response).to have_http_status(200)
end
+ end
- context 'when not signed in' do
- it 'redirects to the sign in page' do
- get action, id: personal_snippet.to_param
+ context 'when not signed in' do
+ it 'redirects to the sign in page' do
+ get action, id: personal_snippet.to_param
- expect(response).to redirect_to(new_user_session_path)
- end
+ expect(response).to redirect_to(new_user_session_path)
end
end
- context 'when the personal snippet is public' do
- let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
-
- context 'when signed in' do
- before do
- sign_in(user)
- end
+ context 'when signed in as an external user' do
+ let(:external_user) { create(:user, external: true) }
- it 'responds with status 200' do
- get action, id: personal_snippet.to_param
+ before do
+ sign_in(external_user)
+ end
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
- end
+ it 'responds with status 404' do
+ get :show, id: personal_snippet.to_param
- context 'CRLF line ending' do
- let(:personal_snippet) do
- create(:personal_snippet, :public, author: user, content: "first line\r\nsecond line\r\nthird line")
- end
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
- it 'returns LF line endings by default' do
- get action, id: personal_snippet.to_param
+ context 'when the personal snippet is public' do
+ let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
- expect(response.body).to eq("first line\nsecond line\nthird line")
- end
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
- it 'does not convert line endings when parameter present' do
- get action, id: personal_snippet.to_param, line_ending: :raw
+ it 'responds with status 200' do
+ get action, id: personal_snippet.to_param
- expect(response.body).to eq("first line\r\nsecond line\r\nthird line")
- end
- end
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response).to have_http_status(200)
end
+ end
- context 'when not signed in' do
- it 'responds with status 200' do
- get action, id: personal_snippet.to_param
+ context 'when not signed in' do
+ it 'responds with status 200' do
+ get action, id: personal_snippet.to_param
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
- end
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response).to have_http_status(200)
end
end
+ end
- context 'when the personal snippet does not exist' do
- context 'when signed in' do
- before do
- sign_in(user)
- end
+ context 'when the personal snippet does not exist' do
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
- it 'responds with status 404' do
- get action, id: 'doesntexist'
+ it 'responds with status 404' do
+ get action, id: 'doesntexist'
- expect(response).to have_http_status(404)
- end
+ expect(response).to have_http_status(404)
end
+ end
- context 'when not signed in' do
- it 'responds with status 404' do
- get action, id: 'doesntexist'
+ context 'when not signed in' do
+ it 'redirects to the sign in page' do
+ get action, id: 'doesntexist'
- expect(response).to have_http_status(404)
- end
+ expect(response).to redirect_to(new_user_session_path)
end
end
end
end
+ describe "GET #raw" do
+ include_examples 'line endings', :raw
+ include_examples 'snippet response', :raw
+ end
+
+ describe "GET #download" do
+ include_examples 'line endings', :download
+ include_examples 'snippet response', :download
+ end
+
+ describe "GET #show" do
+ include_examples 'snippet response', :show
+ end
+
context 'award emoji on snippets' do
let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
let(:another_user) { create(:user) }
diff --git a/spec/features/dashboard/snippets_spec.rb b/spec/features/dashboard/snippets_spec.rb
index 62937688c22..c6ba118220a 100644
--- a/spec/features/dashboard/snippets_spec.rb
+++ b/spec/features/dashboard/snippets_spec.rb
@@ -12,4 +12,51 @@ describe 'Dashboard snippets', feature: true do
it_behaves_like 'paginated snippets'
end
+
+ context 'filtering by visibility' do
+ let(:user) { create(:user) }
+ let!(:snippets) do
+ [
+ create(:personal_snippet, :public, author: user),
+ create(:personal_snippet, :internal, author: user),
+ create(:personal_snippet, :private, author: user),
+ create(:personal_snippet, :public)
+ ]
+ end
+
+ before do
+ login_as(user)
+
+ visit dashboard_snippets_path
+ end
+
+ it 'contains all snippets of logged user' do
+ expect(page).to have_selector('.snippet-row', count: 3)
+
+ expect(page).to have_content(snippets[0].title)
+ expect(page).to have_content(snippets[1].title)
+ expect(page).to have_content(snippets[2].title)
+ end
+
+ it 'contains all private snippets of logged user when clicking on private' do
+ click_link('Private')
+
+ expect(page).to have_selector('.snippet-row', count: 1)
+ expect(page).to have_content(snippets[2].title)
+ end
+
+ it 'contains all internal snippets of logged user when clicking on internal' do
+ click_link('Internal')
+
+ expect(page).to have_selector('.snippet-row', count: 1)
+ expect(page).to have_content(snippets[1].title)
+ end
+
+ it 'contains all public snippets of logged user when clicking on public' do
+ click_link('Public')
+
+ expect(page).to have_selector('.snippet-row', count: 1)
+ expect(page).to have_content(snippets[0].title)
+ end
+ end
end
diff --git a/spec/features/issues/notes_on_issues_spec.rb b/spec/features/issues/notes_on_issues_spec.rb
new file mode 100644
index 00000000000..a4035324d2b
--- /dev/null
+++ b/spec/features/issues/notes_on_issues_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe 'Create notes on issues', :js, :feature do
+ let(:user) { create(:user) }
+
+ shared_examples 'notes with reference' do
+ let(:issue) { create(:issue, project: project) }
+ let(:note_text) { "Check #{mention.to_reference}" }
+
+ before do
+ project.team << [user, :developer]
+ login_as(user)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ fill_in 'note[note]', with: note_text
+ click_button 'Comment'
+
+ wait_for_ajax
+ end
+
+ it 'creates a note with reference and cross references the issue' do
+ page.within('div#notes li.note div.note-text') do
+ expect(page).to have_content(note_text)
+ expect(page.find('a')).to have_content(mention.to_reference)
+ end
+
+ find('div#notes li.note div.note-text a').click
+
+ page.within('div#notes li.note .system-note-message') do
+ expect(page).to have_content('mentioned in issue')
+ expect(page.find('a')).to have_content(issue.to_reference)
+ end
+ end
+ end
+
+ context 'mentioning issue on a private project' do
+ it_behaves_like 'notes with reference' do
+ let(:project) { create(:project, :private) }
+ let(:mention) { create(:issue, project: project) }
+ end
+ end
+
+ context 'mentioning issue on an internal project' do
+ it_behaves_like 'notes with reference' do
+ let(:project) { create(:project, :internal) }
+ let(:mention) { create(:issue, project: project) }
+ end
+ end
+
+ context 'mentioning issue on a public project' do
+ it_behaves_like 'notes with reference' do
+ let(:project) { create(:project, :public) }
+ let(:mention) { create(:issue, project: project) }
+ end
+ end
+
+ context 'mentioning merge request on a private project' do
+ it_behaves_like 'notes with reference' do
+ let(:project) { create(:project, :private) }
+ let(:mention) { create(:merge_request, source_project: project) }
+ end
+ end
+
+ context 'mentioning merge request on an internal project' do
+ it_behaves_like 'notes with reference' do
+ let(:project) { create(:project, :internal) }
+ let(:mention) { create(:merge_request, source_project: project) }
+ end
+ end
+
+ context 'mentioning merge request on a public project' do
+ it_behaves_like 'notes with reference' do
+ let(:project) { create(:project, :public) }
+ let(:mention) { create(:merge_request, source_project: project) }
+ end
+ end
+end
diff --git a/spec/features/projects/snippets_spec.rb b/spec/features/projects/snippets_spec.rb
index d37e8ed4699..18689c17fe9 100644
--- a/spec/features/projects/snippets_spec.rb
+++ b/spec/features/projects/snippets_spec.rb
@@ -4,11 +4,27 @@ describe 'Project snippets', feature: true do
context 'when the project has snippets' do
let(:project) { create(:empty_project, :public) }
let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
- before do
- allow(Snippet).to receive(:default_per_page).and_return(1)
- visit namespace_project_snippets_path(project.namespace, project)
+ let!(:other_snippet) { create(:project_snippet) }
+
+ context 'pagination' do
+ before do
+ allow(Snippet).to receive(:default_per_page).and_return(1)
+
+ visit namespace_project_snippets_path(project.namespace, project)
+ end
+
+ it_behaves_like 'paginated snippets'
end
- it_behaves_like 'paginated snippets'
+ context 'list content' do
+ it 'contains all project snippets' do
+ visit namespace_project_snippets_path(project.namespace, project)
+
+ expect(page).to have_selector('.snippet-row', count: 2)
+
+ expect(page).to have_content(snippets[0].title)
+ expect(page).to have_content(snippets[1].title)
+ end
+ end
end
end
diff --git a/spec/features/snippets/explore_spec.rb b/spec/features/snippets/explore_spec.rb
index 10a4597e467..fd097fe2e74 100644
--- a/spec/features/snippets/explore_spec.rb
+++ b/spec/features/snippets/explore_spec.rb
@@ -1,11 +1,11 @@
require 'rails_helper'
feature 'Explore Snippets', feature: true do
- scenario 'User should see snippets that are not private' do
- public_snippet = create(:personal_snippet, :public)
- internal_snippet = create(:personal_snippet, :internal)
- private_snippet = create(:personal_snippet, :private)
+ let!(:public_snippet) { create(:personal_snippet, :public) }
+ let!(:internal_snippet) { create(:personal_snippet, :internal) }
+ let!(:private_snippet) { create(:personal_snippet, :private) }
+ scenario 'User should see snippets that are not private' do
login_as create(:user)
visit explore_snippets_path
@@ -13,4 +13,21 @@ feature 'Explore Snippets', feature: true do
expect(page).to have_content(internal_snippet.title)
expect(page).not_to have_content(private_snippet.title)
end
+
+ scenario 'External user should see only public snippets' do
+ login_as create(:user, :external)
+ visit explore_snippets_path
+
+ expect(page).to have_content(public_snippet.title)
+ expect(page).not_to have_content(internal_snippet.title)
+ expect(page).not_to have_content(private_snippet.title)
+ end
+
+ scenario 'Not authenticated user should see only public snippets' do
+ visit explore_snippets_path
+
+ expect(page).to have_content(public_snippet.title)
+ expect(page).not_to have_content(internal_snippet.title)
+ expect(page).not_to have_content(private_snippet.title)
+ end
end
diff --git a/spec/features/snippets/internal_snippet_spec.rb b/spec/features/snippets/internal_snippet_spec.rb
new file mode 100644
index 00000000000..2682b66dc26
--- /dev/null
+++ b/spec/features/snippets/internal_snippet_spec.rb
@@ -0,0 +1,23 @@
+require 'rails_helper'
+
+feature 'Internal Snippets', feature: true do
+ let(:internal_snippet) { create(:personal_snippet, :internal) }
+
+ describe 'normal user' do
+ before do
+ login_as :user
+ end
+
+ scenario 'sees internal snippets' do
+ visit snippet_path(internal_snippet)
+
+ expect(page).to have_content(internal_snippet.content)
+ end
+
+ scenario 'sees raw internal snippets' do
+ visit raw_snippet_path(internal_snippet)
+
+ expect(page).to have_content(internal_snippet.content)
+ end
+ end
+end
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 81fa2de1cc3..783f330221c 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -104,6 +104,24 @@ feature 'Triggers', feature: true, js: true do
expect(page).to have_content 'The form contains the following errors'
end
+
+ context 'when GitLab time_zone is ActiveSupport::TimeZone format' do
+ before do
+ allow(Time).to receive(:zone)
+ .and_return(ActiveSupport::TimeZone['Eastern Time (US & Canada)'])
+ end
+
+ scenario 'do fill form with valid data and save' do
+ find('#trigger_trigger_schedule_attributes_active').click
+ fill_in 'trigger_trigger_schedule_attributes_cron', with: '1 * * * *'
+ fill_in 'trigger_trigger_schedule_attributes_cron_timezone', with: 'UTC'
+ fill_in 'trigger_trigger_schedule_attributes_ref', with: 'master'
+ click_button 'Save trigger'
+
+ expect(page.find('.flash-notice'))
+ .to have_content 'Trigger was successfully updated.'
+ end
+ end
end
context 'disabling schedule' do
diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb
index ce7e809ec76..e92865077f7 100644
--- a/spec/features/users/snippets_spec.rb
+++ b/spec/features/users/snippets_spec.rb
@@ -5,14 +5,46 @@ describe 'Snippets tab on a user profile', feature: true, js: true do
context 'when the user has snippets' do
let(:user) { create(:user) }
- let!(:snippets) { create_list(:snippet, 2, :public, author: user) }
- before do
- allow(Snippet).to receive(:default_per_page).and_return(1)
- visit user_path(user)
- page.within('.user-profile-nav') { click_link 'Snippets' }
- wait_for_ajax
+
+ context 'pagination' do
+ let!(:snippets) { create_list(:snippet, 2, :public, author: user) }
+
+ before do
+ allow(Snippet).to receive(:default_per_page).and_return(1)
+ visit user_path(user)
+ page.within('.user-profile-nav') { click_link 'Snippets' }
+ wait_for_ajax
+ end
+
+ it_behaves_like 'paginated snippets', remote: true
end
- it_behaves_like 'paginated snippets', remote: true
+ context 'list content' do
+ let!(:public_snippet) { create(:snippet, :public, author: user) }
+ let!(:internal_snippet) { create(:snippet, :internal, author: user) }
+ let!(:private_snippet) { create(:snippet, :private, author: user) }
+ let!(:other_snippet) { create(:snippet, :public) }
+
+ it 'contains only internal and public snippets of a user when a user is logged in' do
+ login_as(:user)
+ visit user_path(user)
+ page.within('.user-profile-nav') { click_link 'Snippets' }
+ wait_for_ajax
+
+ expect(page).to have_selector('.snippet-row', count: 2)
+
+ expect(page).to have_content(public_snippet.title)
+ expect(page).to have_content(internal_snippet.title)
+ end
+
+ it 'contains only public snippets of a user when a user is not logged in' do
+ visit user_path(user)
+ page.within('.user-profile-nav') { click_link 'Snippets' }
+ wait_for_ajax
+
+ expect(page).to have_selector('.snippet-row', count: 1)
+ expect(page).to have_content(public_snippet.title)
+ end
+ end
end
end
diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb
index d5d111e8d15..5b3591550c1 100644
--- a/spec/finders/groups_finder_spec.rb
+++ b/spec/finders/groups_finder_spec.rb
@@ -3,29 +3,64 @@ require 'spec_helper'
describe GroupsFinder do
describe '#execute' do
let(:user) { create(:user) }
- let!(:private_group) { create(:group, :private) }
- let!(:internal_group) { create(:group, :internal) }
- let!(:public_group) { create(:group, :public) }
- let(:finder) { described_class.new }
- describe 'execute' do
- describe 'without a user' do
- subject { finder.execute }
+ context 'root level groups' do
+ let!(:private_group) { create(:group, :private) }
+ let!(:internal_group) { create(:group, :internal) }
+ let!(:public_group) { create(:group, :public) }
+
+ context 'without a user' do
+ subject { described_class.new.execute }
it { is_expected.to eq([public_group]) }
end
- describe 'with a user' do
- subject { finder.execute(user) }
+ context 'with a user' do
+ subject { described_class.new(user).execute }
context 'normal user' do
- it { is_expected.to eq([public_group, internal_group]) }
+ it { is_expected.to contain_exactly(public_group, internal_group) }
end
context 'external user' do
let(:user) { create(:user, external: true) }
- it { is_expected.to eq([public_group]) }
+ it { is_expected.to contain_exactly(public_group) }
+ end
+
+ context 'user is member of the private group' do
+ before do
+ private_group.add_guest(user)
+ end
+
+ it { is_expected.to contain_exactly(public_group, internal_group, private_group) }
+ end
+ end
+ end
+
+ context 'subgroups' do
+ let!(:parent_group) { create(:group, :public) }
+ let!(:public_subgroup) { create(:group, :public, parent: parent_group) }
+ let!(:internal_subgroup) { create(:group, :internal, parent: parent_group) }
+ let!(:private_subgroup) { create(:group, :private, parent: parent_group) }
+
+ context 'without a user' do
+ it 'only returns public subgroups' do
+ expect(described_class.new(nil, parent: parent_group).execute).to contain_exactly(public_subgroup)
+ end
+ end
+
+ context 'with a user' do
+ it 'returns public and internal subgroups' do
+ expect(described_class.new(user, parent: parent_group).execute).to contain_exactly(public_subgroup, internal_subgroup)
+ end
+
+ context 'being member' do
+ it 'returns public subgroups, internal subgroups, and private subgroups user is member of' do
+ private_subgroup.add_guest(user)
+
+ expect(described_class.new(user, parent: parent_group).execute).to contain_exactly(public_subgroup, internal_subgroup, private_subgroup)
+ end
end
end
end
diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb
index 975e99c5807..aea1d6f8ef3 100644
--- a/spec/finders/snippets_finder_spec.rb
+++ b/spec/finders/snippets_finder_spec.rb
@@ -8,79 +8,133 @@ describe SnippetsFinder do
let(:project1) { create(:empty_project, :public, group: group) }
let(:project2) { create(:empty_project, :private, group: group) }
- context ':all filter' do
+ context 'all snippets visible to a user' do
let!(:snippet1) { create(:personal_snippet, :private) }
let!(:snippet2) { create(:personal_snippet, :internal) }
let!(:snippet3) { create(:personal_snippet, :public) }
+ let!(:project_snippet1) { create(:project_snippet, :private) }
+ let!(:project_snippet2) { create(:project_snippet, :internal) }
+ let!(:project_snippet3) { create(:project_snippet, :public) }
- it "returns all private and internal snippets" do
- snippets = SnippetsFinder.new.execute(user, filter: :all)
- expect(snippets).to include(snippet2, snippet3)
- expect(snippets).not_to include(snippet1)
+ it "returns all public and internal snippets for normal user" do
+ snippets = SnippetsFinder.new(user).execute
+
+ expect(snippets).to include(snippet2, snippet3, project_snippet2, project_snippet3)
+ expect(snippets).not_to include(snippet1, project_snippet1)
end
- it "returns all public snippets" do
- snippets = SnippetsFinder.new.execute(nil, filter: :all)
- expect(snippets).to include(snippet3)
- expect(snippets).not_to include(snippet1, snippet2)
+ it "returns all public snippets for non authorized user" do
+ snippets = SnippetsFinder.new(nil).execute
+
+ expect(snippets).to include(snippet3, project_snippet3)
+ expect(snippets).not_to include(snippet1, snippet2, project_snippet1, project_snippet2)
+ end
+
+ it "returns all public and authored snippets for external user" do
+ external_user = create(:user, :external)
+ authored_snippet = create(:personal_snippet, :internal, author: external_user)
+
+ snippets = SnippetsFinder.new(external_user).execute
+
+ expect(snippets).to include(snippet3, project_snippet3, authored_snippet)
+ expect(snippets).not_to include(snippet1, snippet2, project_snippet1, project_snippet2)
end
end
- context ':public filter' do
+ context 'filter by visibility' do
let!(:snippet1) { create(:personal_snippet, :private) }
let!(:snippet2) { create(:personal_snippet, :internal) }
let!(:snippet3) { create(:personal_snippet, :public) }
- it "returns public public snippets" do
- snippets = SnippetsFinder.new.execute(nil, filter: :public)
+ it "returns public snippets when visibility is PUBLIC" do
+ snippets = SnippetsFinder.new(nil, visibility: Snippet::PUBLIC).execute
expect(snippets).to include(snippet3)
expect(snippets).not_to include(snippet1, snippet2)
end
end
- context ':by_user filter' do
+ context 'filter by scope' do
+ let!(:snippet1) { create(:personal_snippet, :private, author: user) }
+ let!(:snippet2) { create(:personal_snippet, :internal, author: user) }
+ let!(:snippet3) { create(:personal_snippet, :public, author: user) }
+
+ it "returns all snippets for 'all' scope" do
+ snippets = SnippetsFinder.new(user, scope: :all).execute
+
+ expect(snippets).to include(snippet1, snippet2, snippet3)
+ end
+
+ it "returns all snippets for 'are_private' scope" do
+ snippets = SnippetsFinder.new(user, scope: :are_private).execute
+
+ expect(snippets).to include(snippet1)
+ expect(snippets).not_to include(snippet2, snippet3)
+ end
+
+ it "returns all snippets for 'are_interna;' scope" do
+ snippets = SnippetsFinder.new(user, scope: :are_internal).execute
+
+ expect(snippets).to include(snippet2)
+ expect(snippets).not_to include(snippet1, snippet3)
+ end
+
+ it "returns all snippets for 'are_private' scope" do
+ snippets = SnippetsFinder.new(user, scope: :are_public).execute
+
+ expect(snippets).to include(snippet3)
+ expect(snippets).not_to include(snippet1, snippet2)
+ end
+ end
+
+ context 'filter by author' do
let!(:snippet1) { create(:personal_snippet, :private, author: user) }
let!(:snippet2) { create(:personal_snippet, :internal, author: user) }
let!(:snippet3) { create(:personal_snippet, :public, author: user) }
it "returns all public and internal snippets" do
- snippets = SnippetsFinder.new.execute(user1, filter: :by_user, user: user)
+ snippets = SnippetsFinder.new(user1, author: user).execute
+
expect(snippets).to include(snippet2, snippet3)
expect(snippets).not_to include(snippet1)
end
it "returns internal snippets" do
- snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_internal")
+ snippets = SnippetsFinder.new(user, author: user, visibility: Snippet::INTERNAL).execute
+
expect(snippets).to include(snippet2)
expect(snippets).not_to include(snippet1, snippet3)
end
it "returns private snippets" do
- snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_private")
+ snippets = SnippetsFinder.new(user, author: user, visibility: Snippet::PRIVATE).execute
+
expect(snippets).to include(snippet1)
expect(snippets).not_to include(snippet2, snippet3)
end
it "returns public snippets" do
- snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_public")
+ snippets = SnippetsFinder.new(user, author: user, visibility: Snippet::PUBLIC).execute
+
expect(snippets).to include(snippet3)
expect(snippets).not_to include(snippet1, snippet2)
end
it "returns all snippets" do
- snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user)
+ snippets = SnippetsFinder.new(user, author: user).execute
+
expect(snippets).to include(snippet1, snippet2, snippet3)
end
it "returns only public snippets if unauthenticated user" do
- snippets = SnippetsFinder.new.execute(nil, filter: :by_user, user: user)
+ snippets = SnippetsFinder.new(nil, author: user).execute
+
expect(snippets).to include(snippet3)
expect(snippets).not_to include(snippet2, snippet1)
end
end
- context 'by_project filter' do
+ context 'filter by project' do
before do
@snippet1 = create(:project_snippet, :private, project: project1)
@snippet2 = create(:project_snippet, :internal, project: project1)
@@ -88,43 +142,52 @@ describe SnippetsFinder do
end
it "returns public snippets for unauthorized user" do
- snippets = SnippetsFinder.new.execute(nil, filter: :by_project, project: project1)
+ snippets = SnippetsFinder.new(nil, project: project1).execute
+
expect(snippets).to include(@snippet3)
expect(snippets).not_to include(@snippet1, @snippet2)
end
it "returns public and internal snippets for non project members" do
- snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1)
+ snippets = SnippetsFinder.new(user, project: project1).execute
+
expect(snippets).to include(@snippet2, @snippet3)
expect(snippets).not_to include(@snippet1)
end
it "returns public snippets for non project members" do
- snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_public")
+ snippets = SnippetsFinder.new(user, project: project1, visibility: Snippet::PUBLIC).execute
+
expect(snippets).to include(@snippet3)
expect(snippets).not_to include(@snippet1, @snippet2)
end
it "returns internal snippets for non project members" do
- snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_internal")
+ snippets = SnippetsFinder.new(user, project: project1, visibility: Snippet::INTERNAL).execute
+
expect(snippets).to include(@snippet2)
expect(snippets).not_to include(@snippet1, @snippet3)
end
it "does not return private snippets for non project members" do
- snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_private")
+ snippets = SnippetsFinder.new(user, project: project1, visibility: Snippet::PRIVATE).execute
+
expect(snippets).not_to include(@snippet1, @snippet2, @snippet3)
end
it "returns all snippets for project members" do
project1.team << [user, :developer]
- snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1)
+
+ snippets = SnippetsFinder.new(user, project: project1).execute
+
expect(snippets).to include(@snippet1, @snippet2, @snippet3)
end
it "returns private snippets for project members" do
project1.team << [user, :developer]
- snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_private")
+
+ snippets = SnippetsFinder.new(user, project: project1, visibility: Snippet::PRIVATE).execute
+
expect(snippets).to include(@snippet1)
end
end
diff --git a/spec/fixtures/emails/forwarded_new_issue.eml b/spec/fixtures/emails/forwarded_new_issue.eml
new file mode 100644
index 00000000000..258106bb897
--- /dev/null
+++ b/spec/fixtures/emails/forwarded_new_issue.eml
@@ -0,0 +1,25 @@
+Delivered-To: incoming+gitlabhq/gitlabhq+auth_token@appmail.adventuretime.ooo
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+Delivered-To: support@adventuretime.ooo
+To: support@adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject: New Issue by email
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+The reply by email functionality should be extended to allow creating a new issue by email.
+
+* Allow an admin to specify which project the issue should be created under by checking the sender domain.
+* Possibly allow the use of regular expression matches within the subject/body to specify which project the issue should be created under.
diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb
index 28b8def331d..aa13b46cda9 100644
--- a/spec/helpers/submodule_helper_spec.rb
+++ b/spec/helpers/submodule_helper_spec.rb
@@ -105,6 +105,18 @@ describe SubmoduleHelper do
end
context 'submodule on unsupported' do
+ it 'sanitizes unsupported protocols' do
+ stub_url('javascript:alert("XSS");')
+
+ expect(helper.submodule_links(submodule_item)).to eq([nil, nil])
+ end
+
+ it 'sanitizes unsupported protocols disguised as a repository URL' do
+ stub_url('javascript:alert("XSS");foo/bar.git')
+
+ expect(helper.submodule_links(submodule_item)).to eq([nil, nil])
+ end
+
it 'returns original' do
stub_url('http://mygitserver.com/gitlab-org/gitlab-ce')
expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
diff --git a/spec/javascripts/autosave_spec.js b/spec/javascripts/autosave_spec.js
new file mode 100644
index 00000000000..9f9acc392c2
--- /dev/null
+++ b/spec/javascripts/autosave_spec.js
@@ -0,0 +1,134 @@
+import Autosave from '~/autosave';
+import AccessorUtilities from '~/lib/utils/accessor';
+
+describe('Autosave', () => {
+ let autosave;
+
+ describe('class constructor', () => {
+ const key = 'key';
+ const field = jasmine.createSpyObj('field', ['data', 'on']);
+
+ beforeEach(() => {
+ spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').and.returnValue(true);
+ spyOn(Autosave.prototype, 'restore');
+
+ autosave = new Autosave(field, key);
+ });
+
+ it('should set .isLocalStorageAvailable', () => {
+ expect(AccessorUtilities.isLocalStorageAccessSafe).toHaveBeenCalled();
+ expect(autosave.isLocalStorageAvailable).toBe(true);
+ });
+ });
+
+ describe('restore', () => {
+ const key = 'key';
+ const field = jasmine.createSpyObj('field', ['trigger']);
+
+ beforeEach(() => {
+ autosave = {
+ field,
+ key,
+ };
+
+ spyOn(window.localStorage, 'getItem');
+ });
+
+ describe('if .isLocalStorageAvailable is `false`', () => {
+ beforeEach(() => {
+ autosave.isLocalStorageAvailable = false;
+
+ Autosave.prototype.restore.call(autosave);
+ });
+
+ it('should not call .getItem', () => {
+ expect(window.localStorage.getItem).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('if .isLocalStorageAvailable is `true`', () => {
+ beforeEach(() => {
+ autosave.isLocalStorageAvailable = true;
+
+ Autosave.prototype.restore.call(autosave);
+ });
+
+ it('should call .getItem', () => {
+ expect(window.localStorage.getItem).toHaveBeenCalledWith(key);
+ });
+ });
+ });
+
+ describe('save', () => {
+ const field = jasmine.createSpyObj('field', ['val']);
+
+ beforeEach(() => {
+ autosave = jasmine.createSpyObj('autosave', ['reset']);
+ autosave.field = field;
+
+ field.val.and.returnValue('value');
+
+ spyOn(window.localStorage, 'setItem');
+ });
+
+ describe('if .isLocalStorageAvailable is `false`', () => {
+ beforeEach(() => {
+ autosave.isLocalStorageAvailable = false;
+
+ Autosave.prototype.save.call(autosave);
+ });
+
+ it('should not call .setItem', () => {
+ expect(window.localStorage.setItem).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('if .isLocalStorageAvailable is `true`', () => {
+ beforeEach(() => {
+ autosave.isLocalStorageAvailable = true;
+
+ Autosave.prototype.save.call(autosave);
+ });
+
+ it('should call .setItem', () => {
+ expect(window.localStorage.setItem).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('reset', () => {
+ const key = 'key';
+
+ beforeEach(() => {
+ autosave = {
+ key,
+ };
+
+ spyOn(window.localStorage, 'removeItem');
+ });
+
+ describe('if .isLocalStorageAvailable is `false`', () => {
+ beforeEach(() => {
+ autosave.isLocalStorageAvailable = false;
+
+ Autosave.prototype.reset.call(autosave);
+ });
+
+ it('should not call .removeItem', () => {
+ expect(window.localStorage.removeItem).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('if .isLocalStorageAvailable is `true`', () => {
+ beforeEach(() => {
+ autosave.isLocalStorageAvailable = true;
+
+ Autosave.prototype.reset.call(autosave);
+ });
+
+ it('should call .removeItem', () => {
+ expect(window.localStorage.removeItem).toHaveBeenCalledWith(key);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js b/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js
new file mode 100644
index 00000000000..1ed96a67478
--- /dev/null
+++ b/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js
@@ -0,0 +1,47 @@
+import { getUnicodeSupportMap } from '~/behaviors/gl_emoji/unicode_support_map';
+import AccessorUtilities from '~/lib/utils/accessor';
+
+describe('Unicode Support Map', () => {
+ describe('getUnicodeSupportMap', () => {
+ const stringSupportMap = 'stringSupportMap';
+
+ beforeEach(() => {
+ spyOn(AccessorUtilities, 'isLocalStorageAccessSafe');
+ spyOn(window.localStorage, 'getItem');
+ spyOn(window.localStorage, 'setItem');
+ spyOn(JSON, 'parse');
+ spyOn(JSON, 'stringify').and.returnValue(stringSupportMap);
+ });
+
+ describe('if isLocalStorageAvailable is `true`', function () {
+ beforeEach(() => {
+ AccessorUtilities.isLocalStorageAccessSafe.and.returnValue(true);
+
+ getUnicodeSupportMap();
+ });
+
+ it('should call .getItem and .setItem', () => {
+ const allArgs = window.localStorage.setItem.calls.allArgs();
+
+ expect(window.localStorage.getItem).toHaveBeenCalledWith('gl-emoji-user-agent');
+ expect(allArgs[0][0]).toBe('gl-emoji-user-agent');
+ expect(allArgs[0][1]).toBe(navigator.userAgent);
+ expect(allArgs[1][0]).toBe('gl-emoji-unicode-support-map');
+ expect(allArgs[1][1]).toBe(stringSupportMap);
+ });
+ });
+
+ describe('if isLocalStorageAvailable is `false`', function () {
+ beforeEach(() => {
+ AccessorUtilities.isLocalStorageAccessSafe.and.returnValue(false);
+
+ getUnicodeSupportMap();
+ });
+
+ it('should not call .getItem or .setItem', () => {
+ expect(window.localStorage.getItem.calls.count()).toBe(1);
+ expect(window.localStorage.setItem).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/droplab/constants_spec.js b/spec/javascripts/droplab/constants_spec.js
index fd153a49fcd..b9d28db74cc 100644
--- a/spec/javascripts/droplab/constants_spec.js
+++ b/spec/javascripts/droplab/constants_spec.js
@@ -27,6 +27,12 @@ describe('constants', function () {
});
});
+ describe('TEMPLATE_REGEX', function () {
+ it('should be a handlebars templating syntax regex', function() {
+ expect(constants.TEMPLATE_REGEX).toEqual(/\{\{(.+?)\}\}/g);
+ });
+ });
+
describe('IGNORE_CLASS', function () {
it('should be `droplab-item-ignore`', function() {
expect(constants.IGNORE_CLASS).toBe('droplab-item-ignore');
diff --git a/spec/javascripts/droplab/drop_down_spec.js b/spec/javascripts/droplab/drop_down_spec.js
index 7516b301917..e7786e8cc2c 100644
--- a/spec/javascripts/droplab/drop_down_spec.js
+++ b/spec/javascripts/droplab/drop_down_spec.js
@@ -451,7 +451,7 @@ describe('DropDown', function () {
this.html = 'html';
this.template = { firstChild: { outerHTML: 'outerHTML', style: {} } };
- spyOn(utils, 't').and.returnValue(this.html);
+ spyOn(utils, 'template').and.returnValue(this.html);
spyOn(document, 'createElement').and.returnValue(this.template);
spyOn(this.dropdown, 'setImagesSrc');
@@ -459,7 +459,7 @@ describe('DropDown', function () {
});
it('should call utils.t with .templateString and data', function () {
- expect(utils.t).toHaveBeenCalledWith(this.templateString, this.data);
+ expect(utils.template).toHaveBeenCalledWith(this.templateString, this.data);
});
it('should call document.createElement', function () {
diff --git a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js
index 2722882375f..d0f09a561d5 100644
--- a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js
+++ b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js
@@ -76,6 +76,26 @@ describe('RecentSearchesDropdownContent', () => {
});
});
+ describe('if isLocalStorageAvailable is `false`', () => {
+ let el;
+
+ beforeEach(() => {
+ const props = Object.assign({ isLocalStorageAvailable: false }, propsDataWithItems);
+
+ vm = createComponent(props);
+ el = vm.$el;
+ });
+
+ it('should render an info note', () => {
+ const note = el.querySelector('.dropdown-info-note');
+ const items = el.querySelectorAll('.filtered-search-history-dropdown-item');
+
+ expect(note).toBeDefined();
+ expect(note.innerText.trim()).toBe('This feature requires local storage to be enabled');
+ expect(items.length).toEqual(propsDataWithoutItems.items.length);
+ });
+ });
+
describe('computed', () => {
describe('processedItems', () => {
it('with items', () => {
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
index 97af681429b..2f2ead87495 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
@@ -1,3 +1,7 @@
+import * as recentSearchesStoreSrc from '~/filtered_search/stores/recent_searches_store';
+import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
+import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
+
require('~/lib/utils/url_utility');
require('~/lib/utils/common_utils');
require('~/filtered_search/filtered_search_token_keys');
@@ -57,6 +61,36 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
manager.cleanup();
});
+ describe('class constructor', () => {
+ const isLocalStorageAvailable = 'isLocalStorageAvailable';
+ let filteredSearchManager;
+
+ beforeEach(() => {
+ spyOn(RecentSearchesService, 'isAvailable').and.returnValue(isLocalStorageAvailable);
+ spyOn(recentSearchesStoreSrc, 'default');
+
+ filteredSearchManager = new gl.FilteredSearchManager();
+
+ return filteredSearchManager;
+ });
+
+ it('should instantiate RecentSearchesStore with isLocalStorageAvailable', () => {
+ expect(RecentSearchesService.isAvailable).toHaveBeenCalled();
+ expect(recentSearchesStoreSrc.default).toHaveBeenCalledWith({
+ isLocalStorageAvailable,
+ });
+ });
+
+ it('should not instantiate Flash if an RecentSearchesServiceError is caught', () => {
+ spyOn(RecentSearchesService.prototype, 'fetch').and.callFake(() => Promise.reject(new RecentSearchesServiceError()));
+ spyOn(window, 'Flash');
+
+ filteredSearchManager = new gl.FilteredSearchManager();
+
+ expect(window.Flash).not.toHaveBeenCalled();
+ });
+ });
+
describe('search', () => {
const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened';
diff --git a/spec/javascripts/filtered_search/recent_searches_root_spec.js b/spec/javascripts/filtered_search/recent_searches_root_spec.js
new file mode 100644
index 00000000000..d8ba6de5f45
--- /dev/null
+++ b/spec/javascripts/filtered_search/recent_searches_root_spec.js
@@ -0,0 +1,31 @@
+import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
+import * as vueSrc from 'vue';
+
+describe('RecentSearchesRoot', () => {
+ describe('render', () => {
+ let recentSearchesRoot;
+ let data;
+ let template;
+
+ beforeEach(() => {
+ recentSearchesRoot = {
+ store: {
+ state: 'state',
+ },
+ };
+
+ spyOn(vueSrc, 'default').and.callFake((options) => {
+ data = options.data;
+ template = options.template;
+ });
+
+ RecentSearchesRoot.prototype.render.call(recentSearchesRoot);
+ });
+
+ it('should instantiate Vue', () => {
+ expect(vueSrc.default).toHaveBeenCalled();
+ expect(data()).toBe(recentSearchesRoot.store.state);
+ expect(template).toContain(':is-local-storage-available="isLocalStorageAvailable"');
+ });
+ });
+});
diff --git a/spec/javascripts/filtered_search/services/recent_searches_service_error_spec.js b/spec/javascripts/filtered_search/services/recent_searches_service_error_spec.js
new file mode 100644
index 00000000000..ea7c146fa4f
--- /dev/null
+++ b/spec/javascripts/filtered_search/services/recent_searches_service_error_spec.js
@@ -0,0 +1,18 @@
+import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
+
+describe('RecentSearchesServiceError', () => {
+ let recentSearchesServiceError;
+
+ beforeEach(() => {
+ recentSearchesServiceError = new RecentSearchesServiceError();
+ });
+
+ it('instantiates an instance of RecentSearchesServiceError and not an Error', () => {
+ expect(recentSearchesServiceError).toEqual(jasmine.any(RecentSearchesServiceError));
+ expect(recentSearchesServiceError.name).toBe('RecentSearchesServiceError');
+ });
+
+ it('should set a default message', () => {
+ expect(recentSearchesServiceError.message).toBe('Recent Searches Service is unavailable');
+ });
+});
diff --git a/spec/javascripts/filtered_search/services/recent_searches_service_spec.js b/spec/javascripts/filtered_search/services/recent_searches_service_spec.js
index 2a58fb3a7df..c64d4374680 100644
--- a/spec/javascripts/filtered_search/services/recent_searches_service_spec.js
+++ b/spec/javascripts/filtered_search/services/recent_searches_service_spec.js
@@ -1,4 +1,5 @@
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
+import AccessorUtilities from '~/lib/utils/accessor';
describe('RecentSearchesService', () => {
let service;
@@ -9,6 +10,10 @@ describe('RecentSearchesService', () => {
});
describe('fetch', () => {
+ beforeEach(() => {
+ spyOn(RecentSearchesService, 'isAvailable').and.returnValue(true);
+ });
+
it('should default to empty array', (done) => {
const fetchItemsPromise = service.fetch();
@@ -27,11 +32,21 @@ describe('RecentSearchesService', () => {
const fetchItemsPromise = service.fetch();
fetchItemsPromise
- .catch(() => {
+ .catch((error) => {
+ expect(error).toEqual(jasmine.any(SyntaxError));
done();
});
});
+ it('should reject when service is unavailable', (done) => {
+ RecentSearchesService.isAvailable.and.returnValue(false);
+
+ service.fetch().catch((error) => {
+ expect(error).toEqual(jasmine.any(Error));
+ done();
+ });
+ });
+
it('should return items from localStorage', (done) => {
window.localStorage.setItem(service.localStorageKey, '["foo", "bar"]');
const fetchItemsPromise = service.fetch();
@@ -42,15 +57,89 @@ describe('RecentSearchesService', () => {
done();
});
});
+
+ describe('if .isAvailable returns `false`', () => {
+ beforeEach(() => {
+ RecentSearchesService.isAvailable.and.returnValue(false);
+
+ spyOn(window.localStorage, 'getItem');
+
+ RecentSearchesService.prototype.fetch();
+ });
+
+ it('should not call .getItem', () => {
+ expect(window.localStorage.getItem).not.toHaveBeenCalled();
+ });
+ });
});
describe('setRecentSearches', () => {
+ beforeEach(() => {
+ spyOn(RecentSearchesService, 'isAvailable').and.returnValue(true);
+ });
+
it('should save things in localStorage', () => {
const items = ['foo', 'bar'];
service.save(items);
- const newLocalStorageValue =
- window.localStorage.getItem(service.localStorageKey);
+ const newLocalStorageValue = window.localStorage.getItem(service.localStorageKey);
expect(JSON.parse(newLocalStorageValue)).toEqual(items);
});
});
+
+ describe('save', () => {
+ beforeEach(() => {
+ spyOn(window.localStorage, 'setItem');
+ spyOn(RecentSearchesService, 'isAvailable');
+ });
+
+ describe('if .isAvailable returns `true`', () => {
+ const searchesString = 'searchesString';
+ const localStorageKey = 'localStorageKey';
+ const recentSearchesService = {
+ localStorageKey,
+ };
+
+ beforeEach(() => {
+ RecentSearchesService.isAvailable.and.returnValue(true);
+
+ spyOn(JSON, 'stringify').and.returnValue(searchesString);
+
+ RecentSearchesService.prototype.save.call(recentSearchesService);
+ });
+
+ it('should call .setItem', () => {
+ expect(window.localStorage.setItem).toHaveBeenCalledWith(localStorageKey, searchesString);
+ });
+ });
+
+ describe('if .isAvailable returns `false`', () => {
+ beforeEach(() => {
+ RecentSearchesService.isAvailable.and.returnValue(false);
+
+ RecentSearchesService.prototype.save();
+ });
+
+ it('should not call .setItem', () => {
+ expect(window.localStorage.setItem).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('isAvailable', () => {
+ let isAvailable;
+
+ beforeEach(() => {
+ spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').and.callThrough();
+
+ isAvailable = RecentSearchesService.isAvailable();
+ });
+
+ it('should call .isLocalStorageAccessSafe', () => {
+ expect(AccessorUtilities.isLocalStorageAccessSafe).toHaveBeenCalled();
+ });
+
+ it('should return a boolean', () => {
+ expect(typeof isAvailable).toBe('boolean');
+ });
+ });
});
diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js
index c207fb00a47..eb532dff5a1 100644
--- a/spec/javascripts/gl_dropdown_spec.js
+++ b/spec/javascripts/gl_dropdown_spec.js
@@ -44,21 +44,18 @@ require('~/lib/utils/url_utility');
preloadFixtures('static/gl_dropdown.html.raw');
loadJSONFixtures('projects.json');
- function initDropDown(hasRemote, isFilterable) {
- this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({
+ function initDropDown(hasRemote, isFilterable, extraOpts = {}) {
+ const options = Object.assign({
selectable: true,
filterable: isFilterable,
data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData,
search: {
fields: ['name']
},
- text: (project) => {
- (project.name_with_namespace || project.name);
- },
- id: (project) => {
- project.id;
- }
- });
+ text: project => (project.name_with_namespace || project.name),
+ id: project => project.id,
+ }, extraOpts);
+ this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown(options);
}
beforeEach(() => {
@@ -80,6 +77,37 @@ require('~/lib/utils/url_utility');
expect(this.dropdownContainerElement).toHaveClass('open');
});
+ it('escapes HTML as text', () => {
+ this.projectsData[0].name_with_namespace = '<script>alert("testing");</script>';
+
+ initDropDown.call(this, false);
+
+ this.dropdownButtonElement.click();
+
+ expect(
+ $('.dropdown-content li:first-child').text(),
+ ).toBe('<script>alert("testing");</script>');
+ });
+
+ it('should output HTML when highlighting', () => {
+ this.projectsData[0].name_with_namespace = 'testing';
+ $('.dropdown-input .dropdown-input-field').val('test');
+
+ initDropDown.call(this, false, true, {
+ highlight: true,
+ });
+
+ this.dropdownButtonElement.click();
+
+ expect(
+ $('.dropdown-content li:first-child').text(),
+ ).toBe('testing');
+
+ expect(
+ $('.dropdown-content li:first-child a').html(),
+ ).toBe('<b>t</b><b>e</b><b>s</b><b>t</b>ing');
+ });
+
describe('that is open', () => {
beforeEach(() => {
initDropDown.call(this, false, false);
diff --git a/spec/javascripts/lib/utils/accessor_spec.js b/spec/javascripts/lib/utils/accessor_spec.js
new file mode 100644
index 00000000000..b768d6f2a68
--- /dev/null
+++ b/spec/javascripts/lib/utils/accessor_spec.js
@@ -0,0 +1,78 @@
+import AccessorUtilities from '~/lib/utils/accessor';
+
+describe('AccessorUtilities', () => {
+ const testError = new Error('test error');
+
+ describe('isPropertyAccessSafe', () => {
+ let base;
+
+ it('should return `true` if access is safe', () => {
+ base = { testProp: 'testProp' };
+
+ expect(AccessorUtilities.isPropertyAccessSafe(base, 'testProp')).toBe(true);
+ });
+
+ it('should return `false` if access throws an error', () => {
+ base = { get testProp() { throw testError; } };
+
+ expect(AccessorUtilities.isPropertyAccessSafe(base, 'testProp')).toBe(false);
+ });
+
+ it('should return `false` if property is undefined', () => {
+ base = {};
+
+ expect(AccessorUtilities.isPropertyAccessSafe(base, 'testProp')).toBe(false);
+ });
+ });
+
+ describe('isFunctionCallSafe', () => {
+ const base = {};
+
+ it('should return `true` if calling is safe', () => {
+ base.func = () => {};
+
+ expect(AccessorUtilities.isFunctionCallSafe(base, 'func')).toBe(true);
+ });
+
+ it('should return `false` if calling throws an error', () => {
+ base.func = () => { throw new Error('test error'); };
+
+ expect(AccessorUtilities.isFunctionCallSafe(base, 'func')).toBe(false);
+ });
+
+ it('should return `false` if function is undefined', () => {
+ base.func = undefined;
+
+ expect(AccessorUtilities.isFunctionCallSafe(base, 'func')).toBe(false);
+ });
+ });
+
+ describe('isLocalStorageAccessSafe', () => {
+ beforeEach(() => {
+ spyOn(window.localStorage, 'setItem');
+ spyOn(window.localStorage, 'removeItem');
+ });
+
+ it('should return `true` if access is safe', () => {
+ expect(AccessorUtilities.isLocalStorageAccessSafe()).toBe(true);
+ });
+
+ it('should return `false` if access to .setItem isnt safe', () => {
+ window.localStorage.setItem.and.callFake(() => { throw testError; });
+
+ expect(AccessorUtilities.isLocalStorageAccessSafe()).toBe(false);
+ });
+
+ it('should set a test item if access is safe', () => {
+ AccessorUtilities.isLocalStorageAccessSafe();
+
+ expect(window.localStorage.setItem).toHaveBeenCalledWith('isLocalStorageAccessSafe', 'true');
+ });
+
+ it('should remove the test item if access is safe', () => {
+ AccessorUtilities.isLocalStorageAccessSafe();
+
+ expect(window.localStorage.removeItem).toHaveBeenCalledWith('isLocalStorageAccessSafe');
+ });
+ });
+});
diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js b/spec/javascripts/signin_tabs_memoizer_spec.js
index d83d9a57b42..5b4f5933b34 100644
--- a/spec/javascripts/signin_tabs_memoizer_spec.js
+++ b/spec/javascripts/signin_tabs_memoizer_spec.js
@@ -1,3 +1,5 @@
+import AccessorUtilities from '~/lib/utils/accessor';
+
require('~/signin_tabs_memoizer');
((global) => {
@@ -19,6 +21,8 @@ require('~/signin_tabs_memoizer');
beforeEach(() => {
loadFixtures(fixtureTemplate);
+
+ spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').and.returnValue(true);
});
it('does nothing if no tab was previously selected', () => {
@@ -49,5 +53,91 @@ require('~/signin_tabs_memoizer');
expect(memo.readData()).toEqual('#standard');
});
+
+ describe('class constructor', () => {
+ beforeEach(() => {
+ memo = createMemoizer();
+ });
+
+ it('should set .isLocalStorageAvailable', () => {
+ expect(AccessorUtilities.isLocalStorageAccessSafe).toHaveBeenCalled();
+ expect(memo.isLocalStorageAvailable).toBe(true);
+ });
+ });
+
+ describe('saveData', () => {
+ beforeEach(() => {
+ memo = {
+ currentTabKey,
+ };
+
+ spyOn(localStorage, 'setItem');
+ });
+
+ describe('if .isLocalStorageAvailable is `false`', () => {
+ beforeEach(function () {
+ memo.isLocalStorageAvailable = false;
+
+ global.ActiveTabMemoizer.prototype.saveData.call(memo);
+ });
+
+ it('should not call .setItem', () => {
+ expect(localStorage.setItem).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('if .isLocalStorageAvailable is `true`', () => {
+ const value = 'value';
+
+ beforeEach(function () {
+ memo.isLocalStorageAvailable = true;
+
+ global.ActiveTabMemoizer.prototype.saveData.call(memo, value);
+ });
+
+ it('should call .setItem', () => {
+ expect(localStorage.setItem).toHaveBeenCalledWith(currentTabKey, value);
+ });
+ });
+ });
+
+ describe('readData', () => {
+ const itemValue = 'itemValue';
+ let readData;
+
+ beforeEach(() => {
+ memo = {
+ currentTabKey,
+ };
+
+ spyOn(localStorage, 'getItem').and.returnValue(itemValue);
+ });
+
+ describe('if .isLocalStorageAvailable is `false`', () => {
+ beforeEach(function () {
+ memo.isLocalStorageAvailable = false;
+
+ readData = global.ActiveTabMemoizer.prototype.readData.call(memo);
+ });
+
+ it('should not call .getItem and should return `null`', () => {
+ expect(localStorage.getItem).not.toHaveBeenCalled();
+ expect(readData).toBe(null);
+ });
+ });
+
+ describe('if .isLocalStorageAvailable is `true`', () => {
+ beforeEach(function () {
+ memo.isLocalStorageAvailable = true;
+
+ readData = global.ActiveTabMemoizer.prototype.readData.call(memo);
+ });
+
+ it('should call .getItem and return the localStorage value', () => {
+ expect(window.localStorage.getItem).toHaveBeenCalledWith(currentTabKey);
+ expect(readData).toBe(itemValue);
+ });
+ });
+ });
});
})(window);
diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb
index 0864bc7258d..809fda11879 100644
--- a/spec/lib/gitlab/ci/cron_parser_spec.rb
+++ b/spec/lib/gitlab/ci/cron_parser_spec.rb
@@ -60,14 +60,60 @@ describe Gitlab::Ci::CronParser do
end
end
- context 'when cron_timezone is US/Pacific' do
- let(:cron) { '0 0 * * *' }
- let(:cron_timezone) { 'US/Pacific' }
+ context 'when cron_timezone is TZInfo format' do
+ before do
+ allow(Time).to receive(:zone)
+ .and_return(ActiveSupport::TimeZone['UTC'])
+ end
- it_behaves_like "returns time in the future"
+ let(:hour_in_utc) do
+ ActiveSupport::TimeZone[cron_timezone]
+ .now.change(hour: 0).in_time_zone('UTC').hour
+ end
+
+ context 'when cron_timezone is US/Pacific' do
+ let(:cron) { '* 0 * * *' }
+ let(:cron_timezone) { 'US/Pacific' }
+
+ it_behaves_like "returns time in the future"
+
+ it 'converts time in server time zone' do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
+ end
+ end
+
+ context 'when cron_timezone is ActiveSupport::TimeZone format' do
+ before do
+ allow(Time).to receive(:zone)
+ .and_return(ActiveSupport::TimeZone['UTC'])
+ end
+
+ let(:hour_in_utc) do
+ ActiveSupport::TimeZone[cron_timezone]
+ .now.change(hour: 0).in_time_zone('UTC').hour
+ end
+
+ context 'when cron_timezone is Berlin' do
+ let(:cron) { '* 0 * * *' }
+ let(:cron_timezone) { 'Berlin' }
+
+ it_behaves_like "returns time in the future"
+
+ it 'converts time in server time zone' do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
+ end
- it 'converts time in server time zone' do
- expect(subject.hour).to eq((Time.zone.now.in_time_zone(cron_timezone).utc_offset / 60 / 60).abs)
+ context 'when cron_timezone is Eastern Time (US & Canada)' do
+ let(:cron) { '* 0 * * *' }
+ let(:cron_timezone) { 'Eastern Time (US & Canada)' }
+
+ it_behaves_like "returns time in the future"
+
+ it 'converts time in server time zone' do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
end
end
end
@@ -76,9 +122,21 @@ describe Gitlab::Ci::CronParser do
let(:cron) { 'invalid_cron' }
let(:cron_timezone) { 'invalid_cron_timezone' }
- it 'returns nil' do
- is_expected.to be_nil
- end
+ it { is_expected.to be_nil }
+ end
+
+ context 'when cron syntax is quoted' do
+ let(:cron) { "'0 * * * *'" }
+ let(:cron_timezone) { 'UTC' }
+
+ it { expect(subject).to be_nil }
+ end
+
+ context 'when cron syntax is rufus-scheduler syntax' do
+ let(:cron) { 'every 3h' }
+ let(:cron_timezone) { 'UTC' }
+
+ it { expect(subject).to be_nil }
end
end
@@ -96,6 +154,12 @@ describe Gitlab::Ci::CronParser do
it { is_expected.to eq(false) }
end
+
+ context 'when cron syntax is quoted' do
+ let(:cron) { "'0 * * * *'" }
+
+ it { is_expected.to eq(false) }
+ end
end
describe '#cron_timezone_valid?' do
@@ -112,5 +176,11 @@ describe Gitlab::Ci::CronParser do
it { is_expected.to eq(false) }
end
+
+ context 'when cron_timezone is ActiveSupport::TimeZone format' do
+ let(:cron_timezone) { 'Eastern Time (US & Canada)' }
+
+ it { is_expected.to eq(true) }
+ end
end
end
diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb
index 2a86b427806..c6e3524f743 100644
--- a/spec/lib/gitlab/email/receiver_spec.rb
+++ b/spec/lib/gitlab/email/receiver_spec.rb
@@ -4,12 +4,38 @@ require_relative 'email_shared_blocks'
describe Gitlab::Email::Receiver, lib: true do
include_context :email_shared_context
+ context "when the email contains a valid email address in a Delivered-To header" do
+ let(:email_raw) { fixture_file('emails/forwarded_new_issue.eml') }
+ let(:handler) { double(:handler) }
+
+ before do
+ stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
+
+ allow(handler).to receive(:execute)
+ allow(handler).to receive(:metrics_params)
+ end
+
+ it "finds the mail key" do
+ expect(Gitlab::Email::Handler).to receive(:for).with(an_instance_of(Mail::Message), 'gitlabhq/gitlabhq+auth_token').and_return(handler)
+
+ receiver.execute
+ end
+ end
+
context "when we cannot find a capable handler" do
let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "!!!") }
- it "raises a UnknownIncomingEmail" do
+ it "raises an UnknownIncomingEmail error" do
expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail)
end
+
+ context "and the email contains no references header" do
+ let(:email_raw) { fixture_file("emails/auto_reply.eml").gsub(mail_key, "!!!") }
+
+ it "raises an UnknownIncomingEmail error" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail)
+ end
+ end
end
context "when the email is blank" do
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 02ac337ebb6..90c287cac9e 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -1044,7 +1044,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
it "allows ordering by date" do
- expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_DATE)
+ expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_DATE | Rugged::SORT_TOPO)
repository.find_commits(order: :date)
end
diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb
index c5ce06afd73..42f3fc59f04 100644
--- a/spec/lib/gitlab/import_export/fork_spec.rb
+++ b/spec/lib/gitlab/import_export/fork_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'forked project import', services: true do
let(:user) { create(:user) }
let!(:project_with_repo) { create(:project, :test_repo, name: 'test-repo-restorer', path: 'test-repo-restorer') }
- let!(:project) { create(:empty_project) }
+ let!(:project) { create(:empty_project, name: 'test-repo-restorer-no-repo', path: 'test-repo-restorer-no-repo') }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
let(:forked_from_project) { create(:project) }
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 7a0b0b06d4b..e1f606c59f8 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -2,6 +2,7 @@
"description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
"visibility_level": 10,
"archived": false,
+ "description_html": "description",
"labels": [
{
"id": 2,
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index 0e9607c5bd3..14338515892 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -30,6 +30,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::ENABLED)
end
+ it 'has the project html description' do
+ expect(Project.find_by_path('project').description_html).to eq('description')
+ end
+
it 'has the same label associated to two issues' do
expect(ProjectLabel.find_by_title('test2').issues.count).to eq(2)
end
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index d2d89e3b019..1035428b2e7 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:user) { create(:user) }
- let(:project) { setup_project }
+ let!(:project) { setup_project }
before do
project.team << [user, :master]
@@ -189,6 +189,16 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
end
end
end
+
+ context 'project attributes' do
+ it 'contains the html description' do
+ expect(saved_project_json).to include("description_html" => 'description')
+ end
+
+ it 'does not contain the runners token' do
+ expect(saved_project_json).not_to include("runners_token" => 'token')
+ end
+ end
end
end
@@ -209,6 +219,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
releases: [release],
group: group
)
+ project.update_column(:description_html, 'description')
project_label = create(:label, project: project)
group_label = create(:group_label, group: group)
create(:label_link, label: project_label, target: issue)
diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb
index 48d74b07e27..d700af142be 100644
--- a/spec/lib/gitlab/import_export/reader_spec.rb
+++ b/spec/lib/gitlab/import_export/reader_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::ImportExport::Reader, lib: true do
let(:test_config) { 'spec/support/import_export/import_export.yml' }
let(:project_tree_hash) do
{
- only: [:name, :path],
+ except: [:id, :created_at],
include: [:issues, :labels,
{ merge_requests: {
only: [:id],
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 0372e3f7dbf..ebfaab4eacd 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -329,6 +329,28 @@ Project:
- snippets_enabled
- visibility_level
- archived
+- created_at
+- updated_at
+- last_activity_at
+- star_count
+- ci_id
+- shared_runners_enabled
+- build_coverage_regex
+- build_allow_git_fetchs
+- build_timeout
+- pending_delete
+- public_builds
+- last_repository_check_failed
+- last_repository_check_at
+- container_registry_enabled
+- only_allow_merge_if_pipeline_succeeds
+- has_external_issue_tracker
+- request_access_enabled
+- has_external_wiki
+- only_allow_merge_if_all_discussions_are_resolved
+- auto_cancel_pending_pipelines
+- printing_merge_request_link_enabled
+- build_allow_git_fetch
Author:
- name
ProjectFeature:
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index e0ebea63eb4..8e6227d4bec 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -22,8 +22,37 @@ describe Gitlab::ProjectSearchResults, lib: true do
end
describe 'blob search' do
- let(:project) { create(:project, :repository) }
- let(:results) { described_class.new(user, project, 'files').objects('blobs') }
+ let(:project) { create(:project, :public, :repository) }
+
+ subject(:results) { described_class.new(user, project, 'files').objects('blobs') }
+
+ context 'when repository is disabled' do
+ let(:project) { create(:project, :public, :repository, :repository_disabled) }
+
+ it 'hides blobs from members' do
+ project.add_reporter(user)
+
+ is_expected.to be_empty
+ end
+
+ it 'hides blobs from non-members' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when repository is internal' do
+ let(:project) { create(:project, :public, :repository, :repository_private) }
+
+ it 'finds blobs for members' do
+ project.add_reporter(user)
+
+ is_expected.not_to be_empty
+ end
+
+ it 'hides blobs from non-members' do
+ is_expected.to be_empty
+ end
+ end
it 'finds by name' do
expect(results).to include(["files/images/wm.svg", nil])
@@ -70,6 +99,46 @@ describe Gitlab::ProjectSearchResults, lib: true do
end
end
+ describe 'wiki search' do
+ let(:project) { create(:project, :public) }
+ let(:wiki) { build(:project_wiki, project: project) }
+ let!(:wiki_page) { wiki.create_page('Title', 'Content') }
+
+ subject(:results) { described_class.new(user, project, 'Content').objects('wiki_blobs') }
+
+ context 'when wiki is disabled' do
+ let(:project) { create(:project, :public, :wiki_disabled) }
+
+ it 'hides wiki blobs from members' do
+ project.add_reporter(user)
+
+ is_expected.to be_empty
+ end
+
+ it 'hides wiki blobs from non-members' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when wiki is internal' do
+ let(:project) { create(:project, :public, :wiki_private) }
+
+ it 'finds wiki blobs for members' do
+ project.add_reporter(user)
+
+ is_expected.not_to be_empty
+ end
+
+ it 'hides wiki blobs from non-members' do
+ is_expected.to be_empty
+ end
+ end
+
+ it 'finds by content' do
+ expect(results).to include("master:Title.md:1:Content\n")
+ end
+ end
+
it 'does not list issues on private projects' do
issue = create(:issue, project: project)
@@ -79,7 +148,6 @@ describe Gitlab::ProjectSearchResults, lib: true do
end
describe 'confidential issues' do
- let(:project) { create(:empty_project) }
let(:query) { 'issue' }
let(:author) { create(:user) }
let(:assignee) { create(:user) }
@@ -277,6 +345,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
context 'by commit hash' do
let(:project) { create(:project, :public, :repository) }
let(:commit) { project.repository.commit('0b4bc9a') }
+
commit_hashes = { short: '0b4bc9a', full: '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
commit_hashes.each do |type, commit_hash|
diff --git a/spec/models/ci/trigger_schedule_spec.rb b/spec/models/ci/trigger_schedule_spec.rb
index 75d21541cee..92447564d7c 100644
--- a/spec/models/ci/trigger_schedule_spec.rb
+++ b/spec/models/ci/trigger_schedule_spec.rb
@@ -73,4 +73,36 @@ describe Ci::TriggerSchedule, models: true do
end
end
end
+
+ describe '#real_next_run' do
+ subject do
+ Ci::TriggerSchedule.last.real_next_run(worker_cron: worker_cron,
+ worker_time_zone: worker_time_zone)
+ end
+
+ context 'when GitLab time_zone is UTC' do
+ before do
+ allow(Time).to receive(:zone)
+ .and_return(ActiveSupport::TimeZone[worker_time_zone])
+ end
+
+ let(:worker_time_zone) { 'UTC' }
+
+ context 'when cron_timezone is Eastern Time (US & Canada)' do
+ before do
+ create(:ci_trigger_schedule, :nightly,
+ cron_timezone: 'Eastern Time (US & Canada)')
+ end
+
+ let(:worker_cron) { '0 1 2 3 *' }
+
+ it 'returns the next time worker executes' do
+ expect(subject.min).to eq(0)
+ expect(subject.hour).to eq(1)
+ expect(subject.day).to eq(2)
+ expect(subject.month).to eq(3)
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/network/graph_spec.rb b/spec/models/network/graph_spec.rb
index 46b36e11c23..0fe8a591a45 100644
--- a/spec/models/network/graph_spec.rb
+++ b/spec/models/network/graph_spec.rb
@@ -10,17 +10,17 @@ describe Network::Graph, models: true do
expect(graph.notes).to eq( { note_on_commit.commit_id => 1 } )
end
- describe "#commits" do
+ describe '#commits' do
let(:graph) { described_class.new(project, 'refs/heads/master', project.repository.commit, nil) }
- it "returns a list of commits" do
+ it 'returns a list of commits' do
commits = graph.commits
expect(commits).not_to be_empty
expect(commits).to all( be_kind_of(Network::Commit) )
end
- it "sorts the commits by commit date (descending)" do
+ it 'it the commits by commit date (descending)' do
# Remove duplicate timestamps because they make it harder to
# assert that the commits are sorted as expected.
commits = graph.commits.uniq(&:date)
@@ -29,5 +29,20 @@ describe Network::Graph, models: true do
expect(commits).not_to be_empty
expect(commits.map(&:id)).to eq(sorted_commits.map(&:id))
end
+
+ it 'sorts children before parents for commits with the same timestamp' do
+ commits_by_time = graph.commits.group_by(&:date)
+
+ commits_by_time.each do |time, commits|
+ commit_ids = commits.map(&:id)
+
+ commits.each_with_index do |commit, index|
+ parent_indexes = commit.parent_ids.map { |parent_id| commit_ids.find_index(parent_id) }.compact
+
+ # All parents of the current commit should appear after it
+ expect(parent_indexes).to all( be > index )
+ end
+ end
+ end
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index c81f7a26eef..fbd9c33196c 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1880,4 +1880,23 @@ describe Project, models: true do
expect(project.pipeline_status).to be_loaded
end
end
+
+ describe '#append_or_update_attribute' do
+ let(:project) { create(:project) }
+
+ it 'shows full error updating an invalid MR' do
+ error_message = 'Failed to replace merge_requests because one or more of the new records could not be saved.'\
+ ' Validate fork Source project is not a fork of the target project'
+
+ expect { project.append_or_update_attribute(:merge_requests, [create(:merge_request)]) }.
+ to raise_error(ActiveRecord::RecordNotSaved, error_message)
+ end
+
+ it 'updates the project succesfully' do
+ merge_request = create(:merge_request, target_project: project, source_project: project)
+
+ expect { project.append_or_update_attribute(:merge_requests, [merge_request]) }.
+ not_to raise_error
+ end
+ end
end
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 8095d01b69e..ff1d4faeb9f 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -132,46 +132,6 @@ describe Snippet, models: true do
end
end
- describe '.accessible_to' do
- let(:author) { create(:author) }
- let(:project) { create(:empty_project) }
-
- let!(:public_snippet) { create(:snippet, :public) }
- let!(:internal_snippet) { create(:snippet, :internal) }
- let!(:private_snippet) { create(:snippet, :private, author: author) }
-
- let!(:project_public_snippet) { create(:snippet, :public, project: project) }
- let!(:project_internal_snippet) { create(:snippet, :internal, project: project) }
- let!(:project_private_snippet) { create(:snippet, :private, project: project) }
-
- it 'returns only public snippets when user is blank' do
- expect(described_class.accessible_to(nil)).to match_array [public_snippet, project_public_snippet]
- end
-
- it 'returns only public, and internal snippets for regular users' do
- user = create(:user)
-
- expect(described_class.accessible_to(user)).to match_array [public_snippet, internal_snippet, project_public_snippet, project_internal_snippet]
- end
-
- it 'returns public, internal snippets and project private snippets for project members' do
- member = create(:user)
- project.team << [member, :developer]
-
- expect(described_class.accessible_to(member)).to match_array [public_snippet, internal_snippet, project_public_snippet, project_internal_snippet, project_private_snippet]
- end
-
- it 'returns private snippets where the user is the author' do
- expect(described_class.accessible_to(author)).to match_array [public_snippet, internal_snippet, private_snippet, project_public_snippet, project_internal_snippet]
- end
-
- it 'returns all snippets when for admins' do
- admin = create(:admin)
-
- expect(described_class.accessible_to(admin)).to match_array [public_snippet, internal_snippet, private_snippet, project_public_snippet, project_internal_snippet, project_private_snippet]
- end
- end
-
describe '#participants' do
let(:project) { create(:empty_project, :public) }
let(:snippet) { create(:snippet, content: 'foo', project: project) }
diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb
index d0758af57dd..e1771b636b8 100644
--- a/spec/policies/project_snippet_policy_spec.rb
+++ b/spec/policies/project_snippet_policy_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe ProjectSnippetPolicy, models: true do
- let(:current_user) { create(:user) }
+ let(:regular_user) { create(:user) }
+ let(:external_user) { create(:user, :external) }
+ let(:project) { create(:empty_project) }
let(:author_permissions) do
[
@@ -10,13 +12,15 @@ describe ProjectSnippetPolicy, models: true do
]
end
- subject { described_class.abilities(current_user, project_snippet).to_set }
+ def abilities(user, snippet_visibility)
+ snippet = create(:project_snippet, snippet_visibility, project: project)
- context 'public snippet' do
- let(:project_snippet) { create(:project_snippet, :public) }
+ described_class.abilities(user, snippet).to_set
+ end
+ context 'public snippet' do
context 'no user' do
- let(:current_user) { nil }
+ subject { abilities(nil, :public) }
it do
is_expected.to include(:read_project_snippet)
@@ -25,6 +29,17 @@ describe ProjectSnippetPolicy, models: true do
end
context 'regular user' do
+ subject { abilities(regular_user, :public) }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'external user' do
+ subject { abilities(external_user, :public) }
+
it do
is_expected.to include(:read_project_snippet)
is_expected.not_to include(*author_permissions)
@@ -33,10 +48,8 @@ describe ProjectSnippetPolicy, models: true do
end
context 'internal snippet' do
- let(:project_snippet) { create(:project_snippet, :internal) }
-
context 'no user' do
- let(:current_user) { nil }
+ subject { abilities(nil, :internal) }
it do
is_expected.not_to include(:read_project_snippet)
@@ -45,6 +58,28 @@ describe ProjectSnippetPolicy, models: true do
end
context 'regular user' do
+ subject { abilities(regular_user, :internal) }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'external user' do
+ subject { abilities(external_user, :internal) }
+
+ it do
+ is_expected.not_to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'project team member external user' do
+ subject { abilities(external_user, :internal) }
+
+ before { project.team << [external_user, :developer] }
+
it do
is_expected.to include(:read_project_snippet)
is_expected.not_to include(*author_permissions)
@@ -53,10 +88,8 @@ describe ProjectSnippetPolicy, models: true do
end
context 'private snippet' do
- let(:project_snippet) { create(:project_snippet, :private) }
-
context 'no user' do
- let(:current_user) { nil }
+ subject { abilities(nil, :private) }
it do
is_expected.not_to include(:read_project_snippet)
@@ -65,6 +98,8 @@ describe ProjectSnippetPolicy, models: true do
end
context 'regular user' do
+ subject { abilities(regular_user, :private) }
+
it do
is_expected.not_to include(:read_project_snippet)
is_expected.not_to include(*author_permissions)
@@ -72,7 +107,9 @@ describe ProjectSnippetPolicy, models: true do
end
context 'snippet author' do
- let(:project_snippet) { create(:project_snippet, :private, author: current_user) }
+ let(:snippet) { create(:project_snippet, :private, author: regular_user) }
+
+ subject { described_class.abilities(regular_user, snippet).to_set }
it do
is_expected.to include(:read_project_snippet)
@@ -80,8 +117,21 @@ describe ProjectSnippetPolicy, models: true do
end
end
- context 'project team member' do
- before { project_snippet.project.team << [current_user, :developer] }
+ context 'project team member normal user' do
+ subject { abilities(regular_user, :private) }
+
+ before { project.team << [regular_user, :developer] }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'project team member external user' do
+ subject { abilities(external_user, :private) }
+
+ before { project.team << [external_user, :developer] }
it do
is_expected.to include(:read_project_snippet)
@@ -90,7 +140,7 @@ describe ProjectSnippetPolicy, models: true do
end
context 'admin user' do
- let(:current_user) { create(:admin) }
+ subject { abilities(create(:admin), :private) }
it do
is_expected.to include(:read_project_snippet)
diff --git a/spec/support/import_export/import_export.yml b/spec/support/import_export/import_export.yml
index 17136dee000..734d6838f4d 100644
--- a/spec/support/import_export/import_export.yml
+++ b/spec/support/import_export/import_export.yml
@@ -11,9 +11,6 @@ project_tree:
- :user
included_attributes:
- project:
- - :name
- - :path
merge_requests:
- :id
user:
@@ -21,4 +18,7 @@ included_attributes:
excluded_attributes:
merge_requests:
- - :iid \ No newline at end of file
+ - :iid
+ project:
+ - :id
+ - :created_at \ No newline at end of file