diff options
46 files changed, 483 insertions, 137 deletions
diff --git a/app/assets/javascripts/static_site_editor/components/edit_header.vue b/app/assets/javascripts/static_site_editor/components/edit_header.vue new file mode 100644 index 00000000000..5660bfbe5ae --- /dev/null +++ b/app/assets/javascripts/static_site_editor/components/edit_header.vue @@ -0,0 +1,23 @@ +<script> +import { DEFAULT_HEADING } from '../constants'; + +export default { + props: { + title: { + type: String, + required: false, + default: '', + }, + }, + computed: { + heading() { + return this.title || DEFAULT_HEADING; + }, + }, +}; +</script> +<template> + <div> + <h3 ref="sseHeading">{{ heading }}</h3> + </div> +</template> diff --git a/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue b/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue index 7f00fb71b04..efb442d4d09 100644 --- a/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue +++ b/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue @@ -7,6 +7,11 @@ export default { GlLoadingIcon, }, props: { + returnUrl: { + type: String, + required: false, + default: '', + }, saveable: { type: Boolean, required: false, @@ -23,12 +28,17 @@ export default { <template> <div class="d-flex bg-light border-top justify-content-between align-items-center py-3 px-4"> <gl-loading-icon :class="{ invisible: !savingChanges }" size="md" /> - <gl-new-button - variant="success" - :disabled="!saveable || savingChanges" - @click="$emit('submit')" - > - {{ __('Submit Changes') }} - </gl-new-button> + <div> + <gl-new-button v-if="returnUrl" ref="returnUrlLink" :href="returnUrl">{{ + s__('StaticSiteEditor|Return to site') + }}</gl-new-button> + <gl-new-button + variant="success" + :disabled="!saveable || savingChanges" + @click="$emit('submit')" + > + {{ __('Submit Changes') }} + </gl-new-button> + </div> </div> </template> diff --git a/app/assets/javascripts/static_site_editor/components/static_site_editor.vue b/app/assets/javascripts/static_site_editor/components/static_site_editor.vue index 8deae2f2c8a..4d912f5c0b5 100644 --- a/app/assets/javascripts/static_site_editor/components/static_site_editor.vue +++ b/app/assets/javascripts/static_site_editor/components/static_site_editor.vue @@ -3,16 +3,25 @@ import { mapState, mapGetters, mapActions } from 'vuex'; import { GlSkeletonLoader } from '@gitlab/ui'; import EditArea from './edit_area.vue'; +import EditHeader from './edit_header.vue'; import Toolbar from './publish_toolbar.vue'; export default { components: { EditArea, + EditHeader, GlSkeletonLoader, Toolbar, }, computed: { - ...mapState(['content', 'isLoadingContent', 'isSavingChanges', 'isContentLoaded']), + ...mapState([ + 'content', + 'isLoadingContent', + 'isSavingChanges', + 'isContentLoaded', + 'returnUrl', + 'title', + ]), ...mapGetters(['contentChanged']), }, mounted() { @@ -24,7 +33,7 @@ export default { }; </script> <template> - <div class="d-flex justify-content-center h-100 pt-2"> + <div class="d-flex justify-content-center h-100 pt-2"> <div v-if="isLoadingContent" class="w-50 h-50"> <gl-skeleton-loader :width="500" :height="102"> <rect width="500" height="16" rx="4" /> @@ -36,12 +45,14 @@ export default { </gl-skeleton-loader> </div> <div v-if="isContentLoaded" class="d-flex flex-grow-1 flex-column"> + <edit-header class="w-75 align-self-center py-2" :title="title" /> <edit-area class="w-75 h-100 shadow-none align-self-center" :value="content" @input="setContent" /> <toolbar + :return-url="returnUrl" :saveable="contentChanged" :saving-changes="isSavingChanges" @submit="submitChanges" diff --git a/app/assets/javascripts/static_site_editor/constants.js b/app/assets/javascripts/static_site_editor/constants.js index 5081d467016..d7ce2a93a56 100644 --- a/app/assets/javascripts/static_site_editor/constants.js +++ b/app/assets/javascripts/static_site_editor/constants.js @@ -10,3 +10,5 @@ export const SUBMIT_CHANGES_COMMIT_ERROR = s__( export const SUBMIT_CHANGES_MERGE_REQUEST_ERROR = s__( 'StaticSiteEditor|Could not create merge request.', ); + +export const DEFAULT_HEADING = s__('StaticSiteEditor|Static site editor'); diff --git a/app/assets/javascripts/static_site_editor/index.js b/app/assets/javascripts/static_site_editor/index.js index 3d40f3918a4..c6a883c659a 100644 --- a/app/assets/javascripts/static_site_editor/index.js +++ b/app/assets/javascripts/static_site_editor/index.js @@ -3,10 +3,10 @@ import StaticSiteEditor from './components/static_site_editor.vue'; import createStore from './store'; const initStaticSiteEditor = el => { - const { projectId, path: sourcePath } = el.dataset; + const { projectId, returnUrl, path: sourcePath } = el.dataset; const store = createStore({ - initialState: { projectId, sourcePath, username: window.gon.current_username }, + initialState: { projectId, returnUrl, sourcePath, username: window.gon.current_username }, }); return new Vue({ diff --git a/app/assets/javascripts/static_site_editor/store/state.js b/app/assets/javascripts/static_site_editor/store/state.js index d48cc8ed1a4..98a84d9f75d 100644 --- a/app/assets/javascripts/static_site_editor/store/state.js +++ b/app/assets/javascripts/static_site_editor/store/state.js @@ -1,6 +1,7 @@ const createState = (initialState = {}) => ({ username: null, projectId: null, + returnUrl: null, sourcePath: null, isLoadingContent: false, diff --git a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue index 82b3c784f96..a0c161a335a 100644 --- a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue +++ b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue @@ -1,4 +1,5 @@ <script> +import { __ } from '~/locale'; import { roundOffFloat } from '~/lib/utils/common_utils'; import tooltip from '~/vue_shared/directives/tooltip'; @@ -27,6 +28,11 @@ export default { required: false, default: 'neutral', }, + unavailableLabel: { + type: String, + required: false, + default: __('Not available'), + }, successCount: { type: Number, required: true, @@ -103,7 +109,7 @@ export default { <template> <div :class="cssClass" class="stacked-progress-bar"> - <span v-if="!totalCount" class="status-unavailable"> {{ __('Not available') }} </span> + <span v-if="!totalCount" class="status-unavailable">{{ unavailableLabel }}</span> <span v-if="successPercent" v-tooltip diff --git a/app/controllers/groups/registry/repositories_controller.rb b/app/controllers/groups/registry/repositories_controller.rb index 0240c87e699..16aa6e50320 100644 --- a/app/controllers/groups/registry/repositories_controller.rb +++ b/app/controllers/groups/registry/repositories_controller.rb @@ -9,7 +9,7 @@ module Groups respond_to do |format| format.html format.json do - @images = group.container_repositories.with_api_entity_associations + @images = ContainerRepositoriesFinder.new(user: current_user, subject: group).execute.with_api_entity_associations track_event(:list_repositories) diff --git a/app/controllers/projects/static_site_editor_controller.rb b/app/controllers/projects/static_site_editor_controller.rb index 98ec2335899..74f28c3da67 100644 --- a/app/controllers/projects/static_site_editor_controller.rb +++ b/app/controllers/projects/static_site_editor_controller.rb @@ -2,10 +2,13 @@ class Projects::StaticSiteEditorController < Projects::ApplicationController include ExtractsPath + include CreatesCommit + layout 'fullscreen' prepend_before_action :authenticate_user!, only: [:show] before_action :assign_ref_and_path, only: [:show] + before_action :authorize_edit_tree!, only: [:show] def show @config = Gitlab::StaticSiteEditor::Config.new(@repository, @ref, @path, params[:return_url]) diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb index d5650c6828d..ebb686c2aa7 100644 --- a/app/finders/users_finder.rb +++ b/app/finders/users_finder.rb @@ -14,6 +14,7 @@ # active: boolean # blocked: boolean # external: boolean +# without_projects: boolean # class UsersFinder include CreatedAtFilter @@ -36,6 +37,7 @@ class UsersFinder users = by_external(users) users = by_2fa(users) users = by_created_at(users) + users = by_without_projects(users) users = by_custom_attributes(users) users @@ -94,6 +96,12 @@ class UsersFinder users end end + + def by_without_projects(users) + return users unless params[:without_projects] + + users.without_projects + end end UsersFinder.prepend_if_ee('EE::UsersFinder') diff --git a/changelogs/unreleased/210543-update-deploy-ecs.yml b/changelogs/unreleased/210543-update-deploy-ecs.yml new file mode 100644 index 00000000000..47df9039a17 --- /dev/null +++ b/changelogs/unreleased/210543-update-deploy-ecs.yml @@ -0,0 +1,5 @@ +--- +title: Update aws-ecs image location in CI template +merge_request: 27382 +author: +type: changed diff --git a/changelogs/unreleased/212971-group-level-container-registry-show-subgroups-repos.yml b/changelogs/unreleased/212971-group-level-container-registry-show-subgroups-repos.yml new file mode 100644 index 00000000000..73eeb9923ea --- /dev/null +++ b/changelogs/unreleased/212971-group-level-container-registry-show-subgroups-repos.yml @@ -0,0 +1,5 @@ +--- +title: Group level container registry show subgroups repos +merge_request: 29263 +author: +type: fixed diff --git a/changelogs/unreleased/29426-add-api-endpoint-to-get-users-without-projects.yml b/changelogs/unreleased/29426-add-api-endpoint-to-get-users-without-projects.yml new file mode 100644 index 00000000000..ccf3533e96f --- /dev/null +++ b/changelogs/unreleased/29426-add-api-endpoint-to-get-users-without-projects.yml @@ -0,0 +1,5 @@ +--- +title: Add API endpoint to get users without projects +merge_request: 29347 +author: +type: added diff --git a/changelogs/unreleased/add-user-agent-to-container-registry-client.yml b/changelogs/unreleased/add-user-agent-to-container-registry-client.yml new file mode 100644 index 00000000000..2c3db36f981 --- /dev/null +++ b/changelogs/unreleased/add-user-agent-to-container-registry-client.yml @@ -0,0 +1,5 @@ +--- +title: Add Gitlab User-Agent to ContainerRegistry::Client +merge_request: 29294 +author: Sashi Kumar +type: other diff --git a/db/structure.sql b/db/structure.sql index f478fc3d709..d1fdcc9e465 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -397,9 +397,9 @@ CREATE TABLE public.application_settings ( email_restrictions text, npm_package_requests_forwarding boolean DEFAULT true NOT NULL, namespace_storage_size_limit bigint DEFAULT 0 NOT NULL, - issues_create_limit integer DEFAULT 300 NOT NULL, seat_link_enabled boolean DEFAULT true NOT NULL, - container_expiration_policies_enable_historic_entries boolean DEFAULT false NOT NULL + container_expiration_policies_enable_historic_entries boolean DEFAULT false NOT NULL, + issues_create_limit integer DEFAULT 300 NOT NULL ); CREATE SEQUENCE public.application_settings_id_seq diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md index a9a13062a25..3777f551996 100644 --- a/doc/administration/job_artifacts.md +++ b/doc/administration/job_artifacts.md @@ -353,7 +353,7 @@ you can flip the feature flag from a Rails console. ```shell cd /home/git/gitlab - RAILS_ENV=production sudo -u git -H bundle exec rails console + sudo -u git -H bundle exec rails console -e production ``` 1. Flip the switch and disable it: diff --git a/doc/administration/job_logs.md b/doc/administration/job_logs.md index 439320279fe..6020d1d2850 100644 --- a/doc/administration/job_logs.md +++ b/doc/administration/job_logs.md @@ -106,7 +106,7 @@ gitlab-rails console # Installation from source cd /home/git/gitlab -sudo -u git -H bin/rails console RAILS_ENV=production +sudo -u git -H bin/rails console -e production ``` **To check if incremental logging (trace) is enabled:** diff --git a/doc/administration/troubleshooting/navigating_gitlab_via_rails_console.md b/doc/administration/troubleshooting/navigating_gitlab_via_rails_console.md index b78d5490cd2..69af7ea6801 100644 --- a/doc/administration/troubleshooting/navigating_gitlab_via_rails_console.md +++ b/doc/administration/troubleshooting/navigating_gitlab_via_rails_console.md @@ -30,7 +30,7 @@ sudo gitlab-rails console For source installations, you'll have to instead run: ```shell -sudo -u git -H bundle exec rails console RAILS_ENV=production +sudo -u git -H bundle exec rails console -e production ``` Further code examples will all take place inside the Rails console and also diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index eb9b285803d..91d6717de7e 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -2160,6 +2160,11 @@ type Epic implements Noteable { hasIssues: Boolean! """ + Indicates if the epic has a parent epic + """ + hasParent: Boolean! + + """ Current health status of the epic """ healthStatus: EpicHealthStatus diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index f6c3510d6dc..fb642b1222f 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -6337,6 +6337,24 @@ "deprecationReason": null }, { + "name": "hasParent", + "description": "Indicates if the epic has a parent epic", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "healthStatus", "description": "Current health status of the epic", "args": [ diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 082d7decbf9..d1ea825bef3 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -359,6 +359,7 @@ Represents an epic. | `group` | Group! | Group to which the epic belongs | | `hasChildren` | Boolean! | Indicates if the epic has children | | `hasIssues` | Boolean! | Indicates if the epic has direct issues | +| `hasParent` | Boolean! | Indicates if the epic has a parent epic | | `healthStatus` | EpicHealthStatus | Current health status of the epic | | `id` | ID! | ID of the epic | | `iid` | ID! | Internal ID of the epic | diff --git a/doc/api/users.md b/doc/api/users.md index 8d7dad7ae35..90aafcef035 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -75,6 +75,7 @@ GET /users | `order_by` | string | no | Return users ordered by `id`, `name`, `username`, `created_at`, or `updated_at` fields. Default is `id` | | `sort` | string | no | Return users sorted in `asc` or `desc` order. Default is `desc` | | `two_factor` | string | no | Filter users by Two-factor authentication. Filter values are `enabled` or `disabled`. By default it returns all users | +| `without_projects` | boolean | no | Filter users without projects. Default is `false` | ```json [ @@ -207,6 +208,8 @@ You can search users by creation date time range with: GET /users?created_before=2001-01-02T00:00:00.060Z&created_after=1999-01-02T00:00:00.060 ``` +You can search for users without projects with: `/users?without_projects=true` + You can filter by [custom attributes](custom_attributes.md) with: ```plaintext diff --git a/doc/ci/cloud_deployment/index.md b/doc/ci/cloud_deployment/index.md index ccff302750c..f70998a5f49 100644 --- a/doc/ci/cloud_deployment/index.md +++ b/doc/ci/cloud_deployment/index.md @@ -39,7 +39,7 @@ Some credentials are required to be able to run `aws` commands: ```yml deploy: stage: deploy - image: registry.gitlab.com/gitlab-org/cloud-deploy:latest # see the note below + image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest # see the note below script: - aws s3 ... - aws create-deployment ... @@ -47,7 +47,7 @@ Some credentials are required to be able to run `aws` commands: NOTE: **Note:** Please note that the image used in the example above - (`registry.gitlab.com/gitlab-org/cloud-deploy:latest`) is hosted on the [GitLab + (`registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest`) is hosted on the [GitLab Container Registry](../../user/packages/container_registry/index.md) and is ready to use. Alternatively, replace the image with another one hosted on [AWS ECR](#aws-ecr). @@ -119,3 +119,15 @@ After you're all set up on AWS ECS, follow these steps: Finally, your AWS ECS service will be updated with the new revision of the task definition, making the cluster pull the newest version of your application. + +Alternatively, if you don't wish to use the `Deploy-ECS.gitlab-ci.yml` template +to deploy to AWS ECS, you can always use our +`aws-base` Docker image to run your own [AWS CLI commands for ECS](https://docs.aws.amazon.com/cli/latest/reference/ecs/index.html#cli-aws-ecs). + +```yaml +deploy: + stage: deploy + image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest + script: + - aws ecs register-task-definition ... +``` diff --git a/doc/development/database_debugging.md b/doc/development/database_debugging.md index e577ba6ec8f..46cb869fea3 100644 --- a/doc/development/database_debugging.md +++ b/doc/development/database_debugging.md @@ -41,8 +41,8 @@ Access the database via one of these commands (they all get you to the same plac ```ruby gdk psql -d gitlabhq_development -bundle exec rails dbconsole RAILS_ENV=development -bundle exec rails db RAILS_ENV=development +bundle exec rails dbconsole -e development +bundle exec rails db -e development ``` - `\q`: Quit/exit diff --git a/doc/development/import_project.md b/doc/development/import_project.md index 0701279ddea..78efc6ce2ab 100644 --- a/doc/development/import_project.md +++ b/doc/development/import_project.md @@ -78,7 +78,7 @@ The last option is to import a project using a Rails console: gitlab-rails console # For installations from source - sudo -u git -H bundle exec rails console RAILS_ENV=production + sudo -u git -H bundle exec rails console -e production ``` 1. Create a project and run `Project::TreeRestorer`: diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md index efa58cbeae3..9eb5d5add8a 100644 --- a/doc/development/testing_guide/review_apps.md +++ b/doc/development/testing_guide/review_apps.md @@ -136,28 +136,18 @@ browser performance testing using a ### Node pools -Both `review-apps-ce` and `review-apps-ee` clusters are currently set up with -two node pools: +The `review-apps-ee` and `review-apps-ce` clusters are currently set up with +the following node pools: -- a node pool of non-preemptible `n1-standard-2` (2 vCPU, 7.5 GB memory) nodes - dedicated to the `tiller` deployment (see below) with a single node. -- a node pool of preemptible `n1-standard-2` (2 vCPU, 7.5 GB memory) nodes, - with a minimum of 1 node and a maximum of 250 nodes. +- `review-apps-ee` of preemptible `e2-highcpu-16` (16 vCPU, 16 GB memory) nodes with autoscaling +- `review-apps-ce` of preemptible `n1-standard-8` (8 vCPU, 16 GB memory) nodes with autoscaling -### Helm/Tiller +### Helm -The Helm/Tiller version used is defined in the -[`registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base` image](https://gitlab.com/gitlab-org/gitlab-build-images/blob/master/Dockerfile.gitlab-charts-build-base#L4) +The Helm version used is defined in the +[`registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-helm3-kubectl1.14` image](https://gitlab.com/gitlab-org/gitlab-build-images/-/blob/master/Dockerfile.gitlab-helm3-kubectl1.14#L7) used by the `review-deploy` and `review-stop` jobs. -The `tiller` deployment (the Helm server) is deployed to a dedicated node pool -that has the `app=helm` label and a specific -[taint](https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/) -to prevent other pods from being scheduled on this node pool. - -This is to ensure Tiller isn't affected by "noisy" neighbors that could put -their node under pressure. - ## How to ### Get access to the GCP Review Apps cluster @@ -241,7 +231,7 @@ due to Helm or Kubernetes trying to recreate the components. **Where to look for further debugging:** -Look at a recent `review-deploy` job log, and at the Tiller logs. +Look at a recent `review-deploy` job log. **Useful commands:** diff --git a/doc/integration/sourcegraph.md b/doc/integration/sourcegraph.md index c0ce3c30ca6..5da9dd1fbc9 100644 --- a/doc/integration/sourcegraph.md +++ b/doc/integration/sourcegraph.md @@ -42,7 +42,7 @@ gitlab-rails console # Installation from source cd /home/git/gitlab -sudo -u git -H bin/rails console RAILS_ENV=production +sudo -u git -H bin/rails console -e production ``` Then run the following command to enable the feature flag: diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index b0d90ea0345..e98df17d944 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -947,7 +947,7 @@ backup beforehand. For installations from source: ```shell - sudo -u git -H bundle exec rails dbconsole RAILS_ENV=production + sudo -u git -H bundle exec rails dbconsole -e production ``` 1. Check the `ci_group_variables` and `ci_variables` tables: @@ -982,7 +982,7 @@ backup beforehand. For installations from source: ```shell - sudo -u git -H bundle exec rails dbconsole RAILS_ENV=production + sudo -u git -H bundle exec rails dbconsole -e production ``` 1. Clear all the tokens for projects, groups, and the whole instance: @@ -1015,7 +1015,7 @@ backup beforehand. For installations from source: ```shell - sudo -u git -H bundle exec rails dbconsole RAILS_ENV=production + sudo -u git -H bundle exec rails dbconsole -e production ``` 1. Clear all the tokens for pending jobs: diff --git a/doc/security/unlock_user.md b/doc/security/unlock_user.md index befb5d12877..bf3bbbb701e 100644 --- a/doc/security/unlock_user.md +++ b/doc/security/unlock_user.md @@ -16,7 +16,7 @@ To unlock a locked user: sudo gitlab-rails console -e production ## For installations from source - sudo -u git -H bundle exec rails console RAILS_ENV=production + sudo -u git -H bundle exec rails console -e production ``` 1. Find the user to unlock. You can search by email or ID. diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md index d378c119aa8..e5896e62397 100644 --- a/doc/user/project/merge_requests/merge_request_approvals.md +++ b/doc/user/project/merge_requests/merge_request_approvals.md @@ -277,7 +277,7 @@ gitlab-rails console # Installation from source cd /home/git/gitlab -sudo -u git -H bin/rails console RAILS_ENV=production +sudo -u git -H bin/rails console -e production ``` Then run `Feature.enable(:approval_rules)` to enable the updated interface. diff --git a/lib/api/users.rb b/lib/api/users.rb index 1694f3fe3fb..c986414c223 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -82,6 +82,7 @@ module API optional :blocked, type: Boolean, default: false, desc: 'Filters only blocked users' optional :created_after, type: DateTime, desc: 'Return users created after the specified time' optional :created_before, type: DateTime, desc: 'Return users created before the specified time' + optional :without_projects, type: Boolean, default: false, desc: 'Filters only users without projects' all_or_none_of :extern_uid, :provider use :sort_params @@ -94,7 +95,7 @@ module API authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?) unless current_user&.admin? - params.except!(:created_after, :created_before, :order_by, :sort, :two_factor) + params.except!(:created_after, :created_before, :order_by, :sort, :two_factor, :without_projects) end users = UsersFinder.new(current_user, params).execute diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb index 12f7f04634f..56f556c229a 100644 --- a/lib/container_registry/client.rb +++ b/lib/container_registry/client.rb @@ -159,13 +159,13 @@ module ContainerRegistry end def faraday - @faraday ||= Faraday.new(@base_uri) do |conn| + @faraday ||= faraday_base do |conn| initialize_connection(conn, @options, &method(:accept_manifest)) end end def faraday_blob - @faraday_blob ||= Faraday.new(@base_uri) do |conn| + @faraday_blob ||= faraday_base do |conn| initialize_connection(conn, @options) end end @@ -173,12 +173,16 @@ module ContainerRegistry # Create a new request to make sure the Authorization header is not inserted # via the Faraday middleware def faraday_redirect - @faraday_redirect ||= Faraday.new(@base_uri) do |conn| + @faraday_redirect ||= faraday_base do |conn| conn.request :json conn.adapter :net_http end end + def faraday_base(&block) + Faraday.new(@base_uri, headers: { user_agent: "GitLab/#{Gitlab::VERSION}" }, &block) + end + def delete_if_exists(path) result = faraday.delete(path) diff --git a/lib/gitlab/ci/templates/Deploy-ECS.gitlab-ci.yml b/lib/gitlab/ci/templates/Deploy-ECS.gitlab-ci.yml index ecca1731579..a41b399032f 100644 --- a/lib/gitlab/ci/templates/Deploy-ECS.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Deploy-ECS.gitlab-ci.yml @@ -9,7 +9,7 @@ include: - template: Jobs/Build.gitlab-ci.yml .deploy_to_ecs: - image: registry.gitlab.com/gitlab-org/cloud-deploy:latest + image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-ecs:latest script: - ecs update-task-definition diff --git a/lib/gitlab/static_site_editor/config.rb b/lib/gitlab/static_site_editor/config.rb index 4bc0fc95abd..41d54ee0a92 100644 --- a/lib/gitlab/static_site_editor/config.rb +++ b/lib/gitlab/static_site_editor/config.rb @@ -3,33 +3,49 @@ module Gitlab module StaticSiteEditor class Config + SUPPORTED_EXTENSIONS = %w[.md].freeze + def initialize(repository, ref, file_path, return_url) @repository = repository @ref = ref @file_path = file_path @return_url = return_url + @commit_id = repository.commit(ref)&.id if ref end def payload { branch: ref, path: file_path, - commit: commit.id, + commit_id: commit_id, project_id: project.id, project: project.path, namespace: project.namespace.path, - return_url: return_url + return_url: return_url, + is_supported_content: supported_content? } end private - attr_reader :repository, :ref, :file_path, :return_url + attr_reader :repository, :ref, :file_path, :return_url, :commit_id delegate :project, to: :repository - def commit - repository.commit(ref) + def supported_content? + master_branch? && extension_supported? && file_exists? + end + + def master_branch? + ref == 'master' + end + + def extension_supported? + File.extname(file_path).in?(SUPPORTED_EXTENSIONS) + end + + def file_exists? + commit_id.present? && repository.blob_at(commit_id, file_path).present? end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a267e65a925..3b1c8f0d706 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -11891,6 +11891,9 @@ msgstr "" msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab." msgstr "" +msgid "Learn how to enable synchronization" +msgstr "" + msgid "Learn more" msgstr "" @@ -13827,6 +13830,9 @@ msgstr "" msgid "Nothing to preview." msgstr "" +msgid "Nothing to synchronize" +msgstr "" + msgid "Notification events" msgstr "" @@ -19452,6 +19458,9 @@ msgstr "" msgid "StaticSiteEditor|Return to site" msgstr "" +msgid "StaticSiteEditor|Static site editor" +msgstr "" + msgid "StaticSiteEditor|Success!" msgstr "" @@ -19866,6 +19875,12 @@ msgstr "" msgid "Synced" msgstr "" +msgid "Synchronization disabled" +msgstr "" + +msgid "Synchronization of container repositories is disabled." +msgstr "" + msgid "System" msgstr "" diff --git a/spec/controllers/groups/registry/repositories_controller_spec.rb b/spec/controllers/groups/registry/repositories_controller_spec.rb index eadc3a7f739..a84664c6c04 100644 --- a/spec/controllers/groups/registry/repositories_controller_spec.rb +++ b/spec/controllers/groups/registry/repositories_controller_spec.rb @@ -7,6 +7,13 @@ describe Groups::Registry::RepositoriesController do let_it_be(:guest) { create(:user) } let_it_be(:group, reload: true) { create(:group) } + subject do + get :index, params: { + group_id: group, + format: format + } + end + before do stub_container_registry_config(enabled: true) group.add_owner(user) @@ -15,51 +22,67 @@ describe Groups::Registry::RepositoriesController do end shared_examples 'renders a list of repositories' do + let_it_be(:repo) { create_project_with_repo(test_group) } + + it 'returns a list of projects for json format' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_kind_of(Array) + expect(json_response.first).to include( + 'id' => repo.id, + 'name' => repo.name + ) + end + end + + shared_examples 'renders correctly' do context 'when user has access to registry' do - it 'show index page' do - expect(Gitlab::Tracking).not_to receive(:event) + let_it_be(:test_group) { group } - get :index, params: { - group_id: group - } + context 'html format' do + let(:format) { :html } - expect(response).to have_gitlab_http_status(:ok) - end + it 'show index page' do + expect(Gitlab::Tracking).not_to receive(:event) - it 'has the correct response schema' do - get :index, params: { - group_id: group, - format: :json - } + subject - expect(response).to match_response_schema('registry/repositories') - expect(response).to include_pagination_headers + expect(response).to have_gitlab_http_status(:ok) + end end - it 'returns a list of projects for json format' do - project = create(:project, group: group) - repo = create(:container_repository, project: project) - - get :index, params: { - group_id: group, - format: :json - } - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to be_kind_of(Array) - expect(json_response.first).to include( - 'id' => repo.id, - 'name' => repo.name - ) - end + context 'json format' do + let(:format) { :json } + + it 'has the correct response schema' do + subject + + expect(response).to match_response_schema('registry/repositories') + expect(response).to include_pagination_headers + end - it 'tracks the event' do - expect(Gitlab::Tracking).to receive(:event).with(anything, 'list_repositories', {}) + it_behaves_like 'renders a list of repositories' - get :index, params: { - group_id: group, - format: :json - } + it_behaves_like 'a gitlab tracking event', described_class.name, 'list_repositories' + + context 'with project in subgroup' do + let_it_be(:test_group) { create(:group, parent: group ) } + + it_behaves_like 'renders a list of repositories' + + context 'with project in subgroup and group' do + let_it_be(:repo_in_test_group) { create_project_with_repo(test_group) } + let_it_be(:repo_in_group) { create_project_with_repo(group) } + + it 'returns all the projects' do + subject + + expect(json_response).to be_kind_of(Array) + expect(json_response.length).to eq 2 + end + end + end end end @@ -69,20 +92,30 @@ describe Groups::Registry::RepositoriesController do sign_in(guest) end - it 'renders not found' do - get :index, params: { - group_id: group - } - expect(response).to have_gitlab_http_status(:not_found) + context 'json format' do + let(:format) { :json } + + it_behaves_like 'returning response status', :not_found + end + + context 'html format' do + let(:format) { :html } + + it_behaves_like 'returning response status', :not_found end end end context 'GET #index' do - it_behaves_like 'renders a list of repositories' + it_behaves_like 'renders correctly' end context 'GET #show' do - it_behaves_like 'renders a list of repositories' + it_behaves_like 'renders correctly' + end + + def create_project_with_repo(group) + project = create(:project, group: test_group) + create(:container_repository, project: project) end end diff --git a/spec/controllers/projects/static_site_editor_controller_spec.rb b/spec/controllers/projects/static_site_editor_controller_spec.rb index d1224bb75c0..f7c8848b8cf 100644 --- a/spec/controllers/projects/static_site_editor_controller_spec.rb +++ b/spec/controllers/projects/static_site_editor_controller_spec.rb @@ -26,7 +26,21 @@ describe Projects::StaticSiteEditorController do end end - %w[guest developer maintainer].each do |role| + context 'as guest' do + let(:user) { create(:user) } + + before do + project.add_guest(user) + sign_in(user) + get :show, params: default_params + end + + it 'responds with 404 page' do + expect(response).to have_gitlab_http_status(:not_found) + end + end + + %w[developer maintainer].each do |role| context "as #{role}" do let(:user) { create(:user) } diff --git a/spec/frontend/static_site_editor/components/edit_header_spec.js b/spec/frontend/static_site_editor/components/edit_header_spec.js new file mode 100644 index 00000000000..2b0fe226a0b --- /dev/null +++ b/spec/frontend/static_site_editor/components/edit_header_spec.js @@ -0,0 +1,38 @@ +import { shallowMount } from '@vue/test-utils'; + +import EditHeader from '~/static_site_editor/components/edit_header.vue'; +import { DEFAULT_HEADING } from '~/static_site_editor/constants'; + +import { sourceContentTitle } from '../mock_data'; + +describe('~/static_site_editor/components/edit_header.vue', () => { + let wrapper; + + const buildWrapper = (propsData = {}) => { + wrapper = shallowMount(EditHeader, { + propsData: { + ...propsData, + }, + }); + }; + + const findHeading = () => wrapper.find({ ref: 'sseHeading' }); + + beforeEach(() => { + buildWrapper(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the default heading if there is no title prop', () => { + expect(findHeading().text()).toBe(DEFAULT_HEADING); + }); + + it('renders the title prop value in the heading', () => { + buildWrapper({ title: sourceContentTitle }); + + expect(findHeading().text()).toBe(sourceContentTitle); + }); +}); diff --git a/spec/frontend/static_site_editor/components/publish_toolbar_spec.js b/spec/frontend/static_site_editor/components/publish_toolbar_spec.js index 0edc3f4c920..f00fc38430f 100644 --- a/spec/frontend/static_site_editor/components/publish_toolbar_spec.js +++ b/spec/frontend/static_site_editor/components/publish_toolbar_spec.js @@ -3,6 +3,8 @@ import { GlNewButton, GlLoadingIcon } from '@gitlab/ui'; import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue'; +import { returnUrl } from '../mock_data'; + describe('Static Site Editor Toolbar', () => { let wrapper; @@ -15,6 +17,7 @@ describe('Static Site Editor Toolbar', () => { }); }; + const findReturnUrlLink = () => wrapper.find({ ref: 'returnUrlLink' }); const findSaveChangesButton = () => wrapper.find(GlNewButton); const findLoadingIndicator = () => wrapper.find(GlLoadingIcon); @@ -38,6 +41,17 @@ describe('Static Site Editor Toolbar', () => { expect(findLoadingIndicator().classes()).toContain('invisible'); }); + it('does not render returnUrl link', () => { + expect(findReturnUrlLink().exists()).toBe(false); + }); + + it('renders returnUrl link when returnUrl prop exists', () => { + buildWrapper({ returnUrl }); + + expect(findReturnUrlLink().exists()).toBe(true); + expect(findReturnUrlLink().attributes('href')).toBe(returnUrl); + }); + describe('when saveable', () => { it('enables Submit Changes button', () => { buildWrapper({ saveable: true }); diff --git a/spec/frontend/static_site_editor/components/static_site_editor_spec.js b/spec/frontend/static_site_editor/components/static_site_editor_spec.js index 2c4fa0e061a..d427df9bd4b 100644 --- a/spec/frontend/static_site_editor/components/static_site_editor_spec.js +++ b/spec/frontend/static_site_editor/components/static_site_editor_spec.js @@ -7,9 +7,10 @@ import createState from '~/static_site_editor/store/state'; import StaticSiteEditor from '~/static_site_editor/components/static_site_editor.vue'; import EditArea from '~/static_site_editor/components/edit_area.vue'; +import EditHeader from '~/static_site_editor/components/edit_header.vue'; import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue'; -import { sourceContent } from '../mock_data'; +import { sourceContent, sourceContentTitle } from '../mock_data'; const localVue = createLocalVue(); @@ -60,6 +61,7 @@ describe('StaticSiteEditor', () => { }; const findEditArea = () => wrapper.find(EditArea); + const findEditHeader = () => wrapper.find(EditHeader); const findPublishToolbar = () => wrapper.find(PublishToolbar); const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader); @@ -77,16 +79,21 @@ describe('StaticSiteEditor', () => { expect(findEditArea().exists()).toBe(false); }); + it('does not render edit header', () => { + expect(findEditHeader().exists()).toBe(false); + }); + it('does not render toolbar', () => { expect(findPublishToolbar().exists()).toBe(false); }); }); describe('when content is loaded', () => { - const content = 'edit area content'; + const content = sourceContent; + const title = sourceContentTitle; beforeEach(() => { - buildContentLoadedStore({ initialState: { content } }); + buildContentLoadedStore({ initialState: { content, title } }); buildWrapper(); }); @@ -94,6 +101,10 @@ describe('StaticSiteEditor', () => { expect(findEditArea().exists()).toBe(true); }); + it('renders the edit header', () => { + expect(findEditHeader().exists()).toBe(true); + }); + it('does not render skeleton loader', () => { expect(findSkeletonLoader().exists()).toBe(false); }); @@ -102,6 +113,10 @@ describe('StaticSiteEditor', () => { expect(findEditArea().props('value')).toBe(content); }); + it('passes page title to edit header', () => { + expect(findEditHeader().props('title')).toBe(title); + }); + it('renders toolbar', () => { expect(findPublishToolbar().exists()).toBe(true); }); diff --git a/spec/frontend/static_site_editor/mock_data.js b/spec/frontend/static_site_editor/mock_data.js index 1993636ab12..345ae0ce6f6 100644 --- a/spec/frontend/static_site_editor/mock_data.js +++ b/spec/frontend/static_site_editor/mock_data.js @@ -11,11 +11,11 @@ twitter_image: '/images/tweets/handbook-gitlab.png' - TOC {:toc .hidden-md .hidden-lg} `; - export const sourceContentTitle = 'Handbook'; export const username = 'gitlabuser'; export const projectId = '123456'; +export const returnUrl = 'https://www.gitlab.com'; export const sourcePath = 'foobar.md.html'; export const savedContentMeta = { diff --git a/spec/lib/container_registry/client_spec.rb b/spec/lib/container_registry/client_spec.rb index 5d2334a6d8f..0aad6568793 100644 --- a/spec/lib/container_registry/client_spec.rb +++ b/spec/lib/container_registry/client_spec.rb @@ -6,6 +6,21 @@ describe ContainerRegistry::Client do let(:token) { '12345' } let(:options) { { token: token } } let(:client) { described_class.new("http://container-registry", options) } + let(:push_blob_headers) do + { + 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json', + 'Authorization' => "bearer #{token}", + 'Content-Type' => 'application/octet-stream', + 'User-Agent' => "GitLab/#{Gitlab::VERSION}" + } + end + let(:headers_with_accept_types) do + { + 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json', + 'Authorization' => "bearer #{token}", + 'User-Agent' => "GitLab/#{Gitlab::VERSION}" + } + end shared_examples '#repository_manifest' do |manifest_type| let(:manifest) do @@ -25,14 +40,15 @@ describe ContainerRegistry::Client do "size" => 2828661 } ] - } + } end it 'GET /v2/:name/manifests/mytag' do stub_request(:get, "http://container-registry/v2/group/test/manifests/mytag") .with(headers: { - 'Accept' => described_class::ACCEPTED_TYPES.join(', '), - 'Authorization' => "bearer #{token}" + 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json', + 'Authorization' => "bearer #{token}", + 'User-Agent' => "GitLab/#{Gitlab::VERSION}" }) .to_return(status: 200, body: manifest.to_json, headers: { content_type: manifest_type }) @@ -44,12 +60,23 @@ describe ContainerRegistry::Client do it_behaves_like '#repository_manifest', described_class::OCI_MANIFEST_V1_TYPE describe '#blob' do + let(:blob_headers) do + { + 'Accept' => 'application/octet-stream', + 'Authorization' => "bearer #{token}", + 'User-Agent' => "GitLab/#{Gitlab::VERSION}" + } + end + + let(:redirect_header) do + { + 'User-Agent' => "GitLab/#{Gitlab::VERSION}" + } + end + it 'GET /v2/:name/blobs/:digest' do stub_request(:get, "http://container-registry/v2/group/test/blobs/sha256:0123456789012345") - .with(headers: { - 'Accept' => 'application/octet-stream', - 'Authorization' => "bearer #{token}" - }) + .with(headers: blob_headers) .to_return(status: 200, body: "Blob") expect(client.blob('group/test', 'sha256:0123456789012345')).to eq('Blob') @@ -57,15 +84,14 @@ describe ContainerRegistry::Client do it 'follows 307 redirect for GET /v2/:name/blobs/:digest' do stub_request(:get, "http://container-registry/v2/group/test/blobs/sha256:0123456789012345") - .with(headers: { - 'Accept' => 'application/octet-stream', - 'Authorization' => "bearer #{token}" - }) + .with(headers: blob_headers) .to_return(status: 307, body: "", headers: { Location: 'http://redirected' }) # We should probably use hash_excluding here, but that requires an update to WebMock: # https://github.com/bblimke/webmock/blob/master/lib/webmock/matchers/hash_excluding_matcher.rb stub_request(:get, "http://redirected/") - .with { |request| !request.headers.include?('Authorization') } + .with(headers: redirect_header) do |request| + !request.headers.include?('Authorization') + end .to_return(status: 200, body: "Successfully redirected") response = client.blob('group/test', 'sha256:0123456789012345') @@ -76,10 +102,11 @@ describe ContainerRegistry::Client do def stub_upload(path, content, digest, status = 200) stub_request(:post, "http://container-registry/v2/#{path}/blobs/uploads/") + .with(headers: headers_with_accept_types) .to_return(status: status, body: "", headers: { 'location' => 'http://container-registry/next_upload?id=someid' }) stub_request(:put, "http://container-registry/next_upload?digest=#{digest}&id=someid") - .with(body: content) + .with(body: content, headers: push_blob_headers) .to_return(status: status, body: "", headers: {}) end @@ -136,11 +163,20 @@ describe ContainerRegistry::Client do end describe '#put_tag' do + let(:manifest_headers) do + { + 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json', + 'Authorization' => "bearer #{token}", + 'Content-Type' => 'application/vnd.docker.distribution.manifest.v2+json', + 'User-Agent' => "GitLab/#{Gitlab::VERSION}" + } + end + subject { client.put_tag('path', 'tagA', { foo: :bar }) } it 'uploads the manifest and returns the digest' do stub_request(:put, "http://container-registry/v2/path/manifests/tagA") - .with(body: "{\n \"foo\": \"bar\"\n}") + .with(body: "{\n \"foo\": \"bar\"\n}", headers: manifest_headers) .to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:123' }) expect(subject).to eq 'sha256:123' @@ -153,6 +189,7 @@ describe ContainerRegistry::Client do context 'when the tag exists' do before do stub_request(:delete, "http://container-registry/v2/group/test/tags/reference/a") + .with(headers: headers_with_accept_types) .to_return(status: 200, body: "") end @@ -162,6 +199,7 @@ describe ContainerRegistry::Client do context 'when the tag does not exist' do before do stub_request(:delete, "http://container-registry/v2/group/test/tags/reference/a") + .with(headers: headers_with_accept_types) .to_return(status: 404, body: "") end @@ -171,6 +209,7 @@ describe ContainerRegistry::Client do context 'when an error occurs' do before do stub_request(:delete, "http://container-registry/v2/group/test/tags/reference/a") + .with(headers: headers_with_accept_types) .to_return(status: 500, body: "") end diff --git a/spec/lib/gitlab/static_site_editor/config_spec.rb b/spec/lib/gitlab/static_site_editor/config_spec.rb index dea79fb0e92..8f61476722d 100644 --- a/spec/lib/gitlab/static_site_editor/config_spec.rb +++ b/spec/lib/gitlab/static_site_editor/config_spec.rb @@ -18,13 +18,44 @@ describe Gitlab::StaticSiteEditor::Config do it 'returns data for the frontend component' do is_expected.to eq( branch: 'master', - commit: repository.commit.id, + commit_id: repository.commit.id, namespace: 'namespace', path: 'README.md', project: 'project', project_id: project.id, - return_url: 'http://example.com' + return_url: 'http://example.com', + is_supported_content: true ) end + + context 'when branch is not master' do + let(:ref) { 'my-branch' } + + it { is_expected.to include(is_supported_content: false) } + end + + context 'when file does not have a markdown extension' do + let(:file_path) { 'README.txt' } + + it { is_expected.to include(is_supported_content: false) } + end + + context 'when file does not have an extension' do + let(:file_path) { 'README' } + + it { is_expected.to include(is_supported_content: false) } + end + + context 'when file does not exist' do + let(:file_path) { 'UNKNOWN.md' } + + it { is_expected.to include(is_supported_content: false) } + end + + context 'when repository is empty' do + let(:project) { create(:project_empty_repo) } + + it { is_expected.to include(is_supported_content: false) } + end end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index ee0f7545adc..864f6f77f39 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -280,6 +280,18 @@ describe API::Users, :do_not_mock_admin_mode do expect(json_response.first['id']).to eq(user_with_2fa.id) end + it "returns users without projects" do + user_without_projects = create(:user) + create(:project, namespace: user.namespace) + create(:project, namespace: admin.namespace) + + get api('/users', admin), params: { without_projects: true } + + expect(response).to match_response_schema('public_api/v4/user/admins') + expect(json_response.size).to eq(1) + expect(json_response.first['id']).to eq(user_without_projects.id) + end + it 'returns 400 when provided incorrect sort params' do get api('/users', admin), params: { order_by: 'magic', sort: 'asc' } diff --git a/spec/support/shared_examples/requests/response_status_shared_examples.rb b/spec/support/shared_examples/requests/response_status_shared_examples.rb new file mode 100644 index 00000000000..6c450881efe --- /dev/null +++ b/spec/support/shared_examples/requests/response_status_shared_examples.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'returning response status' do |status| + it "returns #{status}" do + subject + + expect(response).to have_gitlab_http_status(status) + end +end diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb index e844b0ad799..f42d581ece4 100644 --- a/spec/uploaders/object_storage_spec.rb +++ b/spec/uploaders/object_storage_spec.rb @@ -597,22 +597,6 @@ describe ObjectStorage do end context 'when local file is used' do - context 'when valid file is used' do - let(:uploaded_file) do - fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg') - end - - it "properly caches the file" do - subject - - expect(uploader).to be_exists - expect(uploader.path).to start_with(uploader_class.root) - expect(uploader.filename).to eq('rails_sample.jpg') - end - end - end - - context 'when local file is used' do let(:temp_file) { Tempfile.new("test") } before do @@ -627,6 +611,14 @@ describe ObjectStorage do context 'when valid file is specified' do let(:uploaded_file) { temp_file } + it 'properly caches the file' do + subject + + expect(uploader).to be_exists + expect(uploader.path).to start_with(uploader_class.root) + expect(uploader.filename).to eq(File.basename(uploaded_file.path)) + end + context 'when object storage and direct upload is specified' do before do stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: true) |