mirror of
https://github.com/discourse/discourse.git
synced 2024-12-23 23:13:50 +08:00
719457e430
This patch adds a new step to services named `try`. It’s useful to rescue exceptions that some steps could raise. That way, if an exception is caught, the service will stop its execution and can be inspected like with any other steps. Just wrap the steps that can raise with a `try` block: ```ruby try do step :step_that_can_raise step :another_step_that_can_raise end ``` By default, `try` will catch any exception inheriting from `StandardError`, but we can specify what exceptions to catch: ```ruby try(ArgumentError, RuntimeError) do step :will_raise end ``` An outcome matcher has been added: `on_exceptions`. By default it will be executed for any exception caught by the `try` step. Here also, we can specify what exceptions to catch: ```ruby on_exceptions(ArgumentError, RuntimeError) do |exception| … end ``` Finally, an RSpec matcher has been added: ```ruby it { is_expected.to fail_with_exception } # or it { is_expected.to fail_with_exception(ArgumentError) } ```
506 lines
11 KiB
Ruby
506 lines
11 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
RSpec.describe Service::Runner do
|
|
class SuccessService
|
|
include Service::Base
|
|
end
|
|
|
|
class FailureService
|
|
include Service::Base
|
|
|
|
step :fail_step
|
|
|
|
def fail_step
|
|
fail!("error")
|
|
end
|
|
end
|
|
|
|
class FailedPolicyService
|
|
include Service::Base
|
|
|
|
policy :test
|
|
|
|
def test
|
|
false
|
|
end
|
|
end
|
|
|
|
class SuccessPolicyService
|
|
include Service::Base
|
|
|
|
policy :test
|
|
|
|
def test
|
|
true
|
|
end
|
|
end
|
|
|
|
class FailedContractService
|
|
include Service::Base
|
|
|
|
params do
|
|
attribute :test
|
|
|
|
validates :test, presence: true
|
|
end
|
|
end
|
|
|
|
class SuccessContractService
|
|
include Service::Base
|
|
|
|
params {}
|
|
end
|
|
|
|
class FailureWithModelService
|
|
include Service::Base
|
|
|
|
model :fake_model, :fetch_fake_model
|
|
|
|
private
|
|
|
|
def fetch_fake_model
|
|
nil
|
|
end
|
|
end
|
|
|
|
class FailureWithOptionalModelService
|
|
include Service::Base
|
|
|
|
model :fake_model, optional: true
|
|
|
|
private
|
|
|
|
def fetch_fake_model
|
|
nil
|
|
end
|
|
end
|
|
|
|
class FailureWithModelErrorsService
|
|
include Service::Base
|
|
|
|
model :fake_model, :fetch_fake_model
|
|
|
|
private
|
|
|
|
def fetch_fake_model
|
|
OpenStruct.new(invalid?: true)
|
|
end
|
|
end
|
|
|
|
class SuccessWithModelService
|
|
include Service::Base
|
|
|
|
model :fake_model, :fetch_fake_model
|
|
|
|
private
|
|
|
|
def fetch_fake_model
|
|
:model_found
|
|
end
|
|
end
|
|
|
|
class SuccessWithModelErrorsService
|
|
include Service::Base
|
|
|
|
model :fake_model, :fetch_fake_model
|
|
|
|
private
|
|
|
|
def fetch_fake_model
|
|
OpenStruct.new
|
|
end
|
|
end
|
|
|
|
class FailureWithCollectionModelService
|
|
include Service::Base
|
|
|
|
model :fake_model, :fetch_fake_model
|
|
|
|
private
|
|
|
|
def fetch_fake_model
|
|
[]
|
|
end
|
|
end
|
|
|
|
class SuccessWithCollectionModelService
|
|
include Service::Base
|
|
|
|
model :fake_model, :fetch_fake_model
|
|
|
|
private
|
|
|
|
def fetch_fake_model
|
|
[:models_found]
|
|
end
|
|
end
|
|
|
|
class RelationModelService
|
|
include Service::Base
|
|
|
|
model :fake_model
|
|
|
|
private
|
|
|
|
def fetch_fake_model
|
|
User.where(admin: false)
|
|
end
|
|
end
|
|
|
|
class FailureTryService
|
|
include Service::Base
|
|
|
|
try { step :raising_step }
|
|
|
|
def raising_step
|
|
raise "BOOM"
|
|
end
|
|
end
|
|
|
|
class SuccessTryService
|
|
include Service::Base
|
|
|
|
try { step :non_raising_step }
|
|
|
|
def non_raising_step
|
|
true
|
|
end
|
|
end
|
|
|
|
describe ".call" do
|
|
subject(:runner) { described_class.call(service, dependencies, &actions_block) }
|
|
|
|
let(:actions_block) { object.instance_eval(actions) }
|
|
let(:service) { SuccessWithModelService }
|
|
let(:actions) { <<-BLOCK }
|
|
proc do |result|
|
|
on_success { |fake_model:| [result, fake_model] }
|
|
end
|
|
BLOCK
|
|
let(:dependencies) { { guardian: stub, params: {} } }
|
|
let(:object) do
|
|
Class
|
|
.new(ApplicationController) do
|
|
def request
|
|
OpenStruct.new
|
|
end
|
|
end
|
|
.new
|
|
end
|
|
|
|
it "allows access to the result object" do
|
|
expect(runner.first).to be_a Service::Base::Context
|
|
end
|
|
|
|
it "allows using keyword args in blocks" do
|
|
expect(runner.last).to eq :model_found
|
|
end
|
|
|
|
context "when using the on_success action" do
|
|
let(:actions) { <<-BLOCK }
|
|
proc do
|
|
on_success { :success }
|
|
end
|
|
BLOCK
|
|
|
|
context "when the service succeeds" do
|
|
it "runs the provided block" do
|
|
expect(runner).to eq :success
|
|
end
|
|
end
|
|
|
|
context "when the service does not succeed" do
|
|
let(:service) { FailureService }
|
|
|
|
it "does not run the provided block" do
|
|
expect(runner).not_to eq :success
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when using the on_failure action" do
|
|
let(:actions) { <<-BLOCK }
|
|
proc do
|
|
on_failure { :fail }
|
|
end
|
|
BLOCK
|
|
|
|
context "when the service fails" do
|
|
let(:service) { FailureService }
|
|
|
|
it "runs the provided block" do
|
|
expect(runner).to eq :fail
|
|
end
|
|
end
|
|
|
|
context "when the service does not fail" do
|
|
let(:service) { SuccessService }
|
|
|
|
it "does not run the provided block" do
|
|
expect(runner).not_to eq :fail
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when using the on_failed_policy action" do
|
|
let(:actions) { <<-BLOCK }
|
|
proc do
|
|
on_failed_policy(:test) { :policy_failure }
|
|
end
|
|
BLOCK
|
|
|
|
context "when the service policy fails" do
|
|
let(:service) { FailedPolicyService }
|
|
|
|
context "when not using the block argument" do
|
|
it "runs the provided block" do
|
|
expect(runner).to eq :policy_failure
|
|
end
|
|
end
|
|
|
|
context "when using the block argument" do
|
|
let(:actions) { <<-BLOCK }
|
|
proc do |result|
|
|
on_failed_policy(:test) { |policy| policy == result["result.policy.test"] }
|
|
end
|
|
BLOCK
|
|
|
|
it "runs the provided block" do
|
|
expect(runner).to be true
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when the service policy does not fail" do
|
|
let(:service) { SuccessPolicyService }
|
|
|
|
it "does not run the provided block" do
|
|
expect(runner).not_to eq :policy_failure
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when using the on_failed_contract action" do
|
|
let(:actions) { <<-BLOCK }
|
|
proc do
|
|
on_failed_contract { :contract_failure }
|
|
end
|
|
BLOCK
|
|
|
|
context "when the service contract fails" do
|
|
let(:service) { FailedContractService }
|
|
|
|
context "when not using the block argument" do
|
|
it "runs the provided block" do
|
|
expect(runner).to eq :contract_failure
|
|
end
|
|
end
|
|
|
|
context "when using the block argument" do
|
|
let(:actions) { <<-BLOCK }
|
|
proc do |result|
|
|
on_failed_contract { |contract| contract == result["result.contract.default"] }
|
|
end
|
|
BLOCK
|
|
|
|
it "runs the provided block" do
|
|
expect(runner).to be true
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when the service contract does not fail" do
|
|
let(:service) { SuccessContractService }
|
|
|
|
it "does not run the provided block" do
|
|
expect(runner).not_to eq :contract_failure
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when using the on_model_not_found action" do
|
|
let(:actions) { <<-BLOCK }
|
|
proc do |result|
|
|
on_success { [result] }
|
|
on_model_not_found(:fake_model) { [:no_model, result] }
|
|
end
|
|
BLOCK
|
|
|
|
context "when fetching a single model" do
|
|
context "when the service uses an optional model" do
|
|
let(:service) { FailureWithOptionalModelService }
|
|
|
|
it "does not run the provided block" do
|
|
expect(runner).not_to include :no_model
|
|
end
|
|
end
|
|
|
|
context "when the service fails without a model" do
|
|
let(:service) { FailureWithModelService }
|
|
|
|
context "when not using the block argument" do
|
|
it "runs the provided block" do
|
|
expect(runner).to include :no_model
|
|
end
|
|
end
|
|
|
|
context "when using the block argument" do
|
|
let(:actions) { <<-BLOCK }
|
|
proc do |result|
|
|
on_model_not_found(:fake_model) { |model| model == result["result.model.fake_model"] }
|
|
end
|
|
BLOCK
|
|
|
|
it "runs the provided block" do
|
|
expect(runner).to be true
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when the service does not fail with a model" do
|
|
let(:service) { SuccessWithModelService }
|
|
|
|
it "does not run the provided block" do
|
|
expect(runner).not_to include :no_model
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when fetching a collection" do
|
|
context "when the service fails without a model" do
|
|
let(:service) { FailureWithCollectionModelService }
|
|
|
|
it "runs the provided block" do
|
|
expect(runner).to include :no_model
|
|
end
|
|
end
|
|
|
|
context "when the service does not fail with a model" do
|
|
let(:service) { SuccessWithCollectionModelService }
|
|
|
|
it "does not run the provided block" do
|
|
expect(runner).not_to include :no_model
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when fetching an ActiveRecord relation" do
|
|
let(:service) { RelationModelService }
|
|
|
|
context "when the service does not fail" do
|
|
before { Fabricate(:user) }
|
|
|
|
it "does not run the provided block" do
|
|
expect(runner).not_to include :no_model
|
|
end
|
|
|
|
it "does not fetch records from the relation" do
|
|
expect(runner.last[:fake_model]).not_to be_loaded
|
|
end
|
|
end
|
|
|
|
context "when the service fails" do
|
|
it "runs the provided block" do
|
|
expect(runner).to include :no_model
|
|
end
|
|
|
|
it "does not fetch records from the relation" do
|
|
expect(runner.last[:fake_model]).not_to be_loaded
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when using the on_model_errors action" do
|
|
let(:actions) { <<-BLOCK }
|
|
proc do
|
|
on_model_errors(:fake_model) { :model_errors }
|
|
end
|
|
BLOCK
|
|
|
|
context "when the service fails with a model containing errors" do
|
|
let(:service) { FailureWithModelErrorsService }
|
|
|
|
context "when not using the block argument" do
|
|
it "runs the provided block" do
|
|
expect(runner).to eq :model_errors
|
|
end
|
|
end
|
|
|
|
context "when using the block argument" do
|
|
let(:actions) { <<-BLOCK }
|
|
proc do
|
|
on_model_errors(:fake_model) { |model| model == OpenStruct.new(invalid?: true) }
|
|
end
|
|
BLOCK
|
|
|
|
it "runs the provided block" do
|
|
expect(runner).to be true
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when the service does not fail with a model containing errors" do
|
|
let(:service) { SuccessWithModelErrorsService }
|
|
|
|
it "does not run the provided block" do
|
|
expect(runner).not_to eq :model_errors
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when using the on_exceptions action" do
|
|
let(:actions) { <<-BLOCK }
|
|
proc do |result|
|
|
on_exceptions { |exception| exception.message == "BOOM" }
|
|
end
|
|
BLOCK
|
|
|
|
context "when the service fails" do
|
|
let(:service) { FailureTryService }
|
|
|
|
it "runs the provided block" do
|
|
expect(runner).to be true
|
|
end
|
|
end
|
|
|
|
context "when the service does not fail" do
|
|
let(:service) { SuccessTryService }
|
|
|
|
it "does not run the provided block" do
|
|
expect(runner).not_to eq true
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when using several actions together" do
|
|
let(:service) { FailureService }
|
|
let(:actions) { <<-BLOCK }
|
|
proc do
|
|
on_success { :success }
|
|
on_failure { :failure }
|
|
on_failed_policy { :policy_failure }
|
|
end
|
|
BLOCK
|
|
|
|
it "runs the first matching action" do
|
|
expect(runner).to eq :failure
|
|
end
|
|
end
|
|
|
|
context "when running in the context of a job" do
|
|
let(:object) { Class.new(Jobs::Base).new }
|
|
let(:actions) { <<-BLOCK }
|
|
proc do
|
|
on_success { :success }
|
|
on_failure { :failure }
|
|
end
|
|
BLOCK
|
|
|
|
it "runs properly" do
|
|
expect(runner).to eq :success
|
|
end
|
|
end
|
|
end
|
|
end
|