meta_states 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +46 -0
- data/LICENSE.txt +21 -0
- data/README.md +255 -0
- data/lib/generators/meta_states/install/install_generator.rb +43 -0
- data/lib/generators/meta_states/install/templates/create_indexes_on_meta_states_states.rb.erb +11 -0
- data/lib/generators/meta_states/install/templates/create_meta_states_states.rb.erb +18 -0
- data/lib/generators/meta_states/install/templates/initializer.rb.erb +42 -0
- data/lib/meta_states/base.rb +70 -0
- data/lib/meta_states/callback.rb +58 -0
- data/lib/meta_states/configuration/model_configuration.rb +64 -0
- data/lib/meta_states/configuration/state_type_configuration.rb +17 -0
- data/lib/meta_states/configuration.rb +142 -0
- data/lib/meta_states/railtie.rb +8 -0
- data/lib/meta_states/state.rb +5 -0
- data/lib/meta_states/stateable.rb +26 -0
- data/lib/meta_states/version.rb +5 -0
- data/lib/meta_states.rb +25 -0
- data/spec/dummy/Gemfile +40 -0
- data/spec/dummy/Gemfile.lock +308 -0
- data/spec/dummy/README.md +24 -0
- data/spec/dummy/Rakefile +8 -0
- data/spec/dummy/app/controllers/application_controller.rb +4 -0
- data/spec/dummy/app/jobs/application_job.rb +9 -0
- data/spec/dummy/app/models/application_record.rb +5 -0
- data/spec/dummy/app/models/company.rb +3 -0
- data/spec/dummy/app/models/user.rb +3 -0
- data/spec/dummy/bin/brakeman +9 -0
- data/spec/dummy/bin/dev +4 -0
- data/spec/dummy/bin/docker-entrypoint +14 -0
- data/spec/dummy/bin/rails +6 -0
- data/spec/dummy/bin/rake +6 -0
- data/spec/dummy/bin/rubocop +10 -0
- data/spec/dummy/bin/setup +36 -0
- data/spec/dummy/bin/thrust +7 -0
- data/spec/dummy/config/application.rb +27 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/credentials.yml.enc +1 -0
- data/spec/dummy/config/database.yml +37 -0
- data/spec/dummy/config/environment.rb +7 -0
- data/spec/dummy/config/environments/development.rb +54 -0
- data/spec/dummy/config/environments/production.rb +69 -0
- data/spec/dummy/config/environments/test.rb +44 -0
- data/spec/dummy/config/initializers/cors.rb +18 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +10 -0
- data/spec/dummy/config/initializers/inflections.rb +18 -0
- data/spec/dummy/config/initializers/meta_states.rb +45 -0
- data/spec/dummy/config/locales/en.yml +31 -0
- data/spec/dummy/config/master.key +1 -0
- data/spec/dummy/config/puma.rb +43 -0
- data/spec/dummy/config/routes.rb +12 -0
- data/spec/dummy/config.ru +8 -0
- data/spec/dummy/db/migrate/20241221171423_create_test_models.rb +15 -0
- data/spec/dummy/db/migrate/20241223212128_create_has_states_states.rb +18 -0
- data/spec/dummy/db/migrate/20250114175939_create_indexes_on_has_states_states.rb +10 -0
- data/spec/dummy/db/schema.rb +44 -0
- data/spec/dummy/db/seeds.rb +11 -0
- data/spec/dummy/log/development.log +416 -0
- data/spec/dummy/log/test.log +55143 -0
- data/spec/dummy/public/robots.txt +1 -0
- data/spec/dummy/storage/development.sqlite3 +0 -0
- data/spec/dummy/storage/test.sqlite3 +0 -0
- data/spec/dummy/tmp/local_secret.txt +1 -0
- data/spec/factories/meta_states.rb +19 -0
- data/spec/generators/meta_states/install_generator_spec.rb +27 -0
- data/spec/generators/templates/config/initializers/meta_states.rb +42 -0
- data/spec/generators/templates/db/migrate/20250605170637_create_indexes_on_meta_states_states.rb +11 -0
- data/spec/generators/templates/db/migrate/20250605170637_create_meta_states_states.rb +18 -0
- data/spec/meta_states/callback_spec.rb +92 -0
- data/spec/meta_states/configuration_spec.rb +218 -0
- data/spec/meta_states/state_limit_spec.rb +107 -0
- data/spec/meta_states/state_metadata_schema_spec.rb +75 -0
- data/spec/meta_states/state_spec.rb +349 -0
- data/spec/meta_states/stateable_spec.rb +183 -0
- data/spec/meta_states_spec.rb +52 -0
- data/spec/rails_helper.rb +19 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/database_cleaner.rb +17 -0
- data/spec/support/factory_bot.rb +8 -0
- data/spec/support/shoulda_matchers.rb +10 -0
- data/spec/tmp/config/initializers/has_states.rb +12 -0
- data/spec/tmp/db/migrate/20241223004024_create_has_states_states.rb +20 -0
- metadata +141 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
d932584e11af23c6015a503f0dfd79d657604b56d2be374b64c230b372e42872e55a7820b77cee6c02bef13a11870077718ebc0ffdf7efb450efb5cdf952cdc6
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# spec/factories/meta_states.rb
|
|
4
|
+
FactoryBot.define do
|
|
5
|
+
factory :state, class: 'MetaStates::State' do
|
|
6
|
+
association :stateable, factory: :user
|
|
7
|
+
state_type { 'kyc' }
|
|
8
|
+
status { 'pending' }
|
|
9
|
+
metadata { {} }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
factory :user do
|
|
13
|
+
sequence(:name) { |n| "User #{n}" }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
factory :company do
|
|
17
|
+
sequence(:name) { |n| "Company #{n}" }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails_helper'
|
|
4
|
+
require 'generators/meta_states/install/install_generator'
|
|
5
|
+
|
|
6
|
+
RSpec.describe MetaStates::InstallGenerator do
|
|
7
|
+
let(:destination) { File.expand_path('../templates', __dir__) }
|
|
8
|
+
|
|
9
|
+
before do
|
|
10
|
+
FileUtils.rm_rf(destination)
|
|
11
|
+
FileUtils.mkdir_p(destination)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'creates migration and initializer files' do
|
|
15
|
+
generator = described_class.new
|
|
16
|
+
generator.destination_root = destination
|
|
17
|
+
generator.install
|
|
18
|
+
|
|
19
|
+
# Check migration exists
|
|
20
|
+
migration = Dir[File.join(destination, 'db/migrate/*_create_meta_states_states.rb')].first
|
|
21
|
+
expect(migration).to be_present
|
|
22
|
+
|
|
23
|
+
# Check initializer exists
|
|
24
|
+
initializer = File.join(destination, 'config/initializers/meta_states.rb')
|
|
25
|
+
expect(File.exist?(initializer)).to be true
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
MetaStates.configure do |config|
|
|
4
|
+
# Configure your models and their state types below
|
|
5
|
+
#
|
|
6
|
+
# Example configuration:
|
|
7
|
+
#
|
|
8
|
+
# config.configure_model User do |model|
|
|
9
|
+
# # KYC state type with its allowed statuses
|
|
10
|
+
# model.state_type :kyc do |type|
|
|
11
|
+
# type.statuses = [
|
|
12
|
+
# 'pending', # Initial state
|
|
13
|
+
# 'documents_required', # Waiting for user documents
|
|
14
|
+
# 'under_review', # Documents being reviewed
|
|
15
|
+
# 'approved', # KYC process completed successfully
|
|
16
|
+
# 'rejected' # KYC process failed
|
|
17
|
+
# ]
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# # Onboarding state type with different statuses
|
|
21
|
+
# model.state_type :onboarding do |type|
|
|
22
|
+
# type.statuses = [
|
|
23
|
+
# 'pending', # Just started
|
|
24
|
+
# 'email_verified', # Email verification complete
|
|
25
|
+
# 'profile_complete', # User filled all required fields
|
|
26
|
+
# 'completed' # Onboarding finished
|
|
27
|
+
# ]
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# config.configure_model Company do |model|
|
|
32
|
+
# model.state_type :verification do |type|
|
|
33
|
+
# type.statuses = [
|
|
34
|
+
# 'pending',
|
|
35
|
+
# 'documents_submitted',
|
|
36
|
+
# 'under_review',
|
|
37
|
+
# 'verified',
|
|
38
|
+
# 'rejected'
|
|
39
|
+
# ]
|
|
40
|
+
# end
|
|
41
|
+
# end
|
|
42
|
+
end
|
data/spec/generators/templates/db/migrate/20250605170637_create_indexes_on_meta_states_states.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class CreateIndexesOnMetaStatesStates < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
change_table :meta_states_states do |t|
|
|
4
|
+
t.index %i[stateable_id state_type]
|
|
5
|
+
t.index %i[stateable_id state_type status]
|
|
6
|
+
t.index %i[stateable_id state_type created_at]
|
|
7
|
+
t.index %i[stateable_id state_type status created_at]
|
|
8
|
+
t.index %i[stateable_type stateable_id]
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class CreateMetaStatesStates < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :meta_states_states do |t|
|
|
4
|
+
t.string :type, null: false
|
|
5
|
+
t.string :state_type
|
|
6
|
+
t.string :status, null: false
|
|
7
|
+
|
|
8
|
+
t.json :metadata, null: false, default: {}
|
|
9
|
+
|
|
10
|
+
t.references :stateable, polymorphic: true, null: false
|
|
11
|
+
|
|
12
|
+
t.timestamps
|
|
13
|
+
|
|
14
|
+
t.index %i[type stateable_id]
|
|
15
|
+
t.index %i[stateable_type stateable_id]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails_helper'
|
|
4
|
+
|
|
5
|
+
# spec/meta_states/callback_spec.rb
|
|
6
|
+
RSpec.describe MetaStates::Callback do
|
|
7
|
+
let(:state) { double('state', state_type: 'kyc', status: 'completed') }
|
|
8
|
+
let(:block) { ->(_s) { :called } }
|
|
9
|
+
|
|
10
|
+
describe '#matches?' do
|
|
11
|
+
context 'with state type only' do
|
|
12
|
+
let(:callback) { described_class.new('kyc', {}, block) }
|
|
13
|
+
|
|
14
|
+
it 'matches on state type' do
|
|
15
|
+
expect(callback.matches?(state)).to be true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "doesn't match different state type" do
|
|
19
|
+
different_state = double('state', state_type: 'identity')
|
|
20
|
+
expect(callback.matches?(different_state)).to be false
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
context 'with status conditions' do
|
|
25
|
+
let(:callback) { described_class.new('kyc', { to: 'completed' }, block) }
|
|
26
|
+
|
|
27
|
+
it 'matches when conditions are met' do
|
|
28
|
+
expect(callback.matches?(state)).to be true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "doesn't match when conditions aren't met" do
|
|
32
|
+
pending_state = double('state', state_type: 'kyc', status: 'pending')
|
|
33
|
+
expect(callback.matches?(pending_state)).to be false
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
context 'with from condition' do
|
|
38
|
+
let(:callback) { described_class.new('kyc', { from: 'pending' }, block) }
|
|
39
|
+
let(:state_with_history) do
|
|
40
|
+
double('state',
|
|
41
|
+
state_type: 'kyc',
|
|
42
|
+
status: 'completed',
|
|
43
|
+
status_before_last_save: 'pending')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'matches when previous status matches' do
|
|
47
|
+
expect(callback.matches?(state_with_history)).to be true
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe '#call' do
|
|
53
|
+
let(:callback) { described_class.new('kyc', {}, block) }
|
|
54
|
+
|
|
55
|
+
it 'executes the stored block with the state' do
|
|
56
|
+
expect(callback.call(state)).to eq(:called)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe 'equality' do
|
|
61
|
+
let(:block1) { ->(_s) { :called } }
|
|
62
|
+
let(:block2) { ->(_s) { :called } }
|
|
63
|
+
|
|
64
|
+
it 'considers callbacks equal if they have the same state_type, conditions, and block' do
|
|
65
|
+
callback1 = described_class.new('kyc', { to: 'completed' }, block1)
|
|
66
|
+
callback2 = described_class.new('kyc', { to: 'completed' }, block1)
|
|
67
|
+
|
|
68
|
+
expect(callback1).to eq(callback2)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'considers callbacks different if they have different blocks' do
|
|
72
|
+
callback1 = described_class.new('kyc', { to: 'completed' }, block1)
|
|
73
|
+
callback2 = described_class.new('kyc', { to: 'completed' }, block2)
|
|
74
|
+
|
|
75
|
+
expect(callback1).not_to eq(callback2)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'considers callbacks different if they have different conditions' do
|
|
79
|
+
callback1 = described_class.new('kyc', { to: 'completed' }, block1)
|
|
80
|
+
callback2 = described_class.new('kyc', { to: 'pending' }, block1)
|
|
81
|
+
|
|
82
|
+
expect(callback1).not_to eq(callback2)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'considers callbacks different if they have different state types' do
|
|
86
|
+
callback1 = described_class.new('kyc', { to: 'completed' }, block1)
|
|
87
|
+
callback2 = described_class.new('identity', { to: 'completed' }, block1)
|
|
88
|
+
|
|
89
|
+
expect(callback1).not_to eq(callback2)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# spec/meta_states/configuration_spec.rb
|
|
4
|
+
require 'rails_helper'
|
|
5
|
+
|
|
6
|
+
RSpec.describe MetaStates::Configuration do
|
|
7
|
+
let(:configuration) { described_class.instance }
|
|
8
|
+
|
|
9
|
+
before do
|
|
10
|
+
configuration.clear_callbacks!
|
|
11
|
+
configuration.model_configurations.clear
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe 'model configuration' do
|
|
15
|
+
it 'configures models with their state types and statuses' do
|
|
16
|
+
configuration.configure_model User do |model|
|
|
17
|
+
model.state_type :kyc do |type|
|
|
18
|
+
type.statuses = %w[pending completed]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
expect(configuration.valid_status?(User, 'kyc', 'pending')).to be true
|
|
23
|
+
expect(configuration.valid_status?(User, 'kyc', 'invalid')).to be false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'allows different statuses for different state types' do
|
|
27
|
+
configuration.configure_model User do |model|
|
|
28
|
+
model.state_type :kyc do |type|
|
|
29
|
+
type.statuses = %w[pending verified]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
model.state_type :onboarding do |type|
|
|
33
|
+
type.statuses = %w[started completed]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
expect(configuration.valid_status?(User, 'kyc', 'verified')).to be true
|
|
38
|
+
expect(configuration.valid_status?(User, 'onboarding', 'verified')).to be false
|
|
39
|
+
expect(configuration.valid_status?(User, 'onboarding', 'completed')).to be true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'allows different configurations for different models' do
|
|
43
|
+
configuration.configure_model User do |model|
|
|
44
|
+
model.state_type :kyc do |type|
|
|
45
|
+
type.statuses = %w[pending verified]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
configuration.configure_model Company do |model|
|
|
50
|
+
model.state_type :onboarding do |type|
|
|
51
|
+
type.statuses = %w[pending active]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
expect(configuration.valid_state_type?(User, 'kyc')).to be true
|
|
56
|
+
expect(configuration.valid_state_type?(Company, 'kyc')).to be false
|
|
57
|
+
expect(configuration.valid_state_type?(Company, 'onboarding')).to be true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'automatically includes Stateable in configured models' do
|
|
61
|
+
configuration.configure_model User do |model|
|
|
62
|
+
model.state_type :kyc do |type|
|
|
63
|
+
type.statuses = ['pending']
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
expect(User.included_modules).to include(MetaStates::Stateable)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'raises error for non-ActiveRecord models' do
|
|
71
|
+
expect do
|
|
72
|
+
configuration.configure_model String do |model|
|
|
73
|
+
model.state_type :test
|
|
74
|
+
end
|
|
75
|
+
end.to raise_error(ArgumentError, /must be an ActiveRecord model/)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'allows setting a limit on the number of states' do
|
|
79
|
+
configuration.configure_model User do |model|
|
|
80
|
+
model.state_type :kyc do |type|
|
|
81
|
+
type.statuses = %w[pending completed]
|
|
82
|
+
type.limit = 2
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
expect(configuration.limit_for(User, 'kyc')).to eq(2)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'returns nil for limit when no limit is set' do
|
|
90
|
+
configuration.configure_model User do |model|
|
|
91
|
+
model.state_type :kyc do |type|
|
|
92
|
+
type.statuses = %w[pending completed]
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
expect(configuration.limit_for(User, 'kyc')).to be_nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
context 'when a metadata schema is set' do
|
|
100
|
+
let(:schema) do
|
|
101
|
+
{
|
|
102
|
+
type: :object,
|
|
103
|
+
properties: {
|
|
104
|
+
name: { type: :string },
|
|
105
|
+
age: { type: :integer, minimum: 18 }
|
|
106
|
+
},
|
|
107
|
+
required: %i[name age]
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
before do
|
|
112
|
+
configuration.configure_model User do |model|
|
|
113
|
+
model.state_type :kyc do |type|
|
|
114
|
+
type.statuses = %w[pending completed]
|
|
115
|
+
type.metadata_schema = schema
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'allows setting a metadata schema for validation' do
|
|
121
|
+
expect(configuration.metadata_schema_for(User, 'kyc')).to eq(schema)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'returns nil for metadata_schema when no schema is set' do
|
|
126
|
+
configuration.configure_model User do |model|
|
|
127
|
+
model.state_type :kyc do |type|
|
|
128
|
+
type.statuses = %w[pending completed]
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
expect(configuration.metadata_schema_for(User, 'kyc')).to be_nil
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
describe 'callbacks' do
|
|
137
|
+
let(:state_completed) { create(:state, state_type: 'kyc', status: 'completed') }
|
|
138
|
+
|
|
139
|
+
before do
|
|
140
|
+
configuration.configure_model User do |model|
|
|
141
|
+
model.state_type :kyc do |type|
|
|
142
|
+
type.statuses = %w[pending completed failed]
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
configuration.on(:kyc, id: :failed_callback, to: 'failed') { |_s| :kyc_failed }
|
|
147
|
+
configuration.on(:kyc, id: :pending_callback, to: 'pending') { |_s| :kyc_pending }
|
|
148
|
+
configuration.on(:kyc, id: :complete_callback, to: 'completed') { |_s| :kyc_completed }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
describe '#on' do
|
|
152
|
+
it 'registers callbacks' do
|
|
153
|
+
expect(configuration.callbacks.size).to eq(3)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it 'matches callbacks for the right state' do
|
|
157
|
+
matching = configuration.matching_callbacks(state_completed)
|
|
158
|
+
|
|
159
|
+
expect(matching.length).to eq(1)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it 'registers callbacks with auto-generated ids' do
|
|
163
|
+
configuration.on(:kyc) { |_s| :some_action }
|
|
164
|
+
expect(configuration.callbacks.keys.last).to match(/\Acallback_\d+\z/)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it 'registers callbacks with custom ids' do
|
|
168
|
+
configuration.on(:kyc, id: :my_custom_callback) { |_s| :some_action }
|
|
169
|
+
expect(configuration.callbacks.keys).to include(:my_custom_callback)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it 'converts string ids to symbols' do
|
|
173
|
+
configuration.on(:kyc, id: 'my_string_id') { |_s| :some_action }
|
|
174
|
+
expect(configuration.callbacks.keys).to include(:my_string_id)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
describe '#off' do
|
|
179
|
+
it 'removes a callback by id' do
|
|
180
|
+
configuration.on(:kyc, id: :removable) { |_s| :some_action }
|
|
181
|
+
expect { configuration.off(:removable) }
|
|
182
|
+
.to change { configuration.callbacks.size }.by(-1)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it 'removes a callback by callback object' do
|
|
186
|
+
callback = configuration.on(:kyc) { |_s| :some_action }
|
|
187
|
+
expect { configuration.off(callback) }
|
|
188
|
+
.to change { configuration.callbacks.size }.by(-1)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it 'properly removes callback when given a callback object' do
|
|
192
|
+
callback = configuration.on(:kyc, id: :test) { |_s| :some_action }
|
|
193
|
+
expect(configuration.callbacks.values).to include(callback)
|
|
194
|
+
|
|
195
|
+
configuration.off(callback)
|
|
196
|
+
expect(configuration.callbacks.values).not_to include(callback)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
describe '#clear_callbacks!' do
|
|
201
|
+
it 'removes all callbacks' do
|
|
202
|
+
expect { configuration.clear_callbacks! }.to change { configuration.callbacks.size }.to(0)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
describe '#matching_callbacks' do
|
|
207
|
+
it 'finds callbacks matching state type and conditions regardless of id' do
|
|
208
|
+
configuration.clear_callbacks!
|
|
209
|
+
configuration.on(:kyc, id: :complete_callback, to: 'completed') { |_s| :kyc_completed }
|
|
210
|
+
|
|
211
|
+
matching = configuration.matching_callbacks(state_completed)
|
|
212
|
+
|
|
213
|
+
expect(matching.size).to eq(1)
|
|
214
|
+
expect(matching.first.call(state_completed)).to eq(:kyc_completed)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe 'State Limit Feature', type: :model do
|
|
6
|
+
before do
|
|
7
|
+
MetaStates.configuration.clear_callbacks!
|
|
8
|
+
MetaStates.configuration.model_configurations.clear
|
|
9
|
+
|
|
10
|
+
MetaStates.configure do |config|
|
|
11
|
+
config.configure_model User do |model|
|
|
12
|
+
model.state_type :double_limit_state do |type|
|
|
13
|
+
type.statuses = %w[pending completed]
|
|
14
|
+
type.limit = 2
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
model.state_type :no_limit_state do |type|
|
|
18
|
+
type.statuses = %w[pending completed]
|
|
19
|
+
# No limit set for this type
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
model.state_type :single_limit_state do |type|
|
|
23
|
+
type.statuses = %w[pending completed]
|
|
24
|
+
type.limit = 1
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe 'state limit validation' do
|
|
31
|
+
let(:user) { create(:user) }
|
|
32
|
+
|
|
33
|
+
context 'with no limit set' do
|
|
34
|
+
it 'allows creating multiple states of the same type' do
|
|
35
|
+
# Create multiple onboarding states (no limit)
|
|
36
|
+
expect do
|
|
37
|
+
3.times { user.add_state('no_limit_state', status: 'pending') }
|
|
38
|
+
end.not_to raise_error
|
|
39
|
+
|
|
40
|
+
expect(user.states.where(state_type: 'no_limit_state').count).to eq(3)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
context 'with a limit set' do
|
|
45
|
+
it 'allows creating states up to the limit' do
|
|
46
|
+
# Create double_limit_state states up to the limit (2)
|
|
47
|
+
expect do
|
|
48
|
+
2.times { user.add_state('double_limit_state', status: 'pending') }
|
|
49
|
+
end.not_to raise_error
|
|
50
|
+
|
|
51
|
+
expect(user.states.where(state_type: 'double_limit_state').count).to eq(2)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'prevents creating states beyond the limit' do
|
|
55
|
+
# Create double_limit_state states up to the limit
|
|
56
|
+
2.times { user.add_state('double_limit_state', status: 'pending') }
|
|
57
|
+
|
|
58
|
+
# Try to create one more (exceeding the limit)
|
|
59
|
+
expect do
|
|
60
|
+
user.add_state('double_limit_state', status: 'pending')
|
|
61
|
+
end.to raise_error(ActiveRecord::RecordInvalid, /maximum number of double_limit_state states/)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'enforces a limit of 1 correctly' do
|
|
65
|
+
# Create one state
|
|
66
|
+
user.add_state('single_limit_state', status: 'pending')
|
|
67
|
+
|
|
68
|
+
# Try to create another one (exceeding the limit of 1)
|
|
69
|
+
expect do
|
|
70
|
+
user.add_state('single_limit_state', status: 'completed')
|
|
71
|
+
end.to raise_error(ActiveRecord::RecordInvalid, /maximum number of single_limit_state states/)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
context 'with different state types' do
|
|
76
|
+
it 'applies limits independently to each state type' do
|
|
77
|
+
# Create states up to the limit for double_limit_state
|
|
78
|
+
2.times { user.add_state('double_limit_state', status: 'pending') }
|
|
79
|
+
|
|
80
|
+
# Create states for no_limit_state (no limit)
|
|
81
|
+
3.times { user.add_state('no_limit_state', status: 'pending') }
|
|
82
|
+
|
|
83
|
+
# Create one state for single_limit_state (limit 1)
|
|
84
|
+
user.add_state('single_limit_state', status: 'pending')
|
|
85
|
+
|
|
86
|
+
# Verify counts
|
|
87
|
+
expect(user.states.where(state_type: 'double_limit_state').count).to eq(2)
|
|
88
|
+
expect(user.states.where(state_type: 'no_limit_state').count).to eq(3)
|
|
89
|
+
expect(user.states.where(state_type: 'single_limit_state').count).to eq(1)
|
|
90
|
+
|
|
91
|
+
# Try to exceed limits
|
|
92
|
+
expect do
|
|
93
|
+
user.add_state('double_limit_state', status: 'completed')
|
|
94
|
+
end.to raise_error(ActiveRecord::RecordInvalid, /maximum number of double_limit_state states/)
|
|
95
|
+
|
|
96
|
+
expect do
|
|
97
|
+
user.add_state('single_limit_state', status: 'completed')
|
|
98
|
+
end.to raise_error(ActiveRecord::RecordInvalid, /maximum number of single_limit_state states/)
|
|
99
|
+
|
|
100
|
+
# But can still add more no_limit_state states
|
|
101
|
+
expect do
|
|
102
|
+
user.add_state('no_limit_state', status: 'completed')
|
|
103
|
+
end.not_to raise_error
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe 'Metadata Schema Validation', type: :model do
|
|
6
|
+
let(:user) { create(:user) }
|
|
7
|
+
let(:schema) do
|
|
8
|
+
{
|
|
9
|
+
type: :object,
|
|
10
|
+
properties: {
|
|
11
|
+
name: { type: :string },
|
|
12
|
+
age: {
|
|
13
|
+
type: :integer,
|
|
14
|
+
minimum: 18
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
required: %i[name age]
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
before do
|
|
22
|
+
MetaStates.configuration.clear_callbacks!
|
|
23
|
+
MetaStates.configuration.model_configurations.clear
|
|
24
|
+
|
|
25
|
+
MetaStates.configure do |config|
|
|
26
|
+
config.configure_model User do |model|
|
|
27
|
+
model.state_type :schema_enabled_state do |type|
|
|
28
|
+
type.statuses = %w[pending completed rejected]
|
|
29
|
+
type.metadata_schema = schema
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
model.state_type :no_schema_state do |type|
|
|
33
|
+
type.statuses = %w[active inactive]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe 'validation' do
|
|
40
|
+
context 'with a valid schema' do
|
|
41
|
+
it 'validates valid metadata' do
|
|
42
|
+
# Invalid metadata (too young < 18)
|
|
43
|
+
expect do
|
|
44
|
+
user.add_state('schema_enabled_state', status: 'pending', metadata: { name: 'John Doe', age: 17 })
|
|
45
|
+
end.to raise_error(ActiveRecord::RecordInvalid, /did not have a minimum value of 18/)
|
|
46
|
+
|
|
47
|
+
# Invalid metadata (missing required fields)
|
|
48
|
+
expect do
|
|
49
|
+
user.add_state('schema_enabled_state', status: 'pending', metadata: { age: 25 })
|
|
50
|
+
end.to raise_error(ActiveRecord::RecordInvalid, /did not contain a required property of 'name'/)
|
|
51
|
+
|
|
52
|
+
# Valid metadata
|
|
53
|
+
state = user.add_state('schema_enabled_state', status: 'pending', metadata: { name: 'John Doe', age: 25 })
|
|
54
|
+
|
|
55
|
+
expect(state).to be_valid
|
|
56
|
+
expect(state.persisted?).to be true
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
context 'with no schema defined' do
|
|
61
|
+
it 'accepts any metadata for state types without a schema' do
|
|
62
|
+
state = user.add_state('no_schema_state', status: 'active', metadata: {
|
|
63
|
+
anything: 'goes',
|
|
64
|
+
nested: {
|
|
65
|
+
data: 'is fine too'
|
|
66
|
+
},
|
|
67
|
+
numbers: [1, 2, 3]
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
expect(state).to be_valid
|
|
71
|
+
expect(state.persisted?).to be true
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|