onc_certification_g10_test_kit 2.0.0.rc1

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.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/lib/inferno/exceptions.rb +31 -0
  4. data/lib/inferno/ext/bloomer.rb +24 -0
  5. data/lib/inferno/repositiories/validators.rb +17 -0
  6. data/lib/inferno/repositiories/value_sets.rb +26 -0
  7. data/lib/inferno/terminology/bcp47.rb +95 -0
  8. data/lib/inferno/terminology/bcp_13.rb +26 -0
  9. data/lib/inferno/terminology/codesystem.rb +49 -0
  10. data/lib/inferno/terminology/expected_manifest.yml +1123 -0
  11. data/lib/inferno/terminology/fhir_package_manager.rb +69 -0
  12. data/lib/inferno/terminology/loader.rb +298 -0
  13. data/lib/inferno/terminology/tasks/check_built_terminology.rb +77 -0
  14. data/lib/inferno/terminology/tasks/cleanup.rb +13 -0
  15. data/lib/inferno/terminology/tasks/cleanup_precursors.rb +23 -0
  16. data/lib/inferno/terminology/tasks/count_codes_in_value_set.rb +20 -0
  17. data/lib/inferno/terminology/tasks/create_value_set_validators.rb +34 -0
  18. data/lib/inferno/terminology/tasks/download_fhir_terminology.rb +27 -0
  19. data/lib/inferno/terminology/tasks/download_umls.rb +109 -0
  20. data/lib/inferno/terminology/tasks/download_umls_notice.rb +20 -0
  21. data/lib/inferno/terminology/tasks/expand_value_set_to_file.rb +36 -0
  22. data/lib/inferno/terminology/tasks/process_umls.rb +91 -0
  23. data/lib/inferno/terminology/tasks/process_umls_translations.rb +85 -0
  24. data/lib/inferno/terminology/tasks/run_umls_jar.rb +75 -0
  25. data/lib/inferno/terminology/tasks/temp_dir.rb +27 -0
  26. data/lib/inferno/terminology/tasks/unzip_umls.rb +42 -0
  27. data/lib/inferno/terminology/tasks/validate_code.rb +36 -0
  28. data/lib/inferno/terminology/tasks.rb +11 -0
  29. data/lib/inferno/terminology/terminology_configuration.rb +52 -0
  30. data/lib/inferno/terminology/terminology_validation.rb +42 -0
  31. data/lib/inferno/terminology/validator.rb +64 -0
  32. data/lib/inferno/terminology/value_set.rb +462 -0
  33. data/lib/inferno/terminology.rb +16 -0
  34. data/lib/onc_certification_g10_test_kit/authorization_request_builder.rb +87 -0
  35. data/lib/onc_certification_g10_test_kit/base_token_refresh_group.rb +48 -0
  36. data/lib/onc_certification_g10_test_kit/bulk_data_authorization.rb +235 -0
  37. data/lib/onc_certification_g10_test_kit/bulk_data_group_export.rb +255 -0
  38. data/lib/onc_certification_g10_test_kit/bulk_data_group_export_validation.rb +474 -0
  39. data/lib/onc_certification_g10_test_kit/bulk_data_jwks.json +58 -0
  40. data/lib/onc_certification_g10_test_kit/bulk_export_validation_tester.rb +171 -0
  41. data/lib/onc_certification_g10_test_kit/configuration_checker.rb +104 -0
  42. data/lib/onc_certification_g10_test_kit/export_kick_off_performer.rb +12 -0
  43. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodyheight.json +3772 -0
  44. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodytemp.json +3772 -0
  45. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodyweight.json +3772 -0
  46. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bp.json +6034 -0
  47. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-heartrate.json +3756 -0
  48. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-resprate.json +3756 -0
  49. data/lib/onc_certification_g10_test_kit/limited_scope_grant_test.rb +66 -0
  50. data/lib/onc_certification_g10_test_kit/multi_patient_api.rb +43 -0
  51. data/lib/onc_certification_g10_test_kit/patient_context_test.rb +30 -0
  52. data/lib/onc_certification_g10_test_kit/profile_guesser.rb +69 -0
  53. data/lib/onc_certification_g10_test_kit/resource_access_test.rb +96 -0
  54. data/lib/onc_certification_g10_test_kit/restricted_access_test.rb +12 -0
  55. data/lib/onc_certification_g10_test_kit/restricted_resource_type_access_group.rb +303 -0
  56. data/lib/onc_certification_g10_test_kit/smart_app_launch_invalid_aud_group.rb +136 -0
  57. data/lib/onc_certification_g10_test_kit/smart_ehr_practitioner_app_group.rb +209 -0
  58. data/lib/onc_certification_g10_test_kit/smart_invalid_token_group.rb +197 -0
  59. data/lib/onc_certification_g10_test_kit/smart_limited_app_group.rb +123 -0
  60. data/lib/onc_certification_g10_test_kit/smart_public_standalone_launch_group.rb +113 -0
  61. data/lib/onc_certification_g10_test_kit/smart_scopes_test.rb +153 -0
  62. data/lib/onc_certification_g10_test_kit/smart_standalone_patient_app_group.rb +177 -0
  63. data/lib/onc_certification_g10_test_kit/terminology_binding_validator.rb +140 -0
  64. data/lib/onc_certification_g10_test_kit/token_revocation_group.rb +133 -0
  65. data/lib/onc_certification_g10_test_kit/unauthorized_access_test.rb +25 -0
  66. data/lib/onc_certification_g10_test_kit/unrestricted_resource_type_access_group.rb +375 -0
  67. data/lib/onc_certification_g10_test_kit/version.rb +3 -0
  68. data/lib/onc_certification_g10_test_kit/visual_inspection_and_attestations_group.rb +470 -0
  69. data/lib/onc_certification_g10_test_kit/well_known_capabilities_test.rb +37 -0
  70. data/lib/onc_certification_g10_test_kit.rb +223 -0
  71. metadata +310 -0
@@ -0,0 +1,462 @@
1
+ require 'sqlite3'
2
+ require 'date'
3
+ require 'fhir_models'
4
+
5
+ require_relative '../exceptions'
6
+ require_relative 'bcp_13'
7
+ require_relative 'bcp47'
8
+ require_relative 'codesystem'
9
+ require_relative 'fhir_package_manager'
10
+
11
+ module Inferno
12
+ module Terminology
13
+ class ValueSet
14
+ # STU3 ValueSets located at: http://hl7.org/fhir/stu3/terminologies-valuesets.html
15
+ # STU3 ValueSet Resource: http://hl7.org/fhir/stu3/valueset.html
16
+ #
17
+ # snomed in umls: https://www.nlm.nih.gov/research/umls/Snomed/snomed_represented.html
18
+
19
+ # The UMLS Database
20
+ attr_accessor :db
21
+ # The FHIR::Model Representation of the ValueSet
22
+ attr_accessor :value_set_model
23
+
24
+ # Flag to say "use the provided expansion" when processing the valueset
25
+ attr_accessor :use_expansions
26
+
27
+ @value_sets_repo = Inferno::Repositories::ValueSets.new
28
+
29
+ class << self
30
+ attr_reader :value_sets_repo
31
+ end
32
+
33
+ # UMLS Vocabulary: https://www.nlm.nih.gov/research/umls/sourcereleasedocs/index.html
34
+ SAB = {
35
+ 'http://www.nlm.nih.gov/research/umls/rxnorm' => {
36
+ abbreviation: 'RXNORM',
37
+ name: 'RxNorm Vocabulary'
38
+ }.freeze,
39
+ 'http://loinc.org' => {
40
+ abbreviation: 'LNC',
41
+ name: 'Logical Observation Identifiers Names and Codes terminology (LOINC)'
42
+ }.freeze,
43
+ 'http://snomed.info/sct' => {
44
+ abbreviation: 'SNOMEDCT_US',
45
+ name: 'Systematized Nomenclature of Medicine-Clinical Terms (SNOMED CT), US Edition'
46
+ }.freeze,
47
+ 'http://www.cms.gov/Medicare/Coding/ICD10' => {
48
+ abbreviation: 'ICD10PCS',
49
+ name: 'ICD-10 Procedure Coding System (ICD-10-PCS)'
50
+ }.freeze,
51
+ 'http://hl7.org/fhir/sid/cvx' => {
52
+ abbreviation: 'CVX',
53
+ name: 'Vaccines Administered (CVX)'
54
+ }.freeze,
55
+ 'http://hl7.org/fhir/sid/icd-10-cm' => {
56
+ abbreviation: 'ICD10CM',
57
+ name: 'International Classification of Diseases, Tenth Revision, Clinical Modification (ICD-10-CM)'
58
+ }.freeze,
59
+ 'http://hl7.org/fhir/sid/icd-9-cm' => {
60
+ abbreviation: 'ICD9CM',
61
+ name: 'International Classification of Diseases, Ninth Revision, Clinical Modification (ICD-9-CM)'
62
+ }.freeze,
63
+ 'http://unitsofmeasure.org' => {
64
+ abbreviation: 'NCI_UCUM',
65
+ name: 'Unified Code for Units of Measure (UCUM)'
66
+ }.freeze,
67
+ 'http://nucc.org/provider-taxonomy' => {
68
+ abbreviation: 'NUCCHCPT',
69
+ name: 'National Uniform Claim Committee - Health Care Provider Taxonomy (NUCCHCPT)'
70
+ }.freeze,
71
+ 'http://www.ama-assn.org/go/cpt' => {
72
+ abbreviation: 'CPT',
73
+ name: 'Current Procedural Terminology (CPT)'
74
+ }.freeze,
75
+ 'urn:oid:2.16.840.1.113883.6.285' => {
76
+ abbreviation: 'HCPCS',
77
+ name: 'Healthcare Common Procedure Coding System (HCPCS)'
78
+ }.freeze,
79
+ 'urn:oid:2.16.840.1.113883.6.13' => {
80
+ abbreviation: 'CDT',
81
+ name: 'Code on Dental Procedures and Nomenclature (CDT)'
82
+ }.freeze
83
+ }.freeze
84
+
85
+ CODE_SYS = {
86
+ 'urn:ietf:bcp:13' => -> { BCP13.code_set },
87
+ 'urn:ietf:bcp:47' => ->(filter = nil) { BCP47.code_set(filter) },
88
+ 'http://ihe.net/fhir/ValueSet/IHE.FormatCode.codesystem' =>
89
+ -> { value_sets_repo.find('http://hl7.org/fhir/ValueSet/formatcodes').value_set },
90
+ 'https://www.usps.com/' =>
91
+ -> { value_sets_repo.find('http://hl7.org/fhir/us/core/ValueSet/us-core-usps-state').value_set }
92
+ }.freeze
93
+
94
+ # https://www.nlm.nih.gov/research/umls/knowledge_sources/metathesaurus/release/attribute_names.html
95
+ FILTER_PROP = {
96
+ 'CLASSTYPE' => 'LCN',
97
+ 'DOC' => 'Doc',
98
+ 'SCALE_TYP' => 'LOINC_SCALE_TYP'
99
+ }.freeze
100
+
101
+ def initialize(database, use_expansions = true) # rubocop:disable Style/OptionalBooleanParameter
102
+ @db = database
103
+ @use_expansions = use_expansions
104
+ end
105
+
106
+ def umls_abbreviation(system)
107
+ return SAB.dig(system, :abbreviation) if system != 'http://nucc.org/provider-taxonomy'
108
+
109
+ @nucc_system ||= # rubocop:disable Naming/MemoizedInstanceVariableName
110
+ if @db.execute("SELECT COUNT(*) FROM mrconso WHERE SAB = 'NUCCPT'").flatten.first.positive?
111
+ 'NUCCPT'
112
+ else
113
+ 'NUCCHCPT'
114
+ end
115
+ end
116
+
117
+ def code_system_metadata(system)
118
+ SAB[system]
119
+ end
120
+
121
+ def value_sets_repo
122
+ self.class.value_sets_repo
123
+ end
124
+
125
+ # The ValueSet [Set]
126
+ def value_set
127
+ return @value_set if @value_set
128
+
129
+ if @use_expansions
130
+ process_with_expansions
131
+ else
132
+ process_value_set
133
+ end
134
+ end
135
+
136
+ # Read the desired valueset from a JSON file
137
+ #
138
+ # @param filename [String] the name of the file
139
+ def read_value_set(filename)
140
+ @value_set_model = FHIR::Json.from_json(File.read(filename))
141
+ end
142
+
143
+ def code_system_set(code_system)
144
+ filter_code_set(code_system)
145
+ end
146
+
147
+ def expansion_as_fhir_value_set
148
+ expansion_backbone = FHIR::ValueSet::Expansion.new
149
+ expansion_backbone.timestamp = DateTime.now.strftime('%Y-%m-%dT%H:%M:%S%:z')
150
+ expansion_backbone.contains = value_set.map do |code|
151
+ FHIR::ValueSet::Expansion::Contains.new({ system: code[:system], code: code[:code] })
152
+ end
153
+ expansion_backbone.total = expansion_backbone.contains.length
154
+ expansion_value_set = @value_set_model.deep_dup # Make a copy so that the original definition is left intact
155
+ expansion_value_set.expansion = expansion_backbone
156
+ expansion_value_set
157
+ end
158
+
159
+ # Return the url of the valueset
160
+ def url
161
+ @value_set_model.url
162
+ end
163
+
164
+ # Return the number of codes in the valueset
165
+ def count
166
+ @value_set.length
167
+ end
168
+
169
+ def included_code_systems
170
+ @value_set_model.compose.include.map(&:system).compact.uniq
171
+ end
172
+
173
+ TOO_COSTLY_URL = 'http://hl7.org/fhir/StructureDefinition/valueset-toocostly'.freeze
174
+ def too_costly?
175
+ @value_set_model&.expansion&.extension&.find { |vs| vs.url == TOO_COSTLY_URL }&.value
176
+ end
177
+
178
+ UNCLOSED_URL = 'http://hl7.org/fhir/StructureDefinition/valueset-unclosed'.freeze
179
+ def unclosed?
180
+ @value_set_model&.expansion&.extension&.find { |vs| vs.url == UNCLOSED_URL }&.value
181
+ end
182
+
183
+ def expansion_present?
184
+ !!@value_set_model&.expansion&.contains
185
+ end
186
+
187
+ # Delegates to process_expanded_valueset if there's already an expansion
188
+ # Otherwise it delegates to process_valueset to do the expansion
189
+ def process_with_expansions
190
+ if expansion_present?
191
+ # This is moved into a nested clause so we can tell in the debug statements which path we're taking
192
+ if too_costly? || unclosed?
193
+ Inferno.logger.debug("ValueSet too costly or unclosed: #{url}")
194
+ process_value_set
195
+ else
196
+ Inferno.logger.debug("Processing expanded valueset: #{url}")
197
+ process_expanded_value_set
198
+ end
199
+ else
200
+ Inferno.logger.debug("Processing composed valueset: #{url}")
201
+ process_value_set
202
+ end
203
+ end
204
+
205
+ # Creates the whole valueset
206
+ #
207
+ # Creates a [Set] representing the valueset
208
+ def process_value_set
209
+ Inferno.logger.debug "Processing #{@value_set_model.url}"
210
+ include_set = Set.new
211
+ @value_set_model.compose.include.each do |include|
212
+ # Cumulative of each include
213
+ include_set.merge(get_code_sets(include))
214
+ end
215
+ @value_set_model.compose.exclude.each do |exclude|
216
+ # Remove excluded codes
217
+ include_set.subtract(get_code_sets(exclude))
218
+ end
219
+ @value_set = include_set
220
+ end
221
+
222
+ def process_expanded_value_set
223
+ include_set = Set.new
224
+ @value_set_model.expansion.contains.each do |contain|
225
+ include_set.add(system: contain.system, code: contain.code)
226
+ end
227
+ @value_set = include_set
228
+ end
229
+
230
+ # Checks if the provided code is in the valueset
231
+ #
232
+ # Codes should be provided as a [Hash] type object
233
+ #
234
+ # e.g. {system: 'http://loinc.org', code: '1234'}
235
+ #
236
+ # @param [Hash] code the code to evaluate
237
+ # @return [Boolean]
238
+ def contains_code?(code)
239
+ @value_set.include? code
240
+ end
241
+
242
+ def generate_bloom
243
+ require 'bloomer'
244
+
245
+ @bf = Bloomer::Scalable.create_with_sufficient_size(value_set.length)
246
+ value_set.each do |cc|
247
+ @bf.add_without_duplication("#{cc[:system]}|#{cc[:code]}")
248
+ end
249
+ @bf
250
+ end
251
+
252
+ # Saves the valueset bloomfilter to a msgpack file
253
+ #
254
+ # @param [String] filename the name of the file
255
+ def save_bloom_to_file(
256
+ filename = "resources/validators/bloom/#{(URI(url).host + URI(url).path).gsub(%r{[./]}, '_')}.msgpack"
257
+ )
258
+ generate_bloom unless @bf
259
+ bloom_file = File.new(filename, 'wb')
260
+ bloom_file.write(@bf.to_msgpack) unless @bf.nil?
261
+ filename
262
+ end
263
+
264
+ # Saves the valueset to a csv
265
+ # @param [String] filename the name of the file
266
+ def save_csv_to_file(filename = "resources/validators/csv/#{(URI(url).host + URI(url).path).gsub(%r{[./]},
267
+ '_')}.csv")
268
+ CSV.open(filename, 'wb') do |csv|
269
+ value_set.each do |code|
270
+ csv << [code[:system], code[:code]]
271
+ end
272
+ end
273
+ end
274
+
275
+ # Load a code system from a file
276
+ #
277
+ # @param [String] filename the file containing the code system JSON
278
+ def self.load_system(filename)
279
+ # TODO: Generalize this
280
+ cs = FHIR::Json.from_json(File.read(filename))
281
+ cs_set = Set.new
282
+ load_codes = lambda do |concept|
283
+ concept.each do |concept_code|
284
+ cs_set.add(system: cs.url, code: concept_code.code)
285
+ load_codes.call(concept_code.concept) unless concept_code.concept.empty?
286
+ end
287
+ end
288
+ load_codes.call(cs.concept)
289
+ cs_set
290
+ end
291
+
292
+ private
293
+
294
+ # Get all the code systems from within an include/exclude and return the set representing the intersection
295
+ #
296
+ # See: http://hl7.org/fhir/stu3/valueset.html#compositions
297
+ #
298
+ # @param [ValueSet::Compose::Include] vscs the FHIR ValueSet include or exclude
299
+ def get_code_sets(vscs)
300
+ intersection_set = nil
301
+
302
+ # Get Concepts
303
+ if !vscs.concept.empty?
304
+ intersection_set = Set.new
305
+ vscs.concept.each do |concept|
306
+ intersection_set.add(system: vscs.system, code: concept.code)
307
+ end
308
+ # Filter based on the filters. Note there cannot be both concepts and filters within a single include/exclude
309
+ elsif !vscs.filter.empty?
310
+ intersection_set = filter_code_set(vscs.system, vscs.filter.first)
311
+ vscs.filter.drop(1).each do |filter|
312
+ intersection_set = intersection_set.intersection(filter_code_set(vscs.system, filter))
313
+ end
314
+ # Import whole code systems if given
315
+ elsif vscs.system
316
+ intersection_set = filter_code_set(vscs.system)
317
+ end
318
+
319
+ unless vscs.valueSet.empty?
320
+ # If no concepts or filtered systems were present and already created the intersection_set
321
+ im_val_set = import_value_set(vscs.valueSet.first)
322
+ vscs.valueSet.drop(1).each do |im_val|
323
+ im_val_set = im_val_set.intersection(im_val)
324
+ end
325
+ intersection_set = intersection_set.nil? ? im_val_set : intersection_set.intersection(im_val_set)
326
+ end
327
+ intersection_set
328
+ end
329
+
330
+ # Provides a codeset based on the system and filters provided
331
+ # @param [String] system the code system url
332
+ # @param [FHIR::ValueSet::Compose::Include::Filter] filter the filter object
333
+ # @return [Set] the filtered set of codes
334
+ def filter_code_set(system, filter = nil, _version = nil)
335
+ fhir_codesystem = File.join(PACKAGE_DIR, "#{FHIRPackageManager.encode_name(system)}.json")
336
+ if CODE_SYS.include? system
337
+ Inferno.logger.debug " loading #{system} codes..."
338
+ return filter.nil? ? CODE_SYS[system].call : CODE_SYS[system].call(filter)
339
+ elsif File.exist?(fhir_codesystem)
340
+ if umls_abbreviation(system).nil?
341
+ fhir_cs = Inferno::Terminology::Codesystem
342
+ .new(FHIR::Json.from_json(File.read(fhir_codesystem)))
343
+
344
+ raise UnknownCodeSystemException, system if fhir_cs.codesystem_model.concept.empty?
345
+
346
+ return fhir_cs.filter_codes(filter)
347
+ end
348
+ end
349
+
350
+ filter_clause = lambda do |filter| # rubocop:disable Lint/ShadowingOuterLocalVariable
351
+ where = +''
352
+ case filter.op
353
+ when 'in'
354
+ col = filter.property
355
+ vals = filter.value.split(',')
356
+ where << "( #{col} = '#{vals[0]}'"
357
+ # Remove the first element after we've used it
358
+ vals.shift
359
+ vals.each do |val|
360
+ where << " OR #{col} = '#{val}' "
361
+ end
362
+ where << ')'
363
+ when '='
364
+ col = filter.property
365
+ where << "#{col} = '#{filter.value}'"
366
+ else
367
+ Inferno.logger.debug "Cannot handle filter operation: #{filter.op}"
368
+ end
369
+ where
370
+ end
371
+
372
+ filtered_set = Set.new
373
+ raise FilterOperationException, filter&.op unless ['=', 'in', 'is-a', nil].include? filter&.op
374
+ raise UnknownCodeSystemException, system if umls_abbreviation(system).nil?
375
+
376
+ # Fix for some weirdness around UMLS and provider taxonomy subsetting
377
+ if system == 'http://nucc.org/provider-taxonomy'
378
+ @db.execute(
379
+ "SELECT code FROM mrconso WHERE SAB = '#{umls_abbreviation(system)}' AND TTY IN('PT', 'OP')"
380
+ ) do |row|
381
+ filtered_set.add(system: system, code: row[0])
382
+ end
383
+ elsif filter.nil?
384
+ @db.execute("SELECT code FROM mrconso WHERE SAB = '#{umls_abbreviation(system)}'") do |row|
385
+ filtered_set.add(system: system, code: row[0])
386
+ end
387
+ elsif ['=', 'in', nil].include? filter&.op
388
+ if FILTER_PROP[filter.property]
389
+ @db.execute(
390
+ "SELECT code FROM mrsat WHERE SAB = '#{umls_abbreviation(system)}' " \
391
+ "AND ATN = '#{fp_self(filter.property)}' AND ATV = '#{fp_self(filter.value)}'"
392
+ ) do |row|
393
+ filtered_set.add(system: system, code: row[0])
394
+ end
395
+ else
396
+ @db.execute(
397
+ "SELECT code FROM mrconso WHERE SAB = '#{umls_abbreviation(system)}' AND #{filter_clause.call(filter)}"
398
+ ) do |row|
399
+ filtered_set.add(system: system, code: row[0])
400
+ end
401
+ end
402
+ elsif filter&.op == 'is-a'
403
+ filtered_set = filter_is_a(system, filter)
404
+ else
405
+ raise FilterOperationException, filter&.op
406
+ end
407
+ filtered_set
408
+ end
409
+
410
+ # Imports the ValueSet with the provided URL from the known local ValueSet Authority
411
+ #
412
+ # @param [Object] url the url of the desired valueset
413
+ # @return [Set] the imported valueset
414
+ def import_value_set(desired_url)
415
+ value_sets_repo.find(desired_url)
416
+ end
417
+
418
+ # Filters UMLS codes for "is-a" filters
419
+ #
420
+ # @param [String] system The code system url
421
+ # @param [FHIR::ValueSet::Compose::Include::Filter] filter the filter object
422
+ # @return [Set] the filtered codes
423
+ def filter_is_a(system, filter)
424
+ children = {}
425
+ find_children = lambda do |_parent, system| # rubocop:disable Lint/ShadowingOuterLocalVariable
426
+ @db.execute("SELECT c1.code, c2.code
427
+ FROM mrrel r
428
+ JOIN mrconso c1 ON c1.aui=r.aui1
429
+ JOIN mrconso c2 ON c2.aui=r.aui2
430
+ WHERE r.rel='CHD' AND r.SAB= '#{umls_abbreviation(system)}'") do |row|
431
+ children[row[0]] ||= []
432
+ children[row[0]] << row[1]
433
+ end
434
+ end
435
+ # Get all the children/parent hierarchy
436
+ find_children.call(filter.value, system)
437
+
438
+ desired_children = Set.new
439
+ subsume = lambda do |parent|
440
+ # Only execute if we haven't processed this parent yet
441
+ par = { system: system, code: parent }
442
+ unless desired_children.include? par
443
+ desired_children.add(system: system, code: parent)
444
+ children[parent]&.each do |child|
445
+ subsume.call(child)
446
+ end
447
+ end
448
+ end
449
+ subsume.call(filter.value)
450
+ desired_children
451
+ end
452
+
453
+ # fp_self is short for filter_prop_or_self
454
+ # @param [String] prop The property name
455
+ # @return [String] either the value from FILTER_PROP for that key, or prop
456
+ # if that key isn't in FILTER_PROP
457
+ def fp_self(prop)
458
+ FILTER_PROP[prop] || prop
459
+ end
460
+ end
461
+ end
462
+ end
@@ -0,0 +1,16 @@
1
+ require_relative 'terminology/loader'
2
+
3
+ module Inferno
4
+ module Terminology
5
+ PACKAGE_DIR = File.join('tmp', 'terminology', 'fhir')
6
+
7
+ def self.code_system_metadata
8
+ @code_system_metadata ||=
9
+ if File.file? File.join('resources', 'terminology', 'validators', 'bloom', 'metadata.yml')
10
+ YAML.load_file(File.join('resources', 'terminology', 'validators', 'bloom', 'metadata.yml'))
11
+ else
12
+ {}
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,87 @@
1
+ require 'json/jwt'
2
+
3
+ module ONCCertificationG10TestKit
4
+ class AuthorizationRequestBuilder
5
+ def self.build(...)
6
+ new(...).authorization_request
7
+ end
8
+
9
+ def self.bulk_data_jwks
10
+ @bulk_data_jwks ||= JSON.parse(File.read(File.join(__dir__, 'bulk_data_jwks.json')))
11
+ end
12
+
13
+ attr_reader :encryption_method, :scope, :iss, :sub, :aud, :content_type, :grant_type, :client_assertion_type, :exp,
14
+ :jti
15
+
16
+ def initialize(
17
+ encryption_method:,
18
+ scope:,
19
+ iss:,
20
+ sub:,
21
+ aud:,
22
+ content_type: 'application/x-www-form-urlencoded',
23
+ grant_type: 'client_credentials',
24
+ client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
25
+ exp: 5.minutes.from_now,
26
+ jti: SecureRandom.hex(32)
27
+ )
28
+ @encryption_method = encryption_method
29
+ @scope = scope
30
+ @iss = iss
31
+ @sub = sub
32
+ @aud = aud
33
+ @content_type = content_type
34
+ @grant_type = grant_type
35
+ @client_assertion_type = client_assertion_type
36
+ @exp = exp
37
+ @jti = jti
38
+ end
39
+
40
+ def bulk_private_key
41
+ @bulk_private_key ||=
42
+ self.class.bulk_data_jwks['keys']
43
+ .select { |key| key['key_ops']&.include?('sign') }
44
+ .find { |key| key['alg'] == encryption_method }
45
+ end
46
+
47
+ def jwt_token
48
+ @jwt_token ||= JSON::JWT.new(iss: iss, sub: sub, aud: aud, exp: exp, jti: jti).compact
49
+ end
50
+
51
+ def jwk
52
+ @jwk ||= JSON::JWK.new(bulk_private_key)
53
+ end
54
+
55
+ def authorization_request_headers
56
+ {
57
+ content_type: content_type,
58
+ accept: 'application/json'
59
+ }.compact
60
+ end
61
+
62
+ def authorization_request_query_values
63
+ {
64
+ 'scope' => scope,
65
+ 'grant_type' => grant_type,
66
+ 'client_assertion_type' => client_assertion_type,
67
+ 'client_assertion' => client_assertion.to_s
68
+ }.compact
69
+ end
70
+
71
+ def client_assertion
72
+ @client_assertion ||=
73
+ begin
74
+ jwt_token.kid = jwk['kid']
75
+ jwk_private_key = jwk.to_key
76
+ jwt_token.sign(jwk_private_key, bulk_private_key['alg'])
77
+ end
78
+ end
79
+
80
+ def authorization_request
81
+ uri = Addressable::URI.new
82
+ uri.query_values = authorization_request_query_values
83
+
84
+ { body: uri.query, headers: authorization_request_headers }
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,48 @@
1
+ module ONCCertificationG10TestKit
2
+ class BaseTokenRefreshGroup < Inferno::TestGroup
3
+ title 'Token Refresh'
4
+ description %(
5
+ # Background
6
+
7
+ The #{title} Sequence tests the ability of the system to successfuly
8
+ exchange a refresh token for an access token. Refresh tokens are typically
9
+ longer lived than access tokens and allow client applications to obtain a
10
+ new access token Refresh tokens themselves cannot provide access to
11
+ resources on the server.
12
+
13
+ Token refreshes are accomplished through a `POST` request to the token
14
+ exchange endpoint as described in the [SMART App Launch
15
+ Framework](http://www.hl7.org/fhir/smart-app-launch/#step-5-later-app-uses-a-refresh-token-to-obtain-a-new-access-token).
16
+
17
+ # Test Methodology
18
+
19
+ This test attempts to exchange the refresh token for a new access token
20
+ and verify that the information returned contains the required fields and
21
+ uses the proper headers.
22
+
23
+ For more information see:
24
+
25
+ * [The OAuth 2.0 Authorization
26
+ Framework](https://tools.ietf.org/html/rfc6749)
27
+ * [Using a refresh token to obtain a new access
28
+ token](http://hl7.org/fhir/smart-app-launch/#step-5-later-app-uses-a-refresh-token-to-obtain-a-new-access-token)
29
+ )
30
+ id :g10_token_refresh
31
+
32
+ test from: :smart_token_refresh,
33
+ id: :g10_token_refresh_without_scopes,
34
+ config: {
35
+ options: { include_scopes: false }
36
+ }
37
+ test from: :smart_token_refresh_body,
38
+ id: :g10_token_refresh_body_without_scopes
39
+ test from: :smart_token_refresh,
40
+ title: 'Server successfully refreshes the access token when optional scope parameter provided',
41
+ id: :g10_token_refresh_with_scopes,
42
+ config: {
43
+ options: { include_scopes: true }
44
+ }
45
+ test from: :smart_token_refresh_body,
46
+ id: :g10_token_refresh_body_with_scopes
47
+ end
48
+ end