anima-core 0.3.0 → 1.0.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 +4 -4
- data/.reek.yml +27 -1
- data/CHANGELOG.md +4 -0
- data/README.md +219 -25
- data/agents/codebase-analyzer.md +88 -0
- data/agents/codebase-pattern-finder.md +83 -0
- data/agents/documentation-researcher.md +59 -0
- data/agents/thoughts-analyzer.md +102 -0
- data/agents/web-search-researcher.md +71 -0
- data/anima-core.gemspec +3 -0
- data/app/channels/session_channel.rb +76 -28
- data/app/jobs/agent_request_job.rb +24 -0
- data/app/jobs/analytical_brain_job.rb +33 -0
- data/app/jobs/count_event_tokens_job.rb +1 -1
- data/app/models/concerns/event/broadcasting.rb +20 -2
- data/app/models/event.rb +1 -1
- data/app/models/goal.rb +91 -0
- data/app/models/session.rb +347 -22
- data/config/application.rb +2 -0
- data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
- data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
- data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
- data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
- data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
- data/db/migrate/20260315140843_create_goals.rb +16 -0
- data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
- data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
- data/lib/agent_loop.rb +65 -9
- data/lib/agents/definition.rb +116 -0
- data/lib/agents/registry.rb +106 -0
- data/lib/analytical_brain/runner.rb +276 -0
- data/lib/analytical_brain/tools/activate_skill.rb +52 -0
- data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
- data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
- data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
- data/lib/analytical_brain/tools/finish_goal.rb +62 -0
- data/lib/analytical_brain/tools/read_workflow.rb +58 -0
- data/lib/analytical_brain/tools/rename_session.rb +63 -0
- data/lib/analytical_brain/tools/set_goal.rb +60 -0
- data/lib/analytical_brain/tools/update_goal.rb +60 -0
- data/lib/analytical_brain.rb +23 -0
- data/lib/anima/cli/mcp/secrets.rb +76 -0
- data/lib/anima/cli/mcp.rb +197 -0
- data/lib/anima/cli.rb +4 -0
- data/lib/anima/installer.rb +168 -0
- data/lib/anima/settings.rb +226 -0
- data/lib/anima/version.rb +1 -1
- data/lib/anima.rb +9 -0
- data/lib/credential_store.rb +103 -0
- data/lib/environment_probe.rb +232 -0
- data/lib/llm/client.rb +29 -10
- data/lib/mcp/client_manager.rb +86 -0
- data/lib/mcp/config.rb +213 -0
- data/lib/mcp/health_check.rb +77 -0
- data/lib/mcp/secrets.rb +73 -0
- data/lib/mcp/stdio_transport.rb +206 -0
- data/lib/providers/anthropic.rb +8 -7
- data/lib/shell_session.rb +11 -10
- data/lib/skills/definition.rb +97 -0
- data/lib/skills/registry.rb +105 -0
- data/lib/tools/edit.rb +3 -4
- data/lib/tools/mcp_tool.rb +114 -0
- data/lib/tools/read.rb +15 -16
- data/lib/tools/registry.rb +14 -12
- data/lib/tools/request_feature.rb +121 -0
- data/lib/tools/return_result.rb +81 -0
- data/lib/tools/spawn_specialist.rb +109 -0
- data/lib/tools/spawn_subagent.rb +111 -0
- data/lib/tools/subagent_prompts.rb +12 -0
- data/lib/tools/web_get.rb +8 -9
- data/lib/tui/app.rb +332 -43
- data/lib/tui/message_store.rb +20 -0
- data/lib/tui/screens/chat.rb +207 -20
- data/lib/workflows/definition.rb +97 -0
- data/lib/workflows/registry.rb +89 -0
- data/skills/activerecord/SKILL.md +255 -0
- data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
- data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
- data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
- data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
- data/skills/activerecord/examples/associations/self_referential.rb +302 -0
- data/skills/activerecord/examples/associations/through_associations.rb +203 -0
- data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
- data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
- data/skills/activerecord/examples/basics/inheritance.rb +377 -0
- data/skills/activerecord/examples/basics/type_casting.rb +317 -0
- data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
- data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
- data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
- data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
- data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
- data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
- data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
- data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
- data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
- data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
- data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
- data/skills/activerecord/examples/querying/optimization.rb +275 -0
- data/skills/activerecord/examples/querying/scopes.rb +260 -0
- data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
- data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
- data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
- data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
- data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
- data/skills/activerecord/references/associations.md +709 -0
- data/skills/activerecord/references/basics.md +622 -0
- data/skills/activerecord/references/callbacks.md +738 -0
- data/skills/activerecord/references/migrations.md +657 -0
- data/skills/activerecord/references/querying.md +655 -0
- data/skills/activerecord/references/validations.md +596 -0
- data/skills/dragonruby/SKILL.md +250 -0
- data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
- data/skills/dragonruby/examples/audio/background_music.rb +29 -0
- data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
- data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
- data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
- data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
- data/skills/dragonruby/examples/core/hello_world.rb +24 -0
- data/skills/dragonruby/examples/core/labels.rb +22 -0
- data/skills/dragonruby/examples/core/sprites.rb +35 -0
- data/skills/dragonruby/examples/core/state_management.rb +29 -0
- data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
- data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
- data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
- data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
- data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
- data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
- data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
- data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
- data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
- data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
- data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
- data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
- data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
- data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
- data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
- data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
- data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
- data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
- data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
- data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
- data/skills/dragonruby/examples/input/controller_input.rb +28 -0
- data/skills/dragonruby/examples/input/directional_input.rb +24 -0
- data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
- data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
- data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
- data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
- data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
- data/skills/dragonruby/examples/rendering/labels.rb +32 -0
- data/skills/dragonruby/examples/rendering/layering.rb +51 -0
- data/skills/dragonruby/examples/rendering/solids.rb +61 -0
- data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
- data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
- data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
- data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
- data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
- data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
- data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
- data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
- data/skills/dragonruby/references/audio.md +396 -0
- data/skills/dragonruby/references/core.md +385 -0
- data/skills/dragonruby/references/distribution.md +434 -0
- data/skills/dragonruby/references/entities.md +516 -0
- data/skills/dragonruby/references/game-logic/persistence.md +386 -0
- data/skills/dragonruby/references/game-logic/state.md +389 -0
- data/skills/dragonruby/references/input.md +414 -0
- data/skills/dragonruby/references/rendering/animation.md +467 -0
- data/skills/dragonruby/references/rendering/primitives.md +403 -0
- data/skills/dragonruby/references/scenes.md +443 -0
- data/skills/draper-decorators/SKILL.md +344 -0
- data/skills/draper-decorators/examples/application_decorator.rb +61 -0
- data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
- data/skills/draper-decorators/examples/model_decorator.rb +152 -0
- data/skills/draper-decorators/references/anti-patterns.md +640 -0
- data/skills/draper-decorators/references/patterns.md +507 -0
- data/skills/draper-decorators/references/testing.md +559 -0
- data/skills/gh-issue.md +182 -0
- data/skills/mcp-server/SKILL.md +177 -0
- data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
- data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
- data/skills/mcp-server/examples/http_client.rb +48 -0
- data/skills/mcp-server/examples/http_server.rb +97 -0
- data/skills/mcp-server/examples/rails_integration.rb +88 -0
- data/skills/mcp-server/examples/stdio_server.rb +108 -0
- data/skills/mcp-server/examples/streaming_client.rb +95 -0
- data/skills/mcp-server/references/gotchas.md +183 -0
- data/skills/mcp-server/references/prompts.md +98 -0
- data/skills/mcp-server/references/resources.md +53 -0
- data/skills/mcp-server/references/server.md +140 -0
- data/skills/mcp-server/references/tools.md +146 -0
- data/skills/mcp-server/references/transport.md +104 -0
- data/skills/ratatui-ruby/SKILL.md +315 -0
- data/skills/ratatui-ruby/references/core-concepts.md +340 -0
- data/skills/ratatui-ruby/references/events.md +387 -0
- data/skills/ratatui-ruby/references/frameworks.md +522 -0
- data/skills/ratatui-ruby/references/layout.md +423 -0
- data/skills/ratatui-ruby/references/styling.md +268 -0
- data/skills/ratatui-ruby/references/testing.md +433 -0
- data/skills/ratatui-ruby/references/widgets.md +532 -0
- data/skills/rspec/SKILL.md +340 -0
- data/skills/rspec/examples/core/basic_structure.rb +69 -0
- data/skills/rspec/examples/core/configuration.rb +126 -0
- data/skills/rspec/examples/core/hooks.rb +126 -0
- data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
- data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
- data/skills/rspec/examples/core/shared_examples.rb +145 -0
- data/skills/rspec/examples/factory_bot/associations.rb +314 -0
- data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
- data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
- data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
- data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
- data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
- data/skills/rspec/examples/factory_bot/traits.rb +293 -0
- data/skills/rspec/examples/factory_bot/transients.rb +229 -0
- data/skills/rspec/examples/matchers/change.rb +115 -0
- data/skills/rspec/examples/matchers/collections.rb +154 -0
- data/skills/rspec/examples/matchers/comparisons.rb +79 -0
- data/skills/rspec/examples/matchers/composing.rb +155 -0
- data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
- data/skills/rspec/examples/matchers/equality.rb +58 -0
- data/skills/rspec/examples/matchers/errors.rb +136 -0
- data/skills/rspec/examples/matchers/output.rb +103 -0
- data/skills/rspec/examples/matchers/predicates.rb +87 -0
- data/skills/rspec/examples/matchers/truthiness.rb +101 -0
- data/skills/rspec/examples/matchers/types.rb +82 -0
- data/skills/rspec/examples/matchers/yield.rb +147 -0
- data/skills/rspec/examples/mocks/any_instance.rb +172 -0
- data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
- data/skills/rspec/examples/mocks/constants.rb +177 -0
- data/skills/rspec/examples/mocks/doubles.rb +139 -0
- data/skills/rspec/examples/mocks/expectations.rb +137 -0
- data/skills/rspec/examples/mocks/message_chains.rb +173 -0
- data/skills/rspec/examples/mocks/ordering.rb +144 -0
- data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
- data/skills/rspec/examples/mocks/responses.rb +223 -0
- data/skills/rspec/examples/mocks/spies.rb +149 -0
- data/skills/rspec/examples/mocks/stubbing.rb +133 -0
- data/skills/rspec/examples/rails/channels.rb +250 -0
- data/skills/rspec/examples/rails/controller_specs.rb +302 -0
- data/skills/rspec/examples/rails/helper_specs.rb +245 -0
- data/skills/rspec/examples/rails/job_specs.rb +256 -0
- data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
- data/skills/rspec/examples/rails/matchers.rb +374 -0
- data/skills/rspec/examples/rails/model_specs.rb +193 -0
- data/skills/rspec/examples/rails/request_specs.rb +275 -0
- data/skills/rspec/examples/rails/routing_specs.rb +276 -0
- data/skills/rspec/examples/rails/system_specs.rb +294 -0
- data/skills/rspec/examples/rails/transactions.rb +254 -0
- data/skills/rspec/examples/rails/view_specs.rb +252 -0
- data/skills/rspec/references/core.md +816 -0
- data/skills/rspec/references/factory_bot.md +641 -0
- data/skills/rspec/references/matchers.md +516 -0
- data/skills/rspec/references/mocks.md +381 -0
- data/skills/rspec/references/rails.md +528 -0
- data/templates/soul.md +40 -0
- data/workflows/commit.md +45 -0
- data/workflows/create_handoff.md +98 -0
- data/workflows/create_note.md +82 -0
- data/workflows/create_plan.md +457 -0
- data/workflows/decompose_ticket.md +109 -0
- data/workflows/feature.md +91 -0
- data/workflows/implement_plan.md +87 -0
- data/workflows/iterate_plan.md +247 -0
- data/workflows/research_codebase.md +210 -0
- data/workflows/resume_handoff.md +217 -0
- data/workflows/review_pr.md +320 -0
- data/workflows/thoughts_init.md +71 -0
- data/workflows/validate_plan.md +166 -0
- metadata +284 -1
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# RSpec Rails: Mailer Specs Examples
|
|
2
|
+
# Source: rspec-rails gem features/mailer_specs/
|
|
3
|
+
|
|
4
|
+
# Mailer specs test ActionMailer classes.
|
|
5
|
+
# Location: spec/mailers/
|
|
6
|
+
|
|
7
|
+
# Basic mailer spec
|
|
8
|
+
RSpec.describe NotificationsMailer, type: :mailer do
|
|
9
|
+
describe "#signup" do
|
|
10
|
+
let(:mail) { NotificationsMailer.signup }
|
|
11
|
+
|
|
12
|
+
it "renders the headers" do
|
|
13
|
+
expect(mail.subject).to eq("Signup")
|
|
14
|
+
expect(mail.to).to eq(["to@example.org"])
|
|
15
|
+
expect(mail.from).to eq(["from@example.com"])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "renders the body" do
|
|
19
|
+
expect(mail.body.encoded).to include("Hi")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Testing with dynamic recipient
|
|
25
|
+
RSpec.describe WelcomeMailer, type: :mailer do
|
|
26
|
+
describe "#welcome" do
|
|
27
|
+
let(:user) { create(:user, email: "john@example.com", name: "John") }
|
|
28
|
+
let(:mail) { WelcomeMailer.welcome(user) }
|
|
29
|
+
|
|
30
|
+
it "sends to the user email" do
|
|
31
|
+
expect(mail.to).to eq(["john@example.com"])
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "personalizes the greeting" do
|
|
35
|
+
expect(mail.body.encoded).to include("Hello John")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "includes welcome link" do
|
|
39
|
+
expect(mail.body.encoded).to include(dashboard_url)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Testing multipart emails (HTML and text)
|
|
45
|
+
RSpec.describe NewsletterMailer, type: :mailer do
|
|
46
|
+
describe "#weekly_digest" do
|
|
47
|
+
let(:user) { create(:user) }
|
|
48
|
+
let(:mail) { NewsletterMailer.weekly_digest(user) }
|
|
49
|
+
|
|
50
|
+
it "has both HTML and text parts" do
|
|
51
|
+
expect(mail.parts.length).to eq(2)
|
|
52
|
+
expect(mail.parts.map(&:content_type)).to include(
|
|
53
|
+
a_string_matching(/text\/plain/),
|
|
54
|
+
a_string_matching(/text\/html/)
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe "HTML part" do
|
|
59
|
+
subject(:html_body) { mail.html_part.body.encoded }
|
|
60
|
+
|
|
61
|
+
it "includes styled content" do
|
|
62
|
+
expect(html_body).to include("<h1>")
|
|
63
|
+
expect(html_body).to include("Weekly Digest")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
describe "text part" do
|
|
68
|
+
subject(:text_body) { mail.text_part.body.encoded }
|
|
69
|
+
|
|
70
|
+
it "includes plain text content" do
|
|
71
|
+
expect(text_body).to include("Weekly Digest")
|
|
72
|
+
expect(text_body).not_to include("<h1>")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Testing email with attachments
|
|
79
|
+
RSpec.describe ReportMailer, type: :mailer do
|
|
80
|
+
describe "#monthly_report" do
|
|
81
|
+
let(:mail) { ReportMailer.monthly_report }
|
|
82
|
+
|
|
83
|
+
it "includes PDF attachment" do
|
|
84
|
+
expect(mail.attachments.length).to eq(1)
|
|
85
|
+
expect(mail.attachments.first.filename).to eq("report.pdf")
|
|
86
|
+
expect(mail.attachments.first.content_type).to start_with("application/pdf")
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Testing delivery
|
|
92
|
+
RSpec.describe OrderMailer, type: :mailer do
|
|
93
|
+
describe "#confirmation" do
|
|
94
|
+
let(:order) { create(:order) }
|
|
95
|
+
let(:mail) { OrderMailer.confirmation(order) }
|
|
96
|
+
|
|
97
|
+
it "delivers email" do
|
|
98
|
+
expect { mail.deliver_now }.to change { ActionMailer::Base.deliveries.count }.by(1)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "queues email for later delivery" do
|
|
102
|
+
expect { mail.deliver_later }.to have_enqueued_job(ActionMailer::MailDeliveryJob)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# send_email matcher (rspec-rails 7.0+)
|
|
108
|
+
RSpec.describe NotificationsMailer, type: :mailer do
|
|
109
|
+
describe "#alert" do
|
|
110
|
+
let(:user) { create(:user, email: "user@example.com") }
|
|
111
|
+
|
|
112
|
+
it "sends email with correct attributes" do
|
|
113
|
+
expect {
|
|
114
|
+
NotificationsMailer.alert(user).deliver_now
|
|
115
|
+
}.to send_email(
|
|
116
|
+
from: "alerts@example.com",
|
|
117
|
+
to: "user@example.com",
|
|
118
|
+
subject: "Alert Notification"
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Testing email previews exist (meta-test)
|
|
125
|
+
RSpec.describe "Mailer Previews" do
|
|
126
|
+
it "has preview for WelcomeMailer" do
|
|
127
|
+
expect(defined?(WelcomeMailerPreview)).to be_truthy
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Testing with parameterized mailers (Rails 5.1+)
|
|
132
|
+
RSpec.describe NotificationsMailer, type: :mailer do
|
|
133
|
+
describe "#with parameters" do
|
|
134
|
+
let(:user) { create(:user) }
|
|
135
|
+
let(:mail) do
|
|
136
|
+
NotificationsMailer.with(user:, urgency: :high).important_update
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "uses parameterized values" do
|
|
140
|
+
expect(mail.to).to eq([user.email])
|
|
141
|
+
expect(mail.subject).to include("[URGENT]")
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Testing email headers
|
|
147
|
+
RSpec.describe TransactionalMailer, type: :mailer do
|
|
148
|
+
describe "#receipt" do
|
|
149
|
+
let(:order) { create(:order) }
|
|
150
|
+
let(:mail) { TransactionalMailer.receipt(order) }
|
|
151
|
+
|
|
152
|
+
it "sets reply-to header" do
|
|
153
|
+
expect(mail.reply_to).to eq(["support@example.com"])
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it "sets custom headers" do
|
|
157
|
+
expect(mail.headers["X-Transaction-ID"]).to eq(order.id.to_s)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it "sets high priority" do
|
|
161
|
+
expect(mail.headers["X-Priority"]).to eq("1")
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Testing mailer with conditional content
|
|
167
|
+
RSpec.describe UserMailer, type: :mailer do
|
|
168
|
+
describe "#password_reset" do
|
|
169
|
+
let(:user) { create(:user) }
|
|
170
|
+
let(:mail) { UserMailer.password_reset(user) }
|
|
171
|
+
|
|
172
|
+
context "when user has two-factor enabled" do
|
|
173
|
+
let(:user) { create(:user, :with_two_factor) }
|
|
174
|
+
|
|
175
|
+
it "includes 2FA reminder" do
|
|
176
|
+
expect(mail.body.encoded).to include("two-factor authentication")
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
context "when user does not have two-factor" do
|
|
181
|
+
it "does not mention 2FA" do
|
|
182
|
+
expect(mail.body.encoded).not_to include("two-factor authentication")
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Testing mailer callbacks
|
|
189
|
+
RSpec.describe AuditableMailer, type: :mailer do
|
|
190
|
+
describe "after_action callback" do
|
|
191
|
+
let(:mail) { AuditableMailer.system_notification }
|
|
192
|
+
|
|
193
|
+
it "logs email delivery" do
|
|
194
|
+
expect(EmailLog).to receive(:create!).with(
|
|
195
|
+
mailer: "AuditableMailer",
|
|
196
|
+
action: "system_notification"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
mail.deliver_now
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Clearing deliveries between examples
|
|
205
|
+
RSpec.describe "Email delivery", type: :mailer do
|
|
206
|
+
before { ActionMailer::Base.deliveries.clear }
|
|
207
|
+
|
|
208
|
+
it "starts with empty deliveries" do
|
|
209
|
+
expect(ActionMailer::Base.deliveries).to be_empty
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it "tracks delivered emails" do
|
|
213
|
+
NotificationsMailer.signup.deliver_now
|
|
214
|
+
expect(ActionMailer::Base.deliveries.count).to eq(1)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Testing I18n in emails
|
|
219
|
+
RSpec.describe LocalizedMailer, type: :mailer do
|
|
220
|
+
describe "#welcome" do
|
|
221
|
+
let(:user) { create(:user, locale: "es") }
|
|
222
|
+
let(:mail) { LocalizedMailer.welcome(user) }
|
|
223
|
+
|
|
224
|
+
it "uses user locale" do
|
|
225
|
+
expect(mail.subject).to eq("Bienvenido")
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# RSpec Rails: Rails-Specific Matchers Examples
|
|
2
|
+
# Source: rspec-rails gem features/matchers/
|
|
3
|
+
|
|
4
|
+
# Rails-specific matchers for testing HTTP responses,
|
|
5
|
+
# redirects, templates, and more.
|
|
6
|
+
|
|
7
|
+
# have_http_status - testing response codes
|
|
8
|
+
RSpec.describe "have_http_status", type: :request do
|
|
9
|
+
describe "numeric status codes" do
|
|
10
|
+
it "matches exact code" do
|
|
11
|
+
get "/widgets"
|
|
12
|
+
expect(response).to have_http_status(200)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "matches created status" do
|
|
16
|
+
post "/widgets", params: { widget: { name: "New" } }
|
|
17
|
+
expect(response).to have_http_status(201)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe "symbolic status names" do
|
|
22
|
+
it "matches :ok" do
|
|
23
|
+
get "/widgets"
|
|
24
|
+
expect(response).to have_http_status(:ok)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "matches :created" do
|
|
28
|
+
post "/api/widgets", params: { widget: { name: "New" } },
|
|
29
|
+
headers: { "ACCEPT" => "application/json" }
|
|
30
|
+
expect(response).to have_http_status(:created)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "matches :not_found" do
|
|
34
|
+
get "/widgets/nonexistent"
|
|
35
|
+
expect(response).to have_http_status(:not_found)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "matches :unprocessable_entity" do
|
|
39
|
+
post "/widgets", params: { widget: { name: "" } }
|
|
40
|
+
expect(response).to have_http_status(:unprocessable_entity)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "matches :unauthorized" do
|
|
44
|
+
get "/admin/dashboard"
|
|
45
|
+
expect(response).to have_http_status(:unauthorized)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe "status type matchers" do
|
|
50
|
+
it "matches :success (any 2xx)" do
|
|
51
|
+
get "/widgets"
|
|
52
|
+
expect(response).to have_http_status(:success)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "matches :redirect (any 3xx)" do
|
|
56
|
+
post "/widgets", params: { widget: { name: "New" } }
|
|
57
|
+
expect(response).to have_http_status(:redirect)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "matches :error (any 5xx)" do
|
|
61
|
+
allow_any_instance_of(WidgetsController).to receive(:index).and_raise(StandardError)
|
|
62
|
+
get "/widgets"
|
|
63
|
+
expect(response).to have_http_status(:error)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "matches :missing (404)" do
|
|
67
|
+
get "/nonexistent"
|
|
68
|
+
expect(response).to have_http_status(:missing)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# redirect_to - testing redirects
|
|
74
|
+
RSpec.describe "redirect_to", type: :controller do
|
|
75
|
+
controller WidgetsController do
|
|
76
|
+
def create
|
|
77
|
+
@widget = Widget.create!(params.require(:widget).permit(:name))
|
|
78
|
+
redirect_to @widget
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe "POST #create" do
|
|
83
|
+
let(:valid_params) { { widget: { name: "Test" } } }
|
|
84
|
+
|
|
85
|
+
it "redirects to URL" do
|
|
86
|
+
post :create, params: valid_params
|
|
87
|
+
expect(response).to redirect_to(widget_url(assigns(:widget)))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "redirects to path" do
|
|
91
|
+
post :create, params: valid_params
|
|
92
|
+
expect(response).to redirect_to("/widgets/#{assigns(:widget).id}")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "redirects to hash" do
|
|
96
|
+
post :create, params: valid_params
|
|
97
|
+
expect(response).to redirect_to(action: :show, id: assigns(:widget).id)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "redirects to record" do
|
|
101
|
+
post :create, params: valid_params
|
|
102
|
+
expect(response).to redirect_to(assigns(:widget))
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# render_template - testing template rendering
|
|
108
|
+
RSpec.describe "render_template", type: :controller do
|
|
109
|
+
controller WidgetsController do
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
describe "GET #index" do
|
|
113
|
+
it "renders index template" do
|
|
114
|
+
get :index
|
|
115
|
+
expect(response).to render_template(:index)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "renders with full path" do
|
|
119
|
+
get :index
|
|
120
|
+
expect(response).to render_template("widgets/index")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "does not render other templates" do
|
|
124
|
+
get :index
|
|
125
|
+
expect(response).not_to render_template("widgets/show")
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
describe "layout rendering" do
|
|
130
|
+
it "renders with application layout" do
|
|
131
|
+
get :index
|
|
132
|
+
expect(response).to render_template("layouts/application")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it "does not render admin layout" do
|
|
136
|
+
get :index
|
|
137
|
+
expect(response).not_to render_template("layouts/admin")
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
describe "partial rendering" do
|
|
142
|
+
render_views
|
|
143
|
+
|
|
144
|
+
it "renders partial" do
|
|
145
|
+
create_list(:widget, 2)
|
|
146
|
+
get :index
|
|
147
|
+
expect(response).to render_template(partial: "_widget")
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# route_to - testing routes
|
|
153
|
+
RSpec.describe "route_to", type: :routing do
|
|
154
|
+
describe "shortcut syntax" do
|
|
155
|
+
it "routes with controller#action" do
|
|
156
|
+
expect(get: "/widgets").to route_to("widgets#index")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it "routes with id" do
|
|
160
|
+
expect(get: "/widgets/1").to route_to("widgets#show", id: "1")
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
describe "hash syntax" do
|
|
165
|
+
it "routes with full hash" do
|
|
166
|
+
expect(get: "/widgets").to route_to(
|
|
167
|
+
controller: "widgets",
|
|
168
|
+
action: "index"
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it "routes with params" do
|
|
173
|
+
expect(get: "/widgets/1").to route_to(
|
|
174
|
+
controller: "widgets",
|
|
175
|
+
action: "show",
|
|
176
|
+
id: "1"
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
describe "negative matching" do
|
|
182
|
+
it "does not route nonexistent paths" do
|
|
183
|
+
expect(get: "/nonexistent").not_to route_to("widgets#index")
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# be_routable - testing route existence
|
|
189
|
+
RSpec.describe "be_routable", type: :routing do
|
|
190
|
+
describe "routable paths" do
|
|
191
|
+
it "matches existing routes" do
|
|
192
|
+
expect(get: "/widgets").to be_routable
|
|
193
|
+
expect(post: "/widgets").to be_routable
|
|
194
|
+
expect(get: "/widgets/1").to be_routable
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
describe "non-routable paths" do
|
|
199
|
+
it "does not match nonexistent routes" do
|
|
200
|
+
expect(put: "/widgets").not_to be_routable
|
|
201
|
+
expect(delete: "/admin/destroy_all").not_to be_routable
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# be_a_new - testing new records
|
|
207
|
+
RSpec.describe "be_a_new", type: :controller do
|
|
208
|
+
controller WidgetsController do
|
|
209
|
+
def new
|
|
210
|
+
@widget = Widget.new
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
describe "GET #new" do
|
|
215
|
+
it "assigns a new widget" do
|
|
216
|
+
get :new
|
|
217
|
+
expect(assigns(:widget)).to be_a_new(Widget)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
describe "after save" do
|
|
222
|
+
let(:widget) { Widget.create!(name: "Saved") }
|
|
223
|
+
|
|
224
|
+
it "is not a new widget" do
|
|
225
|
+
expect(widget).not_to be_a_new(Widget)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
describe "with attributes" do
|
|
230
|
+
it "matches with expected attributes" do
|
|
231
|
+
widget = Widget.new(name: "Test")
|
|
232
|
+
expect(widget).to be_a_new(Widget).with(name: "Test")
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# be_valid - testing model validity
|
|
238
|
+
RSpec.describe "be_valid", type: :model do
|
|
239
|
+
describe Widget do
|
|
240
|
+
it "is valid with valid attributes" do
|
|
241
|
+
widget = Widget.new(name: "Valid Widget")
|
|
242
|
+
expect(widget).to be_valid
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
it "is not valid without required attributes" do
|
|
246
|
+
widget = Widget.new(name: nil)
|
|
247
|
+
expect(widget).not_to be_valid
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
it "validates with context" do
|
|
251
|
+
widget = Widget.new(name: "Test")
|
|
252
|
+
expect(widget).to be_valid(:create)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# have_enqueued_job - testing job enqueuing
|
|
258
|
+
RSpec.describe "have_enqueued_job", type: :job do
|
|
259
|
+
describe "block form" do
|
|
260
|
+
it "matches enqueued job" do
|
|
261
|
+
expect {
|
|
262
|
+
ProcessJob.perform_later("data")
|
|
263
|
+
}.to have_enqueued_job
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
it "matches specific job class" do
|
|
267
|
+
expect {
|
|
268
|
+
ProcessJob.perform_later("data")
|
|
269
|
+
}.to have_enqueued_job(ProcessJob)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
it "matches with arguments" do
|
|
273
|
+
expect {
|
|
274
|
+
ProcessJob.perform_later("data", 123)
|
|
275
|
+
}.to have_enqueued_job.with("data", 123)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
it "matches on queue" do
|
|
279
|
+
expect {
|
|
280
|
+
ProcessJob.perform_later
|
|
281
|
+
}.to have_enqueued_job.on_queue("default")
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
it "matches at time" do
|
|
285
|
+
scheduled_time = 1.hour.from_now
|
|
286
|
+
expect {
|
|
287
|
+
ProcessJob.set(wait_until: scheduled_time).perform_later
|
|
288
|
+
}.to have_enqueued_job.at(scheduled_time)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
describe "imperative form" do
|
|
293
|
+
before { ProcessJob.perform_later }
|
|
294
|
+
|
|
295
|
+
it "verifies job was enqueued" do
|
|
296
|
+
expect(ProcessJob).to have_been_enqueued
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
it "verifies enqueue count" do
|
|
300
|
+
ProcessJob.perform_later
|
|
301
|
+
expect(ProcessJob).to have_been_enqueued.exactly(:twice)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# have_broadcasted_to - testing ActionCable broadcasts
|
|
307
|
+
RSpec.describe "have_broadcasted_to", type: :channel do
|
|
308
|
+
describe "broadcasting messages" do
|
|
309
|
+
it "matches broadcast to channel" do
|
|
310
|
+
expect {
|
|
311
|
+
ActionCable.server.broadcast("notifications", text: "Hello!")
|
|
312
|
+
}.to have_broadcasted_to("notifications")
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
it "matches with message content" do
|
|
316
|
+
expect {
|
|
317
|
+
ActionCable.server.broadcast("notifications", text: "Hello!")
|
|
318
|
+
}.to have_broadcasted_to("notifications").with(text: "Hello!")
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
it "matches broadcast count" do
|
|
322
|
+
expect {
|
|
323
|
+
2.times { ActionCable.server.broadcast("notifications", text: "Hi") }
|
|
324
|
+
}.to have_broadcasted_to("notifications").exactly(:twice)
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
describe "broadcasting to record" do
|
|
329
|
+
let(:user) { create(:user) }
|
|
330
|
+
|
|
331
|
+
it "matches broadcast to model" do
|
|
332
|
+
expect {
|
|
333
|
+
ChatChannel.broadcast_to(user, text: "Message")
|
|
334
|
+
}.to have_broadcasted_to(user)
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# send_email - testing email sending (rspec-rails 7.0+)
|
|
340
|
+
RSpec.describe "send_email", type: :mailer do
|
|
341
|
+
describe "email sending" do
|
|
342
|
+
it "matches sent email" do
|
|
343
|
+
expect {
|
|
344
|
+
NotificationsMailer.welcome(user).deliver_now
|
|
345
|
+
}.to send_email
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
it "matches email attributes" do
|
|
349
|
+
user = create(:user, email: "test@example.com")
|
|
350
|
+
|
|
351
|
+
expect {
|
|
352
|
+
NotificationsMailer.welcome(user).deliver_now
|
|
353
|
+
}.to send_email(
|
|
354
|
+
from: "noreply@example.com",
|
|
355
|
+
to: "test@example.com",
|
|
356
|
+
subject: "Welcome!"
|
|
357
|
+
)
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# match_array for ActiveRecord relations
|
|
363
|
+
RSpec.describe "match_array with relations", type: :model do
|
|
364
|
+
let!(:widgets) { create_list(:widget, 3) }
|
|
365
|
+
|
|
366
|
+
it "matches relation regardless of order" do
|
|
367
|
+
expect(Widget.all).to match_array(widgets)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
it "matches scope results" do
|
|
371
|
+
published = create_list(:widget, 2, :published)
|
|
372
|
+
expect(Widget.published).to match_array(published)
|
|
373
|
+
end
|
|
374
|
+
end
|