Merge pull request #1768 from basecamp/data-exports
Add "data export" feature
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
require "test_helper"
|
||||
|
||||
class Account::ExportsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in_as :david
|
||||
end
|
||||
|
||||
test "create creates an export record and enqueues job" do
|
||||
assert_difference -> { Account::Export.count }, 1 do
|
||||
assert_enqueued_with(job: ExportAccountDataJob) do
|
||||
post account_exports_path
|
||||
end
|
||||
end
|
||||
|
||||
assert_redirected_to account_settings_path
|
||||
assert_equal "Export started. You'll receive an email when it's ready.", flash[:notice]
|
||||
end
|
||||
|
||||
test "create associates export with current user" do
|
||||
post account_exports_path
|
||||
|
||||
export = Account::Export.last
|
||||
assert_equal users(:david), export.user
|
||||
assert_equal Current.account, export.account
|
||||
assert export.pending?
|
||||
end
|
||||
|
||||
test "create rejects request when current export limit is reached" do
|
||||
Account::ExportsController::CURRENT_EXPORT_LIMIT.times do
|
||||
Account::Export.create!(account: Current.account, user: users(:david))
|
||||
end
|
||||
|
||||
assert_no_difference -> { Account::Export.count } do
|
||||
post account_exports_path
|
||||
end
|
||||
|
||||
assert_response :too_many_requests
|
||||
end
|
||||
|
||||
test "create allows request when exports are older than one day" do
|
||||
Account::ExportsController::CURRENT_EXPORT_LIMIT.times do
|
||||
Account::Export.create!(account: Current.account, user: users(:david), created_at: 2.days.ago)
|
||||
end
|
||||
|
||||
assert_difference -> { Account::Export.count }, 1 do
|
||||
post account_exports_path
|
||||
end
|
||||
|
||||
assert_redirected_to account_settings_path
|
||||
end
|
||||
|
||||
test "show displays completed export with download link" do
|
||||
export = Account::Export.create!(account: Current.account, user: users(:david))
|
||||
export.build
|
||||
|
||||
get account_export_path(export)
|
||||
|
||||
assert_response :success
|
||||
assert_select "a#download-link"
|
||||
end
|
||||
|
||||
test "show displays a warning if the export is missing" do
|
||||
get account_export_path("not-really-an-export")
|
||||
|
||||
assert_response :success
|
||||
assert_select "h2", "Download Expired"
|
||||
end
|
||||
|
||||
test "show does not allow access to another user's export" do
|
||||
export = Account::Export.create!(account: Current.account, user: users(:kevin))
|
||||
export.build
|
||||
|
||||
get account_export_path(export)
|
||||
|
||||
assert_response :success
|
||||
assert_select "h2", "Download Expired"
|
||||
end
|
||||
end
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
pending_export:
|
||||
id: <%= ActiveRecord::FixtureSet.identify("pending_export", :uuid) %>
|
||||
account: 37s_uuid
|
||||
user: david_uuid
|
||||
status: pending
|
||||
|
||||
completed_export:
|
||||
id: <%= ActiveRecord::FixtureSet.identify("completed_export", :uuid) %>
|
||||
account: 37s_uuid
|
||||
user: david_uuid
|
||||
status: completed
|
||||
completed_at: <%= 1.hour.ago.to_fs(:db) %>
|
||||
@@ -0,0 +1,16 @@
|
||||
require "test_helper"
|
||||
|
||||
class ExportMailerTest < ActionMailer::TestCase
|
||||
test "completed" do
|
||||
export = Account::Export.create!(account: Current.account, user: users(:david))
|
||||
email = ExportMailer.completed(export)
|
||||
|
||||
assert_emails 1 do
|
||||
email.deliver_now
|
||||
end
|
||||
|
||||
assert_equal [ "david@37signals.com" ], email.to
|
||||
assert_equal "Your Fizzy data export is ready for download", email.subject
|
||||
assert_match %r{/exports/#{export.id}}, email.body.encoded
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
class ExportMailerPreview < ActionMailer::Preview
|
||||
def completed
|
||||
export = Account::Export.new(
|
||||
id: "preview-export-id",
|
||||
account: Account.first,
|
||||
user: User.first
|
||||
)
|
||||
|
||||
ExportMailer.completed(export)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,95 @@
|
||||
require "test_helper"
|
||||
|
||||
class Account::ExportTest < ActiveSupport::TestCase
|
||||
test "build_later enqueues ExportAccountDataJob" do
|
||||
export = Account::Export.create!(account: Current.account, user: users(:david))
|
||||
|
||||
assert_enqueued_with(job: ExportAccountDataJob, args: [ export ]) do
|
||||
export.build_later
|
||||
end
|
||||
end
|
||||
|
||||
test "build generates zip with card JSON files" do
|
||||
export = Account::Export.create!(account: Current.account, user: users(:david))
|
||||
|
||||
export.build
|
||||
|
||||
assert export.completed?
|
||||
assert export.file.attached?
|
||||
assert_equal "application/zip", export.file.content_type
|
||||
end
|
||||
|
||||
test "build sets status to processing then completed" do
|
||||
export = Account::Export.create!(account: Current.account, user: users(:david))
|
||||
|
||||
export.build
|
||||
|
||||
assert export.completed?
|
||||
assert_not_nil export.completed_at
|
||||
end
|
||||
|
||||
test "build sends email when completed" do
|
||||
export = Account::Export.create!(account: Current.account, user: users(:david))
|
||||
|
||||
assert_enqueued_jobs 1, only: ActionMailer::MailDeliveryJob do
|
||||
export.build
|
||||
end
|
||||
end
|
||||
|
||||
test "build sets status to failed on error" do
|
||||
export = Account::Export.create!(account: Current.account, user: users(:david))
|
||||
export.stubs(:generate_zip).raises(StandardError.new("Test error"))
|
||||
|
||||
assert_raises(StandardError) do
|
||||
export.build
|
||||
end
|
||||
|
||||
assert export.failed?
|
||||
end
|
||||
|
||||
test "cleanup deletes exports completed more than 24 hours ago" do
|
||||
old_export = Account::Export.create!(account: Current.account, user: users(:david), status: :completed, completed_at: 25.hours.ago)
|
||||
recent_export = Account::Export.create!(account: Current.account, user: users(:david), status: :completed, completed_at: 23.hours.ago)
|
||||
pending_export = Account::Export.create!(account: Current.account, user: users(:david), status: :pending)
|
||||
|
||||
Account::Export.cleanup
|
||||
|
||||
assert_not Account::Export.exists?(old_export.id)
|
||||
assert Account::Export.exists?(recent_export.id)
|
||||
assert Account::Export.exists?(pending_export.id)
|
||||
end
|
||||
|
||||
test "build includes only accessible cards for user" do
|
||||
user = users(:david)
|
||||
export = Account::Export.create!(account: Current.account, user: user)
|
||||
|
||||
export.build
|
||||
|
||||
assert export.completed?
|
||||
assert export.file.attached?
|
||||
|
||||
# Verify zip contents
|
||||
Tempfile.create([ "test", ".zip" ]) do |temp|
|
||||
temp.binmode
|
||||
export.file.download { |chunk| temp.write(chunk) }
|
||||
temp.rewind
|
||||
|
||||
Zip::File.open(temp.path) do |zip|
|
||||
json_files = zip.glob("*.json")
|
||||
assert json_files.any?, "Zip should contain at least one JSON file"
|
||||
|
||||
# Verify structure of a JSON file
|
||||
json_content = JSON.parse(zip.read(json_files.first.name))
|
||||
assert json_content.key?("number")
|
||||
assert json_content.key?("title")
|
||||
assert json_content.key?("board")
|
||||
assert json_content.key?("creator")
|
||||
assert json_content["creator"].key?("id")
|
||||
assert json_content["creator"].key?("name")
|
||||
assert json_content["creator"].key?("email")
|
||||
assert json_content.key?("description")
|
||||
assert json_content.key?("comments")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
require "test_helper"
|
||||
|
||||
class Card::ExportableTest < ActiveSupport::TestCase
|
||||
test "export_json returns card data as JSON" do
|
||||
card = cards(:logo)
|
||||
|
||||
json = JSON.parse(card.export_json)
|
||||
|
||||
assert_equal 1, json["number"]
|
||||
assert_equal "The logo isn't big enough", json["title"]
|
||||
assert_equal "Writebook", json["board"]
|
||||
assert_equal "Triage", json["status"]
|
||||
assert_equal users(:david).id, json["creator"]["id"]
|
||||
assert_equal "David", json["creator"]["name"]
|
||||
assert_equal "david@37signals.com", json["creator"]["email"]
|
||||
assert_equal "", json["description"]
|
||||
assert_equal 5, json["comments"].count
|
||||
assert_equal card.created_at.iso8601, json["created_at"]
|
||||
assert_equal card.updated_at.iso8601, json["updated_at"]
|
||||
end
|
||||
|
||||
test "export_attachments returns attachment paths and blobs" do
|
||||
card = cards(:logo)
|
||||
|
||||
blob = ActiveStorage::Blob.create_and_upload!(
|
||||
io: file_fixture("moon.jpg").open,
|
||||
filename: "moon.jpg",
|
||||
content_type: "image/jpeg"
|
||||
)
|
||||
attachment_html = ActionText::Attachment.from_attachable(blob).to_html
|
||||
card.update!(description: "<p>Here is an image:</p>#{attachment_html}")
|
||||
|
||||
attachments = card.export_attachments
|
||||
|
||||
assert_equal 1, attachments.count
|
||||
assert_equal file_fixture("moon.jpg").binread, attachments.first[:blob].download
|
||||
assert attachments.first[:path].start_with?("#{card.number}/")
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user