mirror of
https://github.com/discourse/discourse.git
synced 2024-12-25 16:04:11 +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) } ```
330 lines
8.8 KiB
Ruby
330 lines
8.8 KiB
Ruby
# frozen_string_literal: true
|
||
|
||
RSpec.describe Service::StepsInspector do
|
||
class DummyService
|
||
include Service::Base
|
||
|
||
options do
|
||
attribute :my_option, :boolean, default: true
|
||
attribute :my_other_option, :integer, default: 1
|
||
end
|
||
|
||
model :model
|
||
policy :policy
|
||
params do
|
||
attribute :parameter
|
||
|
||
validates :parameter, presence: true
|
||
end
|
||
transaction do
|
||
step :in_transaction_step_1
|
||
step :in_transaction_step_2
|
||
end
|
||
try { step :might_raise }
|
||
step :final_step
|
||
end
|
||
|
||
subject(:inspector) { described_class.new(result) }
|
||
|
||
let(:parameter) { "present" }
|
||
let(:result) { DummyService.call(params: { parameter: parameter }) }
|
||
|
||
before do
|
||
class DummyService
|
||
%i[
|
||
fetch_model
|
||
policy
|
||
in_transaction_step_1
|
||
in_transaction_step_2
|
||
might_raise
|
||
final_step
|
||
].each { |name| define_method(name) { true } }
|
||
end
|
||
end
|
||
|
||
describe "#inspect" do
|
||
subject(:output) { inspector.inspect.strip }
|
||
|
||
context "when service runs without error" do
|
||
it "outputs all the steps of the service" do
|
||
expect(output).to eq <<~OUTPUT.chomp
|
||
[ 1/10] [options] 'default' ✅
|
||
[ 2/10] [model] 'model' ✅
|
||
[ 3/10] [policy] 'policy' ✅
|
||
[ 4/10] [params] 'default' ✅
|
||
[ 5/10] [transaction]
|
||
[ 6/10] [step] 'in_transaction_step_1' ✅
|
||
[ 7/10] [step] 'in_transaction_step_2' ✅
|
||
[ 8/10] [try]
|
||
[ 9/10] [step] 'might_raise' ✅
|
||
[10/10] [step] 'final_step' ✅
|
||
OUTPUT
|
||
end
|
||
end
|
||
|
||
context "when the model step is failing" do
|
||
before do
|
||
class DummyService
|
||
def fetch_model
|
||
false
|
||
end
|
||
end
|
||
end
|
||
|
||
it "shows the failing step" do
|
||
expect(output).to eq <<~OUTPUT.chomp
|
||
[ 1/10] [options] 'default' ✅
|
||
[ 2/10] [model] 'model' ❌
|
||
[ 3/10] [policy] 'policy'
|
||
[ 4/10] [params] 'default'
|
||
[ 5/10] [transaction]
|
||
[ 6/10] [step] 'in_transaction_step_1'
|
||
[ 7/10] [step] 'in_transaction_step_2'
|
||
[ 8/10] [try]
|
||
[ 9/10] [step] 'might_raise'
|
||
[10/10] [step] 'final_step'
|
||
OUTPUT
|
||
end
|
||
end
|
||
|
||
context "when the policy step is failing" do
|
||
before do
|
||
class DummyService
|
||
def policy
|
||
false
|
||
end
|
||
end
|
||
end
|
||
|
||
it "shows the failing step" do
|
||
expect(output).to eq <<~OUTPUT.chomp
|
||
[ 1/10] [options] 'default' ✅
|
||
[ 2/10] [model] 'model' ✅
|
||
[ 3/10] [policy] 'policy' ❌
|
||
[ 4/10] [params] 'default'
|
||
[ 5/10] [transaction]
|
||
[ 6/10] [step] 'in_transaction_step_1'
|
||
[ 7/10] [step] 'in_transaction_step_2'
|
||
[ 8/10] [try]
|
||
[ 9/10] [step] 'might_raise'
|
||
[10/10] [step] 'final_step'
|
||
OUTPUT
|
||
end
|
||
end
|
||
|
||
context "when the params step is failing" do
|
||
let(:parameter) { nil }
|
||
|
||
it "shows the failing step" do
|
||
expect(output).to eq <<~OUTPUT.chomp
|
||
[ 1/10] [options] 'default' ✅
|
||
[ 2/10] [model] 'model' ✅
|
||
[ 3/10] [policy] 'policy' ✅
|
||
[ 4/10] [params] 'default' ❌
|
||
[ 5/10] [transaction]
|
||
[ 6/10] [step] 'in_transaction_step_1'
|
||
[ 7/10] [step] 'in_transaction_step_2'
|
||
[ 8/10] [try]
|
||
[ 9/10] [step] 'might_raise'
|
||
[10/10] [step] 'final_step'
|
||
OUTPUT
|
||
end
|
||
end
|
||
|
||
context "when a common step is failing" do
|
||
before do
|
||
class DummyService
|
||
def in_transaction_step_2
|
||
fail!("step error")
|
||
end
|
||
end
|
||
end
|
||
|
||
it "shows the failing step" do
|
||
expect(output).to eq <<~OUTPUT.chomp
|
||
[ 1/10] [options] 'default' ✅
|
||
[ 2/10] [model] 'model' ✅
|
||
[ 3/10] [policy] 'policy' ✅
|
||
[ 4/10] [params] 'default' ✅
|
||
[ 5/10] [transaction]
|
||
[ 6/10] [step] 'in_transaction_step_1' ✅
|
||
[ 7/10] [step] 'in_transaction_step_2' ❌
|
||
[ 8/10] [try]
|
||
[ 9/10] [step] 'might_raise'
|
||
[10/10] [step] 'final_step'
|
||
OUTPUT
|
||
end
|
||
end
|
||
|
||
context "when a step raises an exception inside the 'try' block" do
|
||
before do
|
||
class DummyService
|
||
def might_raise
|
||
raise "BOOM"
|
||
end
|
||
end
|
||
end
|
||
|
||
it "shows the failing step" do
|
||
expect(output).to eq <<~OUTPUT.chomp
|
||
[ 1/10] [options] 'default' ✅
|
||
[ 2/10] [model] 'model' ✅
|
||
[ 3/10] [policy] 'policy' ✅
|
||
[ 4/10] [params] 'default' ✅
|
||
[ 5/10] [transaction]
|
||
[ 6/10] [step] 'in_transaction_step_1' ✅
|
||
[ 7/10] [step] 'in_transaction_step_2' ✅
|
||
[ 8/10] [try]
|
||
[ 9/10] [step] 'might_raise' 💥
|
||
[10/10] [step] 'final_step'
|
||
OUTPUT
|
||
end
|
||
end
|
||
|
||
context "when running in specs" do
|
||
context "when a successful step is flagged as being an unexpected result" do
|
||
before { result["result.policy.policy"]["spec.unexpected_result"] = true }
|
||
|
||
it "adapts its output accordingly" do
|
||
expect(output).to eq <<~OUTPUT.chomp
|
||
[ 1/10] [options] 'default' ✅
|
||
[ 2/10] [model] 'model' ✅
|
||
[ 3/10] [policy] 'policy' ✅ ⚠️ <= expected to return false but got true instead
|
||
[ 4/10] [params] 'default' ✅
|
||
[ 5/10] [transaction]
|
||
[ 6/10] [step] 'in_transaction_step_1' ✅
|
||
[ 7/10] [step] 'in_transaction_step_2' ✅
|
||
[ 8/10] [try]
|
||
[ 9/10] [step] 'might_raise' ✅
|
||
[10/10] [step] 'final_step' ✅
|
||
OUTPUT
|
||
end
|
||
end
|
||
|
||
context "when a failing step is flagged as being an unexpected result" do
|
||
before do
|
||
class DummyService
|
||
def policy
|
||
false
|
||
end
|
||
end
|
||
result["result.policy.policy"]["spec.unexpected_result"] = true
|
||
end
|
||
|
||
it "adapts its output accordingly" do
|
||
expect(output).to eq <<~OUTPUT.chomp
|
||
[ 1/10] [options] 'default' ✅
|
||
[ 2/10] [model] 'model' ✅
|
||
[ 3/10] [policy] 'policy' ❌ ⚠️ <= expected to return true but got false instead
|
||
[ 4/10] [params] 'default'
|
||
[ 5/10] [transaction]
|
||
[ 6/10] [step] 'in_transaction_step_1'
|
||
[ 7/10] [step] 'in_transaction_step_2'
|
||
[ 8/10] [try]
|
||
[ 9/10] [step] 'might_raise'
|
||
[10/10] [step] 'final_step'
|
||
OUTPUT
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#error" do
|
||
subject(:error) { inspector.error }
|
||
|
||
context "when there are no errors" do
|
||
it "returns nothing" do
|
||
expect(error).to be_blank
|
||
end
|
||
end
|
||
|
||
context "when the model step is failing" do
|
||
context "when the model is missing" do
|
||
before do
|
||
class DummyService
|
||
def fetch_model
|
||
false
|
||
end
|
||
end
|
||
end
|
||
|
||
it "returns an error related to the model" do
|
||
expect(error).to match(/Model not found/)
|
||
end
|
||
end
|
||
|
||
context "when the model has errors" do
|
||
before do
|
||
class DummyService
|
||
def fetch_model
|
||
OpenStruct.new(invalid?: true, errors: ActiveModel::Errors.new(nil))
|
||
end
|
||
end
|
||
end
|
||
|
||
it "returns an error related to the model" do
|
||
expect(error).to match(/ActiveModel::Errors \[\]/)
|
||
end
|
||
end
|
||
end
|
||
|
||
context "when the params step is failing" do
|
||
let(:parameter) { nil }
|
||
|
||
it "returns an error related to the contract" do
|
||
expect(error).to match(/ActiveModel::Error attribute=parameter, type=blank, options={}/)
|
||
end
|
||
|
||
it "returns the provided paramaters" do
|
||
expect(error).to match(/{"parameter"=>nil}/)
|
||
end
|
||
end
|
||
|
||
context "when the policy step is failing" do
|
||
before do
|
||
class DummyService
|
||
def policy
|
||
false
|
||
end
|
||
end
|
||
end
|
||
|
||
context "when there is no reason provided" do
|
||
it "returns nothing" do
|
||
expect(error).to be_blank
|
||
end
|
||
end
|
||
|
||
context "when a reason is provided" do
|
||
before { result["result.policy.policy"][:reason] = "failed" }
|
||
|
||
it "returns the reason" do
|
||
expect(error).to eq "failed"
|
||
end
|
||
end
|
||
end
|
||
|
||
context "when a common step is failing" do
|
||
before { result["result.step.final_step"].fail(error: "my error") }
|
||
|
||
it "returns an error related to the step" do
|
||
expect(error).to eq("my error")
|
||
end
|
||
end
|
||
|
||
context "when an exception occurred inside the 'try' block" do
|
||
before do
|
||
class DummyService
|
||
def might_raise
|
||
raise "BOOM"
|
||
end
|
||
end
|
||
end
|
||
|
||
it "returns an error related to the exception" do
|
||
expect(error).to match(/RuntimeError: BOOM/)
|
||
end
|
||
end
|
||
end
|
||
end
|