From 42c8fd30dacc4091e777c3de4ae0dce096f10d4e Mon Sep 17 00:00:00 2001 From: Kat <54268893+kosiakkatrina@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:06:53 +0000 Subject: [PATCH] Allow downloading CSVs --- app/controllers/csv_downloads_controller.rb | 18 +++++ app/policies/csv_download_policy.rb | 12 ++++ app/services/csv/downloader.rb | 50 ++++++++++++++ config/routes.rb | 6 ++ spec/factories/csv_download.rb | 8 +++ .../requests/csv_downloads_controller_spec.rb | 68 +++++++++++++++++++ 6 files changed, 162 insertions(+) create mode 100644 app/controllers/csv_downloads_controller.rb create mode 100644 app/policies/csv_download_policy.rb create mode 100644 app/services/csv/downloader.rb create mode 100644 spec/factories/csv_download.rb create mode 100644 spec/requests/csv_downloads_controller_spec.rb diff --git a/app/controllers/csv_downloads_controller.rb b/app/controllers/csv_downloads_controller.rb new file mode 100644 index 000000000..b42a9a236 --- /dev/null +++ b/app/controllers/csv_downloads_controller.rb @@ -0,0 +1,18 @@ +class CsvDownloadsController < ApplicationController + before_action :authenticate_user! + + def download + csv_download = CsvDownload.find(params[:id]) + authorize csv_download + + downloader = Csv::Downloader.new(csv_download:) + + if Rails.env.development? + downloader.call + send_file downloader.path, filename: csv_download.filename, type: "text/csv" + else + presigned_url = downloader.presigned_url + redirect_to presigned_url, allow_other_host: true + end + end +end diff --git a/app/policies/csv_download_policy.rb b/app/policies/csv_download_policy.rb new file mode 100644 index 000000000..72d815b58 --- /dev/null +++ b/app/policies/csv_download_policy.rb @@ -0,0 +1,12 @@ +class CsvDownloadPolicy + attr_reader :current_user, :csv_download + + def initialize(current_user, csv_download) + @current_user = current_user + @csv_download = csv_download + end + + def download? + @current_user == @csv_download.user || @current_user.support? || @current_user.organisation == @csv_download.organisation + end +end diff --git a/app/services/csv/downloader.rb b/app/services/csv/downloader.rb new file mode 100644 index 000000000..24545bc41 --- /dev/null +++ b/app/services/csv/downloader.rb @@ -0,0 +1,50 @@ +class Csv::Downloader + attr_reader :csv_download + + delegate :path, to: :file + + def initialize(csv_download:) + @csv_download = csv_download + end + + def call + download + end + + def delete_local_file! + file.unlink + end + + def presigned_url + s3_storage_service.get_presigned_url(csv_download.filename, 60, response_content_disposition: "attachment; filename=#{csv_download.filename}") + end + +private + + def download + io = storage_service.get_file_io(csv_download.filename) + file.write(io.read) + io.close + file.close + end + + def file + @file ||= Tempfile.new + end + + def storage_service + @storage_service ||= if FeatureToggle.upload_enabled? + s3_storage_service + else + local_disk_storage_service + end + end + + def s3_storage_service + Storage::S3Service.new(Configuration::EnvConfigurationService.new, ENV["BULK_UPLOAD_BUCKET"]) + end + + def local_disk_storage_service + Storage::LocalDiskService.new + end +end diff --git a/config/routes.rb b/config/routes.rb index 6ac7b3f34..9b855880d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -382,6 +382,12 @@ Rails.application.routes.draw do end end + resources :csv_downloads, path: "csv-downloads" do + member do + get "download", to: "csv_downloads#download" + end + end + scope via: :all do match "/404", to: "errors#not_found" match "/429", to: "errors#too_many_requests", status: 429 diff --git a/spec/factories/csv_download.rb b/spec/factories/csv_download.rb new file mode 100644 index 000000000..4ee95e368 --- /dev/null +++ b/spec/factories/csv_download.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory :csv_download do + download_type { "lettings" } + user { create(:user) } + organisation { user.organisation } + filename { "lettings.csv" } + end +end diff --git a/spec/requests/csv_downloads_controller_spec.rb b/spec/requests/csv_downloads_controller_spec.rb new file mode 100644 index 000000000..64862cd71 --- /dev/null +++ b/spec/requests/csv_downloads_controller_spec.rb @@ -0,0 +1,68 @@ +require "rails_helper" + +RSpec.describe CsvDownloadsController, type: :request do + describe "GET #download" do + let(:csv_user) { create(:user) } + let(:csv_download) { create(:csv_download, user: csv_user, organisation: csv_user.organisation) } + let(:get_file_io) do + io = StringIO.new + io.write("hello") + io.rewind + io + end + let(:mock_storage_service) { instance_double(Storage::S3Service, get_file_io:, get_presigned_url: "https://example.com") } + + before do + allow(Storage::S3Service).to receive(:new).and_return(mock_storage_service) + end + + context "when user is not signed in" do + it "redirects to sign in page" do + get "/csv-downloads/#{csv_download.id}/download" + expect(response).to redirect_to("/account/sign-in") + end + end + + context "when user is signed in" do + before do + sign_in user + end + + context "and the user is from a different organisation" do + let(:user) { create(:user) } + + before do + get "/csv-downloads/#{csv_download.id}/download" + end + + it "returns page not found" do + expect(response).to have_http_status(:unauthorized) + end + end + + context "and is the user who generated the csv" do + let(:user) { csv_user } + + before do + get "/csv-downloads/#{csv_download.id}/download" + end + + it "allows downloading the csv" do + expect(response).to have_http_status(:found) + end + end + + context "and is the user is from the same organisation" do + let(:user) { create(:user, organisation: csv_user.organisation) } + + before do + get "/csv-downloads/#{csv_download.id}/download" + end + + it "allows downloading the csv" do + expect(response).to have_http_status(:found) + end + end + end + end +end