diff --git a/CHANGELOG.md b/CHANGELOG.md index c0df4663d..f175a2ed8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +# 0.4.19 +* FI-2053: Fix inputs dialog overflow by @AlyssaWang in + https://github.com/inferno-framework/inferno-core/pull/382 +* FI-2038: Prevent modal close on edit by @AlyssaWang in + https://github.com/inferno-framework/inferno-core/pull/383 +* FI-2094: Improve tooltip a11y by @AlyssaWang in + https://github.com/inferno-framework/inferno-core/pull/386 +* FI-2035: Improve error handling for validator errors by @dehall in + https://github.com/inferno-framework/inferno-core/pull/379 +* FI-2070: Inferno Framework Documentation Advanced Test Features Information + Fix by @emichaud998 in + https://github.com/inferno-framework/inferno-core/pull/385 +* FI-2041: Custom suites with no ids now throw standard error by @alisawallace + in https://github.com/inferno-framework/inferno-core/pull/387 +* FI-2156: Dependabot updates by @Jammjammjamm in + https://github.com/inferno-framework/inferno-core/pull/390 +* FI-2086: fix errors on webpack shutdown by @alisawallace in + https://github.com/inferno-framework/inferno-core/pull/389 + # 0.4.18 * Fix a bug which could prevent some test results from appearing until the page is reloaded. diff --git a/Gemfile.lock b/Gemfile.lock index 4fdb00693..7f3f435a4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,8 +1,8 @@ PATH remote: . specs: - inferno_core (0.4.18) - activesupport (~> 6.1) + inferno_core (0.4.19) + activesupport (~> 6.1.7.5) base62-rb (= 0.3.1) blueprinter (= 0.25.2) dotenv (~> 2.7) @@ -19,7 +19,7 @@ PATH oj (= 3.11.0) pry pry-byebug - puma (~> 5.3) + puma (~> 5.6.7) rake (~> 13.0) sequel (~> 5.42.0) sidekiq (~> 6.5.6) @@ -30,7 +30,7 @@ PATH GEM remote: https://rubygems.org/ specs: - activesupport (6.1.7.3) + activesupport (6.1.7.6) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -178,7 +178,7 @@ GEM kramdown (2.4.0) rexml method_source (1.0.0) - mime-types (3.5.0) + mime-types (3.5.1) mime-types-data (~> 3.2015) mime-types-data (3.2023.0808) minitest (5.18.0) @@ -192,11 +192,11 @@ GEM mustermann (= 1.1.2) netrc (0.11.0) nio4r (2.5.9) - nokogiri (1.15.3-arm64-darwin) + nokogiri (1.15.4-arm64-darwin) racc (~> 1.4) - nokogiri (1.15.3-x86_64-darwin) + nokogiri (1.15.4-x86_64-darwin) racc (~> 1.4) - nokogiri (1.15.3-x86_64-linux) + nokogiri (1.15.4-x86_64-linux) racc (~> 1.4) oauth2 (1.4.11) faraday (>= 0.17.3, < 3.0) @@ -217,7 +217,7 @@ GEM byebug (~> 11.0) pry (~> 0.13.0) public_suffix (4.0.7) - puma (5.6.6) + puma (5.6.7) nio4r (~> 2.0) racc (1.7.1) rack (2.2.6.4) @@ -282,9 +282,9 @@ GEM simplecov (~> 0.19) simplecov-html (0.12.3) simplecov_json_formatter (0.1.3) - sqlite3 (1.6.3-arm64-darwin) - sqlite3 (1.6.3-x86_64-darwin) - sqlite3 (1.6.3-x86_64-linux) + sqlite3 (1.6.5-arm64-darwin) + sqlite3 (1.6.5-x86_64-darwin) + sqlite3 (1.6.5-x86_64-linux) strings (0.2.1) strings-ansi (~> 0.2) unicode-display_width (>= 1.5, < 3.0) @@ -325,6 +325,7 @@ PLATFORMS arm64-darwin-21 arm64-darwin-22 x86_64-darwin-20 + x86_64-darwin-22 x86_64-linux DEPENDENCIES diff --git a/Procfile b/Procfile index 5f35ca5da..4433c1748 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,3 @@ web: bundle exec puma worker: bundle exec sidekiq -r ./worker.rb -webpack: npm run start +webpack: ./node_modules/.bin/webpack serve --config ./webpack.config.js --mode=development diff --git a/client/src/components/App/App.tsx b/client/src/components/App/App.tsx index 28407f69c..e77059783 100644 --- a/client/src/components/App/App.tsx +++ b/client/src/components/App/App.tsx @@ -1,13 +1,21 @@ -import { SnackbarProvider } from 'notistack'; import React, { FC, useEffect } from 'react'; import { RouterProvider } from 'react-router-dom'; +import { Theme } from '@mui/material/styles'; +import { SnackbarProvider } from 'notistack'; import { getTestSuites } from '~/api/TestSuitesApi'; import { router } from '~/components/App/Router'; import { TestSuite } from '~/models/testSuiteModels'; import { useAppStore } from '~/store/app'; import { useTestSessionStore } from '~/store/testSession'; import SnackbarCloseButton from 'components/_common/SnackbarCloseButton'; -import lightTheme from '~/styles/theme'; +import { makeStyles } from 'tss-react/mui'; + +const useStyles = makeStyles<{ height: string }>()((theme: Theme, { height }) => ({ + container: { + marginBottom: height, + zIndex: `${theme.zIndex.snackbar} !important`, + }, +})); const App: FC = () => { const footerHeight = useAppStore((state) => state.footerHeight); @@ -18,6 +26,10 @@ const App: FC = () => { const setWindowIsSmall = useAppStore((state) => state.setWindowIsSmall); const testRunInProgress = useTestSessionStore((state) => state.testRunInProgress); + const { classes } = useStyles({ + height: testRunInProgress ? `${72 + footerHeight}px` : `${footerHeight}px`, + }); + // Update UI on window resize useEffect(() => { window.addEventListener('resize', handleResize); @@ -57,9 +69,8 @@ const App: FC = () => { horizontal: 'right', }} action={(id) => } - style={{ - marginBottom: testRunInProgress ? `${72 + footerHeight}px` : `${footerHeight}px`, - zIndex: lightTheme.zIndex.snackbar, + classes={{ + containerAnchorOriginBottomRight: classes.container, }} > diff --git a/client/src/components/Header/Header.tsx b/client/src/components/Header/Header.tsx index 6884c5df3..08c0a9d21 100644 --- a/client/src/components/Header/Header.tsx +++ b/client/src/components/Header/Header.tsx @@ -8,6 +8,7 @@ import { useAppStore } from '~/store/app'; import useStyles from './styles'; import icon from '~/images/inferno_icon.png'; import lightTheme from '~/styles/theme'; +import CustomTooltip from '../_common/CustomTooltip'; export interface HeaderProps { suiteId?: string; @@ -63,7 +64,13 @@ const Header: FC = ({ ) : ( - Inferno logo + + Inferno logo + )} diff --git a/client/src/components/InputsModal/InputCheckboxGroup.tsx b/client/src/components/InputsModal/InputCheckboxGroup.tsx index de65941f1..203ae7bb4 100644 --- a/client/src/components/InputsModal/InputCheckboxGroup.tsx +++ b/client/src/components/InputsModal/InputCheckboxGroup.tsx @@ -108,6 +108,7 @@ const InputCheckboxGroup: FC = ({ control={ = ({ value={oAuthCredentials[field.name as keyof OAuthCredentials]} className={classes.inputField} variant="standard" + color="secondary" fullWidth onChange={(event) => { const value = event.target.value; diff --git a/client/src/components/InputsModal/InputRadioGroup.tsx b/client/src/components/InputsModal/InputRadioGroup.tsx index 929826e5d..a784b38d9 100644 --- a/client/src/components/InputsModal/InputRadioGroup.tsx +++ b/client/src/components/InputsModal/InputRadioGroup.tsx @@ -66,7 +66,7 @@ const InputRadioGroup: FC = ({ {requirement.options?.list_options?.map((option, i) => ( } + control={} label={option.label} key={`radio-button-${i}`} /> diff --git a/client/src/components/InputsModal/InputTextArea.tsx b/client/src/components/InputsModal/InputTextArea.tsx index 4ebd01074..807f42f6e 100644 --- a/client/src/components/InputsModal/InputTextArea.tsx +++ b/client/src/components/InputsModal/InputTextArea.tsx @@ -25,6 +25,7 @@ const InputTextArea: FC = ({ requirement, index, inputsMap, id={`requirement${index}_input`} className={classes.inputField} variant="standard" + color="secondary" fullWidth label={} helperText={requirement.description} diff --git a/client/src/components/InputsModal/InputTextField.tsx b/client/src/components/InputsModal/InputTextField.tsx index 2374b8feb..841354628 100644 --- a/client/src/components/InputsModal/InputTextField.tsx +++ b/client/src/components/InputsModal/InputTextField.tsx @@ -30,6 +30,7 @@ const InputTextField: FC = ({ id={`requirement${index}_input`} className={classes.inputField} variant="standard" + color="secondary" fullWidth label={} helperText={requirement.description} diff --git a/client/src/components/InputsModal/InputsModal.tsx b/client/src/components/InputsModal/InputsModal.tsx index ad65e0764..97e94cef4 100644 --- a/client/src/components/InputsModal/InputsModal.tsx +++ b/client/src/components/InputsModal/InputsModal.tsx @@ -349,6 +349,7 @@ const InputsModal: FC = ({ input: classes.serialInput, }, }} + color="secondary" fullWidth multiline data-testid="serial-input" @@ -399,7 +400,7 @@ const InputsModal: FC = ({ color="secondary" data-testid="cancel-button" onClick={() => closeModal()} - sx={{ mr: 1 }} + sx={{ mr: 1, fontWeight: 'bold' }} > Cancel @@ -409,6 +410,7 @@ const InputsModal: FC = ({ disableElevation onClick={submitClicked} disabled={missingRequiredInput || invalidInput} + sx={{ fontWeight: 'bold' }} > Submit diff --git a/client/src/components/InputsModal/styles.tsx b/client/src/components/InputsModal/styles.tsx index c6327a761..75279782e 100644 --- a/client/src/components/InputsModal/styles.tsx +++ b/client/src/components/InputsModal/styles.tsx @@ -13,7 +13,7 @@ export default makeStyles()((theme: Theme) => ({ color: theme.palette.common.grayDarkest, }, '& label.Mui-focused': { - color: theme.palette.common.orangeDarkest, + color: theme.palette.secondary.main, }, '& label.Mui-disabled': { color: theme.palette.common.gray, @@ -36,7 +36,7 @@ export default makeStyles()((theme: Theme) => ({ margin: '8px 0', borderColor: theme.palette.common.grayLight, '&:focus-within': { - borderColor: theme.palette.primary.main, + borderColor: theme.palette.secondary.main, }, }, serialInput: { diff --git a/client/src/components/PresetsSelector/PresetsSelector.tsx b/client/src/components/PresetsSelector/PresetsSelector.tsx index 37645f195..94672b5ba 100644 --- a/client/src/components/PresetsSelector/PresetsSelector.tsx +++ b/client/src/components/PresetsSelector/PresetsSelector.tsx @@ -109,7 +109,7 @@ const PresetsSelector: FC = ({ presets, testSessionId, getSes diff --git a/client/src/components/TestSuite/TestSuiteDetails/ResultIcon.tsx b/client/src/components/TestSuite/TestSuiteDetails/ResultIcon.tsx index 30c2539c3..e516c5fec 100644 --- a/client/src/components/TestSuite/TestSuiteDetails/ResultIcon.tsx +++ b/client/src/components/TestSuite/TestSuiteDetails/ResultIcon.tsx @@ -27,7 +27,11 @@ const ResultIcon: FC = ({ result, isRunning }) => { if (isRunning && result?.test_run_id !== testRunId) { return ( - + ); } else if (result) { @@ -36,6 +40,8 @@ const ResultIcon: FC = ({ result, isRunning }) => { return ( @@ -45,7 +51,9 @@ const ResultIcon: FC = ({ result, isRunning }) => { return ( @@ -54,7 +62,9 @@ const ResultIcon: FC = ({ result, isRunning }) => { return ( @@ -63,6 +73,8 @@ const ResultIcon: FC = ({ result, isRunning }) => { return ( @@ -71,13 +83,20 @@ const ResultIcon: FC = ({ result, isRunning }) => { case 'omit': return ( - + ); case 'error': return ( @@ -86,26 +105,38 @@ const ResultIcon: FC = ({ result, isRunning }) => { case 'wait': return ( - + ); default: return ( - + + + ); } } else { return ( - + + + ); } }; diff --git a/client/src/components/TestSuite/TestSuiteDetails/TestGroupListItem/TestGroupListItem.tsx b/client/src/components/TestSuite/TestSuiteDetails/TestGroupListItem/TestGroupListItem.tsx index fd587be04..9105c9863 100644 --- a/client/src/components/TestSuite/TestSuiteDetails/TestGroupListItem/TestGroupListItem.tsx +++ b/client/src/components/TestSuite/TestSuiteDetails/TestGroupListItem/TestGroupListItem.tsx @@ -126,7 +126,7 @@ const TestGroupListItem: FC = ({ data-testid={`${testGroup.id}-summary`} aria-controls={`${testGroup.id}-detail`} className={classes.accordionSummary} - expandIcon={view === 'run' && } + expandIcon={view === 'run' && } > diff --git a/client/src/components/TestSuite/TestSuiteDetails/TestListItem/RequestList.tsx b/client/src/components/TestSuite/TestSuiteDetails/TestListItem/RequestList.tsx index e410f1d39..d3675d715 100644 --- a/client/src/components/TestSuite/TestSuiteDetails/TestListItem/RequestList.tsx +++ b/client/src/components/TestSuite/TestSuiteDetails/TestListItem/RequestList.tsx @@ -69,7 +69,13 @@ const RequestList: FC = ({ requests, resultId, updateRequest, if (request.result_id !== resultId) { return ( - + ); } @@ -150,8 +156,9 @@ const RequestList: FC = ({ requests, resultId, updateRequest, = ({ TransitionProps={{ unmountOnExit: true }} onClick={handleAccordionClick} onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { + if (e.key === 'Enter') { + // Don't open/close accordion on enter + setTabIndex(findPopulatedTabIndex()); + } + if (e.key === ' ') { handleAccordionClick(); } }} @@ -228,7 +233,13 @@ const TestListItem: FC = ({ data-testid={`${test.id}-summary`} aria-controls={`${test.id}-detail`} role={view === 'report' ? 'region' : 'button'} - expandIcon={view === 'run' && } + expandIcon={ + view === 'run' && ( + + + + ) + } className={classes.accordionSummary} onKeyDown={(e) => { if (view !== 'report' && e.key === 'Enter') { diff --git a/client/src/components/_common/ActionModal.tsx b/client/src/components/_common/ActionModal.tsx index 45533a81f..9eb82fac4 100644 --- a/client/src/components/_common/ActionModal.tsx +++ b/client/src/components/_common/ActionModal.tsx @@ -31,6 +31,7 @@ const ActionModal: FC = ({ modalVisible, message, cancelTestRu disableElevation onClick={cancelTestRun} data-testid="cancel-button" + sx={{ fontWeight: 'bold' }} > Cancel diff --git a/client/src/components/_common/CustomTab.tsx b/client/src/components/_common/CustomTab.tsx index 507bc4d6c..e59a070f9 100644 --- a/client/src/components/_common/CustomTab.tsx +++ b/client/src/components/_common/CustomTab.tsx @@ -10,7 +10,7 @@ interface CustomTabProps { const CustomTab = styled((props: CustomTabProps) => )( ({ theme }) => ({ pointerEvents: 'auto', - fontWeight: 'bolder', + fontWeight: 'bold', '&:hover, :focus-within': { color: theme.palette.common.grayDarkest, }, diff --git a/client/src/components/_common/SelectionPanel/RadioSelection.tsx b/client/src/components/_common/SelectionPanel/RadioSelection.tsx index 8b6bff6af..bf9f6673f 100644 --- a/client/src/components/_common/SelectionPanel/RadioSelection.tsx +++ b/client/src/components/_common/SelectionPanel/RadioSelection.tsx @@ -3,6 +3,7 @@ import { Box, FormControl, FormControlLabel, FormLabel, Radio, RadioGroup } from import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'; import { RadioOption, RadioOptionSelection } from '~/models/selectionModels'; import CustomTooltip from '~/components/_common/CustomTooltip'; +import useStyles from '~/components/_common/SelectionPanel/styles'; export interface RadioSelectionProps { options: RadioOption[]; @@ -13,6 +14,7 @@ const RadioSelection: FC = ({ options, setSelections: setParentSelections, }) => { + const { classes } = useStyles(); const initialSelectedRadioOptions: RadioOptionSelection[] = options.map((option) => ({ // just grab the first to start // perhaps choices should be persisted in the URL to make it easy to share specific options @@ -39,7 +41,12 @@ const RadioSelection: FC = ({ return ( {options.map((option, i) => ( - + {option.title || option.id} {option.description && ( diff --git a/client/src/components/_common/SelectionPanel/styles.tsx b/client/src/components/_common/SelectionPanel/styles.tsx index 2c2e69625..9b7d2b5bf 100644 --- a/client/src/components/_common/SelectionPanel/styles.tsx +++ b/client/src/components/_common/SelectionPanel/styles.tsx @@ -2,6 +2,11 @@ import { Theme } from '@mui/material/styles'; import { makeStyles } from 'tss-react/mui'; export default makeStyles()((theme: Theme) => ({ + label: { + '& label.Mui-focused': { + color: theme.palette.common.orangeDarkest, + }, + }, optionsList: { display: 'flex', flexDirection: 'column', diff --git a/client/src/index.css b/client/src/index.css index 046864a33..981b5373a 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -22,4 +22,16 @@ body { overscroll-behavior-y: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; +} + +/* Keyboard-only focus indicator */ +button:focus-visible { + border: 1px solid #707070; +} + +/* Darken button backgrounds on hover on text styled buttons */ +button:hover { + &.MuiButton-text { + background-color: #cbd5df60; + } } \ No newline at end of file diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index e5cf68001..5295c70ee 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -14,7 +14,7 @@ GEM execjs coffee-script-source (1.11.1) colorator (1.1.0) - commonmarker (0.23.9) + commonmarker (0.23.10) concurrent-ruby (1.2.2) dnsruby (1.61.9) simpleidn (~> 0.1) diff --git a/docs/advanced-test-features/serving-http-requests.md b/docs/advanced-test-features/serving-http-requests.md index 1498794ed..5fc095301 100644 --- a/docs/advanced-test-features/serving-http-requests.md +++ b/docs/advanced-test-features/serving-http-requests.md @@ -63,15 +63,15 @@ class MyTestSuite < Inferno::TestSuite id :my_test_suite my_html = File.read(File.join(__dir__, 'my_html.html')) - my_html_route_handler = proc { [200, { 'Content-Type' => 'text/html' }, [html]] } + my_html_route_handler = proc { [200, { 'Content-Type' => 'text/html' }, [my_html]] } - # Serve an html page at INFERNO_PATH/my_test_suite/custom/my_html_page + # Serve an html page at INFERNO_PATH/custom/my_test_suite/my_html_page route :get, '/my_html_page', my_html_route_handler my_jwks = File.read(File.join(__dir__, 'my_jwks.json')) my_jwks_route_handler = proc { [200, { 'Content-Type' => 'application/json' }, [my_jwks]] } - # Serve a JSON file at INFERNO_PATH/my_test_suite/custom/.well-known/jwks.json + # Serve a JSON file at INFERNO_PATH/custom/my_test_suite/.well-known/jwks.json route :get, '/.well-known/jwks.json', my_jwks_route_handler end ``` diff --git a/inferno_core.gemspec b/inferno_core.gemspec index e3fb21184..b49d64cf8 100644 --- a/inferno_core.gemspec +++ b/inferno_core.gemspec @@ -11,7 +11,7 @@ Gem::Specification.new do |spec| spec.description = 'Inferno Core is an open source tool for testing data exchanges enabled by the FHIR standand' spec.homepage = 'https://github.com/inferno-framework/inferno-core' spec.license = 'Apache-2.0' - spec.add_runtime_dependency 'activesupport', '~> 6.1' + spec.add_runtime_dependency 'activesupport', '~> 6.1.7.5' spec.add_runtime_dependency 'base62-rb', '0.3.1' spec.add_runtime_dependency 'blueprinter', '0.25.2' spec.add_runtime_dependency 'dotenv', '~> 2.7' @@ -28,7 +28,7 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency 'oj', '3.11.0' spec.add_runtime_dependency 'pry' spec.add_runtime_dependency 'pry-byebug' - spec.add_runtime_dependency 'puma', '~> 5.3' + spec.add_runtime_dependency 'puma', '~> 5.6.7' spec.add_runtime_dependency 'rake', '~> 13.0' spec.add_runtime_dependency 'sequel', '~> 5.42.0' spec.add_runtime_dependency 'sidekiq', '~> 6.5.6' diff --git a/lib/inferno/apps/cli/main.rb b/lib/inferno/apps/cli/main.rb index 07e5368e5..ad7f1c353 100644 --- a/lib/inferno/apps/cli/main.rb +++ b/lib/inferno/apps/cli/main.rb @@ -36,7 +36,7 @@ def start command = "rerun \"#{command}\" --background" end - system command + exec command end desc 'suites', 'List available test suites' diff --git a/lib/inferno/config/boot/suites.rb b/lib/inferno/config/boot/suites.rb index b698b3e55..0e621b774 100644 --- a/lib/inferno/config/boot/suites.rb +++ b/lib/inferno/config/boot/suites.rb @@ -25,5 +25,13 @@ end ObjectSpace.each_object(TracePoint, &:disable) + + Inferno::Entities::TestSuite.descendants.each do |descendant| + # When ID not assigned in custom test suites, Runnable.id will return default ID + # equal to the custom test suite's parent class name + if descendant.id.blank? || descendant.id == 'Inferno::Entities::TestSuite' + raise StandardError, "Error initializing test suite #{descendant.name}: test suite ID is not set" + end + end end end diff --git a/lib/inferno/dsl/fhir_validation.rb b/lib/inferno/dsl/fhir_validation.rb index 33aef90cd..7f1ae81c2 100644 --- a/lib/inferno/dsl/fhir_validation.rb +++ b/lib/inferno/dsl/fhir_validation.rb @@ -116,17 +116,26 @@ def exclude_message(&block) def resource_is_valid?(resource, profile_url, runnable) profile_url ||= FHIR::Definitions.resource_definition(resource.resourceType).url - outcome = FHIR::OperationOutcome.new(JSON.parse(validate(resource, profile_url))) + outcome, http_status = validate(resource, profile_url, runnable) - message_hashes = outcome.issue&.map { |issue| message_hash_from_issue(issue, resource) } || [] + message_hashes = message_hashes_from_outcome(outcome, resource, profile_url) - message_hashes.concat(additional_validation_messages(resource, profile_url)) + message_hashes + .each { |message_hash| runnable.add_message(message_hash[:type], message_hash[:message]) } - filter_messages(message_hashes) + unless http_status == 200 + raise Inferno::Exceptions::ErrorInValidatorException, + 'Error occurred in the validator. Review Messages tab or validator service logs for more information.' + end message_hashes - .each { |message_hash| runnable.add_message(message_hash[:type], message_hash[:message]) } .none? { |message_hash| message_hash[:type] == 'error' } + rescue Inferno::Exceptions::ErrorInValidatorException + raise + rescue StandardError => e + runnable.add_message('error', e.message) + raise Inferno::Exceptions::ErrorInValidatorException, + 'Error occurred in the validator. Review Messages tab or validator service logs for more information.' end # @private @@ -134,6 +143,17 @@ def filter_messages(message_hashes) message_hashes.reject! { |message| exclude_message.call(Entities::Message.new(message)) } if exclude_message end + # @private + def message_hashes_from_outcome(outcome, resource, profile_url) + message_hashes = outcome.issue&.map { |issue| message_hash_from_issue(issue, resource) } || [] + + message_hashes.concat(additional_validation_messages(resource, profile_url)) + + filter_messages(message_hashes) + + message_hashes + end + # @private def message_hash_from_issue(issue, resource) { @@ -171,12 +191,33 @@ def issue_message(issue, resource) # # @param resource [FHIR::Model] # @param profile_url [String] - # @return [String] the body of the validation response - def validate(resource, profile_url) - Faraday.new( - url, - params: { profile: profile_url } - ).post('validate', resource.source_contents).body + # @param runnable [Inferno::Entities::Test] + # @return [[Array(FHIR::OperationOutcome, Number)] the validation response and HTTP status code + def validate(resource, profile_url, runnable) + begin + response = Faraday.new( + url, + params: { profile: profile_url } + ).post('validate', resource.source_contents) + rescue StandardError => e + runnable.add_message('error', e.message) + raise Inferno::Exceptions::ErrorInValidatorException, "Unable to connect to validator at #{url}." + end + outcome = operation_outcome_from_validator_response(response.body, runnable) + + [outcome, response.status] + end + + # @private + def operation_outcome_from_validator_response(response, runnable) + if response.start_with? '{' + FHIR::OperationOutcome.new(JSON.parse(response)) + else + runnable.add_message('error', "Validator Response: #{response}") + raise Inferno::Exceptions::ErrorInValidatorException, + 'Validator response was an unexpected format. '\ + 'Review Messages tab or validator service logs for more information.' + end end end diff --git a/lib/inferno/exceptions.rb b/lib/inferno/exceptions.rb index dc3cc8e3b..bcd059594 100644 --- a/lib/inferno/exceptions.rb +++ b/lib/inferno/exceptions.rb @@ -39,6 +39,16 @@ def result end end + class ErrorInValidatorException < TestResultException + # This extends TestResultException instead of RuntimeError + # to bypass printing the stack trace in the UI. + # (The stack trace of this exception may not be useful, + # instead the message should point to where in the validator an error occurred) + def result + 'error' + end + end + class ParentNotLoadedException < RuntimeError def initialize(klass, id) super("No #{klass.name.demodulize} found with id '#{id}'") diff --git a/lib/inferno/version.rb b/lib/inferno/version.rb index 0f6f1b3fd..3d5ed52cf 100644 --- a/lib/inferno/version.rb +++ b/lib/inferno/version.rb @@ -1,4 +1,4 @@ module Inferno # Standard patterns for gem versions: https://guides.rubygems.org/patterns/ - VERSION = '0.4.18'.freeze + VERSION = '0.4.19'.freeze end diff --git a/package-lock.json b/package-lock.json index 2efbf67dc..1add0b165 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@mui/material": "^5.11.13", "history": "^5.3.0", "js-yaml": "^4.1.0", - "notistack": "^2.0.8", + "notistack": "^3.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.8.1", @@ -11817,6 +11817,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/goober": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.13.tgz", + "integrity": "sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ==", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -17558,31 +17566,24 @@ } }, "node_modules/notistack": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/notistack/-/notistack-2.0.8.tgz", - "integrity": "sha512-/IY14wkFp5qjPgKNvAdfL5Jp6q90+MjgKTPh4c81r/lW70KeuX6b9pE/4f8L4FG31cNudbN9siiFS5ql1aSLRw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/notistack/-/notistack-3.0.1.tgz", + "integrity": "sha512-ntVZXXgSQH5WYfyU+3HfcXuKaapzAJ8fBLQ/G618rn3yvSzEbnOB8ZSOwhX+dAORy/lw+GC2N061JA0+gYWTVA==", "dependencies": { "clsx": "^1.1.0", - "hoist-non-react-statics": "^3.3.0" + "goober": "^2.0.33" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/notistack" }, "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "@mui/material": "^5.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } } }, "node_modules/npm-run-path": { diff --git a/package.json b/package.json index 6dca570eb..3beac0ced 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "@mui/material": "^5.11.13", "history": "^5.3.0", "js-yaml": "^4.1.0", - "notistack": "^2.0.8", + "notistack": "^3.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.8.1", diff --git a/spec/inferno/dsl/fhir_validation_spec.rb b/spec/inferno/dsl/fhir_validation_spec.rb index 717edd159..0458e9d17 100644 --- a/spec/inferno/dsl/fhir_validation_spec.rb +++ b/spec/inferno/dsl/fhir_validation_spec.rb @@ -130,6 +130,45 @@ end end + context 'with error from validator' do + let(:error_outcome) do + { + resourceType: 'OperationOutcome', + issue: [ + { + severity: 'fatal', + code: 'structure', + diagnostics: 'Validator still warming up... Please wait', + details: { + text: 'Validator still warming up... Please wait' + } + } + ] + }.to_json + end + + it 'throws ErrorInValidatorException when validator not ready yet' do + stub_request(:post, "#{validation_url}/validate?profile=#{profile_url}") + .with(body: resource_string) + .to_return(status: 503, body: error_outcome) + + expect do + validator.resource_is_valid?(resource, profile_url, runnable) + end.to raise_error(Inferno::Exceptions::ErrorInValidatorException) + expect(runnable.messages.first[:message]).to include('Validator still warming up... Please wait') + end + + it 'throws ErrorInValidatorException for non-JSON response' do + stub_request(:post, "#{validation_url}/validate?profile=#{profile_url}") + .with(body: resource_string) + .to_return(status: 500, body: 'Internal Server Error') + + expect do + validator.resource_is_valid?(resource, profile_url, runnable) + end.to raise_error(Inferno::Exceptions::ErrorInValidatorException) + end + end + it 'posts the resource with primitive extensions intact' do stub_request(:post, "#{validation_url}/validate?profile=#{profile_url}") .with(body: resource_string)