0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-09 19:36:44 +08:00
discourse/spec/services/custom_emoji/confirm_import_spec.rb
Martin Brennan b736e72b1c
DEV: Retroactive review and refactor of custom emoji bulk import/export (#41860)
Followup bd3906ce64

Converts the new endpoints made in the original PR into
service classes, using AI to drive implementation based
on the service authoring skill:

* CustomEmoji::Export
* CustomEmoji::PreviewImport
* CustomEmoji::ConfirmImport

In addition, some supporting action, lib, and serializer
classes have been added. This fixed a couple of
bugs as well:

* Import names now go through `Emoji.sanitize_emoji_name` like
single-emoji create
* No-group imports no longer persist a literal "default" string
* Two 500s (suspicious zip entries, malformed CSV) became proper 422s
* Duplicate CSV names are flagged at preview instead of exploding the
confirm transaction
*` Emoji.clear_cache `moved outside the transaction
2026-07-22 10:06:54 +10:00

141 lines
4.2 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe CustomEmoji::ConfirmImport do
describe described_class::Contract, type: :model do
it { is_expected.to validate_presence_of(:token) }
end
describe ".call" do
subject(:result) { described_class.call(params:, **dependencies) }
fab!(:current_user, :admin)
fab!(:staged_upload, :upload)
fab!(:existing_emoji) { Fabricate(:custom_emoji, name: "conflict-emoji", group: "old") }
let(:params) { { token:, resolutions: } }
let(:dependencies) { { guardian: current_user.guardian } }
let(:resolutions) { {} }
let(:token) { CustomEmoji::ImportPreviewCache.new(current_user).store(rows) }
let(:rows) do
[
CustomEmoji::ImportRow.new(
index: 0,
name: "confirm-new",
group: "fun",
filename: "confirm-new.png",
category: CustomEmoji::ImportRow::CATEGORY_NEW,
upload_id: staged_upload.id,
),
CustomEmoji::ImportRow.new(
index: 1,
name: "conflict-emoji",
group: nil,
filename: "conflict-emoji.png",
category: CustomEmoji::ImportRow::CATEGORY_CONFLICT_IMAGE,
upload_id: staged_upload.id,
),
CustomEmoji::ImportRow.new(
index: 2,
name: "same-emoji",
group: nil,
filename: "same-emoji.png",
category: CustomEmoji::ImportRow::CATEGORY_IDENTICAL,
),
]
end
context "when contract is invalid" do
let(:token) { "" }
it { is_expected.to fail_a_contract }
end
context "when the token is unknown or expired" do
let(:token) { "unknown-token" }
it { is_expected.to fail_to_find_a_model(:rows) }
end
context "when a row fails to apply" do
let(:rows) do
[
CustomEmoji::ImportRow.new(
index: 0,
name: "rollback-ok",
group: nil,
filename: "rollback-ok.png",
category: CustomEmoji::ImportRow::CATEGORY_NEW,
upload_id: staged_upload.id,
),
CustomEmoji::ImportRow.new(
index: 1,
name: "",
group: nil,
filename: "bad.png",
category: CustomEmoji::ImportRow::CATEGORY_NEW,
upload_id: staged_upload.id,
),
]
end
it { is_expected.to fail_with_exception(ActiveRecord::RecordInvalid) }
it "rolls back emojis created earlier in the batch" do
expect { result }.not_to change { CustomEmoji.count }
end
it "keeps the staged manifest in Redis" do
result
expect(Discourse.redis.exists?("emoji_import_preview:#{current_user.id}:#{token}")).to eq(
true,
)
end
end
context "when everything's ok" do
it { is_expected.to run_successfully }
it "reports created, updated and skipped counts" do
expect(result[:report]).to eq(created: 1, updated: 1, skipped: 1)
end
it "creates the new emoji with its group" do
expect { result }.to change { CustomEmoji.count }.by(1)
expect(CustomEmoji.find_by(name: "confirm-new")).to have_attributes(
group: "fun",
upload_id: staged_upload.id,
user_id: current_user.id,
)
end
it "updates the conflicting emoji with the incoming upload and group" do
expect { result }.to change { existing_emoji.reload.upload_id }.to(staged_upload.id).and(
change { existing_emoji.reload.group }.from("old").to(nil),
)
end
it "logs the applied changes" do
expect { result }.to change {
UserHistory.where(action: UserHistory.actions[:custom_emoji_create]).count
}.by(2)
end
it "deletes the staged manifest from Redis" do
result
expect(Discourse.redis.exists?("emoji_import_preview:#{current_user.id}:#{token}")).to eq(
false,
)
end
end
context "when a conflict is resolved with keep" do
let(:resolutions) { { "conflict-emoji" => "keep" } }
it { is_expected.to run_successfully }
it "leaves the existing emoji untouched" do
expect { result }.not_to change { existing_emoji.reload.attributes }
end
end
end
end