diff --git a/lib/exporter.rb b/lib/exporter.rb index 72601b65..eb5101d3 100644 --- a/lib/exporter.rb +++ b/lib/exporter.rb @@ -39,7 +39,12 @@ class Exporter Sentry.capture_exception(e) @user.export_processing = false @user.save validate: false - raise # so that e.g. the sidekiq job fails + + # delete zipfile on errors + @zipfile.close # so it gets written to disk first + File.delete(@zipfile.name) + + raise # let e.g. the sidekiq job fail so it's retryable later ensure @zipfile.close end diff --git a/lib/use_case/data_export/user.rb b/lib/use_case/data_export/user.rb index f626bc10..86fe7f6e 100644 --- a/lib/use_case/data_export/user.rb +++ b/lib/use_case/data_export/user.rb @@ -68,7 +68,12 @@ module UseCase picture.versions.each do |version, file| export_filename = "pictures/#{file.mounted_as}_#{version}_#{file.filename}" to[export_filename] = if file.url.start_with?("/") - Rails.public_path.join(file.url.sub(%r{\A/+}, "")).read rescue "ceci n'est pas un image" # TODO: fix this + begin + Rails.public_path.join(file.url.sub(%r{\A/+}, "")).read + rescue + # TODO: fix image handling in local development environments!!! see #822 + "ceci n'est pas un image\n" + end else HTTParty.get(file.url).parsed_response end diff --git a/spec/lib/exporter_spec.rb b/spec/lib/exporter_spec.rb index 526b2978..0ad327cb 100644 --- a/spec/lib/exporter_spec.rb +++ b/spec/lib/exporter_spec.rb @@ -1,326 +1,118 @@ # frozen_string_literal: true require "rails_helper" +require "support/example_exporter" +require "base64" + require "exporter" +# This only tests the exporter itself to make sure zip file creation works. RSpec.describe Exporter do include ActiveSupport::Testing::TimeHelpers - let(:user_params) do - { - answered_count: 144, - asked_count: 72, - comment_smiled_count: 15, - commented_count: 12, - confirmation_sent_at: 2.weeks.ago.utc, - confirmed_at: 2.weeks.ago.utc + 1.hour, - created_at: 2.weeks.ago.utc, - current_sign_in_at: 8.hours.ago.utc, - current_sign_in_ip: "198.51.100.220", - last_sign_in_at: 1.hour.ago, - last_sign_in_ip: "192.0.2.14", - locale: "en", - privacy_allow_anonymous_questions: true, - privacy_allow_public_timeline: false, - privacy_allow_stranger_answers: false, - privacy_show_in_search: true, - screen_name: "fizzyraccoon", - show_foreign_themes: true, - sign_in_count: 10, - smiled_count: 28, - profile: { - display_name: "Fizzy Raccoon", - description: "A small raccoon", - location: "Binland", - motivation_header: "", - website: "https://retrospring.net" - } - } - end - let(:user) { FactoryBot.create(:user, **user_params) } + let(:user) { FactoryBot.create(:user, screen_name: "fizzyraccoon", export_processing: true) } let(:instance) { described_class.new(user) } + let(:zipfile_deletion_expected) { false } before do stub_const("APP_CONFIG", { - "hostname" => "example.com", - "https" => true, - "items_per_page" => 5, - "fog" => {} - }) + "site_name" => "justask", + "hostname" => "example.com", + "https" => true, + "items_per_page" => 5, + "fog" => {} + }.with_indifferent_access) end after do - filename = instance.instance_variable_get(:@export_dirname) - FileUtils.rm_r(filename) if File.exist?(filename) - end - - describe "#collect_user_info" do - subject { instance.send(:collect_user_info) } - - it "collects user info" do - subject - expect(instance.instance_variable_get(:@obj)).to eq(user_params.merge({ - administrator: false, - moderator: false, - id: user.id, - updated_at: user.updated_at, - profile_header: user.profile_header, - profile_header_file_name: nil, - profile_header_h: nil, - profile_header_w: nil, - profile_header_x: nil, - profile_header_y: nil, - profile_picture_file_name: nil, - profile_picture_h: nil, - profile_picture_w: nil, - profile_picture_x: nil, - profile_picture_y: nil - })) + filename = instance.instance_variable_get(:@zipfile)&.name + unless File.exist?(filename) + warn "exporter_spec.rb: wanted to clean up #{filename.inspect} but it does not exist!" unless zipfile_deletion_expected + next end + FileUtils.rm_r(filename) end - describe "#collect_questions" do - subject { instance.send(:collect_questions) } + describe "#export" do + let(:export_name) { instance.instance_variable_get(:@export_name) } - context "exporting a user with several questions" do - let!(:questions) { FactoryBot.create_list(:question, 25, user: user) } - - it "collects questions" do - subject - expect(instance.instance_variable_get(:@obj)[:questions]).to eq(questions.map do |q| - { - answer_count: 0, - answers: [], - author_is_anonymous: q.author_is_anonymous, - content: q.content, - created_at: q.reload.created_at, - id: q.id - } - end) + subject do + travel_to(Time.utc(2022, 12, 10, 13, 37, 42)) do + instance.export end end - context "exporting a user with a question which has been answered" do - let!(:question) { FactoryBot.create(:question, user: user, author_is_anonymous: false) } - let!(:answers) { FactoryBot.create_list(:answer, 5, question: question, user: FactoryBot.create(:user)) } - - it "collects questions and answers" do - subject - expect(instance.instance_variable_get(:@obj)[:questions]).to eq([ - { - answer_count: 5, - answers: answers.map do |a| - { - comment_count: 0, - comments: [], - content: a.content, - created_at: a.reload.created_at, - id: a.id, - smile_count: a.smile_count, - user: instance.send(:user_stub, a.user) - } - end, - author_is_anonymous: false, - content: question.content, - created_at: question.reload.created_at, - id: question.id - } - ]) - end - end - end - - describe "#collect_answers" do - let!(:answers) { FactoryBot.create_list(:answer, 25, user: user) } - - subject { instance.send(:collect_answers) } - - it "collects answers" do - subject - expect(instance.instance_variable_get(:@obj)[:answers]).to eq(answers.map do |a| - { - comment_count: 0, - comments: [], - content: a.content, - created_at: a.reload.created_at, - id: a.id, - question: instance.send(:process_question, - a.question.reload, - include_user: true, - include_answers: false), - smile_count: 0 - } - end) - end - end - - describe "#collect_comments" do - let!(:comments) do - FactoryBot.create_list(:comment, - 25, - user: user, - answer: FactoryBot.create(:answer, user: FactoryBot.create(:user))) - end - - subject { instance.send(:collect_comments) } - - it "collects comments" do - subject - expect(instance.instance_variable_get(:@obj)[:comments]).to eq(comments.map do |c| - { - content: c.content, - created_at: c.reload.created_at, - id: c.id, - answer: instance.send(:process_answer, - c.answer, - include_comments: false) - } - end) - end - end - - describe "#collect_smiles" do - let!(:smiles) { FactoryBot.create_list(:smile, 25, user: user) } - - subject { instance.send(:collect_smiles) } - - it "collects reactions" do - subject - expect(instance.instance_variable_get(:@obj)[:smiles]).to eq(smiles.map do |s| - { - id: s.id, - created_at: s.reload.created_at, - answer: { - comment_count: s.parent.comment_count, - content: s.parent.content, - created_at: s.parent.reload.created_at, - id: s.parent.id, - question: { - answer_count: s.parent.question.answer_count, - author_is_anonymous: s.parent.question.author_is_anonymous, - content: s.parent.question.content, - created_at: s.parent.question.reload.created_at, - id: s.parent.question.id, - user: nil # we're not populating this in the factory - }, - smile_count: s.parent.smile_count - } - } - end) - end - end - - describe "#finalize" do - let(:fake_rails_root) { Pathname(Dir.mktmpdir) } - let(:dir) { instance.instance_variable_get(:@export_dirname) } - let(:name) { instance.instance_variable_get(:@export_filename) } - before do - instance.instance_variable_set(:@obj, { - some: { - sample: { - data: "Text" - } - } - }) - - Dir.mkdir("#{fake_rails_root}/public") - FileUtils.cp_r(Rails.root.join("public/images"), "#{fake_rails_root}/public/images") - allow(Rails).to receive(:root).and_return(fake_rails_root) + allow(UseCase::DataExport::Base) + .to receive(:descendants) + .and_return([ExampleExporter]) end - after do - FileUtils.rm_r(fake_rails_root) - end + it "creates a zip file with the expected contents" do + subject - subject { instance.send(:finalize) } + # check created zip file + zip_path = Rails.public_path.join("export/#{export_name}.zip") + expect(File.exist?(zip_path)).to be true - context "exporting a user without a profile picture or header" do - it "prepares files to be archived" do - subject - expect(File.directory?(fake_rails_root.join("public/export"))).to eq(true) - expect(File.directory?("#{dir}/pictures")).to eq(true) - end + Zip::File.open(zip_path) do |zip| + # check for zip comment + expect(zip.comment).to eq "justask export done for fizzyraccoon on 2022-12-10T13:37:42Z\n" - it "outputs JSON" do - subject - path = "#{dir}/#{name}.json" - expect(File.exist?(path)).to eq(true) - expect(JSON.load_file(path, symbolize_names: true)).to eq(instance.instance_variable_get(:@obj)) - end + # check if all files and directories are there + expect(zip.entries.map(&:name).sort).to eq([ + # basic dirs from exporter + "#{export_name}/", + "#{export_name}/pictures/", + # files added by the ExampleExporter + "#{export_name}/textfile.txt", + "#{export_name}/pictures/example.jpg", + "#{export_name}/some.json" + ].sort) - it "outputs YAML" do - subject - path = "#{dir}/#{name}.yml" - expect(File.exist?(path)).to eq(true) - expect(YAML.load_file(path)).to eq(instance.instance_variable_get(:@obj)) - end - - it "outputs XML" do - subject - path = "#{dir}/#{name}.xml" - expect(File.exist?(path)).to eq(true) + # check if the file contents match + expect(zip.file.read("#{export_name}/textfile.txt")).to eq("Sample Text\n") + expect(Base64.encode64(zip.file.read("#{export_name}/pictures/example.jpg"))) + .to eq(Base64.encode64(File.read(File.expand_path("../fixtures/files/banana_racc.jpg", __dir__)))) + expect(zip.file.read("#{export_name}/some.json")).to eq(<<~JSON) + { + "animals": [ + "raccoon", + "fox", + "hyena", + "deer", + "dog" + ], + "big_number": 3457812374589235798, + "booleans": { + "yes": true, + "no": false, + "file_not_found": null + } + } + JSON end end - context "exporting a user with a profile header" do + it "updates the export fields of the user" do + expect { subject }.to change { user.export_processing }.from(true).to(false) + expect(user.export_url).to eq("https://example.com/export/#{export_name}.zip") + expect(user.export_created_at).to eq(Time.utc(2022, 12, 10, 13, 37, 42)) + expect(user).to be_persisted + end + + context "when exporting fails" do + let(:zipfile_deletion_expected) { true } + before do - user.profile_header = Rack::Test::UploadedFile.new(File.open("#{file_fixture_path}/banana_racc.jpg")) - user.save! + allow_any_instance_of(ExampleExporter).to receive(:files).and_raise(ArgumentError.new("just testing")) end - it "exports the header image" do - subject - dirname = instance.instance_variable_get(:@export_dirname) - %i[web mobile retina original].each do |size| - expect(File.exist?("#{dirname}/pictures/header_#{size}_banana_racc.jpg")).to eq(true) - end - end - end + it "deletes the zip file" do + expect { subject }.to raise_error(ArgumentError, "just testing") - context "exporting a user with a profile picture" do - before do - user.profile_picture = Rack::Test::UploadedFile.new(File.open("#{file_fixture_path}/banana_racc.jpg")) - user.save! - end - - it "exports the header image" do - subject - dirname = instance.instance_variable_get(:@export_dirname) - %i[large medium small original].each do |size| - expect(File.exist?("#{dirname}/pictures/picture_#{size}_banana_racc.jpg")).to eq(true) - end - end - end - end - - describe "#publish" do - let(:fake_rails_root) { Pathname(Dir.mktmpdir) } - let(:fake_rails_public_path) { fake_rails_root.join('public') } - let(:name) { instance.instance_variable_get(:@export_filename) } - - before do - FileUtils.mkdir_p("#{fake_rails_root}/public/export") - allow(Rails).to receive(:root).and_return(fake_rails_root) - allow(Rails).to receive(:public_path).and_return(fake_rails_public_path) - - user.export_processing = true - user.save! - end - - after do - FileUtils.rm_r(fake_rails_root) - end - - subject { instance.send(:publish) } - - it "publishes an archive" do - freeze_time do - expect { subject }.to change { user.export_processing }.from(true).to(false) - expect(File.exist?("#{fake_rails_root}/public/export/#{name}.tar.gz")).to eq(true) - expect(user.export_url).to eq("https://example.com/export/#{name}.tar.gz") - expect(user.export_created_at).to eq(Time.now.utc) - expect(user).to be_persisted + zip_path = Rails.public_path.join("export/#{export_name}.zip") + expect(File.exist?(zip_path)).to be false end end end diff --git a/spec/lib/use_case/data_export/answers_spec.rb b/spec/lib/use_case/data_export/answers_spec.rb new file mode 100644 index 00000000..e61d74d0 --- /dev/null +++ b/spec/lib/use_case/data_export/answers_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require "rails_helper" + +require "use_case/data_export/answers" + +describe UseCase::DataExport::Answers, :data_export do + include ActiveSupport::Testing::TimeHelpers + + context "when user doesn't have any answers" do + it "returns an empty set of answers" do + expect(json_file("answers.json")).to eq( + { + answers: [] + } + ) + end + end + + context "when user has made some answer" do + let!(:answer) do + travel_to(Time.utc(2022, 12, 10, 13, 37, 42)) { FactoryBot.create(:answer, user:, content: "Yay, data export!") } + end + + it "returns the answers as json" do + expect(json_file("answers.json")).to eq( + { + answers: [ + { + id: answer.id, + content: "Yay, data export!", + question_id: answer.question.id, + comment_count: 0, + user_id: user.id, + created_at: "2022-12-10T13:37:42.000Z", + updated_at: "2022-12-10T13:37:42.000Z", + smile_count: 0 + } + ] + } + ) + end + end +end diff --git a/spec/lib/use_case/data_export/appendables_spec.rb b/spec/lib/use_case/data_export/appendables_spec.rb new file mode 100644 index 00000000..60523f45 --- /dev/null +++ b/spec/lib/use_case/data_export/appendables_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require "rails_helper" + +require "use_case/data_export/appendables" + +describe UseCase::DataExport::Appendables, :data_export do + include ActiveSupport::Testing::TimeHelpers + + context "when user doesn't have any appendable" do + it "returns an empty set of appendables" do + expect(json_file("appendables.json")).to eq( + { + appendables: [] + } + ) + end + end + + context "when user has smiled some things" do + let(:answer) { FactoryBot.create(:answer, user:) } + let(:comment) { FactoryBot.create(:comment, user:, answer:) } + + let!(:appendables) do + [ + travel_to(Time.utc(2022, 12, 10, 13, 37, 42)) { FactoryBot.create(:comment_smile, user:, parent: comment) }, + travel_to(Time.utc(2022, 12, 10, 13, 39, 21)) { FactoryBot.create(:smile, user:, parent: answer) } + ] + end + + it "returns the appendables as json" do + expect(json_file("appendables.json")).to eq( + { + appendables: [ + { + id: appendables[0].id, + type: "Appendable::Reaction", + user_id: user.id, + parent_id: appendables[0].parent_id, + parent_type: "Comment", + content: "🙂", + created_at: "2022-12-10T13:37:42.000Z", + updated_at: "2022-12-10T13:37:42.000Z" + }, + { + id: appendables[1].id, + type: "Appendable::Reaction", + user_id: user.id, + parent_id: appendables[1].parent_id, + parent_type: "Answer", + content: "🙂", + created_at: "2022-12-10T13:39:21.000Z", + updated_at: "2022-12-10T13:39:21.000Z" + } + ] + } + ) + end + end +end diff --git a/spec/lib/use_case/data_export/comments_spec.rb b/spec/lib/use_case/data_export/comments_spec.rb new file mode 100644 index 00000000..162d1798 --- /dev/null +++ b/spec/lib/use_case/data_export/comments_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require "rails_helper" + +require "use_case/data_export/comments" + +describe UseCase::DataExport::Comments, :data_export do + include ActiveSupport::Testing::TimeHelpers + + context "when user doesn't have any comments" do + it "returns an empty set of comments" do + expect(json_file("comments.json")).to eq( + { + comments: [] + } + ) + end + end + + context "when user has made some comment" do + let(:answer) { FactoryBot.create(:answer, user:) } + + let!(:comment) do + travel_to(Time.utc(2022, 12, 10, 13, 37, 42)) { FactoryBot.create(:comment, user:, answer:, content: "Yay, data export!") } + end + + it "returns the comments as json" do + expect(json_file("comments.json")).to eq( + { + comments: [ + { + id: comment.id, + content: "Yay, data export!", + answer_id: answer.id, + user_id: user.id, + created_at: "2022-12-10T13:37:42.000Z", + updated_at: "2022-12-10T13:37:42.000Z", + smile_count: 0 + } + ] + } + ) + end + end +end diff --git a/spec/lib/use_case/data_export/inbox_entries_spec.rb b/spec/lib/use_case/data_export/inbox_entries_spec.rb new file mode 100644 index 00000000..9682ab76 --- /dev/null +++ b/spec/lib/use_case/data_export/inbox_entries_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require "rails_helper" + +require "use_case/data_export/inbox_entries" + +describe UseCase::DataExport::InboxEntries, :data_export do + include ActiveSupport::Testing::TimeHelpers + + context "when user doesn't have anything in their inbox" do + it "returns an empty set of inbox entries" do + expect(json_file("inbox_entries.json")).to eq( + { + inbox_entries: [] + } + ) + end + end + + context "when user has some questions in their inbox" do + let!(:inbox_entries) do + [ + # using `Inbox.create` here as for some reason FactoryBot.create(:inbox) always sets `new` to `nil`??? + travel_to(Time.utc(2022, 12, 10, 13, 37, 42)) { Inbox.create(user:, question: FactoryBot.create(:question), new: false) }, + travel_to(Time.utc(2022, 12, 10, 13, 39, 21)) { Inbox.create(user:, question: FactoryBot.create(:question), new: true) } + ] + end + + it "returns the inbox entries as json" do + expect(json_file("inbox_entries.json")).to eq( + { + inbox_entries: [ + { + id: inbox_entries[0].id, + user_id: user.id, + question_id: inbox_entries[0].question_id, + new: false, + created_at: "2022-12-10T13:37:42.000Z", + updated_at: "2022-12-10T13:37:42.000Z" + }, + { + id: inbox_entries[1].id, + user_id: user.id, + question_id: inbox_entries[1].question_id, + new: true, + created_at: "2022-12-10T13:39:21.000Z", + updated_at: "2022-12-10T13:39:21.000Z" + } + ] + } + ) + end + end +end diff --git a/spec/lib/use_case/data_export/mute_rules_spec.rb b/spec/lib/use_case/data_export/mute_rules_spec.rb new file mode 100644 index 00000000..9e610b07 --- /dev/null +++ b/spec/lib/use_case/data_export/mute_rules_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require "rails_helper" + +require "use_case/data_export/mute_rules" + +describe UseCase::DataExport::MuteRules, :data_export do + include ActiveSupport::Testing::TimeHelpers + + context "when user doesn't have any mute rules" do + it "returns an empty set of mute rules" do + expect(json_file("mute_rules.json")).to eq( + { + mute_rules: [] + } + ) + end + end + + context "when user has some mute rules" do + let!(:mute_rules) do + [ + travel_to(Time.utc(2022, 12, 10, 13, 37, 42)) { MuteRule.create(user:, muted_phrase: "test") }, + travel_to(Time.utc(2022, 12, 10, 13, 39, 21)) { MuteRule.create(user:, muted_phrase: "python") } + ] + end + + it "returns the mute rules as json" do + expect(json_file("mute_rules.json")).to eq( + { + mute_rules: [ + { + id: mute_rules[0].id, + user_id: user.id, + muted_phrase: "test", + created_at: "2022-12-10T13:37:42.000Z", + updated_at: "2022-12-10T13:37:42.000Z" + }, + { + id: mute_rules[1].id, + user_id: user.id, + muted_phrase: "python", + created_at: "2022-12-10T13:39:21.000Z", + updated_at: "2022-12-10T13:39:21.000Z" + } + ] + } + ) + end + end +end diff --git a/spec/lib/use_case/data_export/questions_spec.rb b/spec/lib/use_case/data_export/questions_spec.rb new file mode 100644 index 00000000..dfb7b78f --- /dev/null +++ b/spec/lib/use_case/data_export/questions_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require "rails_helper" + +require "use_case/data_export/questions" + +describe UseCase::DataExport::Questions, :data_export do + include ActiveSupport::Testing::TimeHelpers + + context "when user doesn't have any questions" do + it "returns an empty set of questions" do + expect(json_file("questions.json")).to eq( + { + questions: [] + } + ) + end + end + + context "when user has made some questions" do + let!(:questions) do + [ + travel_to(Time.utc(2022, 12, 10, 13, 12, 0)) { FactoryBot.create(:question, user:, content: "Yay, data export 1", author_is_anonymous: false, direct: false, answer_count: 12) }, + travel_to(Time.utc(2022, 12, 10, 13, 37, 42)) { FactoryBot.create(:question, user:, content: "Yay, data export 2", author_is_anonymous: false, direct: true, answer_count: 1) }, + travel_to(Time.utc(2022, 12, 10, 13, 39, 21)) { FactoryBot.create(:question, user:, content: "Yay, data export 3", author_is_anonymous: true, direct: true) } + ] + end + + it "returns the questions as json" do + expect(json_file("questions.json")).to eq( + { + questions: [ + { + id: questions[0].id, + content: "Yay, data export 1", + author_is_anonymous: false, + user_id: user.id, + created_at: "2022-12-10T13:12:00.000Z", + updated_at: "2022-12-10T13:12:00.000Z", + answer_count: 12, + direct: false + }, + { + id: questions[1].id, + content: "Yay, data export 2", + author_is_anonymous: false, + user_id: user.id, + created_at: "2022-12-10T13:37:42.000Z", + updated_at: "2022-12-10T13:37:42.000Z", + answer_count: 1, + direct: true + }, + { + id: questions[2].id, + content: "Yay, data export 3", + author_is_anonymous: true, + user_id: user.id, + created_at: "2022-12-10T13:39:21.000Z", + updated_at: "2022-12-10T13:39:21.000Z", + answer_count: 0, + direct: true + } + ] + } + ) + end + end +end diff --git a/spec/lib/use_case/data_export/relationships_spec.rb b/spec/lib/use_case/data_export/relationships_spec.rb new file mode 100644 index 00000000..7af992f9 --- /dev/null +++ b/spec/lib/use_case/data_export/relationships_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require "rails_helper" + +require "use_case/data_export/relationships" + +describe UseCase::DataExport::Relationships, :data_export do + include ActiveSupport::Testing::TimeHelpers + + context "when user doesn't have any relationships" do + it "returns an empty set of relationships" do + expect(json_file("relationships.json")).to eq( + { + relationships: [] + } + ) + end + end + + context "when user has made some relationships" do + let(:other_user) { FactoryBot.create(:user) } + let(:blocked_user) { FactoryBot.create(:user) } + let(:blocking_user) { FactoryBot.create(:user) } + + let!(:relationships) do + { + # user <-> other_user follow each other + user_to_other: travel_to(Time.utc(2022, 12, 10, 13, 12, 0)) { user.follow(other_user) }, + other_to_user: travel_to(Time.utc(2022, 12, 10, 13, 12, 36)) { other_user.follow(user) }, + + # user blocked blocked_user + block: travel_to(Time.utc(2022, 12, 10, 13, 37, 42)) { user.block(blocked_user) }, + + # user is blocked by blocking_user + blocked_by: travel_to(Time.utc(2022, 12, 10, 13, 39, 21)) { blocking_user.block(user) } + } + end + + it "returns the relationships as json" do + expect(json_file("relationships.json")).to eq( + { + relationships: [ + { + id: relationships[:user_to_other].id, + source_id: user.id, + target_id: other_user.id, + created_at: "2022-12-10T13:12:00.000Z", + updated_at: "2022-12-10T13:12:00.000Z", + type: "Relationships::Follow" + }, + { + id: relationships[:block].id, + source_id: user.id, + target_id: blocked_user.id, + created_at: "2022-12-10T13:37:42.000Z", + updated_at: "2022-12-10T13:37:42.000Z", + type: "Relationships::Block" + } + ] + } + ) + end + end +end diff --git a/spec/lib/use_case/data_export/theme_spec.rb b/spec/lib/use_case/data_export/theme_spec.rb new file mode 100644 index 00000000..8a073d89 --- /dev/null +++ b/spec/lib/use_case/data_export/theme_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require "rails_helper" + +require "use_case/data_export/theme" + +describe UseCase::DataExport::Theme, :data_export do + include ActiveSupport::Testing::TimeHelpers + + context "when user doesn't have a theme" do + it "returns nothing" do + expect(subject).to eq({}) + end + end + + context "when user has a theme" do + let!(:theme) do + travel_to(Time.utc(2022, 12, 10, 13, 37, 42)) do + FactoryBot.create(:theme, user:) + end + end + + it "returns the theme as json" do + expect(json_file("theme.json")).to eq( + { + theme: { + id: theme.id, + user_id: user.id, + primary_color: 9342168, + primary_text: 16777215, + danger_color: 14257035, + danger_text: 16777215, + success_color: 12573067, + success_text: 16777215, + warning_color: 14261899, + warning_text: 16777215, + info_color: 9165273, + info_text: 16777215, + dark_color: 6710886, + dark_text: 15658734, + raised_background: 16777215, + background_color: 13026795, + body_text: 3355443, + muted_text: 3355443, + created_at: "2022-12-10T13:37:42.000Z", + updated_at: "2022-12-10T13:37:42.000Z", + input_color: 15789556, + input_text: 6710886, + raised_accent: 16250871, + light_color: 16316922, + light_text: 0, + input_placeholder: 7107965 + } + } + ) + end + end +end diff --git a/spec/lib/use_case/data_export/user_spec.rb b/spec/lib/use_case/data_export/user_spec.rb new file mode 100644 index 00000000..d769605b --- /dev/null +++ b/spec/lib/use_case/data_export/user_spec.rb @@ -0,0 +1,176 @@ +# frozen_string_literal: true + +require "rails_helper" + +require "use_case/data_export/user" + +describe UseCase::DataExport::User, :data_export do + let(:user_params) do + { + email: "fizzyraccoon@bsnss.biz", + answered_count: 144, + asked_count: 72, + comment_smiled_count: 15, + commented_count: 12, + confirmation_sent_at: 2.weeks.ago.utc, + confirmed_at: 2.weeks.ago.utc + 1.hour, + created_at: 2.weeks.ago.utc, + current_sign_in_at: 8.hours.ago.utc, + current_sign_in_ip: "198.51.100.220", + last_sign_in_at: 1.hour.ago, + last_sign_in_ip: "192.0.2.14", + locale: "en", + privacy_allow_anonymous_questions: true, + privacy_allow_public_timeline: false, + privacy_allow_stranger_answers: false, + privacy_show_in_search: true, + screen_name: "fizzyraccoon", + show_foreign_themes: true, + sign_in_count: 10, + smiled_count: 28, + profile: { + display_name: "Fizzy Raccoon", + description: "A small raccoon", + location: "Binland", + motivation_header: "", + website: "https://retrospring.net" + } + } + end + + it "returns the user as json" do + expect(json_file("user.json")).to eq( + { + user: { + id: user.id, + email: "fizzyraccoon@bsnss.biz", + remember_created_at: nil, + sign_in_count: 10, + current_sign_in_at: user.current_sign_in_at.as_json, + last_sign_in_at: user.last_sign_in_at.as_json, + current_sign_in_ip: "198.51.100.220", + last_sign_in_ip: "192.0.2.14", + created_at: user.created_at.as_json, + updated_at: user.updated_at.as_json, + screen_name: "fizzyraccoon", + asked_count: 72, + answered_count: 144, + commented_count: 12, + smiled_count: 28, + profile_picture_file_name: nil, + profile_picture_processing: nil, + profile_picture_x: nil, + profile_picture_y: nil, + profile_picture_w: nil, + profile_picture_h: nil, + privacy_allow_anonymous_questions: true, + privacy_allow_public_timeline: false, + privacy_allow_stranger_answers: false, + privacy_show_in_search: true, + comment_smiled_count: 15, + profile_header_file_name: nil, + profile_header_processing: nil, + profile_header_x: nil, + profile_header_y: nil, + profile_header_w: nil, + profile_header_h: nil, + locale: "en", + confirmed_at: user.confirmed_at.as_json, + confirmation_sent_at: user.confirmation_sent_at.as_json, + unconfirmed_email: nil, + show_foreign_themes: true, + export_url: nil, + export_processing: false, + export_created_at: nil, + otp_module: "disabled", + privacy_lock_inbox: false, + privacy_require_user: false, + privacy_hide_social_graph: false, + privacy_noindex: false + }, + profile: { + display_name: "Fizzy Raccoon", + description: "A small raccoon", + location: "Binland", + website: "https://retrospring.net", + motivation_header: "", + created_at: user.profile.created_at.as_json, + updated_at: user.profile.updated_at.as_json, + anon_display_name: nil + }, + roles: { + administrator: false, + moderator: false + } + } + ) + end + + it "does not have any pictures attached" do + expect(subject.keys.select { _1.start_with?("pictures/") }).to be_empty + end + + context "when user has a profile picture" do + let(:user_params) do + super().merge( + process_profile_picture_upload: true, # force carrierwave_backgrounder to immediately process the image + profile_picture: Rack::Test::UploadedFile.new(Rails.root.join("spec/fixtures/files/banana_racc.jpg"), "image/jpeg"), + profile_picture_x: 571, + profile_picture_y: 353, + profile_picture_w: 474, + profile_picture_h: 474 + ) + end + + it "includes the pictures in the file list" do + expect(subject.keys.select { _1.start_with?("pictures/") }.sort).to eq([ + "pictures/profile_picture_original_banana_racc.jpg", + "pictures/profile_picture_large_banana_racc.jpg", + "pictures/profile_picture_medium_banana_racc.jpg", + "pictures/profile_picture_small_banana_racc.jpg" + ].sort) + end + + it "contains the profile picture info on the exported user" do + expect(json_file("user.json").fetch(:user)).to include( + profile_picture_file_name: "banana_racc.jpg", + profile_picture_x: 571, + profile_picture_y: 353, + profile_picture_w: 474, + profile_picture_h: 474 + ) + end + end + + context "when user has a profile header" do + let(:user_params) do + super().merge( + process_profile_header_upload: true, # force carrierwave_backgrounder to immediately process the image + profile_header: Rack::Test::UploadedFile.new(Rails.root.join("spec/fixtures/files/banana_racc.jpg"), "image/jpeg"), + profile_header_x: 0, + profile_header_y: 412, + profile_header_w: 1813, + profile_header_h: 423 + ) + end + + it "includes the pictures in the file list" do + expect(subject.keys.select { _1.start_with?("pictures/") }.sort).to eq([ + "pictures/profile_header_original_banana_racc.jpg", + "pictures/profile_header_web_banana_racc.jpg", + "pictures/profile_header_mobile_banana_racc.jpg", + "pictures/profile_header_retina_banana_racc.jpg" + ].sort) + end + + it "contains the profile header info on the exported user" do + expect(json_file("user.json").fetch(:user)).to include( + profile_header_file_name: "banana_racc.jpg", + profile_header_x: 0, + profile_header_y: 412, + profile_header_w: 1813, + profile_header_h: 423 + ) + end + end +end diff --git a/spec/shared_examples/data_export.rb b/spec/shared_examples/data_export.rb new file mode 100644 index 00000000..8cd6b84e --- /dev/null +++ b/spec/shared_examples/data_export.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require "json" + +RSpec.shared_context "DataExport" do + let(:user_params) { {} } + + let!(:user) { FactoryBot.create(:user, **user_params) } + + subject { described_class.call(user:) } + + def json_file(filename) = JSON.parse(subject[filename], symbolize_names: true) +end + +RSpec.configure do |c| + c.include_context "DataExport", data_export: true +end diff --git a/spec/support/example_exporter.rb b/spec/support/example_exporter.rb new file mode 100644 index 00000000..66508eab --- /dev/null +++ b/spec/support/example_exporter.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +raise ArgumentError.new("This file should only be required in the 'test' environment! Current environment: #{Rails.env}") unless Rails.env.test? + +require "use_case/data_export/base" + +# an example exporter to be used for the tests of `Exporter` +# +# this only returning basic files, nothing user-specific. each exporter should be tested individually. +class ExampleExporter < UseCase::DataExport::Base + def files = { + "textfile.txt" => "Sample Text\n", + "pictures/example.jpg" => File.read(File.expand_path("../fixtures/files/banana_racc.jpg", __dir__)), + "some.json" => json_file!( + animals: %w[raccoon fox hyena deer dog], + big_number: 3457812374589235798, + booleans: { + yes: true, + no: false, + file_not_found: nil + } + ) + } +end