testscript_engine 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/testscript_engine +108 -0
- data/lib/testscript_engine/assertion.rb +284 -0
- data/lib/testscript_engine/message_handler.rb +332 -0
- data/lib/testscript_engine/operation.rb +160 -0
- data/lib/testscript_engine/testreport_handler.rb +247 -0
- data/lib/testscript_engine/testscript_runnable.rb +252 -0
- data/lib/testscript_engine.rb +134 -0
- metadata +140 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 62797fbc421b27cc8445d64ad741cb2272cf25d33ecbc27e62bbfbafa494d789
|
4
|
+
data.tar.gz: 9fbb57ad276b50107d7bd4eb9ca9a58c9f4137e0efa177dbf9e13b6ac9b23c84
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f97edd627ee92665dde3491725ebabe1fb7309d43edfce3afc7e5bd923746cae05aa4dceefc29c77155d237da208f6acd19c41ef4b7b51336fb801b6de3cb881
|
7
|
+
data.tar.gz: e6e6afe08421e2812604a6834a0e9e05d89018b43fb07543867e314a67816689c41611db444967597e7cd3dfa1de19653b474eeece6a28ddbccd5b168d77978b
|
@@ -0,0 +1,108 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'pry-nav'
|
3
|
+
require 'testscript_engine'
|
4
|
+
|
5
|
+
@test_server_url = "http://hapi.fhir.org/baseR4"
|
6
|
+
@testscript_path = "./"
|
7
|
+
@testreport_path = "./TestReports"
|
8
|
+
|
9
|
+
Dir.glob("#{Dir.getwd}/**").each do |path|
|
10
|
+
@testscript_path = path if path.split('/').last.downcase == 'testscripts'
|
11
|
+
@testreport_path = path if path.split('/').last.downcase == 'testreports'
|
12
|
+
end
|
13
|
+
|
14
|
+
def configuration
|
15
|
+
%(The configuration is as follows: \n
|
16
|
+
SERVER UNDER TEST: [#{@test_server_url}]
|
17
|
+
TESTSCRIPT INPUT DIRECTORY or FILE: [#{@testscript_path}]
|
18
|
+
TESTREPORT OUTPUT DIRECTORY: [#{@testreport_path}] \n
|
19
|
+
Would you like to modify this configuration? [Y/N] )
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate_path(path)
|
23
|
+
while true
|
24
|
+
break if File.file?(path) || File.directory?(path)
|
25
|
+
print " Invalid file or directory path given. Current working directory: [#{Dir.getwd}]. Try again: "
|
26
|
+
path = gets.chomp
|
27
|
+
end
|
28
|
+
path
|
29
|
+
end
|
30
|
+
|
31
|
+
def modify_configuration
|
32
|
+
print "Set [SERVER UNDER TEST] (press return to skip): "
|
33
|
+
input = gets.chomp
|
34
|
+
@test_server_url = input unless input.strip == ""
|
35
|
+
|
36
|
+
print "Set [TESTSCRIPT INPUT DIRECTORY or FILE] (press return to skip): "
|
37
|
+
input = gets.chomp
|
38
|
+
unless input.strip == ""
|
39
|
+
@testscript_path = validate_path(input.strip)
|
40
|
+
end
|
41
|
+
|
42
|
+
print "Set [TESTREPORT OUTPUT DIRECTORY] (press return to skip): "
|
43
|
+
input = gets.chomp
|
44
|
+
unless input.strip == ""
|
45
|
+
@testreport_path = validate_path(input.strip)
|
46
|
+
end
|
47
|
+
|
48
|
+
puts
|
49
|
+
end
|
50
|
+
|
51
|
+
def approve_configuration
|
52
|
+
while true
|
53
|
+
print configuration
|
54
|
+
input = gets.chomp
|
55
|
+
puts
|
56
|
+
if input.strip.downcase == 'y'
|
57
|
+
modify_configuration
|
58
|
+
else
|
59
|
+
break
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
print "Hello from the TestScriptEngine! "
|
65
|
+
approve_configuration
|
66
|
+
|
67
|
+
engine = TestScriptEngine.new(@test_server_url, @testscript_path, @testreport_path)
|
68
|
+
engine.load_scripts
|
69
|
+
engine.make_runnables
|
70
|
+
|
71
|
+
print "Now able to execute runnables. \n"
|
72
|
+
|
73
|
+
while true
|
74
|
+
puts
|
75
|
+
print "The SERVER UNDER TEST is [#{@test_server_url}]. Would you like to change the SERVER UNDER TEST? [Y/N] "
|
76
|
+
input = gets.chomp
|
77
|
+
if input.strip.downcase == 'y'
|
78
|
+
puts
|
79
|
+
print "Set [SERVER UNDER TEST]: "
|
80
|
+
input = gets.chomp
|
81
|
+
@test_server_url = input unless input.strip == ""
|
82
|
+
engine.new_client(@test_server_url)
|
83
|
+
end
|
84
|
+
|
85
|
+
puts
|
86
|
+
|
87
|
+
print "Enter the ID of a runnable to execute, or press return to execute all runnables: "
|
88
|
+
input = gets.chomp
|
89
|
+
if input.strip == ''
|
90
|
+
input = nil
|
91
|
+
else
|
92
|
+
while !engine.verify_runnable(input)
|
93
|
+
print " Invalid runnable ID given. Please try again: "
|
94
|
+
input = gets.chomp
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
engine.execute_runnables(input)
|
99
|
+
|
100
|
+
puts
|
101
|
+
print "Execution finished. Enter (q) to quit, and press any other key to continue execution: "
|
102
|
+
input = gets.chomp
|
103
|
+
break if input == 'q'
|
104
|
+
end
|
105
|
+
|
106
|
+
engine.write_reports
|
107
|
+
|
108
|
+
print "Goodbye!"
|
@@ -0,0 +1,284 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Assertion
|
3
|
+
class AssertionException < StandardError
|
4
|
+
attr_reader :details, :outcome
|
5
|
+
|
6
|
+
def initialize(details, outcome)
|
7
|
+
@details = details
|
8
|
+
@outcome = outcome
|
9
|
+
super(details)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
ASSERT_TYPES_MATCHER = /(?<=\p{Ll})(?=\p{Lu})|(?<=\p{Lu})(?=\p{Lu}\p{Ll})/
|
14
|
+
|
15
|
+
ASSERT_TYPES = [
|
16
|
+
"contentType",
|
17
|
+
"expression",
|
18
|
+
"headerField",
|
19
|
+
"minimumId",
|
20
|
+
"navigationLinks",
|
21
|
+
"path",
|
22
|
+
"requestMethod",
|
23
|
+
"resource",
|
24
|
+
"responseCode",
|
25
|
+
"response",
|
26
|
+
"validateProfileId",
|
27
|
+
"requestURL"
|
28
|
+
]
|
29
|
+
|
30
|
+
CODE_MAP = {
|
31
|
+
'200' => 'okay',
|
32
|
+
'201' => 'created',
|
33
|
+
'204' => 'noContent',
|
34
|
+
'304' => 'notModified',
|
35
|
+
'400' => 'bad',
|
36
|
+
'403' => 'forbidden',
|
37
|
+
'404' => 'notFound',
|
38
|
+
'405' => 'methodNotAllowed',
|
39
|
+
'409' => 'conflict',
|
40
|
+
'410' => 'gone',
|
41
|
+
'412' => 'preconditionFailed',
|
42
|
+
'422' => 'unprocessable'
|
43
|
+
}
|
44
|
+
|
45
|
+
def evaluate(assert)
|
46
|
+
@direction = assert.direction
|
47
|
+
assert_elements = assert.to_hash.keys
|
48
|
+
assert_type = determine_assert_type(assert_elements)
|
49
|
+
|
50
|
+
outcome_message = send(assert_type.to_sym, assert)
|
51
|
+
|
52
|
+
pass(:eval_assert_result, outcome_message)
|
53
|
+
end
|
54
|
+
|
55
|
+
def determine_assert_type(all_elements)
|
56
|
+
assert_type = all_elements.detect { |elem| ASSERT_TYPES.include? elem }
|
57
|
+
return assert_type.split(ASSERT_TYPES_MATCHER).map(&:downcase).join('_')
|
58
|
+
end
|
59
|
+
|
60
|
+
def direction
|
61
|
+
@direction ||= 'response'
|
62
|
+
end
|
63
|
+
|
64
|
+
def determine_expected_value(assert)
|
65
|
+
if assert.value
|
66
|
+
assert.value
|
67
|
+
elsif assert.compareToSourceExpression
|
68
|
+
FHIRPath.evaluate(assert.compareToSourceExpression,
|
69
|
+
get_resource(assert.compareToSourceId).to_hash)
|
70
|
+
elsif assert.compareToSourcePath
|
71
|
+
evaluate_path(assert.compareToSourcePath,
|
72
|
+
get_resource(assert.compareToSourceId))
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def compare(assert_type, received, operator, expected = nil)
|
77
|
+
operator = 'equals' unless operator
|
78
|
+
outcome = begin
|
79
|
+
case operator
|
80
|
+
when 'equals'
|
81
|
+
expected == received
|
82
|
+
when 'notEquals'
|
83
|
+
expected != received
|
84
|
+
when 'in'
|
85
|
+
expected.split(',').include? received
|
86
|
+
when 'notIn'
|
87
|
+
!expected.split(',').include? received
|
88
|
+
when 'greaterThan'
|
89
|
+
received.to_i > expected.to_i
|
90
|
+
when 'lessThan'
|
91
|
+
received.to_i < expected.to_i
|
92
|
+
when 'empty'
|
93
|
+
received.blank?
|
94
|
+
when 'notEmpty'
|
95
|
+
received.present?
|
96
|
+
when 'contains'
|
97
|
+
received&.include? expected
|
98
|
+
when 'notContains'
|
99
|
+
!received&.include? expected
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
if outcome
|
104
|
+
pass_message(assert_type, received, operator, expected)
|
105
|
+
else
|
106
|
+
fail_message = fail_message(assert_type, received, operator, expected)
|
107
|
+
raise AssertionException.new(fail_message, :fail)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def pass_message(assert_type, received, operator, expected)
|
112
|
+
received = Array(received)
|
113
|
+
expected = Array(expected)
|
114
|
+
message = "#{assert_type}: As expected, #{assert_type} #{operator}"
|
115
|
+
message = message + (expected ? " #{expected}." : '.')
|
116
|
+
message + " Found #{received}." if received
|
117
|
+
end
|
118
|
+
|
119
|
+
def fail_message(assert_type, received, operator, expected)
|
120
|
+
received = Array(received)
|
121
|
+
expected = Array(expected)
|
122
|
+
message = "#{assert_type}: Expected #{assert_type} #{operator}"
|
123
|
+
message = message + " #{expected}" if expected
|
124
|
+
message + ", but found #{received}."
|
125
|
+
end
|
126
|
+
|
127
|
+
def content_type(assert)
|
128
|
+
received = request_header(assert.sourceId, 'Content-Type')
|
129
|
+
compare("Content-Type", received, assert.operator, assert.contentType)
|
130
|
+
end
|
131
|
+
|
132
|
+
def expression(assert)
|
133
|
+
resource = get_resource(assert.sourceId)
|
134
|
+
raise AssertionException.new('No resource given by sourceId.', :fail) unless resource
|
135
|
+
|
136
|
+
received = FHIRPath.evaluate(assert.expression, resource.to_hash)
|
137
|
+
expected = determine_expected_value(assert)
|
138
|
+
compare("Expression", received, assert.operator, expected)
|
139
|
+
end
|
140
|
+
|
141
|
+
def header_field(assert)
|
142
|
+
received = begin
|
143
|
+
if direction == 'request'
|
144
|
+
request_header(assert.sourceId, assert.headerField)
|
145
|
+
else
|
146
|
+
response_header(assert.sourceId, assert.headerField)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
expected = determine_expected_value(assert)
|
151
|
+
compare("Header #{assert.headerField}", received, assert.operator, expected)
|
152
|
+
end
|
153
|
+
|
154
|
+
def minimum_id(assert)
|
155
|
+
received = get_resource(assert.sourceId)
|
156
|
+
|
157
|
+
raise AssertionException.new('minimumId assert not yet supported.', :skip)
|
158
|
+
# result = client.validate(received, { profile_uri: assert.validateProfileId })
|
159
|
+
end
|
160
|
+
|
161
|
+
def navigation_links(assert)
|
162
|
+
received = get_resource(assert.sourceId)
|
163
|
+
result = received&.first_link && received&.last_link && received&.next_link
|
164
|
+
|
165
|
+
return "Navigation Links: As expected, all navigation links found." if result
|
166
|
+
|
167
|
+
raise AssertionException.new("Navigation Links: Expected all navigation links, but did not receive.", :fail)
|
168
|
+
end
|
169
|
+
|
170
|
+
def path(assert)
|
171
|
+
resource = get_resource(assert.sourceId)
|
172
|
+
received = evaluate_path(assert.path, resource)
|
173
|
+
expected = determine_expected_value(assert)
|
174
|
+
compare("Path", received, assert.operator, expected)
|
175
|
+
end
|
176
|
+
|
177
|
+
def request_method(assert)
|
178
|
+
request = assert.sourceId ? request_map[assert.sourceId] : reply.request
|
179
|
+
received = request[:method]
|
180
|
+
expected = determine_expected_value(assert)
|
181
|
+
compare("Request Method", received, assert.operator, expected)
|
182
|
+
end
|
183
|
+
|
184
|
+
def resource(assert)
|
185
|
+
received = get_resource(assert.sourceId)
|
186
|
+
compare("Resource", received&.resourceType, assert.operator, assert.resource)
|
187
|
+
end
|
188
|
+
|
189
|
+
def response_code(assert)
|
190
|
+
received = get_response(assert.sourceId)&.[](:code).to_s
|
191
|
+
compare("Response Code", received, assert.operator, assert.responseCode)
|
192
|
+
end
|
193
|
+
|
194
|
+
def response(assert)
|
195
|
+
received_code = get_response(assert.sourceId)&.[](:code).to_s
|
196
|
+
received = CODE_MAP[received_code]
|
197
|
+
compare("Response", received, assert.operator, assert.response)
|
198
|
+
end
|
199
|
+
|
200
|
+
def validate_profile_id(assert)
|
201
|
+
received = get_resource(assert.sourceId)
|
202
|
+
|
203
|
+
raise AssertionException.new('validateProfileId assert not yet supported.', :skip)
|
204
|
+
# result = client.validate(received, { profile_uri: assert.validateProfileId })
|
205
|
+
end
|
206
|
+
|
207
|
+
def request_url(assert)
|
208
|
+
received = get_request(assert.sourceId)[:url]
|
209
|
+
compare("RequestURL", received, assert.operator, assert.requestURL)
|
210
|
+
end
|
211
|
+
|
212
|
+
# <--- TO DO: MOVE TO UTILITIES MODULE --->
|
213
|
+
|
214
|
+
def get_resource(id)
|
215
|
+
if direction == 'request'
|
216
|
+
get_request(id)&.[](:payload)
|
217
|
+
else
|
218
|
+
get_response(id)&.[](:body) || fixtures[id]
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def get_response(id)
|
223
|
+
return response_map[id] if id
|
224
|
+
reply&.response
|
225
|
+
end
|
226
|
+
|
227
|
+
def get_request(id)
|
228
|
+
return request_map[id] if id
|
229
|
+
reply&.request
|
230
|
+
end
|
231
|
+
|
232
|
+
def response_header(responseId = nil, header_name = nil)
|
233
|
+
response = responseId ? response_map[responseId] : reply&.response
|
234
|
+
return unless response
|
235
|
+
|
236
|
+
headers = response[:headers]
|
237
|
+
return unless headers
|
238
|
+
|
239
|
+
headers.transform_keys!(&:downcase)
|
240
|
+
header_name ? headers[header_name.downcase] : headers
|
241
|
+
end
|
242
|
+
|
243
|
+
def request_header(requestId = nil, header_name = nil)
|
244
|
+
request = requestId ? request_map[requestId] : reply&.request
|
245
|
+
return unless request
|
246
|
+
|
247
|
+
headers = request[:headers]
|
248
|
+
return unless headers
|
249
|
+
|
250
|
+
headers.transform_keys!(&:downcase)
|
251
|
+
header_name ? headers[header_name.downcase] : headers
|
252
|
+
end
|
253
|
+
|
254
|
+
def evaluate_path(path, resource)
|
255
|
+
return unless path and resource
|
256
|
+
|
257
|
+
begin
|
258
|
+
# Then, try xpath if necessary
|
259
|
+
result = extract_xpath_value(resource.to_xml, path)
|
260
|
+
rescue
|
261
|
+
# If xpath fails, see if JSON path will work...
|
262
|
+
result = JsonPath.new(path).first(resource.to_json)
|
263
|
+
end
|
264
|
+
return result
|
265
|
+
end
|
266
|
+
|
267
|
+
def extract_xpath_value(resource_xml, resource_xpath)
|
268
|
+
# Massage the xpath if it doesn't have fhir: namespace or if doesn't end in @value
|
269
|
+
# Also make it look in the entire xml document instead of just starting at the root
|
270
|
+
xpath = resource_xpath.split('/').map do |s|
|
271
|
+
s.start_with?('fhir:') || s.length.zero? || s.start_with?('@') ? s : "fhir:#{s}"
|
272
|
+
end.join('/')
|
273
|
+
xpath = "#{xpath}/@value" unless xpath.end_with? '@value'
|
274
|
+
xpath = "//#{xpath}"
|
275
|
+
|
276
|
+
resource_doc = Nokogiri::XML(resource_xml)
|
277
|
+
resource_doc.root.add_namespace_definition('fhir', 'http://hl7.org/fhir')
|
278
|
+
resource_element = resource_doc.xpath(xpath)
|
279
|
+
|
280
|
+
# This doesn't work on warningOnly; consider putting back in place
|
281
|
+
# raise AssertionException.new("[#{resource_xpath}] resolved to multiple values instead of a single value", resource_element.to_s) if resource_element.length>1
|
282
|
+
resource_element.first.value
|
283
|
+
end
|
284
|
+
end
|