testscript_engine 0.0.0

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