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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +231 -0
- data/exe/hyraft +5 -0
- data/lib/hyraft/boot/asset_preloader.rb +185 -0
- data/lib/hyraft/boot/preloaded_static.rb +46 -0
- data/lib/hyraft/boot/preloader.rb +206 -0
- data/lib/hyraft/cli.rb +187 -0
- data/lib/hyraft/compiler/compiler.rb +34 -0
- data/lib/hyraft/compiler/html_purifier.rb +181 -0
- data/lib/hyraft/compiler/javascript_library.rb +281 -0
- data/lib/hyraft/compiler/javascript_obfuscator.rb +141 -0
- data/lib/hyraft/compiler/parser.rb +27 -0
- data/lib/hyraft/compiler/renderer.rb +217 -0
- data/lib/hyraft/engine/circuit.rb +35 -0
- data/lib/hyraft/engine/port.rb +17 -0
- data/lib/hyraft/engine/source.rb +19 -0
- data/lib/hyraft/engine.rb +11 -0
- data/lib/hyraft/router/api_router.rb +65 -0
- data/lib/hyraft/router/web_router.rb +136 -0
- data/lib/hyraft/system_info.rb +26 -0
- data/lib/hyraft/version.rb +5 -0
- data/lib/hyraft.rb +48 -0
- data/templates/do_app/Gemfile +50 -0
- data/templates/do_app/Rakefile +88 -0
- data/templates/do_app/adapter-intake/web-app/display/pages/home/home.hyr +174 -0
- data/templates/do_app/adapter-intake/web-app/request/home_web_adapter.rb +19 -0
- data/templates/do_app/boot.rb +41 -0
- data/templates/do_app/framework/adapters/server/server_api_adapter.rb +51 -0
- data/templates/do_app/framework/adapters/server/server_web_adapter.rb +178 -0
- data/templates/do_app/framework/compiler/style_resolver.rb +33 -0
- data/templates/do_app/framework/errors/error_handler.rb +75 -0
- data/templates/do_app/framework/errors/templates/304.html +22 -0
- data/templates/do_app/framework/errors/templates/400.html +22 -0
- data/templates/do_app/framework/errors/templates/401.html +22 -0
- data/templates/do_app/framework/errors/templates/403.html +22 -0
- data/templates/do_app/framework/errors/templates/404.html +62 -0
- data/templates/do_app/framework/errors/templates/500.html +73 -0
- data/templates/do_app/framework/middleware/cors_middleware.rb +37 -0
- data/templates/do_app/infra/config/environment.rb +86 -0
- data/templates/do_app/infra/config/error_config.rb +80 -0
- data/templates/do_app/infra/config/routes/api_routes.rb +2 -0
- data/templates/do_app/infra/config/routes/web_routes.rb +10 -0
- data/templates/do_app/infra/database/sequel_connection.rb +62 -0
- data/templates/do_app/infra/gems/database.rb +7 -0
- data/templates/do_app/infra/gems/load_all.rb +4 -0
- data/templates/do_app/infra/gems/utilities.rb +1 -0
- data/templates/do_app/infra/gems/web.rb +3 -0
- data/templates/do_app/infra/server/api-server.ru +13 -0
- data/templates/do_app/infra/server/web-server.ru +32 -0
- data/templates/do_app/package.json +9 -0
- data/templates/do_app/public/favicon.ico +0 -0
- data/templates/do_app/public/icons/docs.svg +10 -0
- data/templates/do_app/public/icons/expli.svg +13 -0
- data/templates/do_app/public/icons/git-repo.svg +13 -0
- data/templates/do_app/public/icons/hexagonal-arch.svg +15 -0
- data/templates/do_app/public/icons/template-engine.svg +26 -0
- data/templates/do_app/public/images/hyr-logo.png +0 -0
- data/templates/do_app/public/images/hyr-logo.webp +0 -0
- data/templates/do_app/public/index.html +22 -0
- data/templates/do_app/public/styles/css/main.css +418 -0
- data/templates/do_app/public/styles/css/spa.css +171 -0
- data/templates/do_app/shared/helpers/pagination_helper.rb +44 -0
- data/templates/do_app/shared/helpers/response_formatter.rb +25 -0
- data/templates/do_app/test/acceptance/api/articles_api_acceptance_test.rb +43 -0
- data/templates/do_app/test/acceptance/web/articles_acceptance_test.rb +31 -0
- data/templates/do_app/test/acceptance/web/home_acceptance_test.rb +17 -0
- data/templates/do_app/test/db.rb +106 -0
- data/templates/do_app/test/integration/adapter-exhaust/data-gateway/sequel_articles_gateway_test.rb +79 -0
- data/templates/do_app/test/integration/adapter-intake/api-app/request/articles_api_adapter_test.rb +61 -0
- data/templates/do_app/test/integration/adapter-intake/web-app/request/articles_web_adapter_test.rb +20 -0
- data/templates/do_app/test/integration/adapter-intake/web-app/request/home_web_adapter_test.rb +17 -0
- data/templates/do_app/test/integration/database/migration_test.rb +35 -0
- data/templates/do_app/test/support/mock_api_adapter.rb +82 -0
- data/templates/do_app/test/support/mock_articles_gateway.rb +41 -0
- data/templates/do_app/test/support/mock_web_adapter.rb +85 -0
- data/templates/do_app/test/support/test_patches.rb +33 -0
- data/templates/do_app/test/test_helper.rb +526 -0
- data/templates/do_app/test/unit/engine/circuit/articles_circuit_test.rb +167 -0
- data/templates/do_app/test/unit/engine/port/articles_gateway_port_test.rb +12 -0
- data/templates/do_app/test/unit/engine/source/article_test.rb +37 -0
- 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
|