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,17 @@
|
|
|
1
|
+
require_relative "../../test_helper"
|
|
2
|
+
|
|
3
|
+
class HomeAcceptanceTest < Minitest::Test
|
|
4
|
+
include Rack::Test::Methods
|
|
5
|
+
|
|
6
|
+
def app
|
|
7
|
+
->(env) {
|
|
8
|
+
[200, {'Content-Type' => 'text/html'}, ['Home page']]
|
|
9
|
+
}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_visit_homepage
|
|
13
|
+
get '/'
|
|
14
|
+
assert_equal 200, last_response.status
|
|
15
|
+
assert_includes last_response.body, 'Home page'
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# test/db.rb
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'sequel'
|
|
5
|
+
|
|
6
|
+
# Load environment config
|
|
7
|
+
config = YAML.load_file('env.yml')
|
|
8
|
+
test_config = config['test'] # test /development / production
|
|
9
|
+
|
|
10
|
+
puts "Testing database connection with config:"
|
|
11
|
+
puts " Database: #{test_config['DB_DATABASE']}"
|
|
12
|
+
puts " Adapter: #{test_config['DB_CONNECTION']}"
|
|
13
|
+
puts " Host: #{test_config['DB_HOST']}"
|
|
14
|
+
|
|
15
|
+
begin
|
|
16
|
+
# Map adapter names for Sequel
|
|
17
|
+
adapter = case test_config['DB_CONNECTION'].to_s.downcase
|
|
18
|
+
when 'mysql', 'mysql2' then 'mysql2'
|
|
19
|
+
when 'postgres', 'postgresql', 'pgsql' then 'postgres'
|
|
20
|
+
when 'sqlite', 'sqlite3' then 'sqlite'
|
|
21
|
+
else
|
|
22
|
+
test_config['DB_CONNECTION']
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Build connection options based on adapter type
|
|
26
|
+
connection_options = {
|
|
27
|
+
adapter: adapter,
|
|
28
|
+
database: test_config['DB_DATABASE']
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Add common options for client-server databases
|
|
32
|
+
case adapter
|
|
33
|
+
when 'mysql2', 'postgres'
|
|
34
|
+
connection_options[:host] = test_config['DB_HOST'] if test_config['DB_HOST']
|
|
35
|
+
connection_options[:port] = test_config['DB_PORT'] if test_config['DB_PORT']
|
|
36
|
+
connection_options[:user] = test_config['DB_USERNAME'] if test_config['DB_USERNAME']
|
|
37
|
+
connection_options[:password] = test_config['DB_PASSWORD'] if test_config['DB_PASSWORD']
|
|
38
|
+
connection_options[:encoding] = test_config['DB_CHARSET'] if test_config['DB_CHARSET']
|
|
39
|
+
|
|
40
|
+
# MySQL-specific options
|
|
41
|
+
if adapter == 'mysql2'
|
|
42
|
+
# Only use socket if specified and file exists
|
|
43
|
+
if test_config['DB_SOCKET'] && File.exist?(test_config['DB_SOCKET'])
|
|
44
|
+
connection_options[:socket] = test_config['DB_SOCKET']
|
|
45
|
+
puts " Using socket: #{test_config['DB_SOCKET']}"
|
|
46
|
+
else
|
|
47
|
+
puts " Using TCP connection"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
when 'sqlite'
|
|
52
|
+
# SQLite doesn't need host/port/user/password
|
|
53
|
+
puts " Using SQLite database file"
|
|
54
|
+
else
|
|
55
|
+
puts " Unknown adapter: #{adapter}, using basic connection"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
puts " Connection options: #{connection_options.reject { |k,v| k == :password }}"
|
|
59
|
+
|
|
60
|
+
# Connect to the database
|
|
61
|
+
db = Sequel.connect(connection_options)
|
|
62
|
+
|
|
63
|
+
# Test connection
|
|
64
|
+
db.test_connection
|
|
65
|
+
puts "✅ Database connection successful!"
|
|
66
|
+
|
|
67
|
+
# Show tables
|
|
68
|
+
tables = db.tables
|
|
69
|
+
if tables.any?
|
|
70
|
+
puts "📊 Tables in database: #{tables.join(', ')}"
|
|
71
|
+
else
|
|
72
|
+
puts "📊 No tables in database"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Show database version
|
|
76
|
+
begin
|
|
77
|
+
version = case adapter
|
|
78
|
+
when 'mysql2' then db['SELECT VERSION() as version'].first[:version]
|
|
79
|
+
when 'postgres' then db['SELECT version()'].first[:version].split(',')[0]
|
|
80
|
+
when 'sqlite' then db['SELECT sqlite_version()'].first.values.first
|
|
81
|
+
else "Unknown"
|
|
82
|
+
end
|
|
83
|
+
puts "🔧 Database version: #{version}"
|
|
84
|
+
rescue => e
|
|
85
|
+
puts "🔧 Could not determine database version: #{e.message}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
db.disconnect
|
|
89
|
+
|
|
90
|
+
rescue LoadError => e
|
|
91
|
+
puts "❌ Missing gem: #{e.message}"
|
|
92
|
+
puts "💡 Tip: Make sure the required database gem is installed:"
|
|
93
|
+
case adapter
|
|
94
|
+
when 'mysql2' then puts " gem install mysql2"
|
|
95
|
+
when 'postgres' then puts " gem install pg"
|
|
96
|
+
when 'sqlite' then puts " gem install sqlite3"
|
|
97
|
+
end
|
|
98
|
+
rescue => e
|
|
99
|
+
puts "❌ Database connection failed: #{e.message}"
|
|
100
|
+
puts "💡 Debug info:"
|
|
101
|
+
puts " - Adapter: #{adapter}"
|
|
102
|
+
puts " - Connection options used: #{connection_options.reject { |k,v| k == :password }.inspect}"
|
|
103
|
+
if test_config['DB_SOCKET']
|
|
104
|
+
puts " - Socket file exists: #{File.exist?(test_config['DB_SOCKET'])}"
|
|
105
|
+
end
|
|
106
|
+
end
|
data/templates/do_app/test/integration/adapter-exhaust/data-gateway/sequel_articles_gateway_test.rb
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# test/integration/adapter-exhaust/data-gateway/sequel_articles_gateway_test.rb
|
|
2
|
+
|
|
3
|
+
require_relative "./../../../test_helper"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SequelArticlesGatewayTest < Minitest::Test
|
|
7
|
+
def setup
|
|
8
|
+
skip_if_no_database # This test requires a real database
|
|
9
|
+
@gateway = SequelArticlesGateway.new
|
|
10
|
+
clear_test_data
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def teardown
|
|
14
|
+
clear_test_data if database_available?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_save_new_article
|
|
19
|
+
article = Article.new(title: "Test Article", content: "Test content")
|
|
20
|
+
|
|
21
|
+
saved_article = @gateway.save(article)
|
|
22
|
+
|
|
23
|
+
assert saved_article.id
|
|
24
|
+
assert_equal "Test Article", saved_article.title
|
|
25
|
+
assert_equal "Test content", saved_article.content
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_find_article
|
|
29
|
+
article = Article.new(title: "Find Me", content: "Content")
|
|
30
|
+
saved_article = @gateway.save(article)
|
|
31
|
+
|
|
32
|
+
found_article = @gateway.find(saved_article.id)
|
|
33
|
+
|
|
34
|
+
assert_equal saved_article.id, found_article.id
|
|
35
|
+
assert_equal "Find Me", found_article.title
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_list_articles
|
|
39
|
+
@gateway.save(Article.new(title: "First", content: "Content1"))
|
|
40
|
+
@gateway.save(Article.new(title: "Second", content: "Content2"))
|
|
41
|
+
|
|
42
|
+
articles = @gateway.all
|
|
43
|
+
|
|
44
|
+
assert_equal 2, articles.size
|
|
45
|
+
# Remove the order assertion or fix the expected order
|
|
46
|
+
# Just test that both articles are returned
|
|
47
|
+
titles = articles.map(&:title)
|
|
48
|
+
assert_includes titles, "First"
|
|
49
|
+
assert_includes titles, "Second"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_update_article
|
|
53
|
+
article = Article.new(title: "Original", content: "Content")
|
|
54
|
+
saved_article = @gateway.save(article)
|
|
55
|
+
|
|
56
|
+
saved_article.title = "Updated"
|
|
57
|
+
updated_article = @gateway.save(saved_article)
|
|
58
|
+
|
|
59
|
+
assert_equal "Updated", updated_article.title
|
|
60
|
+
assert_equal saved_article.id, updated_article.id
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_delete_article
|
|
64
|
+
article = Article.new(title: "To Delete", content: "Content")
|
|
65
|
+
saved_article = @gateway.save(article)
|
|
66
|
+
|
|
67
|
+
@gateway.delete(saved_article.id)
|
|
68
|
+
|
|
69
|
+
assert_nil @gateway.find(saved_article.id)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def clear_test_data
|
|
75
|
+
# This should be defined in your test_helper.rb
|
|
76
|
+
db = SequelConnection.db
|
|
77
|
+
db[:articles].delete if db.tables.include?(:articles)
|
|
78
|
+
end
|
|
79
|
+
end
|
data/templates/do_app/test/integration/adapter-intake/api-app/request/articles_api_adapter_test.rb
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# In test/integration/adapter-intake/api-app/request/articles_api_adapter_test.rb
|
|
2
|
+
|
|
3
|
+
def test_create_article
|
|
4
|
+
post '/api/articles',
|
|
5
|
+
{ title: "New API Article", content: "API Content" }.to_json,
|
|
6
|
+
{ 'CONTENT_TYPE' => 'application/json' }
|
|
7
|
+
|
|
8
|
+
assert_equal 201, last_response.status
|
|
9
|
+
response = JSON.parse(last_response.body)
|
|
10
|
+
|
|
11
|
+
# Debug the actual response
|
|
12
|
+
puts "DEBUG: Create response: #{response}"
|
|
13
|
+
|
|
14
|
+
assert response["id"], "Article should have an ID. Response: #{response}"
|
|
15
|
+
assert_equal "New API Article", response["title"]
|
|
16
|
+
assert_equal "API Content", response["content"]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_get_article
|
|
20
|
+
# Create test article through the API first
|
|
21
|
+
post '/api/articles',
|
|
22
|
+
{ title: "Single Article", content: "Single Content" }.to_json,
|
|
23
|
+
{ 'CONTENT_TYPE' => 'application/json' }
|
|
24
|
+
|
|
25
|
+
assert_equal 201, last_response.status
|
|
26
|
+
created_article = JSON.parse(last_response.body)
|
|
27
|
+
puts "DEBUG: Created article: #{created_article}"
|
|
28
|
+
|
|
29
|
+
get "/api/articles/#{created_article['id']}"
|
|
30
|
+
|
|
31
|
+
puts "DEBUG: Get response status: #{last_response.status}"
|
|
32
|
+
puts "DEBUG: Get response body: #{last_response.body}"
|
|
33
|
+
|
|
34
|
+
assert_equal 200, last_response.status
|
|
35
|
+
response = JSON.parse(last_response.body)
|
|
36
|
+
assert_equal "Single Article", response["title"]
|
|
37
|
+
assert_equal "Single Content", response["content"]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_update_article
|
|
41
|
+
# Create article first through the API
|
|
42
|
+
post '/api/articles',
|
|
43
|
+
{ title: "Original", content: "Original Content" }.to_json,
|
|
44
|
+
{ 'CONTENT_TYPE' => 'application/json' }
|
|
45
|
+
|
|
46
|
+
assert_equal 201, last_response.status
|
|
47
|
+
created_article = JSON.parse(last_response.body)
|
|
48
|
+
puts "DEBUG: Created article for update: #{created_article}"
|
|
49
|
+
|
|
50
|
+
put "/api/articles/#{created_article['id']}",
|
|
51
|
+
{ title: "Updated", content: "Updated Content" }.to_json,
|
|
52
|
+
{ 'CONTENT_TYPE' => 'application/json' }
|
|
53
|
+
|
|
54
|
+
puts "DEBUG: Update response status: #{last_response.status}"
|
|
55
|
+
puts "DEBUG: Update response body: #{last_response.body}"
|
|
56
|
+
|
|
57
|
+
assert_equal 200, last_response.status
|
|
58
|
+
response = JSON.parse(last_response.body)
|
|
59
|
+
assert_equal "Updated", response["title"]
|
|
60
|
+
assert_equal "Updated Content", response["content"]
|
|
61
|
+
end
|
data/templates/do_app/test/integration/adapter-intake/web-app/request/articles_web_adapter_test.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# test/integration/adapter-intake/web-app/request/articles_web_adapter_test.rb
|
|
2
|
+
require_relative "../../../../test_helper"
|
|
3
|
+
|
|
4
|
+
class ArticlesWebAdapterTest < Minitest::Test
|
|
5
|
+
def setup
|
|
6
|
+
@adapter = ArticlesWebAdapter.new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def test_index_action
|
|
10
|
+
request = mock('request')
|
|
11
|
+
request.stubs(:params).returns({})
|
|
12
|
+
|
|
13
|
+
response = @adapter.index(request)
|
|
14
|
+
|
|
15
|
+
assert_equal 200, response[:status]
|
|
16
|
+
assert_equal 'pages/articles/index.hyr', response[:display]
|
|
17
|
+
assert response[:locals][:articles]
|
|
18
|
+
assert_kind_of Array, response[:locals][:articles]
|
|
19
|
+
end
|
|
20
|
+
end
|
data/templates/do_app/test/integration/adapter-intake/web-app/request/home_web_adapter_test.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require_relative "../../../../test_helper"
|
|
2
|
+
|
|
3
|
+
class HomeWebAdapterTest < Minitest::Test
|
|
4
|
+
def setup
|
|
5
|
+
@adapter = HomeWebAdapter.new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def test_home_page
|
|
9
|
+
request = mock('request')
|
|
10
|
+
|
|
11
|
+
response = @adapter.home_page(request)
|
|
12
|
+
|
|
13
|
+
assert_equal 200, response[:status]
|
|
14
|
+
assert_equal 'home/home.hyr', response[:display]
|
|
15
|
+
assert_equal "Welcome to Hyraft", response[:locals][:page_title]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# test/integration/database/migration_test.rb
|
|
2
|
+
require_relative "../../test_helper"
|
|
3
|
+
|
|
4
|
+
class MigrationTest < Minitest::Test
|
|
5
|
+
def setup
|
|
6
|
+
skip_if_no_database
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def test_migrations_can_run_successfully
|
|
10
|
+
# Remove the duplicate skip condition - setup already handles it
|
|
11
|
+
db = SequelConnection.db
|
|
12
|
+
assert db, "Database connection should be established"
|
|
13
|
+
|
|
14
|
+
assert db.tables.include?(:articles), "Articles table should exist from migrations"
|
|
15
|
+
|
|
16
|
+
columns = db.schema(:articles).map { |col| col.first }
|
|
17
|
+
|
|
18
|
+
assert_includes columns, :id
|
|
19
|
+
assert_includes columns, :title
|
|
20
|
+
assert_includes columns, :content
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_database_connection_works_for_migrations
|
|
24
|
+
db = SequelConnection.db
|
|
25
|
+
assert db.test_connection
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def skip_if_no_database
|
|
31
|
+
if ENV['TEST_NO_DB'] == '1'
|
|
32
|
+
skip "Skipping database test (TEST_NO_DB=1)"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# test/support/mock_api_adapter.rb
|
|
2
|
+
module MockApiAdapter
|
|
3
|
+
def initialize
|
|
4
|
+
@gateway = MockArticlesGateway.new
|
|
5
|
+
@articles_circuit = ArticlesCircuit.new(@gateway)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def index(env)
|
|
9
|
+
begin
|
|
10
|
+
articles = @articles_circuit.list
|
|
11
|
+
[200, { 'Content-Type' => 'application/json' }, [articles.to_json]]
|
|
12
|
+
rescue => e
|
|
13
|
+
puts "API Error: #{e.message}"
|
|
14
|
+
[500, { 'Content-Type' => 'application/json' }, [{ error: "Internal server error" }.to_json]]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def show(env, id)
|
|
19
|
+
begin
|
|
20
|
+
article = @articles_circuit.find(id)
|
|
21
|
+
if article
|
|
22
|
+
[200, { 'Content-Type' => 'application/json' }, [article.to_json]]
|
|
23
|
+
else
|
|
24
|
+
[404, { 'Content-Type' => 'application/json' }, [{ error: "Article not found" }.to_json]]
|
|
25
|
+
end
|
|
26
|
+
rescue => e
|
|
27
|
+
puts "API Error: #{e.message}"
|
|
28
|
+
[500, { 'Content-Type' => 'application/json' }, [{ error: "Internal server error" }.to_json]]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def create(env)
|
|
33
|
+
request = Rack::Request.new(env)
|
|
34
|
+
begin
|
|
35
|
+
data = JSON.parse(request.body.read)
|
|
36
|
+
article_data = data.transform_keys(&:to_sym)
|
|
37
|
+
article = @articles_circuit.create(**article_data)
|
|
38
|
+
if article
|
|
39
|
+
[201, { 'Content-Type' => 'application/json' }, [article.to_json]]
|
|
40
|
+
else
|
|
41
|
+
[422, { 'Content-Type' => 'application/json' }, [{ error: "Title and content are required" }.to_json]]
|
|
42
|
+
end
|
|
43
|
+
rescue JSON::ParserError
|
|
44
|
+
[400, { 'Content-Type' => 'application/json' }, [{ error: "Invalid JSON" }.to_json]]
|
|
45
|
+
rescue => e
|
|
46
|
+
puts "API Error: #{e.message}"
|
|
47
|
+
[422, { 'Content-Type' => 'application/json' }, [{ error: e.message }.to_json]]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def update(env, id)
|
|
52
|
+
request = Rack::Request.new(env)
|
|
53
|
+
begin
|
|
54
|
+
data = JSON.parse(request.body.read)
|
|
55
|
+
article = @articles_circuit.update(id: id, **data.transform_keys(&:to_sym))
|
|
56
|
+
if article
|
|
57
|
+
[200, { 'Content-Type' => 'application/json' }, [article.to_json]]
|
|
58
|
+
else
|
|
59
|
+
[404, { 'Content-Type' => 'application/json' }, [{ error: "Article not found" }.to_json]]
|
|
60
|
+
end
|
|
61
|
+
rescue JSON::ParserError
|
|
62
|
+
[400, { 'Content-Type' => 'application/json' }, [{ error: "Invalid JSON" }.to_json]]
|
|
63
|
+
rescue => e
|
|
64
|
+
puts "API Update Error: #{e.message}"
|
|
65
|
+
[422, { 'Content-Type' => 'application/json' }, [{ error: e.message }.to_json]]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def delete(env, id)
|
|
70
|
+
begin
|
|
71
|
+
result = @articles_circuit.delete(id)
|
|
72
|
+
if result
|
|
73
|
+
[200, { 'Content-Type' => 'application/json' }, [{ message: "Article deleted successfully" }.to_json]]
|
|
74
|
+
else
|
|
75
|
+
[404, { 'Content-Type' => 'application/json' }, [{ error: "Article not found" }.to_json]]
|
|
76
|
+
end
|
|
77
|
+
rescue => e
|
|
78
|
+
puts "API Error: #{e.message}"
|
|
79
|
+
[500, { 'Content-Type' => 'application/json' }, [{ error: "Internal server error" }.to_json]]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# test/support/mock_articles_gateway.rb
|
|
2
|
+
class MockArticlesGateway
|
|
3
|
+
def initialize
|
|
4
|
+
@storage = {}
|
|
5
|
+
@next_id = 1
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def all
|
|
9
|
+
@storage.values.sort_by { |a| a.id.to_i }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def find(id)
|
|
13
|
+
@storage[id.to_s]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def save(article)
|
|
17
|
+
# Ensure the article has proper timestamps
|
|
18
|
+
current_time = Time.now
|
|
19
|
+
if article.created_at.nil?
|
|
20
|
+
article.created_at = current_time
|
|
21
|
+
end
|
|
22
|
+
article.updated_at = current_time
|
|
23
|
+
|
|
24
|
+
if article.id.nil?
|
|
25
|
+
article.id = @next_id.to_s
|
|
26
|
+
@next_id += 1
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@storage[article.id] = article
|
|
30
|
+
article
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def delete(id)
|
|
34
|
+
!!@storage.delete(id.to_s)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def clear
|
|
38
|
+
@storage.clear
|
|
39
|
+
@next_id = 1
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# test/support/mock_web_adapter.rb
|
|
2
|
+
module MockWebAdapter
|
|
3
|
+
def initialize
|
|
4
|
+
@gateway = MockArticlesGateway.new
|
|
5
|
+
@articles_circuit = ArticlesCircuit.new(@gateway)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def index(request)
|
|
9
|
+
begin
|
|
10
|
+
articles = @articles_circuit.list
|
|
11
|
+
{
|
|
12
|
+
status: 200,
|
|
13
|
+
display: 'pages/articles/index.hyr',
|
|
14
|
+
locals: { articles: articles }
|
|
15
|
+
}
|
|
16
|
+
rescue => e
|
|
17
|
+
puts "Web Adapter Error: #{e.message}"
|
|
18
|
+
{
|
|
19
|
+
status: 500,
|
|
20
|
+
display: 'pages/error.hyr',
|
|
21
|
+
locals: { error: "Internal server error" }
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def show(request, id)
|
|
27
|
+
begin
|
|
28
|
+
article = @articles_circuit.find(id)
|
|
29
|
+
if article
|
|
30
|
+
{
|
|
31
|
+
status: 200,
|
|
32
|
+
display: 'pages/articles/show.hyr',
|
|
33
|
+
locals: { article: article }
|
|
34
|
+
}
|
|
35
|
+
else
|
|
36
|
+
{
|
|
37
|
+
status: 404,
|
|
38
|
+
display: 'pages/error.hyr',
|
|
39
|
+
locals: { error: "Article not found" }
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
rescue => e
|
|
43
|
+
puts "Web Adapter Error: #{e.message}"
|
|
44
|
+
{
|
|
45
|
+
status: 500,
|
|
46
|
+
display: 'pages/error.hyr',
|
|
47
|
+
locals: { error: "Internal server error" }
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def new(request)
|
|
53
|
+
{
|
|
54
|
+
status: 200,
|
|
55
|
+
display: 'pages/articles/new.hyr',
|
|
56
|
+
locals: { article: Article.new }
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def edit(request, id)
|
|
61
|
+
begin
|
|
62
|
+
article = @articles_circuit.find(id)
|
|
63
|
+
if article
|
|
64
|
+
{
|
|
65
|
+
status: 200,
|
|
66
|
+
display: 'pages/articles/edit.hyr',
|
|
67
|
+
locals: { article: article }
|
|
68
|
+
}
|
|
69
|
+
else
|
|
70
|
+
{
|
|
71
|
+
status: 404,
|
|
72
|
+
display: 'pages/error.hyr',
|
|
73
|
+
locals: { error: "Article not found" }
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
rescue => e
|
|
77
|
+
puts "Web Adapter Error: #{e.message}"
|
|
78
|
+
{
|
|
79
|
+
status: 500,
|
|
80
|
+
display: 'pages/error.hyr',
|
|
81
|
+
locals: { error: "Internal server error" }
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# test/support/test_patches.rb
|
|
2
|
+
puts "Applying test patches for TEST_NO_DB=1..."
|
|
3
|
+
|
|
4
|
+
# Patch Article for proper JSON serialization
|
|
5
|
+
class Article
|
|
6
|
+
def to_json(*args)
|
|
7
|
+
to_hash.to_json(*args)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def to_hash
|
|
11
|
+
{
|
|
12
|
+
id: id,
|
|
13
|
+
title: title,
|
|
14
|
+
content: content,
|
|
15
|
+
status: status,
|
|
16
|
+
created_at: created_at,
|
|
17
|
+
updated_at: updated_at
|
|
18
|
+
}.compact
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Redefine adapters to use MockArticlesGateway
|
|
23
|
+
if defined?(ArticlesApiAdapter)
|
|
24
|
+
require_relative 'mock_api_adapter'
|
|
25
|
+
ArticlesApiAdapter.prepend(MockApiAdapter)
|
|
26
|
+
puts "✓ Patched ArticlesApiAdapter"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
if defined?(ArticlesWebAdapter)
|
|
30
|
+
require_relative 'mock_web_adapter'
|
|
31
|
+
ArticlesWebAdapter.prepend(MockWebAdapter)
|
|
32
|
+
puts "✓ Patched ArticlesWebAdapter"
|
|
33
|
+
end
|