Module: Assertion

Included in:
TestScriptRunnable
Defined in:
lib/testscript_engine/assertion.rb

Defined Under Namespace

Classes: AssertionException

Constant Summary collapse

ASSERT_TYPES_MATCHER =
/(?<=\p{Ll})(?=\p{Lu})|(?<=\p{Lu})(?=\p{Lu}\p{Ll})/
ASSERT_TYPES =
[
  "contentType",
  "expression",
  "headerField",
  "minimumId",
  "navigationLinks",
  "path",
  "requestMethod",
  "resource",
  "responseCode",
  "response",
  "validateProfileId",
  "requestURL"
]
CODE_MAP =
{
  '200' => 'okay',
  '201' => 'created',
  '204' => 'noContent',
  '304' => 'notModified',
  '400' => 'bad',
  '403' => 'forbidden',
  '404' => 'notFound',
  '405' => 'methodNotAllowed',
  '409' => 'conflict',
  '410' => 'gone',
  '412' => 'preconditionFailed',
  '422' => 'unprocessable'
}

Instance Method Summary collapse

Instance Method Details

#compare(assert_type, received, operator, expected = nil) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/testscript_engine/assertion.rb', line 78

def compare(assert_type, received, operator, expected = nil)
  operator = 'equals' unless operator
  outcome = begin
    case operator
    when 'equals'
      expected == received
    when 'notEquals'
      expected != received
    when 'in'
      expected.split(',').include? received
    when 'notIn'
      !expected.split(',').include? received
    when 'greaterThan'
      received.to_i > expected.to_i
    when 'lessThan'
      received.to_i < expected.to_i
    when 'empty'
      received.blank?
    when 'notEmpty'
      received.present?
    when 'contains'
      received&.include? expected
    when 'notContains'
      !received&.include? expected
    end
  end

  if outcome
    pass_message(assert_type, received, operator, expected)
  else
    fail_message = fail_message(assert_type, received, operator, expected)
    raise AssertionException.new(fail_message, :fail)
  end
end

#content_type(assert) ⇒ Object



129
130
131
132
# File 'lib/testscript_engine/assertion.rb', line 129

def content_type(assert)
  received = request_header(assert.sourceId, 'Content-Type')
  compare("Content-Type", received, assert.operator, assert.contentType)
end

#determine_assert_type(all_elements) ⇒ Object



57
58
59
60
# File 'lib/testscript_engine/assertion.rb', line 57

def determine_assert_type(all_elements)
  assert_type = all_elements.detect { |elem| ASSERT_TYPES.include? elem }
  return assert_type.split(ASSERT_TYPES_MATCHER).map(&:downcase).join('_')
end

#determine_expected_value(assert) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
# File 'lib/testscript_engine/assertion.rb', line 66

def determine_expected_value(assert)
  if assert.value
    assert.value
  elsif assert.compareToSourceExpression
    FHIRPath.evaluate(assert.compareToSourceExpression,
      get_resource(assert.compareToSourceId).to_hash)
  elsif assert.compareToSourcePath
    evaluate_path(assert.compareToSourcePath,
      get_resource(assert.compareToSourceId))
  end
end

#directionObject



62
63
64
# File 'lib/testscript_engine/assertion.rb', line 62

def direction
  @direction ||= 'response'
end

#evaluate(assert) ⇒ Object



47
48
49
50
51
52
53
54
55
# File 'lib/testscript_engine/assertion.rb', line 47

def evaluate(assert)
  @direction = assert.direction
  assert_elements = assert.to_hash.keys
  assert_type = determine_assert_type(assert_elements)

  outcome_message = send(assert_type.to_sym, assert)

  pass(:eval_assert_result, outcome_message)
end

#evaluate_path(path, resource) ⇒ Object



256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/testscript_engine/assertion.rb', line 256

def evaluate_path(path, resource)
  return unless path and resource

  begin
    # Then, try xpath if necessary
    result = extract_xpath_value(resource.to_xml, path)
  rescue
    # If xpath fails, see if JSON path will work...
    result = JsonPath.new(path).first(resource.to_json)
  end
  return result
end

#expression(assert) ⇒ Object

Raises:



134
135
136
137
138
139
140
141
# File 'lib/testscript_engine/assertion.rb', line 134

def expression(assert)
  resource = get_resource(assert.sourceId)
  raise AssertionException.new('No resource given by sourceId.', :fail) unless resource

  received = FHIRPath.evaluate(assert.expression, resource.to_hash)
  expected = determine_expected_value(assert)
  compare("Expression", received, assert.operator, expected)
end

#extract_xpath_value(resource_xml, resource_xpath) ⇒ Object



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/testscript_engine/assertion.rb', line 269

def extract_xpath_value(resource_xml, resource_xpath)
  # Massage the xpath if it doesn't have fhir: namespace or if doesn't end in @value
  # Also make it look in the entire xml document instead of just starting at the root
  xpath = resource_xpath.split('/').map do |s|
    s.start_with?('fhir:') || s.length.zero? || s.start_with?('@') ? s : "fhir:#{s}"
  end.join('/')
  xpath = "#{xpath}/@value" unless xpath.end_with? '@value'
  xpath = "//#{xpath}"

  resource_doc = Nokogiri::XML(resource_xml)
  resource_doc.root.add_namespace_definition('fhir', 'http://hl7.org/fhir')
  resource_element = resource_doc.xpath(xpath)

  # This doesn't work on warningOnly; consider putting back in place
  # raise AssertionException.new("[#{resource_xpath}] resolved to multiple values instead of a single value", resource_element.to_s) if resource_element.length>1
  resource_element.first.value
end

#fail_message(assert_type, received, operator, expected) ⇒ Object



121
122
123
124
125
126
127
# File 'lib/testscript_engine/assertion.rb', line 121

def fail_message(assert_type, received, operator, expected)
  received = Array(received)
  expected = Array(expected)
  message = "#{assert_type}: Expected #{assert_type} #{operator}"
  message = message + " #{expected}" if expected
  message + ", but found #{received}."
end

#get_request(id) ⇒ Object



229
230
231
232
# File 'lib/testscript_engine/assertion.rb', line 229

def get_request(id)
  return request_map[id] if id
  reply&.request
end

#get_resource(id) ⇒ Object

<— TO DO: MOVE TO UTILITIES MODULE —>



216
217
218
219
220
221
222
# File 'lib/testscript_engine/assertion.rb', line 216

def get_resource(id)
  if direction == 'request'
    get_request(id)&.[](:payload)
  else
    get_response(id)&.[](:body) || fixtures[id]
  end
end

#get_response(id) ⇒ Object



224
225
226
227
# File 'lib/testscript_engine/assertion.rb', line 224

def get_response(id)
  return response_map[id] if id
  reply&.response
end

#header_field(assert) ⇒ Object



143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/testscript_engine/assertion.rb', line 143

def header_field(assert)
  received = begin
    if direction == 'request'
      request_header(assert.sourceId, assert.headerField)
    else
      response_header(assert.sourceId, assert.headerField)
    end
  end

  expected = determine_expected_value(assert)
  compare("Header #{assert.headerField}", received, assert.operator, expected)
end

#minimum_id(assert) ⇒ Object

Raises:



156
157
158
159
160
161
# File 'lib/testscript_engine/assertion.rb', line 156

def minimum_id(assert)
  received = get_resource(assert.sourceId)

  raise AssertionException.new('minimumId assert not yet supported.', :skip)
  # result = client.validate(received, { profile_uri: assert.validateProfileId })
end

Raises:



163
164
165
166
167
168
169
170
# File 'lib/testscript_engine/assertion.rb', line 163

def navigation_links(assert)
  received = get_resource(assert.sourceId)
  result = received&.first_link && received&.last_link && received&.next_link

  return "Navigation Links: As expected, all navigation links found." if result

  raise AssertionException.new("Navigation Links: Expected all navigation links, but did not receive.", :fail)
end

#pass_message(assert_type, received, operator, expected) ⇒ Object



113
114
115
116
117
118
119
# File 'lib/testscript_engine/assertion.rb', line 113

def pass_message(assert_type, received, operator, expected)
  received = Array(received)
  expected = Array(expected)
  message = "#{assert_type}: As expected, #{assert_type} #{operator}"
  message = message + (expected ? " #{expected}."  : '.')
  message + " Found #{received}." if received
end

#path(assert) ⇒ Object



172
173
174
175
176
177
# File 'lib/testscript_engine/assertion.rb', line 172

def path(assert)
  resource = get_resource(assert.sourceId)
  received = evaluate_path(assert.path, resource)
  expected = determine_expected_value(assert)
  compare("Path", received, assert.operator, expected)
end

#request_header(requestId = nil, header_name = nil) ⇒ Object



245
246
247
248
249
250
251
252
253
254
# File 'lib/testscript_engine/assertion.rb', line 245

def request_header(requestId = nil, header_name = nil)
  request = requestId ? request_map[requestId] : reply&.request
  return unless request

  headers = request[:headers]
  return unless headers

  headers.transform_keys!(&:downcase)
  header_name ? headers[header_name.downcase] : headers
end

#request_method(assert) ⇒ Object



179
180
181
182
183
184
# File 'lib/testscript_engine/assertion.rb', line 179

def request_method(assert)
  request = assert.sourceId ? request_map[assert.sourceId] : reply.request
  received = request[:method]
  expected = determine_expected_value(assert)
  compare("Request Method", received, assert.operator, expected)
end

#request_url(assert) ⇒ Object



209
210
211
212
# File 'lib/testscript_engine/assertion.rb', line 209

def request_url(assert)
  received = get_request(assert.sourceId)[:url]
  compare("RequestURL", received, assert.operator, assert.requestURL)
end

#resource(assert) ⇒ Object



186
187
188
189
# File 'lib/testscript_engine/assertion.rb', line 186

def resource(assert)
  received = get_resource(assert.sourceId)
  compare("Resource", received&.resourceType, assert.operator, assert.resource)
end

#response(assert) ⇒ Object



196
197
198
199
200
# File 'lib/testscript_engine/assertion.rb', line 196

def response(assert)
  received_code = get_response(assert.sourceId)&.[](:code).to_s
  received = CODE_MAP[received_code]
  compare("Response", received, assert.operator, assert.response)
end

#response_code(assert) ⇒ Object



191
192
193
194
# File 'lib/testscript_engine/assertion.rb', line 191

def response_code(assert)
  received = get_response(assert.sourceId)&.[](:code).to_s
  compare("Response Code", received, assert.operator, assert.responseCode)
end

#response_header(responseId = nil, header_name = nil) ⇒ Object



234
235
236
237
238
239
240
241
242
243
# File 'lib/testscript_engine/assertion.rb', line 234

def response_header(responseId = nil, header_name = nil)
  response = responseId ? response_map[responseId] : reply&.response
  return unless response

  headers = response[:headers]
  return unless headers

  headers.transform_keys!(&:downcase)
  header_name ? headers[header_name.downcase] : headers
end

#validate_profile_id(assert) ⇒ Object

Raises:



202
203
204
205
206
207
# File 'lib/testscript_engine/assertion.rb', line 202

def validate_profile_id(assert)
  received = get_resource(assert.sourceId)

  raise AssertionException.new('validateProfileId assert not yet supported.', :skip)
  # result = client.validate(received, { profile_uri: assert.validateProfileId })
end