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 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