tng 0.1.4
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.
Potentially problematic release.
This version of tng might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/Gemfile +15 -0
- data/LICENSE.md +32 -0
- data/README.md +427 -0
- data/Rakefile +124 -0
- data/bin/load_dev +22 -0
- data/bin/tng +1122 -0
- data/binaries/tng.bundle +0 -0
- data/binaries/tng.so +0 -0
- data/lib/generators/tng/install_generator.rb +236 -0
- data/lib/tng/analyzers/controller.rb +114 -0
- data/lib/tng/analyzers/model.rb +122 -0
- data/lib/tng/analyzers/service.rb +149 -0
- data/lib/tng/api/http_client.rb +102 -0
- data/lib/tng/railtie.rb +11 -0
- data/lib/tng/services/test_generator.rb +159 -0
- data/lib/tng/services/testng.rb +100 -0
- data/lib/tng/services/user_app_config.rb +77 -0
- data/lib/tng/ui/about_display.rb +64 -0
- data/lib/tng/ui/authentication_warning_display.rb +172 -0
- data/lib/tng/ui/configuration_display.rb +52 -0
- data/lib/tng/ui/controller_test_flow_display.rb +96 -0
- data/lib/tng/ui/display_banner.rb +44 -0
- data/lib/tng/ui/goodbye_display.rb +41 -0
- data/lib/tng/ui/model_test_flow_display.rb +97 -0
- data/lib/tng/ui/post_install_box.rb +82 -0
- data/lib/tng/ui/service_test_flow_display.rb +95 -0
- data/lib/tng/ui/show_help.rb +124 -0
- data/lib/tng/ui/system_status_display.rb +128 -0
- data/lib/tng/ui/theme.rb +248 -0
- data/lib/tng/ui/user_stats_display.rb +160 -0
- data/lib/tng/utils.rb +263 -0
- data/lib/tng/version.rb +5 -0
- data/lib/tng.rb +308 -0
- data/tng.gemspec +56 -0
- metadata +289 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
require "httpx"
|
|
2
|
+
|
|
3
|
+
module Tng
|
|
4
|
+
class HttpClient
|
|
5
|
+
def initialize(api_endpoint, api_key)
|
|
6
|
+
@api_endpoint = api_endpoint
|
|
7
|
+
@api_key = api_key
|
|
8
|
+
@timeout = {
|
|
9
|
+
connect_timeout: 300,
|
|
10
|
+
read_timeout: 300,
|
|
11
|
+
write_timeout: 300,
|
|
12
|
+
request_timeout: 300
|
|
13
|
+
}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def post(path, payload: {}, headers: {})
|
|
17
|
+
default_headers = {
|
|
18
|
+
"Content-Type" => "application/json",
|
|
19
|
+
"Authorization" => "Bearer #{@api_key}",
|
|
20
|
+
"User-Agent" => "TestNG-Rails/#{Tng::VERSION} Ruby/#{RUBY_VERSION}"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
merged_headers = default_headers.merge(headers)
|
|
24
|
+
|
|
25
|
+
response = HTTPX.with(timeout: @timeout).post(
|
|
26
|
+
"#{@api_endpoint}/#{path}",
|
|
27
|
+
json: payload,
|
|
28
|
+
headers: merged_headers
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
debug_response("POST #{path}", response) if debug_enabled?
|
|
32
|
+
response
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def post_binary(path, data, headers: {})
|
|
36
|
+
default_headers = {
|
|
37
|
+
"Content-Type" => "application/octet-stream",
|
|
38
|
+
"Authorization" => "Bearer #{@api_key}",
|
|
39
|
+
"User-Agent" => "TestNG-Rails/#{Tng::VERSION} Ruby/#{RUBY_VERSION}"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
merged_headers = default_headers.merge(headers)
|
|
43
|
+
|
|
44
|
+
response = HTTPX.with(timeout: @timeout).post(
|
|
45
|
+
"#{@api_endpoint}/#{path}",
|
|
46
|
+
body: data,
|
|
47
|
+
headers: merged_headers
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
debug_response("POST #{path} (binary)", response) if debug_enabled?
|
|
51
|
+
response
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def get(path, headers: {})
|
|
55
|
+
default_headers = {
|
|
56
|
+
"Content-Type" => "application/json",
|
|
57
|
+
"Authorization" => "Bearer #{@api_key}",
|
|
58
|
+
"User-Agent" => "TestNG-Rails/#{Tng::VERSION} Ruby/#{RUBY_VERSION}"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
merged_headers = default_headers.merge(headers)
|
|
62
|
+
|
|
63
|
+
response = HTTPX.with(timeout: @timeout).get(
|
|
64
|
+
"#{@api_endpoint}/#{path}",
|
|
65
|
+
headers: merged_headers
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
debug_response("GET #{path}", response) if debug_enabled?
|
|
69
|
+
response
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def ping
|
|
73
|
+
response = HTTPX.with(timeout: @timeout).get("#{@api_endpoint}/ping")
|
|
74
|
+
debug_response("GET /ping", response) if debug_enabled?
|
|
75
|
+
response
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def debug_enabled?
|
|
81
|
+
ENV["DEBUG"] == "1"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def debug_response(request_info, response)
|
|
85
|
+
puts "\n -> DEBUG: #{request_info}"
|
|
86
|
+
puts " Status: #{response.status}"
|
|
87
|
+
puts " Headers: #{response.headers.to_h}"
|
|
88
|
+
|
|
89
|
+
if response.is_a?(HTTPX::ErrorResponse)
|
|
90
|
+
puts " Error: #{response.error&.message}"
|
|
91
|
+
else
|
|
92
|
+
body = response.body.to_s
|
|
93
|
+
if body.length > 500
|
|
94
|
+
puts " Body: #{body[0..500]}... (truncated, total length: #{body.length})"
|
|
95
|
+
else
|
|
96
|
+
puts " Body: #{body}"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
puts " " + "─" * 50
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
data/lib/tng/railtie.rb
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
require "tng/utils"
|
|
5
|
+
require "tng/services/user_app_config"
|
|
6
|
+
|
|
7
|
+
module Tng
|
|
8
|
+
module Services
|
|
9
|
+
class TestGenerator
|
|
10
|
+
GENERATE_TESTS_PATH = "cli/tng_rails/contents/generate_tests"
|
|
11
|
+
|
|
12
|
+
def initialize(http_client)
|
|
13
|
+
@http_client = http_client
|
|
14
|
+
@machine_info = Tng.machine_info
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run_for_controller(controller)
|
|
18
|
+
payload = {
|
|
19
|
+
controller_test: Tng::Analyzers::Controller.read_test_file_for_controller(controller[:path]),
|
|
20
|
+
object_fixture_data: Tng::Utils.fixture_content,
|
|
21
|
+
machine_info: @machine_info,
|
|
22
|
+
ast: Tng::Analyzers::Controller.value_for_controller(controller[:path]),
|
|
23
|
+
test_type: "controller",
|
|
24
|
+
controller: controller,
|
|
25
|
+
routes: Tng::Analyzers::Controller.routes_for_controller(controller[:path]),
|
|
26
|
+
auth_patterns: Tng::Services::UserAppConfig.config_with_source,
|
|
27
|
+
model_info: Tng::Analyzers::Controller.model_info_for_controller(controller[:path]),
|
|
28
|
+
parents: Tng::Analyzers::Controller.parents_for_controller(controller)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
send_request_and_save_test(payload)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def run_for_model(model)
|
|
35
|
+
payload = {
|
|
36
|
+
model_test: Tng::Analyzers::Model.read_test_file_for_model(model[:path]),
|
|
37
|
+
machine_info: @machine_info,
|
|
38
|
+
object_fixture_data: Tng::Utils.fixture_content,
|
|
39
|
+
ast: Tng::Analyzers::Model.value_for_model(model[:path]),
|
|
40
|
+
test_type: "model",
|
|
41
|
+
model: model,
|
|
42
|
+
auth_patterns: Tng::Services::UserAppConfig.config_with_source,
|
|
43
|
+
model_connections: Tng::Analyzers::Model.model_connections(model)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
send_request_and_save_test(payload)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def run_for_service(service)
|
|
50
|
+
payload = {
|
|
51
|
+
service_test: Tng::Analyzers::Service.read_test_file_for_service(service[:path]),
|
|
52
|
+
machine_info: @machine_info,
|
|
53
|
+
object_fixture_data: Tng::Utils.fixture_content,
|
|
54
|
+
ast: Tng::Analyzers::Service.value_for_service(service[:path]),
|
|
55
|
+
test_type: "service",
|
|
56
|
+
service: service
|
|
57
|
+
}
|
|
58
|
+
send_request_and_save_test(payload)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def run_for_controller_method(controller, method_info)
|
|
62
|
+
payload = {
|
|
63
|
+
controller_test: Tng::Analyzers::Controller.read_test_file_for_controller(controller[:path]),
|
|
64
|
+
object_fixture_data: Tng::Utils.fixture_content,
|
|
65
|
+
machine_info: @machine_info,
|
|
66
|
+
ast: Tng::Analyzers::Controller.value_for_controller(controller[:path]),
|
|
67
|
+
test_type: "controller_method",
|
|
68
|
+
controller: controller,
|
|
69
|
+
method: method_info,
|
|
70
|
+
routes: Tng::Analyzers::Controller.routes_for_controller(controller[:path]),
|
|
71
|
+
auth_patterns: Tng::Services::UserAppConfig.config_with_source,
|
|
72
|
+
model_info: Tng::Analyzers::Controller.model_info_for_controller(controller[:path]),
|
|
73
|
+
parents: Tng::Analyzers::Controller.parents_for_controller(controller)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
send_request_and_save_test(payload)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def run_for_model_method(model, method_info)
|
|
80
|
+
payload = {
|
|
81
|
+
model_test: Tng::Analyzers::Model.read_test_file_for_model(model[:path]),
|
|
82
|
+
machine_info: @machine_info,
|
|
83
|
+
object_fixture_data: Tng::Utils.fixture_content,
|
|
84
|
+
ast: Tng::Analyzers::Model.value_for_model(model[:path]),
|
|
85
|
+
test_type: "model_method",
|
|
86
|
+
model: model,
|
|
87
|
+
method: method_info,
|
|
88
|
+
auth_patterns: Tng::Services::UserAppConfig.config_with_source,
|
|
89
|
+
model_connections: Tng::Analyzers::Model.model_connections(model)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
send_request_and_save_test(payload)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def run_for_service_method(service, method_info)
|
|
96
|
+
payload = {
|
|
97
|
+
service_test: Tng::Analyzers::Service.read_test_file_for_service(service[:path]),
|
|
98
|
+
machine_info: @machine_info,
|
|
99
|
+
object_fixture_data: Tng::Utils.fixture_content,
|
|
100
|
+
ast: Tng::Analyzers::Service.value_for_service(service[:path]),
|
|
101
|
+
test_type: "service_method",
|
|
102
|
+
service: service,
|
|
103
|
+
method: method_info,
|
|
104
|
+
auth_patterns: Tng::Services::UserAppConfig.config_with_source
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
send_request_and_save_test(payload)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def send_request_and_save_test(payload)
|
|
113
|
+
marshaled = Marshal.dump(payload)
|
|
114
|
+
compressed = Zlib::Deflate.deflate(marshaled)
|
|
115
|
+
response = @http_client.post_binary(GENERATE_TESTS_PATH, compressed)
|
|
116
|
+
|
|
117
|
+
if response.is_a?(HTTPX::ErrorResponse)
|
|
118
|
+
debug_log("Request failed with error response") if debug_enabled?
|
|
119
|
+
return handle_request_error(response)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
debug_log("Request successful, status: #{response.status}") if debug_enabled?
|
|
123
|
+
|
|
124
|
+
test_content = response.body.to_s
|
|
125
|
+
if test_content.nil? || test_content.empty?
|
|
126
|
+
debug_log("Empty response body received") if debug_enabled?
|
|
127
|
+
return handle_empty_response
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
debug_log("Received test content, length: #{test_content.length}") if debug_enabled?
|
|
131
|
+
Tng::Utils.save_test_file(test_content)
|
|
132
|
+
rescue JSON::ParserError => e
|
|
133
|
+
debug_log("JSON parsing failed: #{e.message}") if debug_enabled?
|
|
134
|
+
puts "❌ Failed to parse generated tests via API: #{e.message}"
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def handle_request_error(response)
|
|
139
|
+
debug_log("Handling request error: #{response.error&.message}") if debug_enabled?
|
|
140
|
+
puts "❌ Request failed: #{response.error&.message}"
|
|
141
|
+
nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def handle_empty_response
|
|
145
|
+
debug_log("Handling empty response") if debug_enabled?
|
|
146
|
+
puts "❌ Failed to generate test via API"
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def debug_enabled?
|
|
151
|
+
ENV["DEBUG"] == "1"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def debug_log(message)
|
|
155
|
+
puts "-> DEBUG [TestGenerator]: #{message}"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
module Services
|
|
2
|
+
class Testng
|
|
3
|
+
STATS_PATH = "cli/tng_rails/stats"
|
|
4
|
+
|
|
5
|
+
def initialize(http_client)
|
|
6
|
+
@http_client = http_client
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def check_system_status
|
|
10
|
+
response = @http_client.ping
|
|
11
|
+
|
|
12
|
+
if response.is_a?(HTTPX::ErrorResponse)
|
|
13
|
+
return {
|
|
14
|
+
status: :error,
|
|
15
|
+
message: "Unable to connect to TNG service: #{response.error&.message}",
|
|
16
|
+
error_type: :connection_error
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
case response.status
|
|
21
|
+
when 200
|
|
22
|
+
begin
|
|
23
|
+
data = JSON.parse(response.body)
|
|
24
|
+
current_version = data["current_version"]
|
|
25
|
+
server_base_url = data["base_url"]
|
|
26
|
+
user_base_url = Tng::Services::UserAppConfig.base_url
|
|
27
|
+
|
|
28
|
+
if current_version > Tng::VERSION
|
|
29
|
+
return {
|
|
30
|
+
status: :version_mismatch,
|
|
31
|
+
message: "Version mismatch detected",
|
|
32
|
+
current_version: current_version,
|
|
33
|
+
gem_version: Tng::VERSION,
|
|
34
|
+
error_type: :version_mismatch
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Check for base URL mismatch
|
|
39
|
+
if server_base_url && user_base_url && server_base_url != user_base_url
|
|
40
|
+
return {
|
|
41
|
+
status: :base_url_mismatch,
|
|
42
|
+
message: "Base URL mismatch detected",
|
|
43
|
+
server_base_url: server_base_url,
|
|
44
|
+
user_base_url: user_base_url,
|
|
45
|
+
current_version: current_version,
|
|
46
|
+
gem_version: Tng::VERSION,
|
|
47
|
+
error_type: :base_url_mismatch
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
{
|
|
52
|
+
status: :ok,
|
|
53
|
+
message: data["message"] || "System is operational",
|
|
54
|
+
current_version: current_version,
|
|
55
|
+
gem_version: Tng::VERSION,
|
|
56
|
+
server_base_url: server_base_url
|
|
57
|
+
}
|
|
58
|
+
rescue JSON::ParserError => e
|
|
59
|
+
{
|
|
60
|
+
status: :error,
|
|
61
|
+
message: "Invalid response from server: #{e.message}",
|
|
62
|
+
error_type: :parse_error
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
else
|
|
66
|
+
{
|
|
67
|
+
status: :service_down,
|
|
68
|
+
message: "TestNG service is currently unavailable (HTTP #{response.status})",
|
|
69
|
+
error_type: :service_unavailable
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def get_user_stats
|
|
75
|
+
headers = {
|
|
76
|
+
"Authorization" => "Bearer #{Tng::Services::UserAppConfig.api_key}",
|
|
77
|
+
"Content-Type" => "application/json",
|
|
78
|
+
"User-Agent" => "Tng/#{Tng::VERSION} Ruby/#{RUBY_VERSION}"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
response = @http_client.get(STATS_PATH, headers: headers)
|
|
82
|
+
|
|
83
|
+
return nil if response.is_a?(HTTPX::ErrorResponse)
|
|
84
|
+
|
|
85
|
+
if response.status == 200
|
|
86
|
+
begin
|
|
87
|
+
stats_data = JSON.parse(response.body)
|
|
88
|
+
puts "✅ Statistics fetched successfully!"
|
|
89
|
+
stats_data
|
|
90
|
+
rescue JSON::ParserError => e
|
|
91
|
+
puts "❌ Failed to parse statistics response: #{e.message}"
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
else
|
|
95
|
+
puts "❌ Failed to fetch statistics (#{response.status})"
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tng
|
|
4
|
+
module Services
|
|
5
|
+
class UserAppConfig
|
|
6
|
+
def self.api_key
|
|
7
|
+
Tng.api_key
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.base_url
|
|
11
|
+
Tng.base_url
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.configured?
|
|
15
|
+
Tng.api_key && Tng.base_url
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.missing_config
|
|
19
|
+
missing = []
|
|
20
|
+
missing << "API key" unless api_key
|
|
21
|
+
missing << "Base URL" unless base_url
|
|
22
|
+
missing
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.config_with_source
|
|
26
|
+
auth_entry_points_with_source = []
|
|
27
|
+
methods = Tng.authentication_methods
|
|
28
|
+
methods.each do |method|
|
|
29
|
+
auth_entry_points_with_source << {
|
|
30
|
+
method: method[:method],
|
|
31
|
+
file: method[:file_location],
|
|
32
|
+
auth_type: method[:auth_type],
|
|
33
|
+
source: extract_method_source(method[:file_location], method[:method])
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
Tng.config[:authentication_entry_points_with_source] = auth_entry_points_with_source
|
|
37
|
+
Tng.config
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.find_method_in_ast(node, method_name)
|
|
41
|
+
return nil unless node.is_a?(Prism::Node)
|
|
42
|
+
|
|
43
|
+
return node if node.is_a?(Prism::DefNode) && node.name == method_name.to_sym
|
|
44
|
+
|
|
45
|
+
node.child_nodes.each do |child|
|
|
46
|
+
result = find_method_in_ast(child, method_name)
|
|
47
|
+
return result if result
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.extract_method_source(file_path, method_name)
|
|
54
|
+
return nil unless file_path && method_name
|
|
55
|
+
|
|
56
|
+
full_path = Rails.root.join(file_path)
|
|
57
|
+
return nil unless File.exist?(full_path)
|
|
58
|
+
|
|
59
|
+
file_content = File.read(full_path)
|
|
60
|
+
result = Prism.parse(file_content)
|
|
61
|
+
|
|
62
|
+
method_node = find_method_in_ast(result.value, method_name)
|
|
63
|
+
return nil unless method_node
|
|
64
|
+
|
|
65
|
+
# Extract the method source from the original file content
|
|
66
|
+
start_line = method_node.location.start_line - 1 # Convert to 0-based index
|
|
67
|
+
end_line = method_node.location.end_line - 1
|
|
68
|
+
|
|
69
|
+
lines = file_content.lines
|
|
70
|
+
method_source = lines[start_line..end_line].join
|
|
71
|
+
method_source.strip
|
|
72
|
+
rescue StandardError
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tty-box"
|
|
4
|
+
require "pastel"
|
|
5
|
+
require "tty-screen"
|
|
6
|
+
require_relative "theme"
|
|
7
|
+
|
|
8
|
+
class AboutDisplay
|
|
9
|
+
def initialize(pastel, prompt, version)
|
|
10
|
+
@pastel = pastel
|
|
11
|
+
@prompt = prompt
|
|
12
|
+
@version = version
|
|
13
|
+
@terminal_width = begin
|
|
14
|
+
TTY::Screen.width
|
|
15
|
+
rescue StandardError
|
|
16
|
+
80
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def display
|
|
21
|
+
about_content = [
|
|
22
|
+
@pastel.public_send(Tng::UI::Theme.color(:secondary)).bold("About Tng"),
|
|
23
|
+
"",
|
|
24
|
+
@pastel.public_send(Tng::UI::Theme.color(:primary),
|
|
25
|
+
"Tng is an LLM-powered test generation tool for Rails applications."),
|
|
26
|
+
"",
|
|
27
|
+
@pastel.public_send(Tng::UI::Theme.color(:accent), "Features:"),
|
|
28
|
+
@pastel.public_send(Tng::UI::Theme.color(:muted), "#{Tng::UI::Theme.icon(:bullet)} Controller test generation"),
|
|
29
|
+
@pastel.public_send(Tng::UI::Theme.color(:muted), "#{Tng::UI::Theme.icon(:bullet)} Model test generation"),
|
|
30
|
+
@pastel.public_send(Tng::UI::Theme.color(:muted), "#{Tng::UI::Theme.icon(:bullet)} Service test generation"),
|
|
31
|
+
@pastel.public_send(Tng::UI::Theme.color(:muted), "#{Tng::UI::Theme.icon(:bullet)} LLM-powered test suggestions"),
|
|
32
|
+
@pastel.public_send(Tng::UI::Theme.color(:muted), "#{Tng::UI::Theme.icon(:bullet)} Test coverage analysis"),
|
|
33
|
+
"",
|
|
34
|
+
@pastel.public_send(Tng::UI::Theme.color(:accent), "Technology:"),
|
|
35
|
+
@pastel.public_send(Tng::UI::Theme.color(:muted),
|
|
36
|
+
"#{Tng::UI::Theme.icon(:bullet)} Static code analysis with Prism"),
|
|
37
|
+
@pastel.public_send(Tng::UI::Theme.color(:muted),
|
|
38
|
+
"#{Tng::UI::Theme.icon(:bullet)} Intelligent pattern recognition"),
|
|
39
|
+
@pastel.public_send(Tng::UI::Theme.color(:muted), "#{Tng::UI::Theme.icon(:bullet)} Rails-specific optimizations"),
|
|
40
|
+
"",
|
|
41
|
+
@pastel.public_send(Tng::UI::Theme.color(:muted), "Version: #{@version}"),
|
|
42
|
+
@pastel.public_send(Tng::UI::Theme.color(:muted),
|
|
43
|
+
"Built with #{Tng::UI::Theme.icon(:heart)} for Rails developers")
|
|
44
|
+
].join("\n")
|
|
45
|
+
|
|
46
|
+
box_width = Tng::UI::Theme.calculate_box_width(@terminal_width)
|
|
47
|
+
style = Tng::UI::Theme.box_style(:default)
|
|
48
|
+
|
|
49
|
+
about_box = TTY::Box.frame(
|
|
50
|
+
title: { top_left: " About Tng " },
|
|
51
|
+
style: style[:style],
|
|
52
|
+
padding: style[:padding],
|
|
53
|
+
width: box_width
|
|
54
|
+
) do
|
|
55
|
+
about_content
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
puts Tng::UI::Theme.center_box(about_box, box_width, @terminal_width)
|
|
59
|
+
@prompt.keypress(Tng::UI::Theme.center_text(
|
|
60
|
+
@pastel.public_send(Tng::UI::Theme.color(:muted),
|
|
61
|
+
"Press any key to continue..."), @terminal_width
|
|
62
|
+
))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tty-box"
|
|
4
|
+
require "pastel"
|
|
5
|
+
require "tty-screen"
|
|
6
|
+
require_relative "theme"
|
|
7
|
+
|
|
8
|
+
class AuthenticationWarningDisplay
|
|
9
|
+
def initialize(pastel)
|
|
10
|
+
@pastel = pastel
|
|
11
|
+
@terminal_width = begin
|
|
12
|
+
TTY::Screen.width
|
|
13
|
+
rescue StandardError
|
|
14
|
+
80
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def display_if_missing
|
|
19
|
+
return false unless authentication_config_missing?
|
|
20
|
+
|
|
21
|
+
display_warning
|
|
22
|
+
handle_user_choice
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def display_warning
|
|
26
|
+
warning_content = [
|
|
27
|
+
@pastel.public_send(Tng::UI::Theme.color(:error)).bold("Authentication Configuration Missing"),
|
|
28
|
+
"",
|
|
29
|
+
@pastel.public_send(Tng::UI::Theme.color(:primary), "TNG needs authentication setup to generate proper tests."),
|
|
30
|
+
"",
|
|
31
|
+
@pastel.public_send(Tng::UI::Theme.color(:accent), "Missing: ") + build_missing_items_summary,
|
|
32
|
+
"",
|
|
33
|
+
@pastel.public_send(Tng::UI::Theme.color(:secondary), "Quick fix:"),
|
|
34
|
+
@pastel.public_send(Tng::UI::Theme.color(:muted),
|
|
35
|
+
"#{Tng::UI::Theme.icon(:bullet)} Edit config/initializers/tng.rb"),
|
|
36
|
+
@pastel.public_send(Tng::UI::Theme.color(:muted),
|
|
37
|
+
"#{Tng::UI::Theme.icon(:bullet)} Uncomment authentication_methods array"),
|
|
38
|
+
@pastel.public_send(Tng::UI::Theme.color(:muted), "#{Tng::UI::Theme.icon(:bullet)} Add your auth method details"),
|
|
39
|
+
"",
|
|
40
|
+
@pastel.public_send(Tng::UI::Theme.color(:success), "Press Enter to continue anyway"),
|
|
41
|
+
@pastel.public_send(Tng::UI::Theme.color(:error), "Press Esc to cancel and fix configuration")
|
|
42
|
+
].join("\n")
|
|
43
|
+
|
|
44
|
+
box_width = Tng::UI::Theme.calculate_box_width(@terminal_width)
|
|
45
|
+
style = Tng::UI::Theme.box_style(:warning)
|
|
46
|
+
|
|
47
|
+
warning_box = TTY::Box.frame(
|
|
48
|
+
title: { top_left: " Auth Warning " },
|
|
49
|
+
style: style[:style],
|
|
50
|
+
padding: style[:padding],
|
|
51
|
+
width: box_width
|
|
52
|
+
) do
|
|
53
|
+
warning_content
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
puts Tng::UI::Theme.center_box(warning_box, box_width, @terminal_width)
|
|
57
|
+
puts
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def handle_user_choice
|
|
63
|
+
require "io/console"
|
|
64
|
+
|
|
65
|
+
loop do
|
|
66
|
+
key = $stdin.getch
|
|
67
|
+
case key
|
|
68
|
+
when "\r", "\n" # Enter key
|
|
69
|
+
return false # Continue anyway
|
|
70
|
+
when "\e" # Escape key
|
|
71
|
+
return true # Cancel (authentication missing)
|
|
72
|
+
when "\u0003" # Ctrl+C
|
|
73
|
+
puts "\n#{@pastel.public_send(Tng::UI::Theme.color(:warning), "Operation cancelled.")}"
|
|
74
|
+
exit(0)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
rescue StandardError
|
|
78
|
+
# Fallback if getch is not available
|
|
79
|
+
puts @pastel.public_send(Tng::UI::Theme.color(:muted), "Press Enter to continue or Ctrl+C to cancel...")
|
|
80
|
+
gets
|
|
81
|
+
false
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def build_missing_items_summary
|
|
85
|
+
issues = []
|
|
86
|
+
|
|
87
|
+
issues << "empty auth methods" if Tng.authentication_enabled && authentication_methods_empty?
|
|
88
|
+
|
|
89
|
+
issues << "invalid auth config" if !authentication_methods_empty? && !authentication_methods_valid?
|
|
90
|
+
|
|
91
|
+
issues << "auth disabled" unless Tng.authentication_enabled
|
|
92
|
+
|
|
93
|
+
issues.empty? ? "unknown issue" : issues.join(", ")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def authentication_config_missing?
|
|
97
|
+
# Check if authentication is enabled but methods are not configured
|
|
98
|
+
return true if Tng.authentication_enabled && authentication_methods_empty?
|
|
99
|
+
|
|
100
|
+
# Check if authentication methods exist but are invalid
|
|
101
|
+
return true if !authentication_methods_empty? && !authentication_methods_valid?
|
|
102
|
+
|
|
103
|
+
false
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def authentication_methods_empty?
|
|
107
|
+
methods = Tng.authentication_methods
|
|
108
|
+
methods.nil? || methods.empty?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def authentication_methods_valid?
|
|
112
|
+
methods = Tng.authentication_methods
|
|
113
|
+
return false if methods.nil? || methods.empty?
|
|
114
|
+
|
|
115
|
+
methods.all? do |method|
|
|
116
|
+
method.is_a?(Hash) &&
|
|
117
|
+
method.key?(:method) && !method[:method].to_s.strip.empty? &&
|
|
118
|
+
method.key?(:file_location) && !method[:file_location].to_s.strip.empty? &&
|
|
119
|
+
method.key?(:auth_type) && !method[:auth_type].to_s.strip.empty?
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def build_missing_items_list
|
|
124
|
+
issues = []
|
|
125
|
+
|
|
126
|
+
if Tng.authentication_enabled && authentication_methods_empty?
|
|
127
|
+
issues << @pastel.public_send(Tng::UI::Theme.color(:error), " • authentication_methods array is empty")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
if !authentication_methods_empty? && !authentication_methods_valid?
|
|
131
|
+
issues << @pastel.public_send(Tng::UI::Theme.color(:error), " • authentication_methods contains invalid entries")
|
|
132
|
+
|
|
133
|
+
Tng.authentication_methods.each_with_index do |method, index|
|
|
134
|
+
next if method.is_a?(Hash)
|
|
135
|
+
|
|
136
|
+
issues << @pastel.public_send(Tng::UI::Theme.color(:muted), " - Entry #{index + 1}: not a valid hash")
|
|
137
|
+
next
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
Tng.authentication_methods.each_with_index do |method, index|
|
|
141
|
+
next unless method.is_a?(Hash)
|
|
142
|
+
|
|
143
|
+
if !method.key?(:method) || method[:method].to_s.strip.empty?
|
|
144
|
+
issues << @pastel.public_send(Tng::UI::Theme.color(:muted),
|
|
145
|
+
" - Entry #{index + 1}: missing or empty 'method'")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
if !method.key?(:file_location) || method[:file_location].to_s.strip.empty?
|
|
149
|
+
issues << @pastel.public_send(Tng::UI::Theme.color(:muted),
|
|
150
|
+
" - Entry #{index + 1}: missing or empty 'file_location'")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
if !method.key?(:auth_type) || method[:auth_type].to_s.strip.empty?
|
|
154
|
+
issues << @pastel.public_send(Tng::UI::Theme.color(:muted),
|
|
155
|
+
" - Entry #{index + 1}: missing or empty 'auth_type'")
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
unless Tng.authentication_enabled
|
|
161
|
+
issues << @pastel.public_send(Tng::UI::Theme.color(:warning),
|
|
162
|
+
" • authentication_enabled is set to false")
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
if issues.empty?
|
|
166
|
+
@pastel.public_send(Tng::UI::Theme.color(:muted),
|
|
167
|
+
" • Configuration appears valid")
|
|
168
|
+
else
|
|
169
|
+
issues.join("\n")
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|