diff options
37 files changed, 869 insertions, 444 deletions
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue index 0ece64692ae..9d898d1a1a1 100644 --- a/app/assets/javascripts/frequent_items/components/frequent_items_list.vue +++ b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue @@ -58,7 +58,7 @@ export default { <template> <div class="frequent-items-list-container"> - <ul class="list-unstyled"> + <ul ref="frequentItemsList" class="list-unstyled"> <li v-if="isListEmpty" :class="{ 'section-failure': isFetchFailed }" class="section-empty"> {{ listEmptyMessage }} </li> diff --git a/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js b/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js index 432a9254558..98bcb8348e2 100644 --- a/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js +++ b/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js @@ -117,3 +117,23 @@ export const scaledSIFormatter = (unit = '', prefixOffset = 0) => { return scaledFormatter(units); }; + +/** + * Returns a function that formats a number scaled using SI units notation. + */ +export const scaledBinaryFormatter = (unit = '', prefixOffset = 0) => { + // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings + const multiplicative = ['Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi']; + const symbols = ['', ...multiplicative]; + + const units = symbols.slice(prefixOffset).map(prefix => { + return `${prefix}${unit}`; + }); + + if (!units.length) { + // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings + throw new RangeError('The unit cannot be converted, please try a different scale'); + } + + return scaledFormatter(units, 1024); +}; diff --git a/app/assets/javascripts/lib/utils/unit_format/index.js b/app/assets/javascripts/lib/utils/unit_format/index.js index daf70ebb5d7..d3aea37e677 100644 --- a/app/assets/javascripts/lib/utils/unit_format/index.js +++ b/app/assets/javascripts/lib/utils/unit_format/index.js @@ -1,9 +1,18 @@ import { s__ } from '~/locale'; -import { suffixFormatter, scaledSIFormatter, numberFormatter } from './formatter_factory'; +import { + suffixFormatter, + scaledSIFormatter, + scaledBinaryFormatter, + numberFormatter, +} from './formatter_factory'; /** * Supported formats + * + * Based on: + * + * https://tc39.es/proposal-unified-intl-numberformat/section6/locales-currencies-tz_proposed_out.html#sec-issanctionedsimpleunitidentifier */ export const SUPPORTED_FORMATS = { // Number @@ -13,15 +22,23 @@ export const SUPPORTED_FORMATS = { // Duration seconds: 'seconds', - miliseconds: 'miliseconds', + milliseconds: 'milliseconds', - // Digital - bytes: 'bytes', + // Digital (Metric) + decimalBytes: 'decimalBytes', kilobytes: 'kilobytes', megabytes: 'megabytes', gigabytes: 'gigabytes', terabytes: 'terabytes', petabytes: 'petabytes', + + // Digital (IEC) + bytes: 'bytes', + kibibytes: 'kibibytes', + mebibytes: 'mebibytes', + gibibytes: 'gibibytes', + tebibytes: 'tebibytes', + pebibytes: 'pebibytes', }; /** @@ -32,6 +49,7 @@ export const SUPPORTED_FORMATS = { */ export const getFormatter = (format = SUPPORTED_FORMATS.number) => { // Number + if (format === SUPPORTED_FORMATS.number) { /** * Formats a number @@ -70,6 +88,7 @@ export const getFormatter = (format = SUPPORTED_FORMATS.number) => { } // Durations + if (format === SUPPORTED_FORMATS.seconds) { /** * Formats a number of seconds @@ -82,9 +101,9 @@ export const getFormatter = (format = SUPPORTED_FORMATS.number) => { */ return suffixFormatter(s__('Units|s')); } - if (format === SUPPORTED_FORMATS.miliseconds) { + if (format === SUPPORTED_FORMATS.milliseconds) { /** - * Formats a number of miliseconds with ms as units + * Formats a number of milliseconds with ms as units * * @function * @param {Number} value - Number to format, `1` is formatted as `1ms` @@ -95,8 +114,9 @@ export const getFormatter = (format = SUPPORTED_FORMATS.number) => { return suffixFormatter(s__('Units|ms')); } - // Digital - if (format === SUPPORTED_FORMATS.bytes) { + // Digital (Metric) + + if (format === SUPPORTED_FORMATS.decimalBytes) { /** * Formats a number of bytes scaled up to larger digital * units for larger numbers. @@ -162,6 +182,76 @@ export const getFormatter = (format = SUPPORTED_FORMATS.number) => { */ return scaledSIFormatter('B', 5); } + + // Digital (IEC) + + if (format === SUPPORTED_FORMATS.bytes) { + /** + * Formats a number of bytes scaled up to larger digital + * units for larger numbers. + * + * @function + * @param {Number} value - Number to format, `1` is formatted as `1B` + * @param {Number} fractionDigits - number of precision decimals + */ + return scaledBinaryFormatter('B'); + } + if (format === SUPPORTED_FORMATS.kibibytes) { + /** + * Formats a number of kilobytes scaled up to larger digital + * units for larger numbers. + * + * @function + * @param {Number} value - Number to format, `1` is formatted as `1kB` + * @param {Number} fractionDigits - number of precision decimals + */ + return scaledBinaryFormatter('B', 1); + } + if (format === SUPPORTED_FORMATS.mebibytes) { + /** + * Formats a number of megabytes scaled up to larger digital + * units for larger numbers. + * + * @function + * @param {Number} value - Number to format, `1` is formatted as `1MB` + * @param {Number} fractionDigits - number of precision decimals + */ + return scaledBinaryFormatter('B', 2); + } + if (format === SUPPORTED_FORMATS.gibibytes) { + /** + * Formats a number of gigabytes scaled up to larger digital + * units for larger numbers. + * + * @function + * @param {Number} value - Number to format, `1` is formatted as `1GB` + * @param {Number} fractionDigits - number of precision decimals + */ + return scaledBinaryFormatter('B', 3); + } + if (format === SUPPORTED_FORMATS.tebibytes) { + /** + * Formats a number of terabytes scaled up to larger digital + * units for larger numbers. + * + * @function + * @param {Number} value - Number to format, `1` is formatted as `1GB` + * @param {Number} fractionDigits - number of precision decimals + */ + return scaledBinaryFormatter('B', 4); + } + if (format === SUPPORTED_FORMATS.pebibytes) { + /** + * Formats a number of petabytes scaled up to larger digital + * units for larger numbers. + * + * @function + * @param {Number} value - Number to format, `1` is formatted as `1PB` + * @param {Number} fractionDigits - number of precision decimals + */ + return scaledBinaryFormatter('B', 5); + } + // Fail so client library addresses issue throw TypeError(`${format} is not a valid number format`); }; diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js deleted file mode 100644 index 4d3dbec435f..00000000000 --- a/app/assets/javascripts/notes/services/notes_service.js +++ /dev/null @@ -1,41 +0,0 @@ -import axios from '~/lib/utils/axios_utils'; -import * as constants from '../constants'; - -export default { - fetchDiscussions(endpoint, filter, persistFilter = true) { - const config = - filter !== undefined - ? { params: { notes_filter: filter, persist_filter: persistFilter } } - : null; - return axios.get(endpoint, config); - }, - replyToDiscussion(endpoint, data) { - return axios.post(endpoint, data); - }, - updateNote(endpoint, data) { - return axios.put(endpoint, data); - }, - createNewNote(endpoint, data) { - return axios.post(endpoint, data); - }, - toggleResolveNote(endpoint, isResolved) { - const { RESOLVE_NOTE_METHOD_NAME, UNRESOLVE_NOTE_METHOD_NAME } = constants; - const method = isResolved ? UNRESOLVE_NOTE_METHOD_NAME : RESOLVE_NOTE_METHOD_NAME; - - return axios[method](endpoint); - }, - poll(data = {}) { - const endpoint = data.notesData.notesPath; - const { lastFetchedAt } = data; - const options = { - headers: { - 'X-Last-Fetched-At': lastFetchedAt ? `${lastFetchedAt}` : undefined, - }, - }; - - return axios.get(endpoint, options); - }, - toggleIssueState(endpoint, data) { - return axios.put(endpoint, data); - }, -}; diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 594e3a14d56..a4b9c64645c 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -8,7 +8,6 @@ import Poll from '../../lib/utils/poll'; import * as types from './mutation_types'; import * as utils from './utils'; import * as constants from '../constants'; -import service from '../services/notes_service'; import loadAwardsHandler from '../../awards_handler'; import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils'; @@ -47,11 +46,17 @@ export const setNotesFetchedState = ({ commit }, state) => export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data); -export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFilter }) => - service.fetchDiscussions(path, filter, persistFilter).then(({ data }) => { +export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFilter }) => { + const config = + filter !== undefined + ? { params: { notes_filter: filter, persist_filter: persistFilter } } + : null; + + return axios.get(path, config).then(({ data }) => { commit(types.SET_INITIAL_DISCUSSIONS, data); dispatch('updateResolvableDiscussionsCounts'); }); +}; export const updateDiscussion = ({ commit, state }, discussion) => { commit(types.UPDATE_DISCUSSION, discussion); @@ -78,7 +83,7 @@ export const deleteNote = ({ dispatch }, note) => }); export const updateNote = ({ commit, dispatch }, { endpoint, note }) => - service.updateNote(endpoint, note).then(({ data }) => { + axios.put(endpoint, note).then(({ data }) => { commit(types.UPDATE_NOTE, data); dispatch('startTaskList'); }); @@ -109,7 +114,7 @@ export const replyToDiscussion = ( { commit, state, getters, dispatch }, { endpoint, data: reply }, ) => - service.replyToDiscussion(endpoint, reply).then(({ data }) => { + axios.post(endpoint, reply).then(({ data }) => { if (data.discussion) { commit(types.UPDATE_DISCUSSION, data.discussion); @@ -126,7 +131,7 @@ export const replyToDiscussion = ( }); export const createNewNote = ({ commit, dispatch }, { endpoint, data: reply }) => - service.createNewNote(endpoint, reply).then(({ data }) => { + axios.post(endpoint, reply).then(({ data }) => { if (!data.errors) { commit(types.ADD_NEW_NOTE, data); @@ -156,20 +161,24 @@ export const resolveDiscussion = ({ state, dispatch, getters }, { discussionId } }); }; -export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved, discussion }) => - service.toggleResolveNote(endpoint, isResolved).then(({ data }) => { - const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE; +export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved, discussion }) => { + const method = isResolved + ? constants.UNRESOLVE_NOTE_METHOD_NAME + : constants.RESOLVE_NOTE_METHOD_NAME; + const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE; + return axios[method](endpoint).then(({ data }) => { commit(mutationType, data); dispatch('updateResolvableDiscussionsCounts'); dispatch('updateMergeRequestWidget'); }); +}; export const closeIssue = ({ commit, dispatch, state }) => { dispatch('toggleStateButtonLoading', true); - return service.toggleIssueState(state.notesData.closePath).then(({ data }) => { + return axios.put(state.notesData.closePath).then(({ data }) => { commit(types.CLOSE_ISSUE); dispatch('emitStateChangedEvent', data); dispatch('toggleStateButtonLoading', false); @@ -178,7 +187,7 @@ export const closeIssue = ({ commit, dispatch, state }) => { export const reopenIssue = ({ commit, dispatch, state }) => { dispatch('toggleStateButtonLoading', true); - return service.toggleIssueState(state.notesData.reopenPath).then(({ data }) => { + return axios.put(state.notesData.reopenPath).then(({ data }) => { commit(types.REOPEN_ISSUE); dispatch('emitStateChangedEvent', data); dispatch('toggleStateButtonLoading', false); @@ -355,11 +364,35 @@ const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => { return resp; }; +const getFetchDataParams = state => { + const endpoint = state.notesData.notesPath; + const options = { + headers: { + 'X-Last-Fetched-At': state.lastFetchedAt ? `${state.lastFetchedAt}` : undefined, + }, + }; + + return { endpoint, options }; +}; + +export const fetchData = ({ commit, state, getters }) => { + const { endpoint, options } = getFetchDataParams(state); + + axios + .get(endpoint, options) + .then(({ data }) => pollSuccessCallBack(data, commit, state, getters)) + .catch(() => Flash(__('Something went wrong while fetching latest comments.'))); +}; + export const poll = ({ commit, state, getters, dispatch }) => { eTagPoll = new Poll({ - resource: service, + resource: { + poll: () => { + const { endpoint, options } = getFetchDataParams(state); + return axios.get(endpoint, options); + }, + }, method: 'poll', - data: state, successCallback: ({ data }) => pollSuccessCallBack(data, commit, state, getters, dispatch), errorCallback: () => Flash(__('Something went wrong while fetching latest comments.')), }); @@ -367,7 +400,7 @@ export const poll = ({ commit, state, getters, dispatch }) => { if (!Visibility.hidden()) { eTagPoll.makeRequest(); } else { - service.poll(state); + fetchData({ commit, state, getters }); } Visibility.change(() => { @@ -387,18 +420,6 @@ export const restartPolling = () => { if (eTagPoll) eTagPoll.restart(); }; -export const fetchData = ({ commit, state, getters }) => { - const requestData = { - endpoint: state.notesData.notesPath, - lastFetchedAt: state.lastFetchedAt, - }; - - service - .poll(requestData) - .then(({ data }) => pollSuccessCallBack(data, commit, state, getters)) - .catch(() => Flash(__('Something went wrong while fetching latest comments.'))); -}; - export const toggleAward = ({ commit, getters }, { awardName, noteId }) => { commit(types.TOGGLE_AWARD, { awardName, note: getters.notesById[noteId] }); }; diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js index 8380cfb6c59..8d779e04673 100644 --- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js +++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import _ from 'underscore'; +import { escape } from 'lodash'; import { s__, n__, sprintf } from '~/locale'; import axios from '../lib/utils/axios_utils'; import PANEL_STATE from './constants'; @@ -69,13 +69,13 @@ export default class PrometheusMetrics { if (metric.active_metrics > 0) { totalExporters += 1; this.$monitoredMetricsList.append( - `<li>${_.escape(metric.group)}<span class="badge">${_.escape( + `<li>${escape(metric.group)}<span class="badge">${escape( metric.active_metrics, )}</span></li>`, ); totalMonitoredMetrics += metric.active_metrics; if (metric.metrics_missing_requirements > 0) { - this.$missingEnvVarMetricsList.append(`<li>${_.escape(metric.group)}</li>`); + this.$missingEnvVarMetricsList.append(`<li>${escape(metric.group)}</li>`); totalMissingEnvVarMetrics += 1; } } diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index a1bfa03a5ac..0de5aae4b0e 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -59,7 +59,7 @@ max-width: 100%; } - &:not(.md-file) img:not(.emoji) { + &:not(.md) img:not(.emoji) { border: 1px solid $white-normal; padding: 5px; margin: 5px 0; diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 507e227c952..4d49c96d268 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -312,6 +312,7 @@ class ProjectPolicy < BasePolicy enable :destroy_artifacts enable :daily_statistics enable :admin_operations + enable :read_deploy_token end rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror diff --git a/app/views/projects/_wiki.html.haml b/app/views/projects/_wiki.html.haml index 6103d86bf5a..57a5d3e2e83 100644 --- a/app/views/projects/_wiki.html.haml +++ b/app/views/projects/_wiki.html.haml @@ -1,6 +1,6 @@ - if @wiki_home.present? %div{ class: container_class } - .md.md-file.prepend-top-default.append-bottom-default + .md.prepend-top-default.append-bottom-default = render_wiki_content(@wiki_home) - else - can_create_wiki = can?(current_user, :create_wiki, @project) diff --git a/app/views/projects/blob/preview.html.haml b/app/views/projects/blob/preview.html.haml index 46e76e4d175..41a0045be89 100644 --- a/app/views/projects/blob/preview.html.haml +++ b/app/views/projects/blob/preview.html.haml @@ -1,5 +1,5 @@ - if markup?(@blob.name) - .file-content.md.md-file + .file-content.md = markup(@blob.name, @content) - else .diff-file diff --git a/app/views/projects/blob/viewers/_markup.html.haml b/app/views/projects/blob/viewers/_markup.html.haml index c71df29354b..8134adcbc32 100644 --- a/app/views/projects/blob/viewers/_markup.html.haml +++ b/app/views/projects/blob/viewers/_markup.html.haml @@ -1,4 +1,4 @@ - blob = viewer.blob - context = blob.respond_to?(:rendered_markup) ? { rendered: blob.rendered_markup } : {} -.file-content.md.md-file +.file-content.md = markup(blob.name, blob.data, context) diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index ebd99cf8605..74798311c2e 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -26,7 +26,7 @@ = (s_("WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}.") % { most_recent_link: most_recent_link, history_link: history_link }).html_safe .prepend-top-default.append-bottom-default - .md.md-file{ data: { qa_selector: 'wiki_page_content' } } + .md{ data: { qa_selector: 'wiki_page_content' } } = render_wiki_content(@page) = render 'sidebar' diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index 5126351b0bb..fa77566dddb 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -23,7 +23,7 @@ %i.fa.fa-file %strong= snippet.file_name - if markup?(snippet.file_name) - .file-content.md.md-file + .file-content.md - snippet_chunks.each do |chunk| - unless chunk[:data].empty? = markup(snippet.file_name, chunk[:data]) diff --git a/changelogs/unreleased/197960-package-detail-activity.yml b/changelogs/unreleased/197960-package-detail-activity.yml new file mode 100644 index 00000000000..3e7d9328d0a --- /dev/null +++ b/changelogs/unreleased/197960-package-detail-activity.yml @@ -0,0 +1,5 @@ +--- +title: Adds new activity panel to package details page +merge_request: 25534 +author: +type: added diff --git a/changelogs/unreleased/205596-empty-state-for-code-review-analytics.yml b/changelogs/unreleased/205596-empty-state-for-code-review-analytics.yml new file mode 100644 index 00000000000..ef81a166231 --- /dev/null +++ b/changelogs/unreleased/205596-empty-state-for-code-review-analytics.yml @@ -0,0 +1,5 @@ +--- +title: Empty state for Code Review Analytics +merge_request: 25793 +author: +type: added diff --git a/changelogs/unreleased/21811-project-list-deploy-tokens.yml b/changelogs/unreleased/21811-project-list-deploy-tokens.yml new file mode 100644 index 00000000000..ead28fe7595 --- /dev/null +++ b/changelogs/unreleased/21811-project-list-deploy-tokens.yml @@ -0,0 +1,5 @@ +--- +title: Add endpoint for listing all deploy tokens for a project +merge_request: 25186 +author: +type: added diff --git a/changelogs/unreleased/update-dast-ado-image-to-0-10-0.yml b/changelogs/unreleased/update-dast-ado-image-to-0-10-0.yml new file mode 100644 index 00000000000..335d553239f --- /dev/null +++ b/changelogs/unreleased/update-dast-ado-image-to-0-10-0.yml @@ -0,0 +1,5 @@ +--- +title: Update DAST auto-deploy-image to v0.10.0 +merge_request: 25922 +author: +type: other diff --git a/doc/api/deploy_tokens.md b/doc/api/deploy_tokens.md index 501e5c4be36..ed05f46dae0 100644 --- a/doc/api/deploy_tokens.md +++ b/doc/api/deploy_tokens.md @@ -2,15 +2,14 @@ ## List all deploy tokens -Get a list of all deploy tokens across all projects of the GitLab instance. +Get a list of all deploy tokens across the GitLab instance. This endpoint requires admin access. ->**Note:** -> This endpoint requires admin access. - -``` +```plaintext GET /deploy_tokens ``` +Example request: + ```shell curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/deploy_tokens" ``` @@ -24,7 +23,47 @@ Example response: "name": "MyToken", "username": "gitlab+deploy-token-1", "expires_at": "2020-02-14T00:00:00.000Z", - "token": "jMRvtPNxrn3crTAGukpZ", + "scopes": [ + "read_repository", + "read_registry" + ] + } +] +``` + +## Project deploy tokens + +Project deploy token API endpoints require project maintainer access or higher. + +### List project deploy tokens + +Get a list of a project's deploy tokens. + +```plaintext +GET /projects/:id/deploy_tokens +``` + +Parameters: + +| Attribute | Type | Required | Description | +|:---------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | + +Example request: + +```shell +curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/deploy_tokens" +``` + +Example response: + +```json +[ + { + "id": 1, + "name": "MyToken", + "username": "gitlab+deploy-token-1", + "expires_at": "2020-02-14T00:00:00.000Z", "scopes": [ "read_repository", "read_registry" diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 6b62b11750c..8d6fc859ca2 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -2609,12 +2609,12 @@ input EpicSetSubscriptionInput { clientMutationId: String """ - The group the epic to (un)subscribe is in + The group the epic to mutate is in """ groupPath: ID! """ - The iid of the epic to (un)subscribe + The iid of the epic to mutate """ iid: ID! @@ -7820,7 +7820,7 @@ input UpdateEpicInput { """ The iid of the epic to mutate """ - iid: String! + iid: ID! """ The IDs of labels to be removed from the epic. diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index ba716f82630..4f2cbe81781 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -24410,6 +24410,20 @@ "fields": null, "inputFields": [ { + "name": "iid", + "description": "The iid of the epic to mutate", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { "name": "groupPath", "description": "The group the epic to mutate is in", "type": { @@ -24520,20 +24534,6 @@ "defaultValue": null }, { - "name": "iid", - "description": "The iid of the epic to mutate", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null - }, - { "name": "stateEvent", "description": "State event for the epic", "type": { @@ -24863,8 +24863,8 @@ "fields": null, "inputFields": [ { - "name": "groupPath", - "description": "The group the epic to (un)subscribe is in", + "name": "iid", + "description": "The iid of the epic to mutate", "type": { "kind": "NON_NULL", "name": null, @@ -24877,8 +24877,8 @@ "defaultValue": null }, { - "name": "iid", - "description": "The iid of the epic to (un)subscribe", + "name": "groupPath", + "description": "The group the epic to mutate is in", "type": { "kind": "NON_NULL", "name": null, diff --git a/doc/user/project/repository/forking_workflow.md b/doc/user/project/repository/forking_workflow.md index 45a32655f96..927c1db804a 100644 --- a/doc/user/project/repository/forking_workflow.md +++ b/doc/user/project/repository/forking_workflow.md @@ -4,11 +4,14 @@ disqus_identifier: 'https://docs.gitlab.com/ee/workflow/forking_workflow.html' # Project forking workflow -Forking a project to your own namespace is useful if you have no write -access to the project you want to contribute to. Even if you do have write -access or can request it, we recommend working together in the same -repository since it is simpler. See our [GitLab Flow](../../../topics/gitlab_flow.md) -document more information about using branches to work together. +Whenever possible, it's recommended to work in a common Git repository and use +[branching strategies](../../../topics/gitlab_flow.md) to manage your work. However, +if you do not have write access for the repository you want to contribute to, you +can create a fork. + +A fork is a personal copy of the repository and all its branches, which you create +in a namespace of your choice. This way you can make changes in your own fork and +submit them through a merge request to the repo you don't have access to. ## Creating a fork @@ -27,7 +30,7 @@ Forking a project is, in most cases, a two-step process. The fork is created. The permissions you have in the namespace are the permissions you will have in the fork. -CAUTION: **CAUTION:** +CAUTION: **Caution:** In GitLab 12.6 and later, when project owners [reduce a project's visibility](../../../public_access/public_access.md#reducing-visibility), it **removes the relationship** between a project and all its forks. @@ -37,10 +40,11 @@ You can use [repository mirroring](repository_mirroring.md) to keep your fork sy The main difference is that with repository mirroring your remote fork will be automatically kept up-to-date. -Without mirroring, to work locally you'll have to user `git pull` to update your local repo with the fork on GitLab. You'll have to fetch locally and push it back to the remote repo to update it. +Without mirroring, to work locally you'll have to use `git pull` to update your local repo +with the upstream project, then push the changes back to your fork to update it. CAUTION: **Caution:** -With mirroring, before approving a merge request you'll likely to be asked to sync, hence automating it is recommend. +With mirroring, before approving a merge request, you'll likely be asked to sync; hence automating it is recommend. Read more about [How to keep your fork up to date with its origin](https://about.gitlab.com/blog/2016/12/01/how-to-keep-your-fork-up-to-date-with-its-origin/). diff --git a/lib/api/deploy_tokens.rb b/lib/api/deploy_tokens.rb index bf82c63260d..3224157ca05 100644 --- a/lib/api/deploy_tokens.rb +++ b/lib/api/deploy_tokens.rb @@ -4,8 +4,6 @@ module API class DeployTokens < Grape::API include PaginationParams - before { authenticated_as_admin! } - desc 'Return all deploy tokens' do detail 'This feature was introduced in GitLab 12.9.' success Entities::DeployToken @@ -14,7 +12,27 @@ module API use :pagination end get 'deploy_tokens' do + authenticated_as_admin! + present paginate(DeployToken.all), with: Entities::DeployToken end + + params do + requires :id, type: Integer, desc: 'The ID of a project' + end + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + params do + use :pagination + end + desc 'List deploy tokens for a project' do + detail 'This feature was introduced in GitLab 12.9' + success Entities::DeployToken + end + get ':id/deploy_tokens' do + authorize!(:read_deploy_token, user_project) + + present paginate(user_project.deploy_tokens), with: Entities::DeployToken + end + end end end diff --git a/lib/api/entities/deploy_token.rb b/lib/api/entities/deploy_token.rb index cac6846a845..9c5bf54e299 100644 --- a/lib/api/entities/deploy_token.rb +++ b/lib/api/entities/deploy_token.rb @@ -3,7 +3,8 @@ module API module Entities class DeployToken < Grape::Entity - expose :id, :name, :username, :expires_at, :token, :scopes + # exposing :token is a security risk and should be avoided + expose :id, :name, :username, :expires_at, :scopes end end end diff --git a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml index 78ee9b28605..3cf4910fe86 100644 --- a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml @@ -1,5 +1,5 @@ .dast-auto-deploy: - image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.9.1" + image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.10.0" dast_environment_deploy: extends: .dast-auto-deploy diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7028e44eac2..32e1b704a15 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4820,6 +4820,9 @@ msgstr "" msgid "Code Review" msgstr "" +msgid "Code Review Analytics displays a table of open merge requests considered to be in code review. There are currently no merge requests in review for this project and/or filters." +msgstr "" + msgid "Code owner approval is required" msgstr "" @@ -13602,22 +13605,22 @@ msgstr "" msgid "PackageRegistry|NuGet Command" msgstr "" -msgid "PackageRegistry|Registry Setup" +msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}" msgstr "" -msgid "PackageRegistry|Remove package" +msgid "PackageRegistry|Published to the repository at %{timestamp}" msgstr "" -msgid "PackageRegistry|There are no packages yet" +msgid "PackageRegistry|Registry Setup" msgstr "" -msgid "PackageRegistry|There was a problem fetching the details for this package." +msgid "PackageRegistry|Remove package" msgstr "" -msgid "PackageRegistry|There was an error fetching the pipeline information." +msgid "PackageRegistry|There are no packages yet" msgstr "" -msgid "PackageRegistry|Unable to fetch pipeline information" +msgid "PackageRegistry|There was a problem fetching the details for this package." msgstr "" msgid "PackageRegistry|Unable to load package" @@ -22468,6 +22471,9 @@ msgstr "" msgid "You don't have any deployments right now." msgstr "" +msgid "You don't have any open merge requests" +msgstr "" + msgid "You don't have any projects available." msgstr "" diff --git a/spec/fixtures/api/schemas/public_api/v4/deploy_token.json b/spec/fixtures/api/schemas/public_api/v4/deploy_token.json new file mode 100644 index 00000000000..c8a8b8d1e7d --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/deploy_token.json @@ -0,0 +1,31 @@ +{ + "type": "object", + "required": [ + "id", + "name", + "username", + "expires_at", + "scopes" + ], + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "username": { + "type": "string" + }, + "expires_at": { + "type": "date" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false +}
\ No newline at end of file diff --git a/spec/fixtures/api/schemas/public_api/v4/deploy_tokens.json b/spec/fixtures/api/schemas/public_api/v4/deploy_tokens.json new file mode 100644 index 00000000000..71c30cb2a73 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/deploy_tokens.json @@ -0,0 +1,6 @@ +{ + "type": "array", + "items": { + "$ref": "deploy_token.json" + } +}
\ No newline at end of file diff --git a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js index 925699f5623..ab5784b8f7a 100644 --- a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js +++ b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js @@ -1,9 +1,7 @@ import { shallowMount } from '@vue/test-utils'; import { trimText } from 'helpers/text_helper'; import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue'; -import mockData from '../mock_data'; // can also use 'mockGroup', but not useful to test here - -const mockProject = mockData(); +import { mockProject } from '../mock_data'; // can also use 'mockGroup', but not useful to test here describe('FrequentItemsListItemComponent', () => { let wrapper; diff --git a/spec/frontend/frequent_items/components/frequent_items_list_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_spec.js new file mode 100644 index 00000000000..238fd508053 --- /dev/null +++ b/spec/frontend/frequent_items/components/frequent_items_list_spec.js @@ -0,0 +1,101 @@ +import { mount } from '@vue/test-utils'; +import frequentItemsListComponent from '~/frequent_items/components/frequent_items_list.vue'; +import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue'; +import { mockFrequentProjects } from '../mock_data'; + +describe('FrequentItemsListComponent', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = mount(frequentItemsListComponent, { + propsData: { + namespace: 'projects', + items: mockFrequentProjects, + isFetchFailed: false, + hasSearchQuery: false, + matcher: 'lab', + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('computed', () => { + describe('isListEmpty', () => { + it('should return `true` or `false` representing whether if `items` is empty or not with projects', () => { + createComponent({ + items: [], + }); + + expect(wrapper.vm.isListEmpty).toBe(true); + + wrapper.setProps({ + items: mockFrequentProjects, + }); + + expect(wrapper.vm.isListEmpty).toBe(false); + }); + }); + + describe('fetched item messages', () => { + it('should return appropriate empty list message based on value of `localStorageFailed` prop with projects', () => { + createComponent({ + isFetchFailed: true, + }); + + expect(wrapper.vm.listEmptyMessage).toBe( + 'This feature requires browser localStorage support', + ); + + wrapper.setProps({ + isFetchFailed: false, + }); + + expect(wrapper.vm.listEmptyMessage).toBe('Projects you visit often will appear here'); + }); + }); + + describe('searched item messages', () => { + it('should return appropriate empty list message based on value of `searchFailed` prop with projects', () => { + createComponent({ + hasSearchQuery: true, + isFetchFailed: true, + }); + + expect(wrapper.vm.listEmptyMessage).toBe('Something went wrong on our end.'); + + wrapper.setProps({ + isFetchFailed: false, + }); + + expect(wrapper.vm.listEmptyMessage).toBe('Sorry, no projects matched your search'); + }); + }); + }); + + describe('template', () => { + it('should render component element with list of projects', () => { + createComponent(); + + return wrapper.vm.$nextTick(() => { + expect(wrapper.classes('frequent-items-list-container')).toBe(true); + expect(wrapper.findAll({ ref: 'frequentItemsList' })).toHaveLength(1); + expect(wrapper.findAll(frequentItemsListItemComponent)).toHaveLength(5); + }); + }); + + it('should render component element with empty message', () => { + createComponent({ + items: [], + }); + + return wrapper.vm.$nextTick(() => { + expect(wrapper.vm.$el.querySelectorAll('li.section-empty')).toHaveLength(1); + expect(wrapper.findAll(frequentItemsListItemComponent)).toHaveLength(0); + }); + }); + }); +}); diff --git a/spec/frontend/frequent_items/mock_data.js b/spec/frontend/frequent_items/mock_data.js index 81f34053543..5cd4cddd877 100644 --- a/spec/frontend/frequent_items/mock_data.js +++ b/spec/frontend/frequent_items/mock_data.js @@ -1,9 +1,57 @@ import { TEST_HOST } from 'helpers/test_constants'; -export default () => ({ +export const mockFrequentProjects = [ + { + id: 1, + name: 'GitLab Community Edition', + namespace: 'gitlab-org / gitlab-ce', + webUrl: `${TEST_HOST}/gitlab-org/gitlab-foss`, + avatarUrl: null, + frequency: 1, + lastAccessedOn: Date.now(), + }, + { + id: 2, + name: 'GitLab CI', + namespace: 'gitlab-org / gitlab-ci', + webUrl: `${TEST_HOST}/gitlab-org/gitlab-ci`, + avatarUrl: null, + frequency: 9, + lastAccessedOn: Date.now(), + }, + { + id: 3, + name: 'Typeahead.Js', + namespace: 'twitter / typeahead-js', + webUrl: `${TEST_HOST}/twitter/typeahead-js`, + avatarUrl: '/uploads/-/system/project/avatar/7/TWBS.png', + frequency: 2, + lastAccessedOn: Date.now(), + }, + { + id: 4, + name: 'Intel', + namespace: 'platform / hardware / bsp / intel', + webUrl: `${TEST_HOST}/platform/hardware/bsp/intel`, + avatarUrl: null, + frequency: 3, + lastAccessedOn: Date.now(), + }, + { + id: 5, + name: 'v4.4', + namespace: 'platform / hardware / bsp / kernel / common / v4.4', + webUrl: `${TEST_HOST}/platform/hardware/bsp/kernel/common/v4.4`, + avatarUrl: null, + frequency: 8, + lastAccessedOn: Date.now(), + }, +]; + +export const mockProject = { id: 1, name: 'GitLab Community Edition', namespace: 'gitlab-org / gitlab-ce', webUrl: `${TEST_HOST}/gitlab-org/gitlab-foss`, avatarUrl: null, -}); +}; diff --git a/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js b/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js index 071ecde6a6d..26b942c3567 100644 --- a/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js +++ b/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js @@ -2,6 +2,7 @@ import { numberFormatter, suffixFormatter, scaledSIFormatter, + scaledBinaryFormatter, } from '~/lib/utils/unit_format/formatter_factory'; describe('unit_format/formatter_factory', () => { @@ -12,28 +13,28 @@ describe('unit_format/formatter_factory', () => { }); it('formats a integer', () => { - expect(formatNumber(1)).toEqual('1'); - expect(formatNumber(100)).toEqual('100'); - expect(formatNumber(1000)).toEqual('1,000'); - expect(formatNumber(10000)).toEqual('10,000'); - expect(formatNumber(1000000)).toEqual('1,000,000'); + expect(formatNumber(1)).toBe('1'); + expect(formatNumber(100)).toBe('100'); + expect(formatNumber(1000)).toBe('1,000'); + expect(formatNumber(10000)).toBe('10,000'); + expect(formatNumber(1000000)).toBe('1,000,000'); }); it('formats a floating point number', () => { - expect(formatNumber(0.1)).toEqual('0.1'); - expect(formatNumber(0.1, 0)).toEqual('0'); - expect(formatNumber(0.1, 2)).toEqual('0.10'); - expect(formatNumber(0.1, 3)).toEqual('0.100'); + expect(formatNumber(0.1)).toBe('0.1'); + expect(formatNumber(0.1, 0)).toBe('0'); + expect(formatNumber(0.1, 2)).toBe('0.10'); + expect(formatNumber(0.1, 3)).toBe('0.100'); - expect(formatNumber(12.345)).toEqual('12.345'); - expect(formatNumber(12.345, 2)).toEqual('12.35'); - expect(formatNumber(12.345, 4)).toEqual('12.3450'); + expect(formatNumber(12.345)).toBe('12.345'); + expect(formatNumber(12.345, 2)).toBe('12.35'); + expect(formatNumber(12.345, 4)).toBe('12.3450'); }); it('formats a large integer with a length limit', () => { - expect(formatNumber(10 ** 7, undefined)).toEqual('10,000,000'); - expect(formatNumber(10 ** 7, undefined, 9)).toEqual('1.00e+7'); - expect(formatNumber(10 ** 7, undefined, 10)).toEqual('10,000,000'); + expect(formatNumber(10 ** 7, undefined)).toBe('10,000,000'); + expect(formatNumber(10 ** 7, undefined, 9)).toBe('1.00e+7'); + expect(formatNumber(10 ** 7, undefined, 10)).toBe('10,000,000'); }); }); @@ -44,112 +45,112 @@ describe('unit_format/formatter_factory', () => { }); it('formats a integer', () => { - expect(formatSuffix(1)).toEqual('1pop.'); - expect(formatSuffix(100)).toEqual('100pop.'); - expect(formatSuffix(1000)).toEqual('1,000pop.'); - expect(formatSuffix(10000)).toEqual('10,000pop.'); - expect(formatSuffix(1000000)).toEqual('1,000,000pop.'); + expect(formatSuffix(1)).toBe('1pop.'); + expect(formatSuffix(100)).toBe('100pop.'); + expect(formatSuffix(1000)).toBe('1,000pop.'); + expect(formatSuffix(10000)).toBe('10,000pop.'); + expect(formatSuffix(1000000)).toBe('1,000,000pop.'); }); it('formats a floating point number', () => { - expect(formatSuffix(0.1)).toEqual('0.1pop.'); - expect(formatSuffix(0.1, 0)).toEqual('0pop.'); - expect(formatSuffix(0.1, 2)).toEqual('0.10pop.'); - expect(formatSuffix(0.1, 3)).toEqual('0.100pop.'); + expect(formatSuffix(0.1)).toBe('0.1pop.'); + expect(formatSuffix(0.1, 0)).toBe('0pop.'); + expect(formatSuffix(0.1, 2)).toBe('0.10pop.'); + expect(formatSuffix(0.1, 3)).toBe('0.100pop.'); - expect(formatSuffix(12.345)).toEqual('12.345pop.'); - expect(formatSuffix(12.345, 2)).toEqual('12.35pop.'); - expect(formatSuffix(12.345, 4)).toEqual('12.3450pop.'); + expect(formatSuffix(12.345)).toBe('12.345pop.'); + expect(formatSuffix(12.345, 2)).toBe('12.35pop.'); + expect(formatSuffix(12.345, 4)).toBe('12.3450pop.'); }); it('formats a negative integer', () => { - expect(formatSuffix(-1)).toEqual('-1pop.'); - expect(formatSuffix(-100)).toEqual('-100pop.'); - expect(formatSuffix(-1000)).toEqual('-1,000pop.'); - expect(formatSuffix(-10000)).toEqual('-10,000pop.'); - expect(formatSuffix(-1000000)).toEqual('-1,000,000pop.'); + expect(formatSuffix(-1)).toBe('-1pop.'); + expect(formatSuffix(-100)).toBe('-100pop.'); + expect(formatSuffix(-1000)).toBe('-1,000pop.'); + expect(formatSuffix(-10000)).toBe('-10,000pop.'); + expect(formatSuffix(-1000000)).toBe('-1,000,000pop.'); }); it('formats a floating point nugative number', () => { - expect(formatSuffix(-0.1)).toEqual('-0.1pop.'); - expect(formatSuffix(-0.1, 0)).toEqual('-0pop.'); - expect(formatSuffix(-0.1, 2)).toEqual('-0.10pop.'); - expect(formatSuffix(-0.1, 3)).toEqual('-0.100pop.'); + expect(formatSuffix(-0.1)).toBe('-0.1pop.'); + expect(formatSuffix(-0.1, 0)).toBe('-0pop.'); + expect(formatSuffix(-0.1, 2)).toBe('-0.10pop.'); + expect(formatSuffix(-0.1, 3)).toBe('-0.100pop.'); - expect(formatSuffix(-12.345)).toEqual('-12.345pop.'); - expect(formatSuffix(-12.345, 2)).toEqual('-12.35pop.'); - expect(formatSuffix(-12.345, 4)).toEqual('-12.3450pop.'); + expect(formatSuffix(-12.345)).toBe('-12.345pop.'); + expect(formatSuffix(-12.345, 2)).toBe('-12.35pop.'); + expect(formatSuffix(-12.345, 4)).toBe('-12.3450pop.'); }); it('formats a large integer', () => { - expect(formatSuffix(10 ** 7)).toEqual('10,000,000pop.'); - expect(formatSuffix(10 ** 10)).toEqual('10,000,000,000pop.'); + expect(formatSuffix(10 ** 7)).toBe('10,000,000pop.'); + expect(formatSuffix(10 ** 10)).toBe('10,000,000,000pop.'); }); it('formats a large integer with a length limit', () => { - expect(formatSuffix(10 ** 7, undefined, 10)).toEqual('1.00e+7pop.'); - expect(formatSuffix(10 ** 10, undefined, 10)).toEqual('1.00e+10pop.'); + expect(formatSuffix(10 ** 7, undefined, 10)).toBe('1.00e+7pop.'); + expect(formatSuffix(10 ** 10, undefined, 10)).toBe('1.00e+10pop.'); }); }); describe('scaledSIFormatter', () => { describe('scaled format', () => { - let formatScaled; + let formatGibibytes; beforeEach(() => { - formatScaled = scaledSIFormatter('B'); + formatGibibytes = scaledSIFormatter('B'); }); it('formats bytes', () => { - expect(formatScaled(12.345)).toEqual('12.345B'); - expect(formatScaled(12.345, 0)).toEqual('12B'); - expect(formatScaled(12.345, 1)).toEqual('12.3B'); - expect(formatScaled(12.345, 2)).toEqual('12.35B'); + expect(formatGibibytes(12.345)).toBe('12.345B'); + expect(formatGibibytes(12.345, 0)).toBe('12B'); + expect(formatGibibytes(12.345, 1)).toBe('12.3B'); + expect(formatGibibytes(12.345, 2)).toBe('12.35B'); }); - it('formats bytes in a scale', () => { - expect(formatScaled(1)).toEqual('1B'); - expect(formatScaled(10)).toEqual('10B'); - expect(formatScaled(10 ** 2)).toEqual('100B'); - expect(formatScaled(10 ** 3)).toEqual('1kB'); - expect(formatScaled(10 ** 4)).toEqual('10kB'); - expect(formatScaled(10 ** 5)).toEqual('100kB'); - expect(formatScaled(10 ** 6)).toEqual('1MB'); - expect(formatScaled(10 ** 7)).toEqual('10MB'); - expect(formatScaled(10 ** 8)).toEqual('100MB'); - expect(formatScaled(10 ** 9)).toEqual('1GB'); - expect(formatScaled(10 ** 10)).toEqual('10GB'); - expect(formatScaled(10 ** 11)).toEqual('100GB'); + it('formats bytes in a decimal scale', () => { + expect(formatGibibytes(1)).toBe('1B'); + expect(formatGibibytes(10)).toBe('10B'); + expect(formatGibibytes(10 ** 2)).toBe('100B'); + expect(formatGibibytes(10 ** 3)).toBe('1kB'); + expect(formatGibibytes(10 ** 4)).toBe('10kB'); + expect(formatGibibytes(10 ** 5)).toBe('100kB'); + expect(formatGibibytes(10 ** 6)).toBe('1MB'); + expect(formatGibibytes(10 ** 7)).toBe('10MB'); + expect(formatGibibytes(10 ** 8)).toBe('100MB'); + expect(formatGibibytes(10 ** 9)).toBe('1GB'); + expect(formatGibibytes(10 ** 10)).toBe('10GB'); + expect(formatGibibytes(10 ** 11)).toBe('100GB'); }); }); describe('scaled format with offset', () => { - let formatScaled; + let formatGigaBytes; beforeEach(() => { // formats gigabytes - formatScaled = scaledSIFormatter('B', 3); + formatGigaBytes = scaledSIFormatter('B', 3); }); it('formats floating point numbers', () => { - expect(formatScaled(12.345)).toEqual('12.345GB'); - expect(formatScaled(12.345, 0)).toEqual('12GB'); - expect(formatScaled(12.345, 1)).toEqual('12.3GB'); - expect(formatScaled(12.345, 2)).toEqual('12.35GB'); + expect(formatGigaBytes(12.345)).toBe('12.345GB'); + expect(formatGigaBytes(12.345, 0)).toBe('12GB'); + expect(formatGigaBytes(12.345, 1)).toBe('12.3GB'); + expect(formatGigaBytes(12.345, 2)).toBe('12.35GB'); }); it('formats large numbers scaled', () => { - expect(formatScaled(1)).toEqual('1GB'); - expect(formatScaled(1, 1)).toEqual('1.0GB'); - expect(formatScaled(10)).toEqual('10GB'); - expect(formatScaled(10 ** 2)).toEqual('100GB'); - expect(formatScaled(10 ** 3)).toEqual('1TB'); - expect(formatScaled(10 ** 4)).toEqual('10TB'); - expect(formatScaled(10 ** 5)).toEqual('100TB'); - expect(formatScaled(10 ** 6)).toEqual('1PB'); - expect(formatScaled(10 ** 7)).toEqual('10PB'); - expect(formatScaled(10 ** 8)).toEqual('100PB'); - expect(formatScaled(10 ** 9)).toEqual('1EB'); + expect(formatGigaBytes(1)).toBe('1GB'); + expect(formatGigaBytes(1, 1)).toBe('1.0GB'); + expect(formatGigaBytes(10)).toBe('10GB'); + expect(formatGigaBytes(10 ** 2)).toBe('100GB'); + expect(formatGigaBytes(10 ** 3)).toBe('1TB'); + expect(formatGigaBytes(10 ** 4)).toBe('10TB'); + expect(formatGigaBytes(10 ** 5)).toBe('100TB'); + expect(formatGigaBytes(10 ** 6)).toBe('1PB'); + expect(formatGigaBytes(10 ** 7)).toBe('10PB'); + expect(formatGigaBytes(10 ** 8)).toBe('100PB'); + expect(formatGigaBytes(10 ** 9)).toBe('1EB'); }); it('formatting of too large numbers is not suported', () => { @@ -159,41 +160,116 @@ describe('unit_format/formatter_factory', () => { }); describe('scaled format with negative offset', () => { - let formatScaled; + let formatMilligrams; beforeEach(() => { - formatScaled = scaledSIFormatter('g', -1); + formatMilligrams = scaledSIFormatter('g', -1); }); it('formats floating point numbers', () => { - expect(formatScaled(12.345)).toEqual('12.345mg'); - expect(formatScaled(12.345, 0)).toEqual('12mg'); - expect(formatScaled(12.345, 1)).toEqual('12.3mg'); - expect(formatScaled(12.345, 2)).toEqual('12.35mg'); + expect(formatMilligrams(1.0)).toBe('1mg'); + expect(formatMilligrams(12.345)).toBe('12.345mg'); + expect(formatMilligrams(12.345, 0)).toBe('12mg'); + expect(formatMilligrams(12.345, 1)).toBe('12.3mg'); + expect(formatMilligrams(12.345, 2)).toBe('12.35mg'); }); it('formats large numbers scaled', () => { - expect(formatScaled(1)).toEqual('1mg'); - expect(formatScaled(1, 1)).toEqual('1.0mg'); - expect(formatScaled(10)).toEqual('10mg'); - expect(formatScaled(10 ** 2)).toEqual('100mg'); - expect(formatScaled(10 ** 3)).toEqual('1g'); - expect(formatScaled(10 ** 4)).toEqual('10g'); - expect(formatScaled(10 ** 5)).toEqual('100g'); - expect(formatScaled(10 ** 6)).toEqual('1kg'); - expect(formatScaled(10 ** 7)).toEqual('10kg'); - expect(formatScaled(10 ** 8)).toEqual('100kg'); + expect(formatMilligrams(10)).toBe('10mg'); + expect(formatMilligrams(10 ** 2)).toBe('100mg'); + expect(formatMilligrams(10 ** 3)).toBe('1g'); + expect(formatMilligrams(10 ** 4)).toBe('10g'); + expect(formatMilligrams(10 ** 5)).toBe('100g'); + expect(formatMilligrams(10 ** 6)).toBe('1kg'); + expect(formatMilligrams(10 ** 7)).toBe('10kg'); + expect(formatMilligrams(10 ** 8)).toBe('100kg'); }); it('formats negative numbers scaled', () => { - expect(formatScaled(-12.345)).toEqual('-12.345mg'); - expect(formatScaled(-12.345, 0)).toEqual('-12mg'); - expect(formatScaled(-12.345, 1)).toEqual('-12.3mg'); - expect(formatScaled(-12.345, 2)).toEqual('-12.35mg'); - - expect(formatScaled(-10)).toEqual('-10mg'); - expect(formatScaled(-100)).toEqual('-100mg'); - expect(formatScaled(-(10 ** 4))).toEqual('-10g'); + expect(formatMilligrams(-12.345)).toBe('-12.345mg'); + expect(formatMilligrams(-12.345, 0)).toBe('-12mg'); + expect(formatMilligrams(-12.345, 1)).toBe('-12.3mg'); + expect(formatMilligrams(-12.345, 2)).toBe('-12.35mg'); + + expect(formatMilligrams(-10)).toBe('-10mg'); + expect(formatMilligrams(-100)).toBe('-100mg'); + expect(formatMilligrams(-(10 ** 4))).toBe('-10g'); + }); + }); + }); + + describe('scaledBinaryFormatter', () => { + describe('scaled format', () => { + let formatScaledBin; + + beforeEach(() => { + formatScaledBin = scaledBinaryFormatter('B'); + }); + + it('formats bytes', () => { + expect(formatScaledBin(12.345)).toBe('12.345B'); + expect(formatScaledBin(12.345, 0)).toBe('12B'); + expect(formatScaledBin(12.345, 1)).toBe('12.3B'); + expect(formatScaledBin(12.345, 2)).toBe('12.35B'); + }); + + it('formats bytes in a binary scale', () => { + expect(formatScaledBin(1)).toBe('1B'); + expect(formatScaledBin(10)).toBe('10B'); + expect(formatScaledBin(100)).toBe('100B'); + expect(formatScaledBin(1000)).toBe('1,000B'); + expect(formatScaledBin(10000)).toBe('9.766KiB'); + + expect(formatScaledBin(1 * 1024)).toBe('1KiB'); + expect(formatScaledBin(10 * 1024)).toBe('10KiB'); + expect(formatScaledBin(100 * 1024)).toBe('100KiB'); + + expect(formatScaledBin(1 * 1024 ** 2)).toBe('1MiB'); + expect(formatScaledBin(10 * 1024 ** 2)).toBe('10MiB'); + expect(formatScaledBin(100 * 1024 ** 2)).toBe('100MiB'); + + expect(formatScaledBin(1 * 1024 ** 3)).toBe('1GiB'); + expect(formatScaledBin(10 * 1024 ** 3)).toBe('10GiB'); + expect(formatScaledBin(100 * 1024 ** 3)).toBe('100GiB'); + }); + }); + + describe('scaled format with offset', () => { + let formatGibibytes; + + beforeEach(() => { + formatGibibytes = scaledBinaryFormatter('B', 3); + }); + + it('formats floating point numbers', () => { + expect(formatGibibytes(12.888)).toBe('12.888GiB'); + expect(formatGibibytes(12.888, 0)).toBe('13GiB'); + expect(formatGibibytes(12.888, 1)).toBe('12.9GiB'); + expect(formatGibibytes(12.888, 2)).toBe('12.89GiB'); + }); + + it('formats large numbers scaled', () => { + expect(formatGibibytes(1)).toBe('1GiB'); + expect(formatGibibytes(10)).toBe('10GiB'); + expect(formatGibibytes(100)).toBe('100GiB'); + expect(formatGibibytes(1000)).toBe('1,000GiB'); + + expect(formatGibibytes(1 * 1024)).toBe('1TiB'); + expect(formatGibibytes(10 * 1024)).toBe('10TiB'); + expect(formatGibibytes(100 * 1024)).toBe('100TiB'); + + expect(formatGibibytes(1 * 1024 ** 2)).toBe('1PiB'); + expect(formatGibibytes(10 * 1024 ** 2)).toBe('10PiB'); + expect(formatGibibytes(100 * 1024 ** 2)).toBe('100PiB'); + + expect(formatGibibytes(1 * 1024 ** 3)).toBe('1EiB'); + expect(formatGibibytes(10 * 1024 ** 3)).toBe('10EiB'); + expect(formatGibibytes(100 * 1024 ** 3)).toBe('100EiB'); + }); + + it('formatting of too large numbers is not suported', () => { + // formatting YB is out of range + expect(() => scaledBinaryFormatter('B', 9)).toThrow(); }); }); }); diff --git a/spec/frontend/lib/utils/unit_format/index_spec.js b/spec/frontend/lib/utils/unit_format/index_spec.js index e0991f2909b..5b2fdf1f02b 100644 --- a/spec/frontend/lib/utils/unit_format/index_spec.js +++ b/spec/frontend/lib/utils/unit_format/index_spec.js @@ -3,109 +3,149 @@ import { getFormatter, SUPPORTED_FORMATS } from '~/lib/utils/unit_format'; describe('unit_format', () => { describe('when a supported format is provided, the returned function formats', () => { it('numbers, by default', () => { - expect(getFormatter()(1)).toEqual('1'); + expect(getFormatter()(1)).toBe('1'); }); it('numbers', () => { const formatNumber = getFormatter(SUPPORTED_FORMATS.number); - expect(formatNumber(1)).toEqual('1'); - expect(formatNumber(100)).toEqual('100'); - expect(formatNumber(1000)).toEqual('1,000'); - expect(formatNumber(10000)).toEqual('10,000'); - expect(formatNumber(1000000)).toEqual('1,000,000'); + expect(formatNumber(1)).toBe('1'); + expect(formatNumber(100)).toBe('100'); + expect(formatNumber(1000)).toBe('1,000'); + expect(formatNumber(10000)).toBe('10,000'); + expect(formatNumber(1000000)).toBe('1,000,000'); }); it('percent', () => { const formatPercent = getFormatter(SUPPORTED_FORMATS.percent); - expect(formatPercent(1)).toEqual('100%'); - expect(formatPercent(1, 2)).toEqual('100.00%'); + expect(formatPercent(1)).toBe('100%'); + expect(formatPercent(1, 2)).toBe('100.00%'); - expect(formatPercent(0.1)).toEqual('10%'); - expect(formatPercent(0.5)).toEqual('50%'); + expect(formatPercent(0.1)).toBe('10%'); + expect(formatPercent(0.5)).toBe('50%'); - expect(formatPercent(0.888888)).toEqual('89%'); - expect(formatPercent(0.888888, 2)).toEqual('88.89%'); - expect(formatPercent(0.888888, 5)).toEqual('88.88880%'); + expect(formatPercent(0.888888)).toBe('89%'); + expect(formatPercent(0.888888, 2)).toBe('88.89%'); + expect(formatPercent(0.888888, 5)).toBe('88.88880%'); - expect(formatPercent(2)).toEqual('200%'); - expect(formatPercent(10)).toEqual('1,000%'); + expect(formatPercent(2)).toBe('200%'); + expect(formatPercent(10)).toBe('1,000%'); }); it('percentunit', () => { const formatPercentHundred = getFormatter(SUPPORTED_FORMATS.percentHundred); - expect(formatPercentHundred(1)).toEqual('1%'); - expect(formatPercentHundred(1, 2)).toEqual('1.00%'); + expect(formatPercentHundred(1)).toBe('1%'); + expect(formatPercentHundred(1, 2)).toBe('1.00%'); - expect(formatPercentHundred(88.8888)).toEqual('89%'); - expect(formatPercentHundred(88.8888, 2)).toEqual('88.89%'); - expect(formatPercentHundred(88.8888, 5)).toEqual('88.88880%'); + expect(formatPercentHundred(88.8888)).toBe('89%'); + expect(formatPercentHundred(88.8888, 2)).toBe('88.89%'); + expect(formatPercentHundred(88.8888, 5)).toBe('88.88880%'); - expect(formatPercentHundred(100)).toEqual('100%'); - expect(formatPercentHundred(100, 2)).toEqual('100.00%'); + expect(formatPercentHundred(100)).toBe('100%'); + expect(formatPercentHundred(100, 2)).toBe('100.00%'); - expect(formatPercentHundred(200)).toEqual('200%'); - expect(formatPercentHundred(1000)).toEqual('1,000%'); + expect(formatPercentHundred(200)).toBe('200%'); + expect(formatPercentHundred(1000)).toBe('1,000%'); }); it('seconds', () => { - expect(getFormatter(SUPPORTED_FORMATS.seconds)(1)).toEqual('1s'); + expect(getFormatter(SUPPORTED_FORMATS.seconds)(1)).toBe('1s'); }); - it('miliseconds', () => { - const formatMiliseconds = getFormatter(SUPPORTED_FORMATS.miliseconds); + it('milliseconds', () => { + const formatMilliseconds = getFormatter(SUPPORTED_FORMATS.milliseconds); - expect(formatMiliseconds(1)).toEqual('1ms'); - expect(formatMiliseconds(100)).toEqual('100ms'); - expect(formatMiliseconds(1000)).toEqual('1,000ms'); - expect(formatMiliseconds(10000)).toEqual('10,000ms'); - expect(formatMiliseconds(1000000)).toEqual('1,000,000ms'); + expect(formatMilliseconds(1)).toBe('1ms'); + expect(formatMilliseconds(100)).toBe('100ms'); + expect(formatMilliseconds(1000)).toBe('1,000ms'); + expect(formatMilliseconds(10000)).toBe('10,000ms'); + expect(formatMilliseconds(1000000)).toBe('1,000,000ms'); }); - it('bytes', () => { - const formatBytes = getFormatter(SUPPORTED_FORMATS.bytes); - - expect(formatBytes(1)).toEqual('1B'); - expect(formatBytes(1, 1)).toEqual('1.0B'); - - expect(formatBytes(10)).toEqual('10B'); - expect(formatBytes(10 ** 2)).toEqual('100B'); - expect(formatBytes(10 ** 3)).toEqual('1kB'); - expect(formatBytes(10 ** 4)).toEqual('10kB'); - expect(formatBytes(10 ** 5)).toEqual('100kB'); - expect(formatBytes(10 ** 6)).toEqual('1MB'); - expect(formatBytes(10 ** 7)).toEqual('10MB'); - expect(formatBytes(10 ** 8)).toEqual('100MB'); - expect(formatBytes(10 ** 9)).toEqual('1GB'); - expect(formatBytes(10 ** 10)).toEqual('10GB'); - expect(formatBytes(10 ** 11)).toEqual('100GB'); + it('decimalBytes', () => { + const formatDecimalBytes = getFormatter(SUPPORTED_FORMATS.decimalBytes); + + expect(formatDecimalBytes(1)).toBe('1B'); + expect(formatDecimalBytes(1, 1)).toBe('1.0B'); + + expect(formatDecimalBytes(10)).toBe('10B'); + expect(formatDecimalBytes(10 ** 2)).toBe('100B'); + expect(formatDecimalBytes(10 ** 3)).toBe('1kB'); + expect(formatDecimalBytes(10 ** 4)).toBe('10kB'); + expect(formatDecimalBytes(10 ** 5)).toBe('100kB'); + expect(formatDecimalBytes(10 ** 6)).toBe('1MB'); + expect(formatDecimalBytes(10 ** 7)).toBe('10MB'); + expect(formatDecimalBytes(10 ** 8)).toBe('100MB'); + expect(formatDecimalBytes(10 ** 9)).toBe('1GB'); + expect(formatDecimalBytes(10 ** 10)).toBe('10GB'); + expect(formatDecimalBytes(10 ** 11)).toBe('100GB'); }); it('kilobytes', () => { - expect(getFormatter(SUPPORTED_FORMATS.kilobytes)(1)).toEqual('1kB'); - expect(getFormatter(SUPPORTED_FORMATS.kilobytes)(1, 1)).toEqual('1.0kB'); + expect(getFormatter(SUPPORTED_FORMATS.kilobytes)(1)).toBe('1kB'); + expect(getFormatter(SUPPORTED_FORMATS.kilobytes)(1, 1)).toBe('1.0kB'); }); it('megabytes', () => { - expect(getFormatter(SUPPORTED_FORMATS.megabytes)(1)).toEqual('1MB'); - expect(getFormatter(SUPPORTED_FORMATS.megabytes)(1, 1)).toEqual('1.0MB'); + expect(getFormatter(SUPPORTED_FORMATS.megabytes)(1)).toBe('1MB'); + expect(getFormatter(SUPPORTED_FORMATS.megabytes)(1, 1)).toBe('1.0MB'); }); it('gigabytes', () => { - expect(getFormatter(SUPPORTED_FORMATS.gigabytes)(1)).toEqual('1GB'); - expect(getFormatter(SUPPORTED_FORMATS.gigabytes)(1, 1)).toEqual('1.0GB'); + expect(getFormatter(SUPPORTED_FORMATS.gigabytes)(1)).toBe('1GB'); + expect(getFormatter(SUPPORTED_FORMATS.gigabytes)(1, 1)).toBe('1.0GB'); }); it('terabytes', () => { - expect(getFormatter(SUPPORTED_FORMATS.terabytes)(1)).toEqual('1TB'); - expect(getFormatter(SUPPORTED_FORMATS.terabytes)(1, 1)).toEqual('1.0TB'); + expect(getFormatter(SUPPORTED_FORMATS.terabytes)(1)).toBe('1TB'); + expect(getFormatter(SUPPORTED_FORMATS.terabytes)(1, 1)).toBe('1.0TB'); }); it('petabytes', () => { - expect(getFormatter(SUPPORTED_FORMATS.petabytes)(1)).toEqual('1PB'); - expect(getFormatter(SUPPORTED_FORMATS.petabytes)(1, 1)).toEqual('1.0PB'); + expect(getFormatter(SUPPORTED_FORMATS.petabytes)(1)).toBe('1PB'); + expect(getFormatter(SUPPORTED_FORMATS.petabytes)(1, 1)).toBe('1.0PB'); + }); + + it('bytes', () => { + const formatBytes = getFormatter(SUPPORTED_FORMATS.bytes); + + expect(formatBytes(1)).toBe('1B'); + expect(formatBytes(1, 1)).toBe('1.0B'); + + expect(formatBytes(10)).toBe('10B'); + expect(formatBytes(100)).toBe('100B'); + expect(formatBytes(1000)).toBe('1,000B'); + + expect(formatBytes(1 * 1024)).toBe('1KiB'); + expect(formatBytes(1 * 1024 ** 2)).toBe('1MiB'); + expect(formatBytes(1 * 1024 ** 3)).toBe('1GiB'); + }); + + it('kibibytes', () => { + expect(getFormatter(SUPPORTED_FORMATS.kibibytes)(1)).toBe('1KiB'); + expect(getFormatter(SUPPORTED_FORMATS.kibibytes)(1, 1)).toBe('1.0KiB'); + }); + + it('mebibytes', () => { + expect(getFormatter(SUPPORTED_FORMATS.mebibytes)(1)).toBe('1MiB'); + expect(getFormatter(SUPPORTED_FORMATS.mebibytes)(1, 1)).toBe('1.0MiB'); + }); + + it('gibibytes', () => { + expect(getFormatter(SUPPORTED_FORMATS.gibibytes)(1)).toBe('1GiB'); + expect(getFormatter(SUPPORTED_FORMATS.gibibytes)(1, 1)).toBe('1.0GiB'); + }); + + it('tebibytes', () => { + expect(getFormatter(SUPPORTED_FORMATS.tebibytes)(1)).toBe('1TiB'); + expect(getFormatter(SUPPORTED_FORMATS.tebibytes)(1, 1)).toBe('1.0TiB'); + }); + + it('pebibytes', () => { + expect(getFormatter(SUPPORTED_FORMATS.pebibytes)(1)).toBe('1PiB'); + expect(getFormatter(SUPPORTED_FORMATS.pebibytes)(1, 1)).toBe('1.0PiB'); }); }); diff --git a/spec/frontend/notes/components/note_app_spec.js b/spec/frontend/notes/components/note_app_spec.js index a51c7c57f6c..2d0cca18647 100644 --- a/spec/frontend/notes/components/note_app_spec.js +++ b/spec/frontend/notes/components/note_app_spec.js @@ -5,7 +5,6 @@ import { mount } from '@vue/test-utils'; import { setTestTimeout } from 'helpers/timeout'; import axios from '~/lib/utils/axios_utils'; import NotesApp from '~/notes/components/notes_app.vue'; -import service from '~/notes/services/notes_service'; import createStore from '~/notes/stores'; import '~/behaviors/markdown/render_gfm'; // TODO: use generated fixture (https://gitlab.com/gitlab-org/gitlab-foss/issues/62491) @@ -192,7 +191,6 @@ describe('note_app', () => { describe('individual note', () => { beforeEach(() => { axiosMock.onAny().reply(mockData.getIndividualNoteResponse); - jest.spyOn(service, 'updateNote'); wrapper = mountComponent(); return waitForDiscussionsRequest().then(() => { wrapper.find('.js-note-edit').trigger('click'); @@ -203,18 +201,18 @@ describe('note_app', () => { expect(wrapper.find('.js-vue-issue-note-form').exists()).toBe(true); }); - it('calls the service to update the note', () => { + it('calls the store action to update the note', () => { + jest.spyOn(axios, 'put').mockImplementation(() => Promise.resolve({ data: {} })); wrapper.find('.js-vue-issue-note-form').value = 'this is a note'; wrapper.find('.js-vue-issue-save').trigger('click'); - expect(service.updateNote).toHaveBeenCalled(); + expect(axios.put).toHaveBeenCalled(); }); }); describe('discussion note', () => { beforeEach(() => { axiosMock.onAny().reply(mockData.getDiscussionNoteResponse); - jest.spyOn(service, 'updateNote'); wrapper = mountComponent(); return waitForDiscussionsRequest().then(() => { wrapper.find('.js-note-edit').trigger('click'); @@ -226,10 +224,11 @@ describe('note_app', () => { }); it('updates the note and resets the edit form', () => { + jest.spyOn(axios, 'put').mockImplementation(() => Promise.resolve({ data: {} })); wrapper.find('.js-vue-issue-note-form').value = 'this is a note'; wrapper.find('.js-vue-issue-save').trigger('click'); - expect(service.updateNote).toHaveBeenCalled(); + expect(axios.put).toHaveBeenCalled(); }); }); }); diff --git a/spec/javascripts/frequent_items/components/frequent_items_list_spec.js b/spec/javascripts/frequent_items/components/frequent_items_list_spec.js deleted file mode 100644 index 3fcd79480cc..00000000000 --- a/spec/javascripts/frequent_items/components/frequent_items_list_spec.js +++ /dev/null @@ -1,90 +0,0 @@ -import Vue from 'vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import frequentItemsListComponent from '~/frequent_items/components/frequent_items_list.vue'; -import { mockFrequentProjects } from '../mock_data'; - -const createComponent = (namespace = 'projects') => { - const Component = Vue.extend(frequentItemsListComponent); - - return mountComponent(Component, { - namespace, - items: mockFrequentProjects, - isFetchFailed: false, - hasSearchQuery: false, - matcher: 'lab', - }); -}; - -describe('FrequentItemsListComponent', () => { - let vm; - - beforeEach(() => { - vm = createComponent(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('computed', () => { - describe('isListEmpty', () => { - it('should return `true` or `false` representing whether if `items` is empty or not with projects', () => { - vm.items = []; - - expect(vm.isListEmpty).toBe(true); - - vm.items = mockFrequentProjects; - - expect(vm.isListEmpty).toBe(false); - }); - }); - - describe('fetched item messages', () => { - it('should return appropriate empty list message based on value of `localStorageFailed` prop with projects', () => { - vm.isFetchFailed = true; - - expect(vm.listEmptyMessage).toBe('This feature requires browser localStorage support'); - - vm.isFetchFailed = false; - - expect(vm.listEmptyMessage).toBe('Projects you visit often will appear here'); - }); - }); - - describe('searched item messages', () => { - it('should return appropriate empty list message based on value of `searchFailed` prop with projects', () => { - vm.hasSearchQuery = true; - vm.isFetchFailed = true; - - expect(vm.listEmptyMessage).toBe('Something went wrong on our end.'); - - vm.isFetchFailed = false; - - expect(vm.listEmptyMessage).toBe('Sorry, no projects matched your search'); - }); - }); - }); - - describe('template', () => { - it('should render component element with list of projects', done => { - vm.items = mockFrequentProjects; - - Vue.nextTick(() => { - expect(vm.$el.classList.contains('frequent-items-list-container')).toBe(true); - expect(vm.$el.querySelectorAll('ul.list-unstyled').length).toBe(1); - expect(vm.$el.querySelectorAll('li.frequent-items-list-item-container').length).toBe(5); - done(); - }); - }); - - it('should render component element with empty message', done => { - vm.items = []; - - Vue.nextTick(() => { - expect(vm.$el.querySelectorAll('li.section-empty').length).toBe(1); - expect(vm.$el.querySelectorAll('li.frequent-items-list-item-container').length).toBe(0); - done(); - }); - }); - }); -}); diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index c0501fb16c6..dd1588036b6 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -178,67 +178,63 @@ describe Issue do let(:namespace) { build(:namespace, path: 'sample-namespace') } let(:project) { build(:project, name: 'sample-project', namespace: namespace) } let(:issue) { build(:issue, iid: 1, project: project) } - let(:group) { create(:group, name: 'Group', path: 'sample-group') } context 'when nil argument' do it 'returns issue id' do expect(issue.to_reference).to eq "#1" end - end - context 'when full is true' do - it 'returns complete path to the issue' do - expect(issue.to_reference(full: true)).to eq 'sample-namespace/sample-project#1' - expect(issue.to_reference(project, full: true)).to eq 'sample-namespace/sample-project#1' - expect(issue.to_reference(group, full: true)).to eq 'sample-namespace/sample-project#1' - end - end - - context 'when same project argument' do - it 'returns issue id' do - expect(issue.to_reference(project)).to eq("#1") + it 'returns complete path to the issue with full: true' do + expect(issue.to_reference(full: true)).to eq 'sample-namespace/sample-project#1' end end - context 'when cross namespace project argument' do - let(:another_namespace_project) { create(:project, name: 'another-project') } + context 'when argument is a project' do + context 'when same project' do + it 'returns issue id' do + expect(issue.to_reference(project)).to eq("#1") + end - it 'returns complete path to the issue' do - expect(issue.to_reference(another_namespace_project)).to eq 'sample-namespace/sample-project#1' + it 'returns full reference with full: true' do + expect(issue.to_reference(project, full: true)).to eq 'sample-namespace/sample-project#1' + end end - end - it 'supports a cross-project reference' do - another_project = build(:project, name: 'another-project', namespace: project.namespace) - expect(issue.to_reference(another_project)).to eq "sample-project#1" - end - - context 'when same namespace / cross-project argument' do - let(:another_project) { create(:project, namespace: namespace) } + context 'when cross-project in same namespace' do + let(:another_project) do + build(:project, name: 'another-project', namespace: project.namespace) + end - it 'returns path to the issue with the project name' do - expect(issue.to_reference(another_project)).to eq 'sample-project#1' + it 'returns a cross-project reference' do + expect(issue.to_reference(another_project)).to eq "sample-project#1" + end end - end - context 'when different namespace / cross-project argument' do - let(:another_namespace) { create(:namespace, path: 'another-namespace') } - let(:another_project) { create(:project, path: 'another-project', namespace: another_namespace) } + context 'when cross-project in different namespace' do + let(:another_namespace) { build(:namespace, path: 'another-namespace') } + let(:another_namespace_project) { build(:project, path: 'another-project', namespace: another_namespace) } - it 'returns full path to the issue' do - expect(issue.to_reference(another_project)).to eq 'sample-namespace/sample-project#1' + it 'returns complete path to the issue' do + expect(issue.to_reference(another_namespace_project)).to eq 'sample-namespace/sample-project#1' + end end end context 'when argument is a namespace' do - context 'with same project path' do + context 'when same as issue' do it 'returns path to the issue with the project name' do expect(issue.to_reference(namespace)).to eq 'sample-project#1' end + + it 'returns full reference with full: true' do + expect(issue.to_reference(namespace, full: true)).to eq 'sample-namespace/sample-project#1' + end end - context 'with different project path' do - it 'returns full path to the issue' do + context 'when different to issue namespace' do + let(:group) { build(:group, name: 'Group', path: 'sample-group') } + + it 'returns full path to the issue with full: true' do expect(issue.to_reference(group)).to eq 'sample-namespace/sample-project#1' end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index e1466ad2b73..635349955b1 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -52,7 +52,7 @@ describe ProjectPolicy do admin_snippet admin_project_member admin_note admin_wiki admin_project admin_commit_status admin_build admin_container_image admin_pipeline admin_environment admin_deployment destroy_release add_cluster - daily_statistics + daily_statistics read_deploy_token ] end diff --git a/spec/requests/api/deploy_tokens_spec.rb b/spec/requests/api/deploy_tokens_spec.rb index 0e3256edcd8..9aa181db7fc 100644 --- a/spec/requests/api/deploy_tokens_spec.rb +++ b/spec/requests/api/deploy_tokens_spec.rb @@ -3,43 +3,84 @@ require 'spec_helper' describe API::DeployTokens do + let(:user) { create(:user) } let(:creator) { create(:user) } let(:project) { create(:project, creator_id: creator.id) } let!(:deploy_token) { create(:deploy_token, projects: [project]) } describe 'GET /deploy_tokens' do - subject { get api('/deploy_tokens', user) } + subject do + get api('/deploy_tokens', user) + response + end context 'when unauthenticated' do let(:user) { nil } - it 'rejects the response as unauthorized' do - subject - - expect(response).to have_gitlab_http_status(:unauthorized) - end + it { is_expected.to have_gitlab_http_status(:unauthorized) } end context 'when authenticated as non-admin user' do let(:user) { creator } - it 'rejects the response as forbidden' do - subject - - expect(response).to have_gitlab_http_status(:forbidden) - end + it { is_expected.to have_gitlab_http_status(:forbidden) } end context 'when authenticated as admin' do let(:user) { create(:admin) } + it { is_expected.to have_gitlab_http_status(:ok) } + it 'returns all deploy tokens' do subject - expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.first['id']).to eq(deploy_token.id) + expect(response).to match_response_schema('public_api/v4/deploy_tokens') + end + end + end + + describe 'GET /projects/:id/deploy_tokens' do + subject do + get api("/projects/#{project.id}/deploy_tokens", user) + response + end + + context 'when unauthenticated' do + let(:user) { nil } + + it { is_expected.to have_gitlab_http_status(:not_found) } + end + + context 'when authenticated as non-admin user' do + before do + project.add_developer(user) + end + + it { is_expected.to have_gitlab_http_status(:forbidden) } + end + + context 'when authenticated as maintainer' do + let!(:other_deploy_token) { create(:deploy_token) } + + before do + project.add_maintainer(user) + end + + it { is_expected.to have_gitlab_http_status(:ok) } + + it 'returns all deploy tokens for the project' do + subject + + expect(response).to include_pagination_headers + expect(response).to match_response_schema('public_api/v4/deploy_tokens') + end + + it 'does not return deploy tokens for other projects' do + subject + + token_ids = json_response.map { |token| token['id'] } + expect(token_ids).not_to include(other_deploy_token.id) end end end |