diff --git a/spec/fixtures/images/image_no_extension b/spec/fixtures/images/image_no_extension
new file mode 100644
index 00000000000..5c900b61f70
Binary files /dev/null and b/spec/fixtures/images/image_no_extension differ
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/requests/uploads_controller_spec.rb
similarity index 50%
rename from spec/controllers/uploads_controller_spec.rb
rename to spec/requests/uploads_controller_spec.rb
index a98cd9ebe77..31130e15c04 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/requests/uploads_controller_spec.rb
@@ -1,17 +1,14 @@
 require 'rails_helper'
 
 describe UploadsController do
-
-  context '.create' do
-
+  describe '#create' do
     it 'requires you to be logged in' do
-      post :create, format: :json
+      post "/uploads.json"
       expect(response.status).to eq(403)
     end
 
     context 'logged in' do
-
-      before { @user = log_in :user }
+      let!(:user) { sign_in(Fabricate(:user)) }
 
       let(:logo) do
         Rack::Test::UploadedFile.new(file_from_fixtures("logo.png"))
@@ -26,151 +23,132 @@ describe UploadsController do
       end
 
       it 'expects a type' do
-        expect do
-          post :create, params: { format: :json, file: logo }
-        end.to raise_error(ActionController::ParameterMissing)
-      end
-
-      it 'can look up long urls' do
-        upload = Fabricate(:upload)
-        post :lookup_urls, params: { short_urls: [upload.short_url], format: :json }
-        result = JSON.parse(response.body)
-        expect(result[0]["url"]).to eq(upload.url)
+        post "/uploads.json", params: { file: logo }
+        expect(response.status).to eq(400)
       end
 
       it 'is successful with an image' do
-        Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything)
-
-        post :create, params: { file: logo, type: "avatar", format: :json }
-
+        post "/uploads.json", params: { file: logo, type: "avatar" }
         expect(response.status).to eq 200
-        expect(JSON.parse(response.body)["id"]).to be
+        expect(JSON.parse(response.body)["id"]).to be_present
+        expect(Jobs::CreateAvatarThumbnails.jobs.size).to eq(1)
       end
 
       it 'is successful with an attachment' do
         SiteSetting.authorized_extensions = "*"
 
-        Jobs.expects(:enqueue).never
-
-        post :create, params: { file: text_file, type: "composer", format: :json }
-
+        post "/uploads.json", params: { file: text_file, type: "composer" }
         expect(response.status).to eq 200
+
+        expect(Jobs::CreateAvatarThumbnails.jobs.size).to eq(0)
         id = JSON.parse(response.body)["id"]
         expect(id).to be
       end
 
       it 'is successful with api' do
         SiteSetting.authorized_extensions = "*"
-        controller.stubs(:is_api?).returns(true)
-
-        FinalDestination.stubs(:lookup_ip).returns("1.2.3.4")
-
-        Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything)
+        api_key = Fabricate(:api_key, user: user).key
 
         url = "http://example.com/image.png"
         png = File.read(Rails.root + "spec/fixtures/images/logo.png")
 
         stub_request(:get, url).to_return(status: 200, body: png)
 
-        post :create, params: { url: url, type: "avatar", format: :json }
+        post "/uploads.json", params: { url: url, type: "avatar", api_key: api_key, api_username: user.username }
 
         json = ::JSON.parse(response.body)
 
-        expect(response.status).to eq 200
-        expect(json["id"]).to be
+        expect(response.status).to eq(200)
+        expect(Jobs::CreateAvatarThumbnails.jobs.size).to eq(1)
+        expect(json["id"]).to be_present
         expect(json["short_url"]).to eq("upload://qUm0DGR49PAZshIi7HxMd3cAlzn.png")
       end
 
       it 'correctly sets retain_hours for admins' do
-        log_in :admin
-        Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything).never
+        sign_in(Fabricate(:admin))
 
-        post :create, params: {
+        post "/uploads.json", params: {
           file: logo,
           retain_hours: 100,
           type: "profile_background",
-          format: :json
         }
 
         id = JSON.parse(response.body)["id"]
+        expect(Jobs::CreateAvatarThumbnails.jobs.size).to eq(0)
         expect(Upload.find(id).retain_hours).to eq(100)
       end
 
       it 'requires a file' do
-        Jobs.expects(:enqueue).never
-
-        post :create, params: { type: "composer", format: :json }
+        post "/uploads.json", params: { type: "composer" }
 
+        expect(Jobs::CreateAvatarThumbnails.jobs.size).to eq(0)
         message = JSON.parse(response.body)
         expect(response.status).to eq 422
         expect(message["errors"]).to contain_exactly(I18n.t("upload.file_missing"))
       end
 
       it 'properly returns errors' do
+        SiteSetting.authorized_extensions = "*"
         SiteSetting.max_attachment_size_kb = 1
 
-        Jobs.expects(:enqueue).never
+        post "/uploads.json", params: { file: text_file, type: "avatar" }
 
-        post :create, params: { file: text_file, type: "avatar", format: :json }
-
-        expect(response.status).to eq 422
+        expect(response.status).to eq(422)
+        expect(Jobs::CreateAvatarThumbnails.jobs.size).to eq(0)
         errors = JSON.parse(response.body)["errors"]
-        expect(errors).to be
+        expect(errors.first).to eq(I18n.t("upload.attachments.too_large", max_size_kb: 1))
       end
 
       it 'ensures allow_uploaded_avatars is enabled when uploading an avatar' do
         SiteSetting.allow_uploaded_avatars = false
-        post :create, params: { file: logo, type: "avatar", format: :json }
-        expect(response).to_not be_success
+        post "/uploads.json", params: { file: logo, type: "avatar" }
+        expect(response.status).to eq(422)
       end
 
       it 'ensures sso_overrides_avatar is not enabled when uploading an avatar' do
         SiteSetting.sso_overrides_avatar = true
-        post :create, params: { file: logo, type: "avatar", format: :json }
-        expect(response).to_not be_success
+        post "/uploads.json", params: { file: logo, type: "avatar" }
+        expect(response.status).to eq(422)
       end
 
       it 'allows staff to upload any file in PM' do
         SiteSetting.authorized_extensions = "jpg"
         SiteSetting.allow_staff_to_upload_any_file_in_pm = true
-        @user.update_columns(moderator: true)
+        user.update_columns(moderator: true)
 
-        post :create, params: {
+        post "/uploads.json", params: {
           file: text_file,
           type: "composer",
           for_private_message: "true",
-          format: :json
         }
 
         expect(response).to be_success
         id = JSON.parse(response.body)["id"]
-        expect(id).to be
+        expect(id).to be_present
       end
 
       it 'respects `authorized_extensions_for_staff` setting when staff upload file' do
         SiteSetting.authorized_extensions = ""
         SiteSetting.authorized_extensions_for_staff = "*"
-        @user.update_columns(moderator: true)
+        user.update_columns(moderator: true)
 
-        post :create, params: {
+        post "/uploads.json", params: {
           file: text_file,
           type: "composer",
-          format: :json
         }
 
         expect(response).to be_success
         data = JSON.parse(response.body)
-        expect(data["id"]).to be
+        expect(data["id"]).to be_present
       end
 
       it 'ignores `authorized_extensions_for_staff` setting when non-staff upload file' do
         SiteSetting.authorized_extensions = ""
         SiteSetting.authorized_extensions_for_staff = "*"
 
-        post :create, params: {
+        post "/uploads.json", params: {
           file: text_file,
           type: "composer",
-          format: :json
         }
 
         data = JSON.parse(response.body)
@@ -178,73 +156,87 @@ describe UploadsController do
       end
 
       it 'returns an error when it could not determine the dimensions of an image' do
-        Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything).never
+        post "/uploads.json", params: { file: fake_jpg, type: "composer" }
 
-        post :create, params: { file: fake_jpg, type: "composer", format: :json }
-
-        expect(response.status).to eq 422
+        expect(response.status).to eq(422)
+        expect(Jobs::CreateAvatarThumbnails.jobs.size).to eq(0)
         message = JSON.parse(response.body)["errors"]
         expect(message).to contain_exactly(I18n.t("upload.images.size_not_found"))
       end
-
     end
-
   end
 
-  context '.show' do
-
+  describe '#show' do
     let(:site) { "default" }
     let(:sha) { Digest::SHA1.hexdigest("discourse") }
+    let(:user) { Fabricate(:user) }
+
+    def upload_file(file)
+      fake_logo = Rack::Test::UploadedFile.new(file_from_fixtures(file))
+      SiteSetting.authorized_extensions = "*"
+      sign_in(user)
+
+      post "/uploads.json", params: {
+        file: fake_logo,
+        type: "composer",
+      }
+      url = JSON.parse(response.body)["url"]
+      upload = Upload.where(url: url).first
+      upload
+    end
 
     it "returns 404 when using external storage" do
-      store = stub(internal?: false)
-      Discourse.stubs(:store).returns(store)
-      Upload.expects(:find_by).never
+      SiteSetting.enable_s3_uploads = true
+      SiteSetting.s3_access_key_id = "fakeid7974664"
+      SiteSetting.s3_secret_access_key = "fakesecretid7974664"
 
-      get :show, params: { site: site, sha: sha, extension: "pdf" }
+      get "/uploads/#{site}/#{sha}.pdf"
       expect(response.response_code).to eq(404)
     end
 
     it "returns 404 when the upload doesn't exist" do
-      Upload.stubs(:find_by).returns(nil)
-
-      get :show, params: { site: site, sha: sha, extension: "pdf" }
-      expect(response.response_code).to eq(404)
+      get "/uploads/#{site}/#{sha}.pdf"
+      expect(response.status).to eq(404)
     end
 
     it 'uses send_file' do
-      upload = build(:upload)
-      Upload.expects(:find_by).with(sha1: sha).returns(upload)
-
-      controller.stubs(:render)
-      controller.expects(:send_file)
-
-      get :show, params: { site: site, sha: sha, extension: "zip" }
+      upload = upload_file("logo.png")
+      get "/uploads/#{site}/#{upload.sha1}.#{upload.extension}"
+      expect(response.status).to eq(200)
+      expect(response.headers["Content-Disposition"]).to eq("attachment; filename=\"logo.png\"")
     end
 
     it "handles file without extension" do
       SiteSetting.authorized_extensions = "*"
-      Fabricate(:upload, original_filename: "image_file", sha1: sha)
-      controller.stubs(:render)
-      controller.expects(:send_file)
+      upload = upload_file("image_no_extension")
 
-      get :show, params: { site: site, sha: sha, format: :json }
-      expect(response).to be_success
+      get "/uploads/#{site}/#{upload.sha1}.json"
+      expect(response.status).to eq(200)
+      expect(response.headers["Content-Disposition"]).to eq("attachment; filename=\"image_no_extension\"")
     end
 
     context "prevent anons from downloading files" do
-
-      before { SiteSetting.prevent_anons_from_downloading_files = true }
-
       it "returns 404 when an anonymous user tries to download a file" do
-        Upload.expects(:find_by).never
+        upload = upload_file("logo.png")
+        delete "/session/#{user.username}.json" # upload a file, then sign out
 
-        get :show, params: { site: site, sha: sha, extension: "pdf", format: :json }
-        expect(response.response_code).to eq(404)
+        SiteSetting.prevent_anons_from_downloading_files = true
+        get "/uploads/#{site}/#{upload.sha1}.#{upload.extension}"
+        expect(response.status).to eq(404)
       end
-
     end
-
   end
 
+  describe '#lookup_urls' do
+    it 'can look up long urls' do
+      sign_in(Fabricate(:user))
+      upload = Fabricate(:upload)
+
+      post "/uploads/lookup-urls.json", params: { short_urls: [upload.short_url] }
+      expect(response.status).to eq(200)
+
+      result = JSON.parse(response.body)
+      expect(result[0]["url"]).to eq(upload.url)
+    end
+  end
 end