tram-policy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe <%= klass %>, "#valid?" do
4
+ subject(:policy) { described_class[<%= policy_signature.join(", ") %>] }
5
+
6
+ <% (parsed_params + parsed_options).each do |name| -%>
7
+ let(:<%= name %>) { FactoryGirl.build :<%= name %> }
8
+ <% end -%>
9
+
10
+ it { is_expected.to be_valid }
11
+ <% parsed_validators.each do |validator| %>
12
+ it "is invalid when not <%= validator %>" do
13
+ policy # modify it correspondingly
14
+ expect { policy }.to be_invalid_at # add tags to check
15
+ end
16
+ <% end -%>
17
+ end
@@ -0,0 +1,26 @@
1
+ class Tram::Policy
2
+ if Object.const_defined? "ActiveSupport::Inflector"
3
+ Inflector = ActiveSupport::Inflector
4
+ elsif Object.const_defined? "Inflecto"
5
+ Inflector = ::Inflecto
6
+ else
7
+ module Inflector
8
+ def self.underscore(name)
9
+ name.dup.tap do |n|
10
+ n.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
11
+ n.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
12
+ n.gsub!("::", "/")
13
+ n.tr!("-", "_")
14
+ n.downcase!
15
+ end
16
+ end
17
+
18
+ def self.camelize(name)
19
+ name.dup.tap do |n|
20
+ n.gsub!(/(?:\A|_+)(.)/) { $1.upcase }
21
+ n.gsub!(%r{(?:[/|-]+)(.)}) { "::#{$1.upcase}" }
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,112 @@
1
+ require "rspec"
2
+
3
+ # Checks that a block provides policy that has errors under given tags
4
+ # It also check that selected messages has translations to all available locales
5
+ #
6
+ # @example
7
+ # subject(:policy) { UserPolicy[name: nil] }
8
+ # expect { policy }.to be_invalid_at field: "name", level: "error"
9
+ #
10
+ # You have to wrap expectation to a block called for available locales.
11
+ #
12
+ RSpec::Matchers.define :be_invalid_at do |**tags|
13
+ supports_block_expectations
14
+
15
+ # ****************************************************************************
16
+ # Result collectors for all available locations
17
+ # ****************************************************************************
18
+
19
+ attr_accessor :policy
20
+
21
+ def errors
22
+ @errors ||= {}
23
+ end
24
+
25
+ def tags
26
+ @tags ||= {}
27
+ end
28
+
29
+ def messages
30
+ @messages ||= {}
31
+ end
32
+
33
+ # ****************************************************************************
34
+ # Helpers to provide results for all locales
35
+ # ****************************************************************************
36
+
37
+ # Runs block in every available locale
38
+ def in_available_locales
39
+ Array(I18n.available_locales).flat_map do |locale|
40
+ I18n.with_locale(locale) { yield }
41
+ end
42
+ end
43
+
44
+ # Collects results for the current locale
45
+ def prepare_results(policy_block, tags, locale = I18n.locale)
46
+ localized_policy = policy_block.call
47
+ localized_errors = localized_policy&.errors || []
48
+ self.policy = localized_policy.inspect
49
+ errors[locale] = localized_errors.by_tags(tags)
50
+ messages[locale] = localized_errors.full_messages
51
+ end
52
+
53
+ # ****************************************************************************
54
+ # Checkers for collected results
55
+ # ****************************************************************************
56
+
57
+ # Checks if selected errors are present in all available locales
58
+ def errored?
59
+ errors.values.map(&:any?).reduce(true, &:&) == true
60
+ end
61
+
62
+ # Checks if selected errors are absent in all available locales
63
+ def not_errored?
64
+ errors.values.map(&:empty?).reduce(true, &:&) == true
65
+ end
66
+
67
+ # Checks if all collected errors are translated
68
+ def translated?
69
+ texts = errors.values.flatten.map(&:message)
70
+ texts.select { |text| text.start_with?("translation missing:") }.empty?
71
+ end
72
+
73
+ def report_errors
74
+ text = "Actual errors:\n"
75
+ messages.each do |locale, list|
76
+ text << " #{locale}:\n"
77
+ list.each { |item| text << " - #{item}\n" }
78
+ end
79
+ text
80
+ end
81
+
82
+ # ****************************************************************************
83
+ # Positive matcher
84
+ # ****************************************************************************
85
+
86
+ match do |policy_block|
87
+ in_available_locales { prepare_results(policy_block, tags) }
88
+ errored? && translated?
89
+ end
90
+
91
+ failure_message do |_|
92
+ text = "#{policy} should have had errors with tags: #{tags}, "
93
+ text << "whose messages are translated in all available locales.\n"
94
+ text << report_errors
95
+ text
96
+ end
97
+
98
+ # ****************************************************************************
99
+ # Negative matcher
100
+ # ****************************************************************************
101
+
102
+ match_when_negated do |policy_block|
103
+ in_available_locales { prepare_results(policy_block, tags) }
104
+ not_errored?
105
+ end
106
+
107
+ failure_message_when_negated do |_|
108
+ text = "#{policy} should not have had any error with tags: #{tags}.\n"
109
+ text << report_errors
110
+ text
111
+ end
112
+ end
@@ -0,0 +1,18 @@
1
+ class Tram::Policy
2
+ # An exception to be risen by [Tram::Policy#validate!]
3
+ class ValidationError < RuntimeError
4
+ # Policy object whose validation has caused the exception
5
+ #
6
+ # @return [Tram::Policy]
7
+ #
8
+ attr_reader :policy
9
+
10
+ private
11
+
12
+ def initialize(policy, filter)
13
+ @policy = policy
14
+ messages = policy.errors.reject(&filter).map(&:full_message)
15
+ super (["Validation failed with errors:"] + messages).join("\n- ")
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,27 @@
1
+ begin
2
+ require "pry"
3
+ rescue
4
+ nil
5
+ end
6
+ require "bundler/setup"
7
+ require "tram/policy"
8
+ require "tram/policy/matchers"
9
+ require "rspec/its"
10
+
11
+ RSpec.configure do |config|
12
+ config.example_status_persistence_file_path = ".rspec_status"
13
+ config.expect_with :rspec do |c|
14
+ c.syntax = :expect
15
+ end
16
+
17
+ config.order = :random
18
+ config.filter_run focus: true
19
+ config.run_all_when_everything_filtered = true
20
+
21
+ # Prepare the Test namespace for constants defined in specs
22
+ config.around(:each) do |example|
23
+ Test = Class.new(Module)
24
+ example.run
25
+ Object.send :remove_const, :Test
26
+ end
27
+ end
@@ -0,0 +1,61 @@
1
+ RSpec.describe Tram::Policy::Error do
2
+ subject(:error) { described_class.new "Something bad happened", tags }
3
+
4
+ let(:tags) { { level: "warning" } }
5
+
6
+ describe "#message" do
7
+ subject { error.message }
8
+ it { is_expected.to eq "Something bad happened" }
9
+ end
10
+
11
+ describe "#full_message" do
12
+ subject { error.full_message }
13
+
14
+ context "with tags:" do
15
+ it { is_expected.to eq "Something bad happened {:level=>\"warning\"}" }
16
+ end
17
+
18
+ context "without tags:" do
19
+ let(:tags) { {} }
20
+ it { is_expected.to eq "Something bad happened" }
21
+ end
22
+ end
23
+
24
+ describe "#to_h" do
25
+ subject { error.to_h }
26
+ it { is_expected.to eq message: "Something bad happened", level: "warning" }
27
+ end
28
+
29
+ describe "#==" do
30
+ subject { error == other }
31
+
32
+ context "when other object has the same #to_h:" do
33
+ let(:other) { double to_h: error.to_h }
34
+ it { is_expected.to eq true }
35
+ end
36
+
37
+ context "when other object has different #to_h:" do
38
+ let(:other) { double to_h: error.to_h.merge(foo: :bar) }
39
+ it { is_expected.to eq false }
40
+ end
41
+
42
+ context "when other object not respond to #to_h:" do
43
+ let(:other) { double }
44
+ it { is_expected.to eq false }
45
+ end
46
+ end
47
+
48
+ describe "arbitrary tag" do
49
+ subject { error.send tag }
50
+
51
+ context "when tag is defined:" do
52
+ let(:tag) { "level" }
53
+ it { is_expected.to eq "warning" }
54
+ end
55
+
56
+ context "when tag not defined:" do
57
+ let(:tag) { :weight }
58
+ it { is_expected.to be_nil }
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,112 @@
1
+ RSpec.describe Tram::Policy::Errors do
2
+ let(:policy) { double :policy, t: "OMG!" }
3
+ let(:errors) { described_class.new(policy) }
4
+
5
+ describe ".new" do
6
+ subject { errors }
7
+
8
+ it { is_expected.to be_kind_of Enumerable }
9
+ it { is_expected.to respond_to :empty? }
10
+ it { is_expected.to be_empty }
11
+ its(:policy) { is_expected.to eql policy }
12
+ end
13
+
14
+ describe "#add" do
15
+ subject { errors.add :omg, level: "info", field: "name" }
16
+ let(:error) { errors.to_a.last }
17
+
18
+ it "adds an error to the collection:" do
19
+ expect { 2.times { subject } }.to change { errors.count }.by 1
20
+
21
+ expect(error).to be_kind_of Tram::Policy::Error
22
+ expect(error).to eq message: "OMG!", level: "info", field: "name"
23
+ end
24
+ end
25
+
26
+ describe "#merge" do
27
+ let(:other) { described_class.new(policy) }
28
+
29
+ before do
30
+ errors.add "D'OH!", level: "disaster"
31
+ other.add "OUCH!", level: "error"
32
+ end
33
+
34
+ context "without a block:" do
35
+ subject { errors.merge(other) }
36
+
37
+ it "merges other collection as is" do
38
+ expect(subject).to be_a Tram::Policy::Errors
39
+ expect(subject.map(&:to_h)).to match_array [
40
+ { message: "OMG!", level: "disaster" },
41
+ { message: "OMG!", level: "error" }
42
+ ]
43
+ end
44
+ end
45
+
46
+ context "with a block:" do
47
+ subject { errors.merge(other) { |err| err.merge(source: "Homer") } }
48
+
49
+ it "merges filtered collection as is" do
50
+ expect(subject).to be_a Tram::Policy::Errors
51
+ expect(subject.map(&:to_h)).to match_array [
52
+ { message: "OMG!", level: "disaster" },
53
+ { message: "OMG!", level: "error", source: "Homer" }
54
+ ]
55
+ end
56
+ end
57
+
58
+ context "not errors:" do
59
+ subject { errors.merge 1 }
60
+ it { is_expected.to eql errors }
61
+ end
62
+ end
63
+
64
+ describe "#messages" do
65
+ subject { errors.messages }
66
+
67
+ it { is_expected.to eq [] }
68
+
69
+ context "with errors added:" do
70
+ before { errors.add "OMG!", level: "info", field: "name" }
71
+ it { is_expected.to eq %w[OMG!] }
72
+ end
73
+ end
74
+
75
+ describe "#full_messages" do
76
+ subject { errors.full_messages }
77
+
78
+ it { is_expected.to eq [] }
79
+
80
+ context "with errors added:" do
81
+ before { errors.add "OMG!", level: "info", field: "name" }
82
+ it { is_expected.to eq ["OMG! {:level=>\"info\", :field=>\"name\"}"] }
83
+ end
84
+ end
85
+
86
+ describe "#by_tags" do
87
+ before do
88
+ errors.add :foo, field: "name", level: "error"
89
+ errors.add :foo, field: "email", level: "info"
90
+ errors.add :foo, field: "email", level: "error"
91
+ end
92
+
93
+ context "with filter" do
94
+ subject { errors.by_tags level: "error" }
95
+
96
+ it "returns selected errors only" do
97
+ expect(subject.map(&:to_h)).to match_array [
98
+ { message: "OMG!", field: "name", level: "error" },
99
+ { message: "OMG!", field: "email", level: "error" }
100
+ ]
101
+ end
102
+ end
103
+
104
+ context "without a filter" do
105
+ subject { errors.by_tags }
106
+
107
+ it "returns selected all errors" do
108
+ expect(subject.map(&:to_h)).to match_array errors.to_a
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,14 @@
1
+ RSpec.describe Tram::Policy::Inflector do
2
+ let(:snake) { "test/admin2_user_new_policy" }
3
+ let(:camel) { "Test::Admin2UserNewPolicy" }
4
+
5
+ describe "#underscore" do
6
+ subject { described_class.underscore "Test::Admin2USERNew-Policy" }
7
+ it { is_expected.to eq snake }
8
+ end
9
+
10
+ describe "#camelize" do
11
+ subject { described_class.camelize snake }
12
+ it { is_expected.to eq camel }
13
+ end
14
+ end
@@ -0,0 +1,70 @@
1
+ RSpec.describe "RSpec matchers:" do
2
+ subject { Test::UserPolicy[name: nil] }
3
+
4
+ before do
5
+ I18n.available_locales = %i[en]
6
+ I18n.backend.store_translations \
7
+ :en, { "test/user_policy" => { "name_presence" => "Name is absent" } }
8
+
9
+ class Test::UserPolicy < Tram::Policy
10
+ option :name
11
+
12
+ validate :name_presence
13
+
14
+ private
15
+
16
+ def name_presence
17
+ return if name
18
+ errors.add :name_presence, field: "name"
19
+ end
20
+ end
21
+ end
22
+
23
+ describe "to be_invalid_at" do
24
+ it "passes when some translated error present w/o tags constraint" do
25
+ expect do
26
+ expect { subject }.to be_invalid_at
27
+ end.not_to raise_error
28
+ end
29
+
30
+ it "passes when some translated error present under given tags" do
31
+ expect do
32
+ expect { subject }.to be_invalid_at field: "name"
33
+ end.not_to raise_error
34
+ end
35
+
36
+ it "fails when no errors present under given tags" do
37
+ expect do
38
+ expect { subject }.to be_invalid_at field: "email"
39
+ end.to raise_error RSpec::Expectations::ExpectationNotMetError
40
+ end
41
+
42
+ it "fails when some translations are absent" do
43
+ I18n.available_locales = %i[ru en]
44
+
45
+ expect do
46
+ expect { subject }.to be_invalid_at field: "name"
47
+ end.to raise_error RSpec::Expectations::ExpectationNotMetError
48
+ end
49
+ end
50
+
51
+ describe "not_to be_invalid_at" do
52
+ it "passes when no errors present under given tags" do
53
+ expect do
54
+ expect { subject }.not_to be_invalid_at field: "email"
55
+ end.not_to raise_error
56
+ end
57
+
58
+ it "fails when some error present under given tags" do
59
+ expect do
60
+ expect { subject }.not_to be_invalid_at field: "name"
61
+ end.to raise_error RSpec::Expectations::ExpectationNotMetError
62
+ end
63
+
64
+ it "fails when some error present w/o tags constraint" do
65
+ expect do
66
+ expect { subject }.not_to be_invalid_at
67
+ end.to raise_error RSpec::Expectations::ExpectationNotMetError
68
+ end
69
+ end
70
+ end