hyraft 0.1.0.alpha1

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.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/CODE_OF_CONDUCT.md +132 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +231 -0
  6. data/exe/hyraft +5 -0
  7. data/lib/hyraft/boot/asset_preloader.rb +185 -0
  8. data/lib/hyraft/boot/preloaded_static.rb +46 -0
  9. data/lib/hyraft/boot/preloader.rb +206 -0
  10. data/lib/hyraft/cli.rb +187 -0
  11. data/lib/hyraft/compiler/compiler.rb +34 -0
  12. data/lib/hyraft/compiler/html_purifier.rb +181 -0
  13. data/lib/hyraft/compiler/javascript_library.rb +281 -0
  14. data/lib/hyraft/compiler/javascript_obfuscator.rb +141 -0
  15. data/lib/hyraft/compiler/parser.rb +27 -0
  16. data/lib/hyraft/compiler/renderer.rb +217 -0
  17. data/lib/hyraft/engine/circuit.rb +35 -0
  18. data/lib/hyraft/engine/port.rb +17 -0
  19. data/lib/hyraft/engine/source.rb +19 -0
  20. data/lib/hyraft/engine.rb +11 -0
  21. data/lib/hyraft/router/api_router.rb +65 -0
  22. data/lib/hyraft/router/web_router.rb +136 -0
  23. data/lib/hyraft/system_info.rb +26 -0
  24. data/lib/hyraft/version.rb +5 -0
  25. data/lib/hyraft.rb +48 -0
  26. data/templates/do_app/Gemfile +50 -0
  27. data/templates/do_app/Rakefile +88 -0
  28. data/templates/do_app/adapter-intake/web-app/display/pages/home/home.hyr +174 -0
  29. data/templates/do_app/adapter-intake/web-app/request/home_web_adapter.rb +19 -0
  30. data/templates/do_app/boot.rb +41 -0
  31. data/templates/do_app/framework/adapters/server/server_api_adapter.rb +51 -0
  32. data/templates/do_app/framework/adapters/server/server_web_adapter.rb +178 -0
  33. data/templates/do_app/framework/compiler/style_resolver.rb +33 -0
  34. data/templates/do_app/framework/errors/error_handler.rb +75 -0
  35. data/templates/do_app/framework/errors/templates/304.html +22 -0
  36. data/templates/do_app/framework/errors/templates/400.html +22 -0
  37. data/templates/do_app/framework/errors/templates/401.html +22 -0
  38. data/templates/do_app/framework/errors/templates/403.html +22 -0
  39. data/templates/do_app/framework/errors/templates/404.html +62 -0
  40. data/templates/do_app/framework/errors/templates/500.html +73 -0
  41. data/templates/do_app/framework/middleware/cors_middleware.rb +37 -0
  42. data/templates/do_app/infra/config/environment.rb +86 -0
  43. data/templates/do_app/infra/config/error_config.rb +80 -0
  44. data/templates/do_app/infra/config/routes/api_routes.rb +2 -0
  45. data/templates/do_app/infra/config/routes/web_routes.rb +10 -0
  46. data/templates/do_app/infra/database/sequel_connection.rb +62 -0
  47. data/templates/do_app/infra/gems/database.rb +7 -0
  48. data/templates/do_app/infra/gems/load_all.rb +4 -0
  49. data/templates/do_app/infra/gems/utilities.rb +1 -0
  50. data/templates/do_app/infra/gems/web.rb +3 -0
  51. data/templates/do_app/infra/server/api-server.ru +13 -0
  52. data/templates/do_app/infra/server/web-server.ru +32 -0
  53. data/templates/do_app/package.json +9 -0
  54. data/templates/do_app/public/favicon.ico +0 -0
  55. data/templates/do_app/public/icons/docs.svg +10 -0
  56. data/templates/do_app/public/icons/expli.svg +13 -0
  57. data/templates/do_app/public/icons/git-repo.svg +13 -0
  58. data/templates/do_app/public/icons/hexagonal-arch.svg +15 -0
  59. data/templates/do_app/public/icons/template-engine.svg +26 -0
  60. data/templates/do_app/public/images/hyr-logo.png +0 -0
  61. data/templates/do_app/public/images/hyr-logo.webp +0 -0
  62. data/templates/do_app/public/index.html +22 -0
  63. data/templates/do_app/public/styles/css/main.css +418 -0
  64. data/templates/do_app/public/styles/css/spa.css +171 -0
  65. data/templates/do_app/shared/helpers/pagination_helper.rb +44 -0
  66. data/templates/do_app/shared/helpers/response_formatter.rb +25 -0
  67. data/templates/do_app/test/acceptance/api/articles_api_acceptance_test.rb +43 -0
  68. data/templates/do_app/test/acceptance/web/articles_acceptance_test.rb +31 -0
  69. data/templates/do_app/test/acceptance/web/home_acceptance_test.rb +17 -0
  70. data/templates/do_app/test/db.rb +106 -0
  71. data/templates/do_app/test/integration/adapter-exhaust/data-gateway/sequel_articles_gateway_test.rb +79 -0
  72. data/templates/do_app/test/integration/adapter-intake/api-app/request/articles_api_adapter_test.rb +61 -0
  73. data/templates/do_app/test/integration/adapter-intake/web-app/request/articles_web_adapter_test.rb +20 -0
  74. data/templates/do_app/test/integration/adapter-intake/web-app/request/home_web_adapter_test.rb +17 -0
  75. data/templates/do_app/test/integration/database/migration_test.rb +35 -0
  76. data/templates/do_app/test/support/mock_api_adapter.rb +82 -0
  77. data/templates/do_app/test/support/mock_articles_gateway.rb +41 -0
  78. data/templates/do_app/test/support/mock_web_adapter.rb +85 -0
  79. data/templates/do_app/test/support/test_patches.rb +33 -0
  80. data/templates/do_app/test/test_helper.rb +526 -0
  81. data/templates/do_app/test/unit/engine/circuit/articles_circuit_test.rb +167 -0
  82. data/templates/do_app/test/unit/engine/port/articles_gateway_port_test.rb +12 -0
  83. data/templates/do_app/test/unit/engine/source/article_test.rb +37 -0
  84. metadata +291 -0
@@ -0,0 +1,526 @@
1
+ # test/test_helper.rb
2
+ ENV['APP_ENV'] = 'test'
3
+
4
+ # Load Hyraft framework first
5
+ require 'hyraft'
6
+
7
+ require 'minitest/autorun'
8
+ require 'minitest/reporters'
9
+ require 'minitest/focus'
10
+ require 'mocha/minitest'
11
+ require 'rack/test'
12
+
13
+ Minitest::Reporters.use!(
14
+ Minitest::Reporters::SpecReporter.new(color: true)
15
+ )
16
+
17
+ # Database testing configuration
18
+ ENV['TEST_NO_DB'] ||= '0'
19
+
20
+ def database_available?
21
+ ENV['TEST_NO_DB'] == '0'
22
+ end
23
+
24
+ def skip_if_no_database
25
+ skip "Database tests disabled (TEST_NO_DB=1)" unless database_available?
26
+ end
27
+
28
+ # Safe require helper that doesn't fail if files don't exist
29
+ def safe_require_root(path)
30
+ full_path = File.expand_path("../#{path}.rb", __dir__)
31
+ if File.exist?(full_path)
32
+ require_relative "../#{path}"
33
+ true
34
+ else
35
+ puts "Note: #{path} not found (this is normal in framework tests)"
36
+ false
37
+ end
38
+ end
39
+
40
+ # Try to load core components
41
+ components_loaded = safe_require_root('engine/source/article')
42
+ safe_require_root('engine/port/articles_gateway_port')
43
+ safe_require_root('engine/circuit/articles_circuit')
44
+
45
+ # Define mock classes ONLY if the real ones aren't loaded
46
+ unless defined?(Article)
47
+ puts "Creating mock classes for framework testing..."
48
+
49
+ # Mock Article class inheriting from Engine::Source
50
+ class Article < Engine::Source
51
+ attr_accessor :title, :content, :created_at, :updated_at, :status
52
+
53
+ def initialize(id: nil, title: '', content: '', created_at: nil, updated_at: nil, status: :draft)
54
+ super(id: id)
55
+ @title = title
56
+ @content = content
57
+ @created_at = created_at || Time.now
58
+ @updated_at = updated_at
59
+ @status = status
60
+ end
61
+
62
+ def to_hash
63
+ super.merge({
64
+ title: @title,
65
+ content: @content,
66
+ created_at: @created_at,
67
+ updated_at: @updated_at,
68
+ status: @status
69
+ })
70
+ end
71
+
72
+ def publish
73
+ @status = :published
74
+ @updated_at = Time.now
75
+ self
76
+ end
77
+ end
78
+ end
79
+
80
+ unless defined?(ArticlesGatewayPort)
81
+ # Mock ArticlesGatewayPort class inheriting from Engine::Port
82
+ # This should be ABSTRACT to match test expectations - raises NotImplementedError
83
+ class ArticlesGatewayPort < Engine::Port
84
+ def save(entity)
85
+ raise NotImplementedError, "Subclass must implement save"
86
+ end
87
+
88
+ def all
89
+ raise NotImplementedError, "Subclass must implement all"
90
+ end
91
+
92
+ def find(id)
93
+ raise NotImplementedError, "Subclass must implement find"
94
+ end
95
+
96
+ def delete(id)
97
+ raise NotImplementedError, "Subclass must implement delete"
98
+ end
99
+ end
100
+ end
101
+
102
+ # Create a concrete implementation for other tests that need working gateways
103
+ unless defined?(MockArticlesGateway)
104
+ class MockArticlesGateway < ArticlesGatewayPort
105
+ def save(entity)
106
+ return nil if entity.nil?
107
+ entity.id ||= rand(1000).to_s
108
+ entity
109
+ end
110
+
111
+ def all
112
+ [Article.new]
113
+ end
114
+
115
+ def find(id)
116
+ Article.new(id: id, title: "Mock Article")
117
+ end
118
+
119
+ def delete(id)
120
+ true
121
+ end
122
+ end
123
+ end
124
+
125
+
126
+
127
+
128
+ unless defined?(ArticlesCircuit)
129
+ # Mock ArticlesCircuit class inheriting from Engine::Circuit
130
+ class ArticlesCircuit < Engine::Circuit
131
+ def initialize(gateway = MockArticlesGateway.new) # ← Change this line
132
+ @gateway = gateway
133
+ end
134
+
135
+ def create(title:, content:)
136
+ return nil if title.to_s.strip.empty? || content.to_s.strip.empty?
137
+
138
+ articles = @gateway.all
139
+ max_id = articles.map { |a| a.id.to_i }.max || 0
140
+ id = (max_id + 1).to_s
141
+ article = Article.new(id: id, title: title, content: content)
142
+ @gateway.save(article)
143
+ article
144
+ end
145
+
146
+ def list
147
+ @gateway.all
148
+ end
149
+
150
+ def find(id)
151
+ @gateway.find(id)
152
+ end
153
+
154
+ def update(id:, title:, content:)
155
+ return nil if title.to_s.strip.empty? || content.to_s.strip.empty?
156
+
157
+ article = @gateway.find(id)
158
+ return nil unless article
159
+
160
+ updated_article = Article.new(id: id, title: title, content: content)
161
+ @gateway.save(updated_article)
162
+ end
163
+
164
+ def delete(id)
165
+ article = @gateway.find(id)
166
+ return nil unless article
167
+
168
+ @gateway.delete(id)
169
+ end
170
+
171
+ def execute(input = {})
172
+ operation = input[:operation]
173
+ params = input[:params] || {}
174
+
175
+ case operation
176
+ when :create then create(**params)
177
+ when :list then list
178
+ when :find then find(params[:id])
179
+ when :update then update(**params)
180
+ when :delete then delete(params[:id])
181
+ else
182
+ raise "Unknown operation: #{operation}"
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+
189
+
190
+ # Mock adapter classes for when real adapters don't exist
191
+ unless defined?(ArticlesWebAdapter)
192
+ class ArticlesWebAdapter
193
+ def initialize
194
+ # Use mock circuit for testing
195
+ @articles = ArticlesCircuit.new(ArticlesGatewayPort.new)
196
+ end
197
+
198
+ # GET /articles
199
+ def index(request)
200
+ articles = @articles.list || []
201
+
202
+ {
203
+ status: 200,
204
+ locals: {
205
+ articles: articles
206
+ },
207
+ display: 'pages/articles/index.hyr'
208
+ }
209
+ end
210
+
211
+ # GET /articles/:id
212
+ def show(request)
213
+ id = request.params['route_params'].first
214
+ article = @articles.find(id)
215
+
216
+ if article
217
+ {
218
+ status: 200,
219
+ locals: {
220
+ article: article
221
+ },
222
+ display: 'pages/articles/show.hyr'
223
+ }
224
+ else
225
+ not_found_response(request)
226
+ end
227
+ end
228
+
229
+ # GET /articles/new - Show create form
230
+ def new(request)
231
+ {
232
+ status: 200,
233
+ locals: {},
234
+ display: 'pages/articles/new.hyr'
235
+ }
236
+ end
237
+
238
+ # POST /articles - Create article
239
+ def create(request)
240
+ data = request.params['data'] || {}
241
+
242
+ if data['title'] && data['content']
243
+ article = @articles.create(
244
+ title: data['title'],
245
+ content: data['content']
246
+ )
247
+
248
+ {
249
+ status: 303,
250
+ headers: { 'Location' => "/articles" },
251
+ locals: {}
252
+ }
253
+ else
254
+ {
255
+ status: 422,
256
+ locals: {
257
+ error: "Title and content are required"
258
+ },
259
+ display: 'pages/articles/new.hyr'
260
+ }
261
+ end
262
+ end
263
+
264
+ # GET /articles/:id/edit - Show edit form
265
+ def edit(request)
266
+ id = request.params['route_params'].first
267
+ article = @articles.find(id)
268
+
269
+ if article
270
+ {
271
+ status: 200,
272
+ locals: {
273
+ article: article
274
+ },
275
+ display: 'pages/articles/edit.hyr'
276
+ }
277
+ else
278
+ not_found_response(request)
279
+ end
280
+ end
281
+
282
+ # PUT /articles/:id - Update article
283
+ def update(request)
284
+ id = request.params['route_params'].first
285
+ data = request.params['data'] || {}
286
+
287
+ updated_article = @articles.update(
288
+ id: id,
289
+ title: data['title'],
290
+ content: data['content']
291
+ )
292
+
293
+ if updated_article
294
+ {
295
+ status: 303,
296
+ headers: { 'Location' => "/articles" },
297
+ locals: {}
298
+ }
299
+ else
300
+ {
301
+ status: 422,
302
+ locals: {
303
+ article: @articles.find(id),
304
+ error: "Failed to update article"
305
+ },
306
+ display: 'pages/articles/edit.hyr'
307
+ }
308
+ end
309
+ end
310
+
311
+ # DELETE /articles/:id - Delete article
312
+ def delete(request)
313
+ id = request.params['route_params'].first
314
+ @articles.delete(id)
315
+
316
+ {
317
+ status: 303,
318
+ headers: { 'Location' => "/articles" },
319
+ locals: {}
320
+ }
321
+ end
322
+
323
+ private
324
+
325
+ def not_found_response(request)
326
+ {
327
+ status: 404,
328
+ locals: {
329
+ error: "Article not found",
330
+ back_url: '/articles',
331
+ back_text: 'Back to articles'
332
+ },
333
+ display: 'pages/articles/404.hyr'
334
+ }
335
+ end
336
+ end
337
+ end
338
+
339
+ unless defined?(HomeWebAdapter)
340
+ class HomeWebAdapter
341
+ def call(request)
342
+ {
343
+ status: 200,
344
+ locals: {},
345
+ display: 'pages/home/index.hyr'
346
+ }
347
+ end
348
+ end
349
+ end
350
+
351
+ unless defined?(ArticlesApiAdapter)
352
+ class ArticlesApiAdapter
353
+ def index(request)
354
+ articles = [Article.new].map(&:to_hash)
355
+ {
356
+ status: 200,
357
+ locals: { articles: articles },
358
+ display: nil # JSON response
359
+ }
360
+ end
361
+
362
+ def show(request)
363
+ id = request.params['route_params'].first
364
+ {
365
+ status: 200,
366
+ locals: { article: Article.new(id: id).to_hash },
367
+ display: nil # JSON response
368
+ }
369
+ end
370
+ end
371
+ end
372
+
373
+ # Conditionally load database components
374
+ if database_available?
375
+ puts "Loading database components..."
376
+ if safe_require_root('infra/database/sequel_connection')
377
+ safe_require_root('adapter-exhaust/data-gateway/sequel_articles_gateway')
378
+ end
379
+ else
380
+ puts "Skipping database components (TEST_NO_DB=1)"
381
+ # Load mock components instead
382
+ if File.exist?(File.join(__dir__, 'support', 'mock_articles_gateway.rb'))
383
+ require_relative 'support/mock_articles_gateway'
384
+ end
385
+ end
386
+
387
+ # Load adapters if they exist
388
+ safe_require_root('adapter-intake/web-app/request/articles_web_adapter')
389
+ safe_require_root('adapter-intake/web-app/request/home_web_adapter')
390
+
391
+ # Load API adapter if it exists
392
+ begin
393
+ safe_require_root('adapter-intake/api-app/request/articles_api_adapter')
394
+ rescue LoadError => e
395
+ puts "Note: ArticlesApiAdapter not found: #{e.message}"
396
+ end
397
+
398
+ # Load test support files
399
+ if Dir.exist?(File.join(__dir__, 'support'))
400
+ Dir[File.join(__dir__, 'support', '*.rb')].each { |f| require f }
401
+ end
402
+
403
+ # Apply test patches when not using database
404
+ if File.exist?(File.join(__dir__, 'support', 'test_patches.rb'))
405
+ require_relative 'support/test_patches' unless database_available?
406
+ end
407
+
408
+ class Minitest::Test
409
+ def setup
410
+ setup_test_database if database_available?
411
+ end
412
+
413
+ def teardown
414
+ clear_test_data if database_available?
415
+ end
416
+
417
+ def setup_test_database
418
+ return unless database_available?
419
+
420
+ # Only setup database if SequelConnection is available
421
+ if defined?(SequelConnection) && SequelConnection.respond_to?(:db)
422
+ db = SequelConnection.db
423
+
424
+ # Run migrations if they haven't been run
425
+ migrations_dir = File.join(__dir__, '..', 'infra', 'database', 'migrations')
426
+ if Dir.exist?(migrations_dir) && db.tables.include?(:schema_migrations)
427
+ puts "Running test database migrations..."
428
+ Sequel::Migrator.run(db, migrations_dir)
429
+ end
430
+ end
431
+ end
432
+
433
+ def clear_test_data
434
+ return unless database_available?
435
+
436
+ if defined?(SequelConnection) && SequelConnection.respond_to?(:db)
437
+ db = SequelConnection.db
438
+ db[:articles].delete if db.tables.include?(:articles)
439
+ end
440
+ end
441
+
442
+ def create_test_article(attributes = {})
443
+ Article.new(
444
+ id: attributes[:id] || rand(1000).to_s,
445
+ title: attributes[:title] || "Test Article",
446
+ content: attributes[:content] || "Test content",
447
+ created_at: attributes[:created_at] || Time.now,
448
+ updated_at: attributes[:updated_at] || Time.now,
449
+ status: attributes[:status] || :draft
450
+ )
451
+ end
452
+
453
+
454
+ # In the Minitest::Test class, update the articles_gateway method:
455
+ def articles_gateway
456
+ if database_available? && defined?(SequelArticlesGateway)
457
+ SequelArticlesGateway.new
458
+ else
459
+ # Always use the concrete mock implementation
460
+ MockArticlesGateway.new
461
+ end
462
+ end
463
+
464
+ # Helper to create circuit with appropriate gateway
465
+ def articles_circuit
466
+ ArticlesCircuit.new(articles_gateway)
467
+ end
468
+ end
469
+
470
+
471
+
472
+ # Run all tests
473
+ =begin
474
+
475
+ rake test
476
+
477
+ # Or run specific test types
478
+ rake test:unit
479
+ rake test:integration
480
+
481
+ # Run single test file
482
+ ruby -I test test/unit/engine/source/article_test.rb
483
+
484
+
485
+ # Check what require statements you currently have
486
+ grep -r "require_relative.*test_helper" test/
487
+
488
+
489
+ ruby test/db.rb
490
+
491
+ to Run test and save to database:
492
+
493
+ APP_ENV=test hyr s thin
494
+
495
+ to run in PROD:
496
+
497
+ APP_ENV=production hyr s thin
498
+
499
+ APP_ENV=production hyr s thin --api
500
+
501
+
502
+
503
+ # Run only unit tests (no database needed)
504
+ TEST_NO_DB=1 rake test test/unit/
505
+
506
+ # Run only acceptance tests
507
+ TEST_NO_DB=1 rake test test/acceptance/
508
+
509
+ # Run only integration tests that don't need database
510
+ TEST_NO_DB=1 rake test test/integration/adapter-intake/
511
+
512
+
513
+ # Run WITHOUT database (--------------------------)
514
+ TEST_NO_DB=1 rake test
515
+
516
+ # Run WITH database (if available)
517
+ rake test
518
+
519
+
520
+ Environment Server Migrations
521
+
522
+ Development hyr s thin hyraft-rule-migrate migrate
523
+ Test APP_ENV=test hyr s thin APP_ENV=test hyraft-rule-migrate migrate
524
+ Production APP_ENV=production hyr s thin APP_ENV=production hyraft-rule-migrate migrate
525
+
526
+ =end
@@ -0,0 +1,167 @@
1
+ # test/unit/engine/circuit/articles_circuit_test.rb
2
+ require_relative "../../../test_helper"
3
+
4
+ class ArticlesCircuitTest < Minitest::Test
5
+ def setup
6
+ @mock_gateway = mock('gateway')
7
+ @circuit = ArticlesCircuit.new(@mock_gateway)
8
+ end
9
+
10
+ def test_create_article
11
+ article_data = { title: "Test", content: "Content" }
12
+ mock_article = Article.new(id: "1", title: "Test", content: "Content")
13
+
14
+ @mock_gateway.expects(:all).returns([])
15
+ @mock_gateway.expects(:save).returns(mock_article)
16
+
17
+ result = @circuit.create(**article_data)
18
+
19
+ assert_equal "1", result.id
20
+ assert_equal "Test", result.title
21
+ end
22
+
23
+ def test_create_article_with_empty_title
24
+ article_data = { title: "", content: "Content" }
25
+
26
+ # Gateway should not be called when validation fails
27
+ @mock_gateway.expects(:all).never
28
+ @mock_gateway.expects(:save).never
29
+
30
+ result = @circuit.create(**article_data)
31
+
32
+ assert_nil result
33
+ end
34
+
35
+ def test_create_article_with_empty_content
36
+ article_data = { title: "Test", content: "" }
37
+
38
+ # Gateway should not be called when validation fails
39
+ @mock_gateway.expects(:all).never
40
+ @mock_gateway.expects(:save).never
41
+
42
+ result = @circuit.create(**article_data)
43
+
44
+ assert_nil result
45
+ end
46
+
47
+ def test_create_article_with_nil_values
48
+ article_data = { title: nil, content: "Content" }
49
+
50
+ @mock_gateway.expects(:all).never
51
+ @mock_gateway.expects(:save).never
52
+
53
+ result = @circuit.create(**article_data)
54
+
55
+ assert_nil result
56
+ end
57
+
58
+ def test_create_article_with_whitespace_only
59
+ article_data = { title: " ", content: "Content" }
60
+
61
+ @mock_gateway.expects(:all).never
62
+ @mock_gateway.expects(:save).never
63
+
64
+ result = @circuit.create(**article_data)
65
+
66
+ assert_nil result
67
+ end
68
+
69
+ def test_list_articles
70
+ mock_articles = [
71
+ Article.new(id: "1", title: "Article 1", content: "Content 1"),
72
+ Article.new(id: "2", title: "Article 2", content: "Content 2")
73
+ ]
74
+
75
+ @mock_gateway.expects(:all).returns(mock_articles)
76
+
77
+ result = @circuit.list
78
+
79
+ assert_equal 2, result.size
80
+ assert_equal "Article 1", result.first.title
81
+ assert_equal "Article 2", result.last.title
82
+ end
83
+
84
+ def test_find_article
85
+ mock_article = Article.new(id: "1", title: "Test Article", content: "Test Content")
86
+
87
+ @mock_gateway.expects(:find).with("1").returns(mock_article)
88
+
89
+ result = @circuit.find("1")
90
+
91
+ assert_equal "1", result.id
92
+ assert_equal "Test Article", result.title
93
+ assert_equal "Test Content", result.content
94
+ end
95
+
96
+ def test_find_nonexistent_article
97
+ @mock_gateway.expects(:find).with("999").returns(nil)
98
+
99
+ result = @circuit.find("999")
100
+
101
+ assert_nil result
102
+ end
103
+
104
+ def test_update_article
105
+ existing_article = Article.new(id: "1", title: "Old Title", content: "Old Content")
106
+ updated_article = Article.new(id: "1", title: "New Title", content: "New Content")
107
+
108
+ @mock_gateway.expects(:find).with("1").returns(existing_article)
109
+ # Don't expect specific object instance, just any Article with id "1"
110
+ @mock_gateway.expects(:save).with(instance_of(Article)).returns(updated_article)
111
+
112
+ result = @circuit.update(id: "1", title: "New Title", content: "New Content")
113
+
114
+ assert_equal "1", result.id
115
+ assert_equal "New Title", result.title
116
+ assert_equal "New Content", result.content
117
+ end
118
+
119
+ def test_update_article_with_empty_title
120
+ # No gateway calls should happen when validation fails
121
+ @mock_gateway.expects(:find).never
122
+ @mock_gateway.expects(:save).never
123
+
124
+ result = @circuit.update(id: "1", title: "", content: "New Content")
125
+
126
+ assert_nil result
127
+ end
128
+
129
+ def test_update_article_with_empty_content
130
+ # No gateway calls should happen when validation fails
131
+ @mock_gateway.expects(:find).never
132
+ @mock_gateway.expects(:save).never
133
+
134
+ result = @circuit.update(id: "1", title: "New Title", content: "")
135
+
136
+ assert_nil result
137
+ end
138
+
139
+ def test_update_nonexistent_article
140
+ @mock_gateway.expects(:find).with("999").returns(nil)
141
+ @mock_gateway.expects(:save).never
142
+
143
+ result = @circuit.update(id: "999", title: "New Title", content: "New Content")
144
+
145
+ assert_nil result
146
+ end
147
+
148
+ def test_delete_article
149
+ existing_article = Article.new(id: "1", title: "Test Article", content: "Test Content")
150
+
151
+ @mock_gateway.expects(:find).with("1").returns(existing_article)
152
+ @mock_gateway.expects(:delete).with("1").returns(true)
153
+
154
+ result = @circuit.delete("1")
155
+
156
+ assert_equal true, result
157
+ end
158
+
159
+ def test_delete_nonexistent_article
160
+ @mock_gateway.expects(:find).with("999").returns(nil)
161
+ @mock_gateway.expects(:delete).never
162
+
163
+ result = @circuit.delete("999")
164
+
165
+ assert_nil result
166
+ end
167
+ end
@@ -0,0 +1,12 @@
1
+ require_relative "./../../../test_helper"
2
+
3
+ class ArticlesGatewayPortTest < Minitest::Test
4
+ def test_interface_methods
5
+ port = ArticlesGatewayPort.new
6
+
7
+ assert_raises(NotImplementedError) { port.save(nil) }
8
+ assert_raises(NotImplementedError) { port.all }
9
+ assert_raises(NotImplementedError) { port.find("1") }
10
+ assert_raises(NotImplementedError) { port.delete("1") }
11
+ end
12
+ end