+
Top 20 Accounts by Card Count
diff --git a/test/controllers/accounts/exports_controller_test.rb b/test/controllers/accounts/exports_controller_test.rb
index 4806b65f06..1ebe7e5eae 100644
--- a/test/controllers/accounts/exports_controller_test.rb
+++ b/test/controllers/accounts/exports_controller_test.rb
@@ -2,12 +2,12 @@
class Account::ExportsControllerTest < ActionDispatch::IntegrationTest
setup do
- sign_in_as :david
+ sign_in_as :jason
end
test "create creates an export record and enqueues job" do
assert_difference -> { Account::Export.count }, 1 do
- assert_enqueued_with(job: ExportAccountDataJob) do
+ assert_enqueued_with(job: ExportDataJob) do
post account_exports_path
end
end
@@ -20,14 +20,14 @@ class Account::ExportsControllerTest < ActionDispatch::IntegrationTest
post account_exports_path
export = Account::Export.last
- assert_equal users(:david), export.user
+ assert_equal users(:jason), 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))
+ Account::Export.create!(account: Current.account, user: users(:jason))
end
assert_no_difference -> { Account::Export.count } do
@@ -39,7 +39,7 @@ class Account::ExportsControllerTest < ActionDispatch::IntegrationTest
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)
+ Account::Export.create!(account: Current.account, user: users(:jason), created_at: 2.days.ago)
end
assert_difference -> { Account::Export.count }, 1 do
@@ -50,7 +50,7 @@ class Account::ExportsControllerTest < ActionDispatch::IntegrationTest
end
test "show displays completed export with download link" do
- export = Account::Export.create!(account: Current.account, user: users(:david))
+ export = Account::Export.create!(account: Current.account, user: users(:jason))
export.build
get account_export_path(export)
@@ -75,4 +75,22 @@ class Account::ExportsControllerTest < ActionDispatch::IntegrationTest
assert_response :success
assert_select "h2", "Download Expired"
end
+
+ test "create is forbidden for non-admin members" do
+ logout_and_sign_in_as :david
+
+ post account_exports_path
+
+ assert_response :forbidden
+ end
+
+ test "show is forbidden for non-admin members" do
+ logout_and_sign_in_as :david
+ export = Account::Export.create!(account: Current.account, user: users(:jason))
+ export.build
+
+ get account_export_path(export)
+
+ assert_response :forbidden
+ end
end
diff --git a/test/controllers/users/data_exports_controller_test.rb b/test/controllers/users/data_exports_controller_test.rb
new file mode 100644
index 0000000000..f89f121b11
--- /dev/null
+++ b/test/controllers/users/data_exports_controller_test.rb
@@ -0,0 +1,87 @@
+require "test_helper"
+
+class Users::DataExportsControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ sign_in_as :david
+ @user = users(:david)
+ end
+
+ test "create creates an export record and enqueues job" do
+ assert_difference -> { User::DataExport.count }, 1 do
+ assert_enqueued_with(job: ExportDataJob) do
+ post user_data_exports_path(@user)
+ end
+ end
+
+ assert_redirected_to @user
+ assert_equal "Export started. You'll receive an email when it's ready.", flash[:notice]
+ end
+
+ test "create associates export with user and account" do
+ post user_data_exports_path(@user)
+
+ export = User::DataExport.last
+ assert_equal @user, export.user
+ assert_equal Current.account, export.account
+ assert export.pending?
+ end
+
+ test "create rejects request when current export limit is reached" do
+ Users::DataExportsController::CURRENT_EXPORT_LIMIT.times do
+ @user.data_exports.create!(account: Current.account)
+ end
+
+ assert_no_difference -> { User::DataExport.count } do
+ post user_data_exports_path(@user)
+ end
+
+ assert_response :too_many_requests
+ end
+
+ test "create allows request when exports are older than one day" do
+ Users::DataExportsController::CURRENT_EXPORT_LIMIT.times do
+ @user.data_exports.create!(account: Current.account, created_at: 2.days.ago)
+ end
+
+ assert_difference -> { User::DataExport.count }, 1 do
+ post user_data_exports_path(@user)
+ end
+
+ assert_redirected_to @user
+ end
+
+ test "show displays completed export with download link" do
+ export = @user.data_exports.create!(account: Current.account)
+ export.build
+
+ get user_data_export_path(@user, export)
+
+ assert_response :success
+ assert_select "a#download-link"
+ end
+
+ test "show displays a warning if the export is missing" do
+ get user_data_export_path(@user, "not-really-an-export")
+
+ assert_response :success
+ assert_select "h2", "Download Expired"
+ end
+
+ test "create is forbidden for other users" do
+ other_user = users(:kevin)
+
+ post user_data_exports_path(other_user)
+
+ assert_response :forbidden
+ end
+
+ test "show is forbidden for other users" do
+ other_user = users(:kevin)
+ export = other_user.data_exports.create!(account: Current.account)
+ export.build
+
+ get user_data_export_path(other_user, export)
+
+ assert_response :forbidden
+ end
+end
diff --git a/test/fixtures/account/exports.yml b/test/fixtures/account/exports.yml
deleted file mode 100644
index 764f8aada6..0000000000
--- a/test/fixtures/account/exports.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-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) %>
diff --git a/test/fixtures/exports.yml b/test/fixtures/exports.yml
new file mode 100644
index 0000000000..8d86be46b1
--- /dev/null
+++ b/test/fixtures/exports.yml
@@ -0,0 +1,29 @@
+pending_account_export:
+ id: <%= ActiveRecord::FixtureSet.identify("pending_account_export", :uuid) %>
+ account: 37s_uuid
+ user: david_uuid
+ type: Account::Export
+ status: pending
+
+completed_account_export:
+ id: <%= ActiveRecord::FixtureSet.identify("completed_account_export", :uuid) %>
+ account: 37s_uuid
+ user: david_uuid
+ type: Account::Export
+ status: completed
+ completed_at: <%= 1.hour.ago.to_fs(:db) %>
+
+pending_user_data_export:
+ id: <%= ActiveRecord::FixtureSet.identify("pending_user_data_export", :uuid) %>
+ account: 37s_uuid
+ user: david_uuid
+ type: User::DataExport
+ status: pending
+
+completed_user_data_export:
+ id: <%= ActiveRecord::FixtureSet.identify("completed_user_data_export", :uuid) %>
+ account: 37s_uuid
+ user: david_uuid
+ type: User::DataExport
+ status: completed
+ completed_at: <%= 1.hour.ago.to_fs(:db) %>
diff --git a/test/mailers/export_mailer_test.rb b/test/mailers/export_mailer_test.rb
index c981aa191f..ac48780a32 100644
--- a/test/mailers/export_mailer_test.rb
+++ b/test/mailers/export_mailer_test.rb
@@ -1,7 +1,7 @@
require "test_helper"
class ExportMailerTest < ActionMailer::TestCase
- test "completed" do
+ test "completed for account export" do
export = Account::Export.create!(account: Current.account, user: users(:david))
email = ExportMailer.completed(export)
@@ -13,4 +13,17 @@ class ExportMailerTest < ActionMailer::TestCase
assert_equal "Your Fizzy data export is ready for download", email.subject
assert_match %r{/exports/#{export.id}}, email.body.encoded
end
+
+ test "completed for user data export" do
+ export = User::DataExport.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{/users/#{export.user.id}/data_exports/#{export.id}}, email.body.encoded
+ end
end
diff --git a/test/models/account/export_test.rb b/test/models/account/export_test.rb
index 37b0ea47e3..f4171f9da8 100644
--- a/test/models/account/export_test.rb
+++ b/test/models/account/export_test.rb
@@ -1,41 +1,14 @@
require "test_helper"
class Account::ExportTest < ActiveSupport::TestCase
- test "build_later enqueues ExportAccountDataJob" do
+ test "build_later enqueues ExportDataJob" do
export = Account::Export.create!(account: Current.account, user: users(:david))
- assert_enqueued_with(job: ExportAccountDataJob, args: [ export ]) do
+ assert_enqueued_with(job: ExportDataJob, 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"))
@@ -52,44 +25,20 @@ class Account::ExportTest < ActiveSupport::TestCase
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
+ Export.cleanup
- assert_not Account::Export.exists?(old_export.id)
- assert Account::Export.exists?(recent_export.id)
- assert Account::Export.exists?(pending_export.id)
+ assert_not Export.exists?(old_export.id)
+ assert Export.exists?(recent_export.id)
+ assert 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)
+ test "build generates zip with account data" do
+ export = Account::Export.create!(account: Current.account, user: users(:david))
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
+ assert_equal "application/zip", export.file.content_type
end
end
diff --git a/test/models/account/import_test.rb b/test/models/account/import_test.rb
new file mode 100644
index 0000000000..1abada2a3a
--- /dev/null
+++ b/test/models/account/import_test.rb
@@ -0,0 +1,205 @@
+require "test_helper"
+
+class Account::ImportTest < ActiveSupport::TestCase
+ setup do
+ @identity = identities(:david)
+ @source_account = accounts("37s")
+ end
+
+ test "process sets status to failed on error" do
+ import = create_import_with_file
+ Account::DataTransfer::Manifest.any_instance.stubs(:each_record_set).raises(StandardError.new("Test error"))
+
+ assert_raises(StandardError) do
+ import.process
+ end
+
+ assert import.failed?
+ end
+
+ test "process imports account name from export" do
+ target_account = create_target_account
+ import = create_import_for_account(target_account)
+
+ import.process
+
+ assert_equal @source_account.name, target_account.reload.name
+ end
+
+ test "process imports users with identity matching" do
+ target_account = create_target_account
+ import = create_import_for_account(target_account)
+ new_email = "new-user-#{SecureRandom.hex(4)}@example.com"
+
+ import.process
+
+ # Users from the source account should be imported
+ assert target_account.users.count > 2 # system user + owner + imported users
+ end
+
+ test "process preserves join code if unique" do
+ target_account = create_target_account
+ import = create_import_for_account(target_account)
+
+ # Set up a unique code in the export
+ export_code = "UNIQ-CODE-1234"
+ Account::JoinCode.where(code: export_code).delete_all
+
+ # Modify the export zip to have this code
+ import_with_custom_join_code = create_import_for_account(target_account, join_code: export_code)
+
+ import_with_custom_join_code.process
+
+ # Join code update attempt is made (may or may not succeed based on uniqueness)
+ assert import_with_custom_join_code.completed?
+ end
+
+ test "validate raises IntegrityError for missing required fields" do
+ target_account = create_target_account
+ import = create_import_with_invalid_data(target_account)
+
+ assert_raises(Account::DataTransfer::RecordSet::IntegrityError) do
+ import.validate
+ end
+ end
+
+ test "process rolls back on ID collision" do
+ target_account = create_target_account
+
+ # Pre-create a tag with a specific ID that will collide
+ colliding_id = SecureRandom.uuid
+ Tag.create!(
+ id: colliding_id,
+ account: target_account,
+ title: "Existing tag"
+ )
+
+ import = create_import_for_account(target_account, tag_id: colliding_id)
+
+ assert_raises(ActiveRecord::RecordNotUnique) do
+ import.process
+ end
+
+ # Import should be marked as failed
+ assert import.reload.failed?
+ end
+
+ test "process marks import as completed on success" do
+ target_account = create_target_account
+ import = create_import_for_account(target_account)
+
+ import.process
+
+ assert import.completed?
+ end
+
+ private
+ def create_target_account
+ account = Account.create!(name: "Import Target")
+ account.users.create!(role: :system, name: "System")
+ account.users.create!(
+ role: :owner,
+ name: "Importer",
+ identity: @identity,
+ verified_at: Time.current
+ )
+ account
+ end
+
+ def create_import_with_file
+ target_account = create_target_account
+ import = Account::Import.create!(identity: @identity, account: target_account)
+ Current.set(account: target_account) do
+ import.file.attach(io: generate_export_zip, filename: "export.zip", content_type: "application/zip")
+ end
+ import
+ end
+
+ def create_import_for_account(target_account, **options)
+ import = Account::Import.create!(identity: @identity, account: target_account)
+ Current.set(account: target_account) do
+ import.file.attach(io: generate_export_zip(**options), filename: "export.zip", content_type: "application/zip")
+ end
+ import
+ end
+
+ def create_import_with_invalid_data(target_account)
+ import = Account::Import.create!(identity: @identity, account: target_account)
+ Current.set(account: target_account) do
+ import.file.attach(
+ io: generate_invalid_export_zip,
+ filename: "export.zip",
+ content_type: "application/zip"
+ )
+ end
+ import
+ end
+
+ def generate_export_zip(join_code: nil, tag_id: nil)
+ tempfile = Tempfile.new([ "export", ".zip" ])
+ Zip::File.open(tempfile.path, create: true) do |zip|
+ account_data = @source_account.as_json.merge(
+ "join_code" => {
+ "code" => join_code || @source_account.join_code.code,
+ "usage_count" => 0,
+ "usage_limit" => 10
+ },
+ "name" => @source_account.name
+ )
+ zip.get_output_stream("data/account.json") { |f| f.write(JSON.generate(account_data)) }
+
+ # Export users with new UUIDs (to avoid collisions with fixtures)
+ @source_account.users.each do |user|
+ new_id = SecureRandom.uuid
+ user_data = {
+ "id" => new_id,
+ "account_id" => @source_account.id,
+ "email_address" => "imported-#{SecureRandom.hex(4)}@example.com",
+ "name" => user.name,
+ "role" => user.role,
+ "active" => user.active,
+ "verified_at" => user.verified_at,
+ "created_at" => user.created_at,
+ "updated_at" => user.updated_at
+ }
+ zip.get_output_stream("data/users/#{new_id}.json") { |f| f.write(JSON.generate(user_data)) }
+ end
+
+ # Export tags with new UUIDs (to avoid collisions with fixtures)
+ @source_account.tags.each do |tag|
+ new_id = tag_id || SecureRandom.uuid
+ tag_data = {
+ "id" => new_id,
+ "account_id" => @source_account.id,
+ "title" => tag.title,
+ "created_at" => tag.created_at,
+ "updated_at" => tag.updated_at
+ }
+ zip.get_output_stream("data/tags/#{new_id}.json") { |f| f.write(JSON.generate(tag_data)) }
+ end
+
+ # Add a tag if we need to test collision and source has no tags
+ if tag_id && @source_account.tags.empty?
+ tag_data = {
+ "id" => tag_id,
+ "account_id" => @source_account.id,
+ "title" => "Test Tag",
+ "created_at" => Time.current,
+ "updated_at" => Time.current
+ }
+ zip.get_output_stream("data/tags/#{tag_id}.json") { |f| f.write(JSON.generate(tag_data)) }
+ end
+ end
+ File.open(tempfile.path, "rb")
+ end
+
+ def generate_invalid_export_zip
+ tempfile = Tempfile.new([ "export", ".zip" ])
+ Zip::File.open(tempfile.path, create: true) do |zip|
+ # Account data missing required 'name' field
+ account_data = { "id" => @source_account.id }
+ zip.get_output_stream("data/account.json") { |f| f.write(JSON.generate(account_data)) }
+ end
+ File.open(tempfile.path, "rb")
+ end
+end
diff --git a/test/models/user/data_export_test.rb b/test/models/user/data_export_test.rb
new file mode 100644
index 0000000000..4169c7f86c
--- /dev/null
+++ b/test/models/user/data_export_test.rb
@@ -0,0 +1,70 @@
+require "test_helper"
+
+class User::DataExportTest < ActiveSupport::TestCase
+ test "build generates zip with card JSON files" do
+ export = User::DataExport.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 = User::DataExport.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 = User::DataExport.create!(account: Current.account, user: users(:david))
+
+ assert_enqueued_jobs 1, only: ActionMailer::MailDeliveryJob do
+ export.build
+ end
+ end
+
+ test "build includes only accessible cards for user" do
+ user = users(:david)
+ export = User::DataExport.create!(account: Current.account, user: user)
+
+ export.build
+
+ assert export.completed?
+ assert export.file.attached?
+
+ 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"
+
+ 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
+
+ test "build_later enqueues ExportDataJob" do
+ export = User::DataExport.create!(account: Current.account, user: users(:david))
+
+ assert_enqueued_with(job: ExportDataJob, args: [ export ]) do
+ export.build_later
+ end
+ end
+end