testscript_engine 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|