develoz-rails 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/.github/workflows/ci.yml +31 -0
- data/.github/workflows/release.yml +459 -0
- data/.rubocop.yml +52 -0
- data/.sisyphus/notepads/develoz-rails-template/learnings.md +855 -0
- data/.tool-versions +2 -0
- data/CHANGELOG.md +28 -0
- data/CONTRIBUTING.md +120 -0
- data/README.md +76 -0
- data/Rakefile +18 -0
- data/config/generators.yml +105 -0
- data/docs/guide/installation.md +574 -0
- data/exe/develoz +6 -0
- data/install +98 -0
- data/lib/develoz/canonical_fetcher.rb +56 -0
- data/lib/develoz/cli.rb +84 -0
- data/lib/develoz/generators/base.rb +134 -0
- data/lib/develoz/manifest.rb +44 -0
- data/lib/develoz/options.rb +55 -0
- data/lib/develoz/version.rb +5 -0
- data/lib/develoz/version_resolver.rb +103 -0
- data/lib/develoz.rb +15 -0
- data/lib/generators/develoz/active_resource/active_resource_generator.rb +25 -0
- data/lib/generators/develoz/active_resource/templates/application_resource.rb.tt +7 -0
- data/lib/generators/develoz/active_resource/templates/example_resource.rb.tt +5 -0
- data/lib/generators/develoz/admin/admin_generator.rb +44 -0
- data/lib/generators/develoz/admin/templates/admin_base_controller.rb.tt +7 -0
- data/lib/generators/develoz/admin/templates/admin_layout.html.erb.tt +38 -0
- data/lib/generators/develoz/admin/templates/dashboard_controller.rb.tt +7 -0
- data/lib/generators/develoz/admin/templates/dashboard_index.html.erb.tt +2 -0
- data/lib/generators/develoz/admin/templates/routes.rb.tt +3 -0
- data/lib/generators/develoz/agents_docs/agents_docs_generator.rb +50 -0
- data/lib/generators/develoz/agents_docs/templates/.github/pull_request_template.md.tt +13 -0
- data/lib/generators/develoz/agents_docs/templates/AGENTS.md.tt +36 -0
- data/lib/generators/develoz/agents_docs/templates/docs/development.md.tt +121 -0
- data/lib/generators/develoz/agents_docs/templates/docs/performance.md.tt +46 -0
- data/lib/generators/develoz/agents_docs/templates/docs/testing.md.tt +92 -0
- data/lib/generators/develoz/agents_docs/templates/spec/cassettes/Example_API/fetches_data_from_an_external_API.yml.tt +23 -0
- data/lib/generators/develoz/agents_docs/templates/spec/requests/example_api_spec.rb.tt +14 -0
- data/lib/generators/develoz/agents_docs/templates/spec/support/faraday.rb.tt +19 -0
- data/lib/generators/develoz/agents_docs/templates/spec/support/vcr.rb.tt +11 -0
- data/lib/generators/develoz/api/api_generator.rb +55 -0
- data/lib/generators/develoz/api/templates/app/blueprints/example_blueprint.rb.tt +6 -0
- data/lib/generators/develoz/api/templates/app/controllers/api/v1/base_controller.rb.tt +96 -0
- data/lib/generators/develoz/api/templates/config/initializers/blueprinter.rb.tt +16 -0
- data/lib/generators/develoz/api/templates/config/initializers/rswag_api.rb.tt +15 -0
- data/lib/generators/develoz/api/templates/config/initializers/rswag_ui.rb.tt +17 -0
- data/lib/generators/develoz/api/templates/spec/requests/api/v1/examples_spec.rb.tt +25 -0
- data/lib/generators/develoz/auth/auth_generator.rb +82 -0
- data/lib/generators/develoz/auth/templates/app/controllers/concerns/authentication.rb.tt +53 -0
- data/lib/generators/develoz/auth/templates/app/controllers/passwords_controller.rb.tt +35 -0
- data/lib/generators/develoz/auth/templates/app/controllers/sessions_controller.rb.tt +22 -0
- data/lib/generators/develoz/auth/templates/app/mailers/passwords_mailer.rb.tt +8 -0
- data/lib/generators/develoz/auth/templates/app/models/current.rb.tt +5 -0
- data/lib/generators/develoz/auth/templates/app/models/user.rb.tt +7 -0
- data/lib/generators/develoz/auth/templates/app/views/passwords/edit.html.erb.tt +9 -0
- data/lib/generators/develoz/auth/templates/app/views/passwords_mailer/reset.html.erb.tt +6 -0
- data/lib/generators/develoz/auth/templates/app/views/sessions/new.html.erb.tt +11 -0
- data/lib/generators/develoz/auth/templates/db/migrate/create_users.rb.tt +13 -0
- data/lib/generators/develoz/auth/templates/spec/requests/passwords_spec.rb.tt +60 -0
- data/lib/generators/develoz/auth/templates/spec/requests/sessions_spec.rb.tt +37 -0
- data/lib/generators/develoz/ci/ci_generator.rb +66 -0
- data/lib/generators/develoz/ci/templates/.github/workflows/ci.yml.tt +18 -0
- data/lib/generators/develoz/ci/templates/.haml-lint.yml.tt +12 -0
- data/lib/generators/develoz/ci/templates/.markdownlint.json.tt +10 -0
- data/lib/generators/develoz/ci/templates/.reek.yml.tt +28 -0
- data/lib/generators/develoz/ci/templates/.rubocop.yml.tt +22 -0
- data/lib/generators/develoz/ci/templates/.stylelintrc.json.tt +4 -0
- data/lib/generators/develoz/ci/templates/.yamllint.tt +14 -0
- data/lib/generators/develoz/ci/templates/Rakefile.tt +73 -0
- data/lib/generators/develoz/ci/templates/bin/ci.tt +6 -0
- data/lib/generators/develoz/ci/templates/biome.json.tt +41 -0
- data/lib/generators/develoz/ci/templates/config/ci.rb.tt +23 -0
- data/lib/generators/develoz/concerns/concerns_generator.rb +48 -0
- data/lib/generators/develoz/concerns/templates/app/models/concerns/configurable.rb.tt +120 -0
- data/lib/generators/develoz/concerns/templates/app/models/concerns/optimized_finders.rb.tt +93 -0
- data/lib/generators/develoz/concerns/templates/app/models/concerns/searchable_concern.rb.tt +31 -0
- data/lib/generators/develoz/concerns/templates/app/models/concerns/transitionable.rb.tt +144 -0
- data/lib/generators/develoz/concerns/templates/app/models/configuration.rb.tt +104 -0
- data/lib/generators/develoz/concerns/templates/db/migrate/add_status_transitions.rb.tt +30 -0
- data/lib/generators/develoz/concerns/templates/db/migrate/create_configurations.rb.tt +25 -0
- data/lib/generators/develoz/concerns/templates/spec/models/concerns/configurable_spec.rb.tt +172 -0
- data/lib/generators/develoz/concerns/templates/spec/models/concerns/optimized_finders_spec.rb.tt +200 -0
- data/lib/generators/develoz/concerns/templates/spec/models/concerns/searchable_concern_spec.rb.tt +84 -0
- data/lib/generators/develoz/concerns/templates/spec/models/concerns/transitionable_spec.rb.tt +234 -0
- data/lib/generators/develoz/concerns/templates/spec/models/configuration_spec.rb.tt +223 -0
- data/lib/generators/develoz/database/database_generator.rb +44 -0
- data/lib/generators/develoz/database/templates/config/database.yml.tt +53 -0
- data/lib/generators/develoz/database/templates/config/initializers/pg_search.rb.tt +5 -0
- data/lib/generators/develoz/database/templates/db/migrate/create_pg_extensions.rb.tt +13 -0
- data/lib/generators/develoz/db_backup/db_backup_generator.rb +47 -0
- data/lib/generators/develoz/db_backup/templates/backup_rake.tt +48 -0
- data/lib/generators/develoz/db_backup/templates/bin_db_backup.tt +25 -0
- data/lib/generators/develoz/db_backup/templates/compose_service.tt +14 -0
- data/lib/generators/develoz/doc_specs/doc_specs_generator.rb +33 -0
- data/lib/generators/develoz/doc_specs/templates/bin/generate-docs.tt +392 -0
- data/lib/generators/develoz/doc_specs/templates/lib/tasks/docs_check.rake.tt +8 -0
- data/lib/generators/develoz/doc_specs/templates/spec/support/doc_screenshot_helper.rb.tt +17 -0
- data/lib/generators/develoz/doc_specs/templates/spec/system/example_doc_spec.rb.tt +38 -0
- data/lib/generators/develoz/docker/docker_generator.rb +54 -0
- data/lib/generators/develoz/docker/templates/bin_dev.tt +5 -0
- data/lib/generators/develoz/docker/templates/bin_docker_entrypoint.tt +8 -0
- data/lib/generators/develoz/docker/templates/bin_run.tt +152 -0
- data/lib/generators/develoz/docker/templates/bin_setup.tt +15 -0
- data/lib/generators/develoz/docker/templates/docker-compose.yml.tt +59 -0
- data/lib/generators/develoz/docker/templates/dockerfile_dev.tt +24 -0
- data/lib/generators/develoz/docs_render/docs_render_generator.rb +46 -0
- data/lib/generators/develoz/docs_render/templates/app/assets/stylesheets/documentation.scss.tt +145 -0
- data/lib/generators/develoz/docs_render/templates/app/controllers/docs_controller.rb.tt +7 -0
- data/lib/generators/develoz/docs_render/templates/app/javascript/docs.js.tt +15 -0
- data/lib/generators/develoz/docs_render/templates/app/models/document.rb.tt +189 -0
- data/lib/generators/develoz/docs_render/templates/app/views/docs/show.html.erb.tt +7 -0
- data/lib/generators/develoz/docs_render/templates/config/initializers/redcarpet_rouge.rb.tt +16 -0
- data/lib/generators/develoz/frontend_core/frontend_core_generator.rb +40 -0
- data/lib/generators/develoz/frontend_core/templates/.annotaterb.yml.tt +16 -0
- data/lib/generators/develoz/frontend_core/templates/config/importmap.rb.tt +12 -0
- data/lib/generators/develoz/frontend_core/templates/config/initializers/pagy.rb.tt +10 -0
- data/lib/generators/develoz/install/install_generator.rb +51 -0
- data/lib/generators/develoz/kamal/kamal_generator.rb +59 -0
- data/lib/generators/develoz/kamal/templates/accessories_postgres.yml.tt +14 -0
- data/lib/generators/develoz/kamal/templates/deploy.yml.tt +43 -0
- data/lib/generators/develoz/kamal/templates/dockerfile_prod.tt +54 -0
- data/lib/generators/develoz/kamal/templates/kamal_secrets.tt +22 -0
- data/lib/generators/develoz/maintenance/maintenance_generator.rb +29 -0
- data/lib/generators/develoz/maintenance/templates/app/tasks/maintenance/example_task.rb.tt +20 -0
- data/lib/generators/develoz/maintenance/templates/lib/tasks/maintenance_counters.rake.tt +27 -0
- data/lib/generators/develoz/push/push_generator.rb +61 -0
- data/lib/generators/develoz/push/templates/app/javascript/pwa/subscription.js.tt +57 -0
- data/lib/generators/develoz/push/templates/app/models/push_subscription.rb.tt +14 -0
- data/lib/generators/develoz/push/templates/app/services/push_notification_service.rb.tt +36 -0
- data/lib/generators/develoz/push/templates/app/views/pwa/sw_push_handlers.js.tt +49 -0
- data/lib/generators/develoz/push/templates/constants_env.tt +5 -0
- data/lib/generators/develoz/push/templates/db/migrate/create_push_subscriptions.rb.tt +15 -0
- data/lib/generators/develoz/pwa/pwa_generator.rb +46 -0
- data/lib/generators/develoz/pwa/templates/app/controllers/pwa_controller.rb.tt +17 -0
- data/lib/generators/develoz/pwa/templates/app/javascript/pwa/registration.js.tt +25 -0
- data/lib/generators/develoz/pwa/templates/app/views/pwa/manifest.json.erb.tt +24 -0
- data/lib/generators/develoz/pwa/templates/app/views/pwa/offline.html.erb.tt +51 -0
- data/lib/generators/develoz/pwa/templates/app/views/pwa/service-worker.js.tt +109 -0
- data/lib/generators/develoz/pwa/templates/routes.rb.tt +3 -0
- data/lib/generators/develoz/solid/solid_generator.rb +52 -0
- data/lib/generators/develoz/solid/templates/app/jobs/application_job.rb.tt +7 -0
- data/lib/generators/develoz/solid/templates/config/cable.yml.tt +10 -0
- data/lib/generators/develoz/solid/templates/config/cache.yml.tt +16 -0
- data/lib/generators/develoz/solid/templates/config/initializers/mission_control.rb.tt +8 -0
- data/lib/generators/develoz/solid/templates/config/initializers/solid.rb.tt +7 -0
- data/lib/generators/develoz/solid/templates/config/queue.yml.tt +18 -0
- data/lib/generators/develoz/solid/templates/config/recurring.yml.tt +20 -0
- data/lib/generators/develoz/strict_loading/strict_loading_generator.rb +17 -0
- data/lib/generators/develoz/strict_loading/templates/config/initializers/strict_loading.rb.tt +13 -0
- data/lib/generators/develoz/testing/templates/rspec.tt +2 -0
- data/lib/generators/develoz/testing/templates/rspec_parallel.tt +2 -0
- data/lib/generators/develoz/testing/templates/spec/rails_helper.rb.tt +32 -0
- data/lib/generators/develoz/testing/templates/spec/spec_helper.rb.tt +31 -0
- data/lib/generators/develoz/testing/testing_generator.rb +39 -0
- data/lib/generators/develoz/tooling/templates/constants.rb.tt +17 -0
- data/lib/generators/develoz/tooling/templates/env.example.tt +4 -0
- data/lib/generators/develoz/tooling/templates/vscode/extensions.json.tt +8 -0
- data/lib/generators/develoz/tooling/templates/vscode/settings.json.tt +26 -0
- data/lib/generators/develoz/tooling/templates/vscode/tasks.json.tt +26 -0
- data/lib/generators/develoz/tooling/tooling_generator.rb +33 -0
- data/lib/generators/develoz/ui/templates/bin/setup_develoz_ui.tt +20 -0
- data/lib/generators/develoz/ui/ui_generator.rb +57 -0
- data/lib/generators/develoz/versioning/templates/app/helpers/application_helper.rb.tt +7 -0
- data/lib/generators/develoz/versioning/templates/app/views/shared/_app_version.html.erb.tt +3 -0
- data/lib/generators/develoz/versioning/templates/spec/helpers/application_helper_spec.rb.tt +11 -0
- data/lib/generators/develoz/versioning/versioning_generator.rb +48 -0
- data/lib/tasks/canonical.rake +27 -0
- data/templates/.keep +0 -0
- data/templates/CANONICAL_SOURCES.md +93 -0
- data/templates/pull_request_template.md.tt +13 -0
- metadata +383 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails_helper"
|
|
4
|
+
|
|
5
|
+
describe Configurable do
|
|
6
|
+
# This spec tests the Configurable concern.
|
|
7
|
+
# It creates a test model with a polymorphic configuration association.
|
|
8
|
+
before(:all) do
|
|
9
|
+
ActiveRecord::Schema.define do
|
|
10
|
+
create_table :configurable_records, force: true do |t|
|
|
11
|
+
t.string :name
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
after(:all) do
|
|
17
|
+
ActiveRecord::Schema.define { drop_table :configurable_records, if_exists: true }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
let(:test_class) do
|
|
21
|
+
stub_const("ConfigurableRecord", Class.new(ApplicationRecord) do
|
|
22
|
+
self.table_name = "configurable_records"
|
|
23
|
+
include Configurable
|
|
24
|
+
configurable :highest_format_only, :boolean
|
|
25
|
+
configurable :report_groups, :array
|
|
26
|
+
configurable :feature_flags, :hash, global_fallback: true
|
|
27
|
+
configurable :page_size, default: 25
|
|
28
|
+
end)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
let(:record) { test_class.create!(name: "test") }
|
|
32
|
+
|
|
33
|
+
before(:each) do
|
|
34
|
+
Configuration.destroy_all
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe ".configurable with a boolean attribute" do
|
|
38
|
+
it "casts an assigned value and exposes it through the predicate" do
|
|
39
|
+
record.highest_format_only = "true"
|
|
40
|
+
|
|
41
|
+
expect(record.highest_format_only?).to be(true)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "reads the predicate from its own configuration key" do
|
|
45
|
+
record.update_configuration(:highest_format_only, true)
|
|
46
|
+
|
|
47
|
+
expect(record.highest_format_only?).to be(true)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "persists the assigned value to its own configuration key on save" do
|
|
51
|
+
record.update!(highest_format_only: "true")
|
|
52
|
+
|
|
53
|
+
expect(record.configuration_value(:highest_format_only)).to be(true)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "leaves the configuration untouched when nothing was assigned" do
|
|
57
|
+
record.update_configuration(:highest_format_only, true)
|
|
58
|
+
|
|
59
|
+
expect { record.update!(name: "changed") }.not_to(change do
|
|
60
|
+
record.configuration_value(:highest_format_only)
|
|
61
|
+
end)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe ".configurable with a value attribute" do
|
|
66
|
+
it "reads the scoped value for the key" do
|
|
67
|
+
record.update_configuration(:report_groups, %w[APAC EMEA])
|
|
68
|
+
|
|
69
|
+
expect(record.report_groups).to eq(%w[APAC EMEA])
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "returns the default when no configuration exists" do
|
|
73
|
+
expect(record.report_groups).to eq([])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "persists an assigned value to its own key on save" do
|
|
77
|
+
record.update!(report_groups: %w[APAC])
|
|
78
|
+
|
|
79
|
+
expect(record.configuration_value(:report_groups)).to eq(%w[APAC])
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "deletes the scoped record when assigned nil" do
|
|
83
|
+
record.update_configuration(:report_groups, %w[APAC])
|
|
84
|
+
|
|
85
|
+
expect { record.update!(report_groups: nil) }
|
|
86
|
+
.to change { record.configuration_for(:report_groups) }.to(nil)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it "returns the default while a nil assignment is pending" do
|
|
90
|
+
record.report_groups = nil
|
|
91
|
+
|
|
92
|
+
expect(record.report_groups).to eq([])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "uses an explicit default" do
|
|
96
|
+
expect(record.page_size).to eq(25)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe ".configurable with global_fallback" do
|
|
101
|
+
it "merges the global value under the scoped one" do
|
|
102
|
+
Configuration.create!(key: "feature_flags", value: { "a" => true, "b" => true })
|
|
103
|
+
record.update_configuration(:feature_flags, { "b" => false })
|
|
104
|
+
|
|
105
|
+
expect(record.feature_flags).to eq("a" => true, "b" => false)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "falls back to the global value when no scoped override exists" do
|
|
109
|
+
Configuration.create!(key: "feature_flags", value: { "a" => true })
|
|
110
|
+
|
|
111
|
+
expect(record.feature_flags).to eq("a" => true)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
describe "#configuration_for" do
|
|
116
|
+
it "returns the active scoped configuration for the key" do
|
|
117
|
+
config = record.update_configuration("demo", { "enabled" => true })
|
|
118
|
+
|
|
119
|
+
expect(record.configuration_for("demo")).to eq(config)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "ignores inactive configurations" do
|
|
123
|
+
Configuration.create!(key: "demo", configurable_type: "ConfigurableRecord",
|
|
124
|
+
configurable_id: record.id, value: { "enabled" => true }, active: false)
|
|
125
|
+
|
|
126
|
+
expect(record.configuration_for("demo")).to be_nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "ignores configurations scoped to a different record" do
|
|
130
|
+
other = test_class.create!(name: "other")
|
|
131
|
+
other.update_configuration("demo", { "enabled" => true })
|
|
132
|
+
|
|
133
|
+
expect(record.configuration_for("demo")).to be_nil
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
describe "#configuration_value" do
|
|
138
|
+
it "returns the stored value" do
|
|
139
|
+
record.update_configuration("demo", { "count" => 3 })
|
|
140
|
+
|
|
141
|
+
expect(record.configuration_value("demo")).to eq("count" => 3)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it "returns the default when no configuration exists" do
|
|
145
|
+
expect(record.configuration_value("demo", default: "fallback")).to eq("fallback")
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
describe "#update_configuration" do
|
|
150
|
+
it "creates a configuration when none exists" do
|
|
151
|
+
expect { record.update_configuration("demo", { "enabled" => true }) }
|
|
152
|
+
.to change(Configuration, :count).by(1)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "updates the existing configuration in place" do
|
|
156
|
+
record.update_configuration("demo", { "enabled" => false })
|
|
157
|
+
|
|
158
|
+
expect { record.update_configuration("demo", { "enabled" => true }) }
|
|
159
|
+
.not_to change(Configuration, :count)
|
|
160
|
+
expect(record.configuration_value("demo")).to eq("enabled" => true)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
describe "#delete_configuration" do
|
|
165
|
+
it "removes the scoped configuration" do
|
|
166
|
+
record.update_configuration("demo", { "enabled" => true })
|
|
167
|
+
|
|
168
|
+
expect { record.delete_configuration("demo") }.to change(Configuration, :count).by(-1)
|
|
169
|
+
expect(record.configuration_for("demo")).to be_nil
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
data/lib/generators/develoz/concerns/templates/spec/models/concerns/optimized_finders_spec.rb.tt
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails_helper"
|
|
4
|
+
|
|
5
|
+
describe OptimizedFinders do
|
|
6
|
+
# This spec tests the OptimizedFinders module.
|
|
7
|
+
# It creates a parent/child relationship to exercise both class-level and
|
|
8
|
+
# association-level finders.
|
|
9
|
+
before(:all) do
|
|
10
|
+
ActiveRecord::Schema.define do
|
|
11
|
+
create_table :parents, force: true do |t|
|
|
12
|
+
t.string :name
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
create_table :children, force: true do |t|
|
|
16
|
+
t.references :parent, foreign_key: true
|
|
17
|
+
t.string :license
|
|
18
|
+
t.string :variant
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
after(:all) do
|
|
24
|
+
ActiveRecord::Schema.define do
|
|
25
|
+
drop_table :children, if_exists: true
|
|
26
|
+
drop_table :parents, if_exists: true
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
let(:parent_class) do
|
|
31
|
+
stub_const("Parent", Class.new(ApplicationRecord) do
|
|
32
|
+
self.table_name = "parents"
|
|
33
|
+
has_many :children, class_name: "Child", foreign_key: :parent_id
|
|
34
|
+
end)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
let(:child_class) do
|
|
38
|
+
stub_const("Child", Class.new(ApplicationRecord) do
|
|
39
|
+
self.table_name = "children"
|
|
40
|
+
belongs_to :parent, class_name: "Parent", optional: true
|
|
41
|
+
|
|
42
|
+
def format
|
|
43
|
+
variant&.upcase
|
|
44
|
+
end
|
|
45
|
+
end)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
let(:parent) { parent_class.create!(name: "parent") }
|
|
49
|
+
|
|
50
|
+
before do
|
|
51
|
+
parent_class
|
|
52
|
+
child_class
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe ".find_for" do
|
|
56
|
+
it "finds a record by single attribute" do
|
|
57
|
+
record = child_class.create!(license: "buy", variant: "hd")
|
|
58
|
+
|
|
59
|
+
expect(child_class.find_for(license: "buy")).to eq(record)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "finds a record by multiple attributes" do
|
|
63
|
+
record = child_class.create!(license: "buy", variant: "hd")
|
|
64
|
+
child_class.create!(license: "rent", variant: "hd")
|
|
65
|
+
|
|
66
|
+
expect(child_class.find_for(license: "buy", variant: "hd")).to eq(record)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "returns nil when no match is found" do
|
|
70
|
+
child_class.create!(license: "buy", variant: "hd")
|
|
71
|
+
|
|
72
|
+
expect(child_class.find_for(license: "rent", variant: "sd")).to be_nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe ".for" do
|
|
77
|
+
it "finds records by single attribute" do
|
|
78
|
+
r1 = child_class.create!(license: "buy", variant: "hd")
|
|
79
|
+
r2 = child_class.create!(license: "buy", variant: "sd")
|
|
80
|
+
child_class.create!(license: "rent", variant: "hd")
|
|
81
|
+
|
|
82
|
+
results = child_class.for(license: "buy")
|
|
83
|
+
|
|
84
|
+
expect(results).to contain_exactly(r1, r2)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "returns empty array when no match is found" do
|
|
88
|
+
child_class.create!(license: "buy", variant: "hd")
|
|
89
|
+
|
|
90
|
+
expect(child_class.for(license: "rent", variant: "sd")).to be_empty
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
describe ".not_for" do
|
|
95
|
+
it "finds records NOT matching the given attributes" do
|
|
96
|
+
child_class.create!(license: "buy", variant: "hd")
|
|
97
|
+
rent = child_class.create!(license: "rent", variant: "hd")
|
|
98
|
+
|
|
99
|
+
results = child_class.not_for(license: "buy")
|
|
100
|
+
|
|
101
|
+
expect(results).to contain_exactly(rent)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
describe ".gather" do
|
|
106
|
+
it "plucks values across all records" do
|
|
107
|
+
child_class.create!(license: "buy", variant: "hd")
|
|
108
|
+
child_class.create!(license: "rent", variant: "sd")
|
|
109
|
+
|
|
110
|
+
expect(child_class.gather(:license)).to contain_exactly("buy", "rent")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it "returns tuples for multiple columns" do
|
|
114
|
+
child_class.create!(license: "buy", variant: "hd")
|
|
115
|
+
child_class.create!(license: "rent", variant: "sd")
|
|
116
|
+
|
|
117
|
+
expect(child_class.gather(:license, :variant)).to contain_exactly(%w[buy hd], %w[rent sd])
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
describe "association-level optimization" do
|
|
122
|
+
it "uses in-memory search without SQL when association is loaded" do
|
|
123
|
+
record = child_class.create!(parent:, license: "buy", variant: "hd")
|
|
124
|
+
child_class.create!(parent:, license: "rent", variant: "hd")
|
|
125
|
+
|
|
126
|
+
parent.children.load
|
|
127
|
+
|
|
128
|
+
query_count = 0
|
|
129
|
+
counter = ->(*, **) { query_count += 1 }
|
|
130
|
+
ActiveSupport::Notifications.subscribed(counter, "sql.active_record") do
|
|
131
|
+
found = parent.children.find_for(license: "buy", variant: "hd")
|
|
132
|
+
expect(found).to eq(record)
|
|
133
|
+
end
|
|
134
|
+
expect(query_count).to eq(0)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it "uses database query when association is not loaded" do
|
|
138
|
+
record = child_class.create!(parent:, license: "buy", variant: "hd")
|
|
139
|
+
|
|
140
|
+
found = parent.children.find_for(license: "buy", variant: "hd")
|
|
141
|
+
expect(found).to eq(record)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it "uses in-memory search for non-database attributes" do
|
|
145
|
+
record = child_class.create!(parent:, license: "buy", variant: "hd")
|
|
146
|
+
|
|
147
|
+
expect(child_class.column_names).not_to include("format")
|
|
148
|
+
|
|
149
|
+
found = parent.children.find_for(format: "HD")
|
|
150
|
+
expect(found).to eq(record)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it "filters loaded associations in memory, including array values" do
|
|
154
|
+
buy = child_class.create!(parent:, license: "buy", variant: "hd")
|
|
155
|
+
rent = child_class.create!(parent:, license: "rent", variant: "sd")
|
|
156
|
+
child_class.create!(parent:, license: "trial", variant: "hd")
|
|
157
|
+
parent.children.load
|
|
158
|
+
|
|
159
|
+
expect(parent.children.for(license: %w[buy rent])).to contain_exactly(buy, rent)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it "filters unloaded associations in the database" do
|
|
163
|
+
buy = child_class.create!(parent:, license: "buy", variant: "hd")
|
|
164
|
+
child_class.create!(parent:, license: "rent", variant: "sd")
|
|
165
|
+
|
|
166
|
+
expect(parent.children.for(license: "buy")).to contain_exactly(buy)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it "rejects matching records from loaded associations in memory" do
|
|
170
|
+
child_class.create!(parent:, license: "buy", variant: "hd")
|
|
171
|
+
rent = child_class.create!(parent:, license: "rent", variant: "sd")
|
|
172
|
+
parent.children.load
|
|
173
|
+
|
|
174
|
+
expect(parent.children.not_for(license: "buy")).to contain_exactly(rent)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "rejects matching records from unloaded associations in the database" do
|
|
178
|
+
child_class.create!(parent:, license: "buy", variant: "hd")
|
|
179
|
+
rent = child_class.create!(parent:, license: "rent", variant: "sd")
|
|
180
|
+
|
|
181
|
+
expect(parent.children.not_for(license: "buy")).to contain_exactly(rent)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it "gathers one or multiple attributes from a loaded association" do
|
|
185
|
+
child_class.create!(parent:, license: "buy", variant: "hd")
|
|
186
|
+
child_class.create!(parent:, license: "rent", variant: "sd")
|
|
187
|
+
parent.children.load
|
|
188
|
+
|
|
189
|
+
expect(parent.children.gather(:license)).to contain_exactly("buy", "rent")
|
|
190
|
+
expect(parent.children.gather(:license, :variant)).to contain_exactly(%w[buy hd], %w[rent sd])
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it "gathers attributes from an unloaded association in the database" do
|
|
194
|
+
child_class.create!(parent:, license: "buy", variant: "hd")
|
|
195
|
+
child_class.create!(parent:, license: "rent", variant: "sd")
|
|
196
|
+
|
|
197
|
+
expect(parent.children.gather(:license)).to contain_exactly("buy", "rent")
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
data/lib/generators/develoz/concerns/templates/spec/models/concerns/searchable_concern_spec.rb.tt
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails_helper"
|
|
4
|
+
|
|
5
|
+
describe SearchableConcern do
|
|
6
|
+
# This spec tests the SearchableConcern module.
|
|
7
|
+
# Replace `SearchableModel` with a real model in your app that calls `searchable_by`.
|
|
8
|
+
before(:all) do
|
|
9
|
+
ActiveRecord::Schema.define do
|
|
10
|
+
create_table :searchable_models, force: true do |t|
|
|
11
|
+
t.string :name
|
|
12
|
+
t.string :description
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
after(:all) do
|
|
18
|
+
ActiveRecord::Schema.define { drop_table :searchable_models, if_exists: true }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
let(:model_class) do
|
|
22
|
+
stub_const("SearchableModel", Class.new(ApplicationRecord) do
|
|
23
|
+
self.table_name = "searchable_models"
|
|
24
|
+
include SearchableConcern
|
|
25
|
+
searchable_by :name, :description
|
|
26
|
+
end)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
describe ".searchable_by" do
|
|
30
|
+
it "adds search_by_text scope to model" do
|
|
31
|
+
expect(model_class).to respond_to(:search_by_text)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "returns records matching search text on configured columns" do
|
|
35
|
+
match = model_class.create!(name: "The Matrix")
|
|
36
|
+
no_match = model_class.create!(name: "Inception")
|
|
37
|
+
|
|
38
|
+
results = model_class.search_by_text("matrix")
|
|
39
|
+
|
|
40
|
+
expect(results).to include(match)
|
|
41
|
+
expect(results).not_to include(no_match)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "supports prefix matching" do
|
|
45
|
+
record = model_class.create!(name: "Interstellar")
|
|
46
|
+
|
|
47
|
+
results = model_class.search_by_text("inter")
|
|
48
|
+
|
|
49
|
+
expect(results).to include(record)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "is idempotent when called multiple times on the same class" do
|
|
53
|
+
expect { model_class.searchable_by(:name) }.not_to raise_error
|
|
54
|
+
expect(model_class.ancestors.count(PgSearch::Model)).to eq(1)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "returns empty when no matches found" do
|
|
58
|
+
model_class.create!(name: "The Matrix")
|
|
59
|
+
|
|
60
|
+
results = model_class.search_by_text("xyz123nonexistent")
|
|
61
|
+
|
|
62
|
+
expect(results).to be_empty
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe ".search" do
|
|
67
|
+
it "delegates to search_by_text when query is present" do
|
|
68
|
+
match = model_class.create!(name: "The Matrix")
|
|
69
|
+
model_class.create!(name: "Inception")
|
|
70
|
+
|
|
71
|
+
results = model_class.search("matrix")
|
|
72
|
+
|
|
73
|
+
expect(results).to include(match)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "returns all records when query is blank" do
|
|
77
|
+
match1 = model_class.create!(name: "The Matrix")
|
|
78
|
+
match2 = model_class.create!(name: "Inception")
|
|
79
|
+
|
|
80
|
+
expect(model_class.search("")).to include(match1, match2)
|
|
81
|
+
expect(model_class.search(nil)).to include(match1, match2)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails_helper"
|
|
4
|
+
|
|
5
|
+
describe Transitionable do
|
|
6
|
+
# This spec tests the Transitionable concern.
|
|
7
|
+
# It creates a test table with status + status_transitions columns.
|
|
8
|
+
before(:all) do
|
|
9
|
+
ActiveRecord::Schema.define do
|
|
10
|
+
create_table :transitionable_records, force: true do |t|
|
|
11
|
+
t.string :status, default: "pending", null: false
|
|
12
|
+
t.jsonb :status_transitions, default: [], null: false
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
after(:all) do
|
|
19
|
+
ActiveRecord::Schema.define { drop_table :transitionable_records, if_exists: true }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
let(:test_class) do
|
|
23
|
+
stub_const("TransitionableRecord", Class.new(ApplicationRecord) do
|
|
24
|
+
self.table_name = "transitionable_records"
|
|
25
|
+
extend Transitionable
|
|
26
|
+
end)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
describe ".states with kwargs transitions map" do
|
|
30
|
+
before { test_class.states(pending: %i[paid], paid: []) }
|
|
31
|
+
|
|
32
|
+
it "declares an enum keyed by the declared states" do
|
|
33
|
+
expect(test_class.statuses.keys).to eq(%w[pending paid])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "exposes the transition map" do
|
|
37
|
+
expect(test_class.transitions).to eq(pending: %i[paid].freeze, paid: [].freeze)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe ".states with positional transitions hash" do
|
|
42
|
+
it "parses the hash and exposes the transitions" do
|
|
43
|
+
test_class.states({ draft: %i[review], review: %i[draft published] })
|
|
44
|
+
|
|
45
|
+
expect(test_class.statuses.keys).to eq(%w[draft review])
|
|
46
|
+
expect(test_class.transitions[:review]).to eq(%i[draft published].freeze)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe ".states with positional array of states" do
|
|
51
|
+
it "declares free-mode states (transitions is nil)" do
|
|
52
|
+
test_class.states(%i[draft review published])
|
|
53
|
+
|
|
54
|
+
expect(test_class.statuses.keys).to eq(%w[draft review published])
|
|
55
|
+
expect(test_class.transitions).to be_nil
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
describe ".states with splat positional symbols" do
|
|
60
|
+
it "declares free-mode states (transitions is nil)" do
|
|
61
|
+
test_class.states(:a, :b, :c)
|
|
62
|
+
|
|
63
|
+
expect(test_class.statuses.keys).to eq(%w[a b c])
|
|
64
|
+
expect(test_class.transitions).to be_nil
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe ".states validation" do
|
|
69
|
+
it "raises when given both kwargs and positional args" do
|
|
70
|
+
expect { test_class.states(:a, pending: %i[paid]) }
|
|
71
|
+
.to raise_error(ArgumentError, /transitions hash or a list of states/)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "raises when given no states" do
|
|
75
|
+
expect { test_class.states }
|
|
76
|
+
.to raise_error(ArgumentError, /at least one state/)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
describe "integration with strict transitions" do
|
|
81
|
+
before { test_class.states(pending: %i[paid], paid: %i[cancelled], cancelled: []) }
|
|
82
|
+
|
|
83
|
+
it "uses the first declared state as default" do
|
|
84
|
+
expect(test_class.create!.status).to eq("pending")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "records the initial status as the first transition on create" do
|
|
88
|
+
record = test_class.create!
|
|
89
|
+
|
|
90
|
+
expect(record.status_transitions.length).to eq(1)
|
|
91
|
+
expect(record.status_transitions.first).to include("to_state" => "pending")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "does not record an initial transition when status_transitions is pre-populated" do
|
|
95
|
+
record = test_class.create!(
|
|
96
|
+
status_transitions: [ { "to_state" => "pending", "created_at" => "2024-01-01T00:00:00.000000Z" } ]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
expect(record.status_transitions.length).to eq(1)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "transitions and appends to status_transitions" do
|
|
103
|
+
record = test_class.create!
|
|
104
|
+
|
|
105
|
+
expect { record.paid_status! }
|
|
106
|
+
.to change(record, :status).from("pending").to("paid")
|
|
107
|
+
.and(change { record.status_transitions.length }.by(1))
|
|
108
|
+
|
|
109
|
+
expect(record.status_transitions.last["to_state"]).to eq("paid")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "is a no-op when already in the target state" do
|
|
113
|
+
record = test_class.create!
|
|
114
|
+
transitions_before = record.status_transitions
|
|
115
|
+
|
|
116
|
+
record.pending_status!
|
|
117
|
+
|
|
118
|
+
expect(record.status_transitions).to eq(transitions_before)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "records metadata when transition_metadata is set" do
|
|
122
|
+
record = test_class.create!(status: "paid")
|
|
123
|
+
|
|
124
|
+
record.update!(status: "cancelled", transition_metadata: { reason: "customer_request" })
|
|
125
|
+
|
|
126
|
+
expect(record.status_transitions.last).to include(
|
|
127
|
+
"to_state" => "cancelled",
|
|
128
|
+
"metadata" => { "reason" => "customer_request" }
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it "raises ActiveRecord::RecordInvalid on a disallowed transition" do
|
|
133
|
+
record = test_class.create!
|
|
134
|
+
|
|
135
|
+
expect { record.cancelled_status! }.to raise_error(
|
|
136
|
+
ActiveRecord::RecordInvalid,
|
|
137
|
+
/Status cannot transition from "pending" to "cancelled"/
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
describe "#can_be_<state>?" do
|
|
143
|
+
before { test_class.states(pending: %i[paid], paid: []) }
|
|
144
|
+
|
|
145
|
+
it "returns true for an allowed transition" do
|
|
146
|
+
record = test_class.create!
|
|
147
|
+
expect(record.can_be_paid?).to be true
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it "returns false for a disallowed transition" do
|
|
151
|
+
record = test_class.create!(status: "paid")
|
|
152
|
+
expect(record.can_be_pending?).to be false
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "returns false when the current state is nil" do
|
|
156
|
+
record = test_class.new(status: nil)
|
|
157
|
+
record.clear_changes_information
|
|
158
|
+
|
|
159
|
+
expect(record.can_be_paid?).to be false
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
context "when in free mode (transitions is nil)" do
|
|
163
|
+
before do
|
|
164
|
+
test_class.states(%i[draft published])
|
|
165
|
+
allow(test_class).to receive(:transitions).and_return(nil)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it "allows any declared state" do
|
|
169
|
+
record = test_class.create!
|
|
170
|
+
expect(record.can_be_published?).to be true
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
describe "#last_transition" do
|
|
176
|
+
before { test_class.states(pending: %i[paid], paid: []) }
|
|
177
|
+
|
|
178
|
+
it "returns nil when status_transitions is empty" do
|
|
179
|
+
record = test_class.create!
|
|
180
|
+
record.update_column(:status_transitions, [])
|
|
181
|
+
|
|
182
|
+
expect(record.last_transition).to be_nil
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it "returns the latest entry as a Struct" do
|
|
186
|
+
record = test_class.create!(status: "paid")
|
|
187
|
+
|
|
188
|
+
transition = record.last_transition
|
|
189
|
+
|
|
190
|
+
expect(transition.to_state).to eq("paid")
|
|
191
|
+
expect(transition.metadata).to eq({})
|
|
192
|
+
expect(transition.created_at).to be_present
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
describe "#status_times" do
|
|
197
|
+
before { test_class.states(pending: %i[paid], paid: []) }
|
|
198
|
+
|
|
199
|
+
it "calculates the time spent between transitions" do
|
|
200
|
+
created_at = Time.zone.parse("2024-01-01T00:00:00Z")
|
|
201
|
+
pending_at = created_at + 1.minute
|
|
202
|
+
paid_at = pending_at + 2.minutes
|
|
203
|
+
record = test_class.create!(
|
|
204
|
+
created_at:,
|
|
205
|
+
status: "paid",
|
|
206
|
+
status_transitions: [
|
|
207
|
+
{ "to_state" => "pending", "created_at" => pending_at.iso8601(6) },
|
|
208
|
+
{ "to_state" => "paid", "created_at" => paid_at.iso8601(6) }
|
|
209
|
+
]
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
expect(record.status_times).to eq(
|
|
213
|
+
[
|
|
214
|
+
{ status: "pending", previous_time: created_at, time: pending_at, duration: 1.minute },
|
|
215
|
+
{ status: "paid", previous_time: pending_at, time: paid_at, duration: 2.minutes }
|
|
216
|
+
]
|
|
217
|
+
)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
describe ".in_state" do
|
|
222
|
+
before { test_class.states(pending: %i[paid], paid: []) }
|
|
223
|
+
|
|
224
|
+
it "filters records by status" do
|
|
225
|
+
pending_record = test_class.create!
|
|
226
|
+
paid_record = test_class.create!(status: "paid")
|
|
227
|
+
|
|
228
|
+
in_pending = test_class.in_state(:pending)
|
|
229
|
+
expect(in_pending).to include(pending_record)
|
|
230
|
+
expect(in_pending).not_to include(paid_record)
|
|
231
|
+
expect(test_class.in_state(:pending, :paid)).to include(pending_record, paid_record)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|