From 223b134baa23980d2b2268000c39a4d531dcb04e Mon Sep 17 00:00:00 2001 From: regisoc Date: Wed, 4 Jun 2025 19:52:58 +0000 Subject: [PATCH 01/65] instrument API PUT endpoint to exception based validate --- .../candidate/visit/instrument/instrument.class.inc | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc b/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc index 4ddbac10fab..a656c29d7c3 100644 --- a/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc +++ b/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc @@ -183,12 +183,16 @@ class Instrument extends Endpoint implements \LORIS\Middleware\ETagCalculator ); } - if (!$this->_instrument->validate($data)) { - return new \LORIS\Http\Response\JSON\Forbidden( - 'Could not update.' + // validate given data against the instrument + try { + $this->_instrument->validate($data); + } catch (\LorisException $th) { + return new \LORIS\Http\Response\JSON\BadRequest( + $th->getMessage() ); } + // update values try { $this->_instrument->clearInstrument(); $version = $request->getAttribute('LORIS-API-Version'); From f5e0a1ca2d1b5a64c1a094232de6ec5ab0edc4b2 Mon Sep 17 00:00:00 2001 From: regisoc Date: Wed, 4 Jun 2025 19:57:03 +0000 Subject: [PATCH 02/65] internal array_all for php < 8.4 --- htdocs/index.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/htdocs/index.php b/htdocs/index.php index 7c2b3cfc2e7..d122cb19e90 100644 --- a/htdocs/index.php +++ b/htdocs/index.php @@ -29,6 +29,20 @@ // See: https://www.php.net/manual/en/session.configuration.php#ini.session.use-strict-mode ini_set('session.use_strict_mode', '1'); +// TODO: Remove this code once PHP 8.4 becomes the minimal PHP version in LORIS. +if (version_compare(PHP_VERSION, '8.4', '<')) { + // @phan-file-suppress PhanRedefineFunctionInternal + + // phpcs:ignore + function array_all(array $array, callable $callable): bool { + foreach ($array as $key => $value) { + if (! $callable($value, $key)) + return false; + } + return true; + } +} + // FIXME: The code in NDB_Client should mostly be replaced by middleware. $client = new \NDB_Client; $client->initialize(); From 40c24a50d5e055c62c85e9eb4f12cbea37157b63 Mon Sep 17 00:00:00 2001 From: regisoc Date: Wed, 4 Jun 2025 20:20:07 +0000 Subject: [PATCH 03/65] default instrument metadata element names --- php/libraries/NDB_BVL_Instrument.class.inc | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/php/libraries/NDB_BVL_Instrument.class.inc b/php/libraries/NDB_BVL_Instrument.class.inc index 63ff722c2f6..374c327fe91 100644 --- a/php/libraries/NDB_BVL_Instrument.class.inc +++ b/php/libraries/NDB_BVL_Instrument.class.inc @@ -198,6 +198,20 @@ abstract class NDB_BVL_Instrument extends NDB_Page */ protected $selectMultipleElements = []; + /** + * Array containing all metadata elements in an instrument. + * + * @var array + * + * @access protected + */ + protected array $metadataElements = [ + "Date_taken", + "Examiner", + "Window_Difference", + "Candidate_Age", + ]; + /** * Factory generates a new instrument instance of type * $instrument, and runs the setup() method on that new From 1cd9002abd50c3d01f79bc3dcdce445f94f4355d Mon Sep 17 00:00:00 2001 From: regisoc Date: Wed, 4 Jun 2025 20:21:07 +0000 Subject: [PATCH 04/65] instrument - validation method update --- php/libraries/NDB_BVL_Instrument.class.inc | 193 ++++++++++++++++++++- 1 file changed, 189 insertions(+), 4 deletions(-) diff --git a/php/libraries/NDB_BVL_Instrument.class.inc b/php/libraries/NDB_BVL_Instrument.class.inc index 374c327fe91..2b8daa35dc7 100644 --- a/php/libraries/NDB_BVL_Instrument.class.inc +++ b/php/libraries/NDB_BVL_Instrument.class.inc @@ -2972,15 +2972,200 @@ abstract class NDB_BVL_Instrument extends NDB_Page } /** - * Validate whether the values submitted are valid + * Validate the given array keys against the instrument default structure. + * Throw a LorisException if any check does not pass. + * + * @param array $instrumentData instrument data to check + * + * @throws \LorisException + * + * @return void + */ + private function _validateKeys( + array $instrumentData + ): void { + // get keys from new data + $dataKeys = array_keys($instrumentData); + + // load default instrument data keys + $defaultDictionary = $this->defaultInstanceData(); + $defaultDataKeys = array_keys($defaultDictionary); + + // filter out metadata fields, keep only instrument data fields + $defaultDataKeys = array_values( + array_filter( + $defaultDataKeys, + fn($k) => !in_array( + $k, + $this->metadataElements, + true + ) + ) + ); + + // missing keys + $missingKeys = array_diff($defaultDataKeys, $dataKeys); + if (!empty($missingKeys)) { + $i = implode(",", $missingKeys); + throw new \LorisException("Missing keys: {$i}."); + } + + // additional keys: only warn + $additionalKeys = array_diff($dataKeys, $defaultDataKeys); + if (!empty($additionalKeys)) { + $i = implode(",", $additionalKeys); + error_log("[instrument:validation] Additional keys will be ignored: {$i}."); + } + } + + /** + * Validate the given array values against the instrument dictionary items. + * Throw a LorisException if any check does not pass. + * + * @param array $instrumentData instrument data to check + * + * @throws \LorisException + * + * @return void + */ + private function _validateValues( + array $instrumentData + ): void { + // get datetime fn, and format from db + $getDateTime = fn($s, $f) => DateTime::createFromFormat($f, $s); + $config = $this->loris->getConfiguration(); + $dateFormat = $config->getSetting('dateDisplayFormat'); + + // define all check fn - primitive + $isString = fn($f) => is_string($f) && "{$f}" === $f; + $isBoolean = fn($f) => is_bool($f) && (bool)$f === $f; + $isInteger = fn($f) => is_integer($f) && (int)$f === $f; + $isFloat = fn($f) => is_float($f) && (float)$f === $f; + + // define all check fn - duration fn + $isDuration = fn($f) => $isInteger($f) && $f >= 0; + + // define all check fn - date/time fn + $isDate = fn($f, $fmt) => ($getDateTime($f, $fmt) !== false) + && ($getDateTime($f, $fmt)->format($fmt) === $f); + $isTime = fn($f, $fmt) => ($getDateTime("2025-10-10 {$f}", $fmt) !== false) + && ($getDateTime("2025-10-10 {$f}", $fmt)->format("H:i:s") === $f); + + // check types + foreach ($this->getDataDictionary() as $dictionaryItem) { + // current fieldname + $fieldName = $dictionaryItem->fieldname; + + // skip filtered out fields + if (in_array($fieldName, $this->metadataElements, true)) { + continue; + } + + // get the expected type for that field + $expectedType = $dictionaryItem->getDataType(); + + // get the field data value + $fieldToCheck = $instrumentData[$fieldName]; + + // if an enumeration, get the possible option keys + $optionKeys = match ("{$expectedType}") { + "enumeration" => $expectedType->getOptions(), + default => null + }; + + // is it a single/multi-enumeration + $isMultiEnum = in_array( + $fieldName, + $this->selectMultipleElements, + true + ); + + // run the validation method depending on the field type + $isValid = match ("{$expectedType}") { + "string", "URI" => $isString($fieldToCheck), + "boolean" => $isBoolean($fieldToCheck), + "integer" => $isInteger($fieldToCheck), + "decimal" => $isFloat($fieldToCheck), + "date" => $isDate($fieldToCheck, $dateFormat), + "time" => $isTime($fieldToCheck, $dateFormat), + "duration" => $isDuration($fieldToCheck), + "enumeration" => $isString($fieldToCheck) && ( + $isMultiEnum + // select: the given value must be in the list of options + ? in_array( + $fieldToCheck, + $optionKeys, + true + ) + // multi-select: ALL given values must be in the list of options + : array_all( + explode("{@}", $fieldToCheck), + fn($v) => in_array( + $v, + $optionKeys, + true + ) + ) + ), + default => throw new \LorisException( + "Unknown type '{$expectedType}' for field: {$fieldToCheck}" + ) + }; + + // if not valid, format exception message + if (!$isValid) { + // expected format + $expectedFormat = match ("{$expectedType}") { + "date" => " (format: 'YYYY-MM-DD')", + "time" => " (format: 'HH:mm:ss')", + "enumeration" => ", possible answers: '" + . implode("','", $optionKeys) + . "'", + default => "", + }; + + // multi-enumeration? + $multi = ""; + if ($isMultiEnum) { + $multi = "multi-"; + // add delimiter info + $expectedFormat .= " with delimiter '{@}'"; + } + + // message + $msg = "Field not valid: {$fieldName}. "; + $msg .= "Expected: {$multi}{$expectedType}"; + $msg .= "{$expectedFormat}"; + + // + throw new \LorisException($msg); + } + } + } + + /** + * Validate whether the data submitted are valid against this instrument. + * Checks both keys and values. Throws a LorisException if any test fails. * * @param array $values an array of values submitted * - * @return boolean true if values are valid + * @throws \LorisException + * + * @return void */ - function validate(array $values): bool + public function validate(array $values): void { - return $this->determineDataEntryAllowed(); + // data to check even exist + $dataToCheck = $values[$this->testName] ?? null; + if ($dataToCheck === null) { + throw new \LorisException("No instrument key provided."); + } + + // validate the keys against the instrument dictionary entries + $this->_validateKeys($dataToCheck); + + // validate the values against the instrument dictionary entries + $this->_validateValues($dataToCheck); } /** From 40563a7403160d2147b6a66a69ff4bca6e992a24 Mon Sep 17 00:00:00 2001 From: regisoc Date: Wed, 4 Jun 2025 20:26:48 +0000 Subject: [PATCH 05/65] instrument flag endpoint update --- .../candidate/visit/instrument/flags.class.inc | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc b/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc index 63ae828be63..ee90d9ed737 100644 --- a/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc +++ b/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc @@ -169,9 +169,11 @@ class Flags extends Endpoint implements \LORIS\Middleware\ETagCalculator 'Invalid request' ); } - if (!$this->_instrument->validate($data)) { - return new \LORIS\Http\Response\JSON\Forbidden( - 'Could not update.' + try { + $this->_instrument->validate($data); + } catch (\LorisException $th) { + return new \LORIS\Http\Response\JSON\BadRequest( + "Could not update. {$th->getMessage()}" ); } @@ -242,9 +244,11 @@ class Flags extends Endpoint implements \LORIS\Middleware\ETagCalculator ); } - if (!$this->_instrument->validate($data)) { - return new \LORIS\Http\Response\JSON\Forbidden( - 'Could not update.' + try { + $this->_instrument->validate($data); + } catch (\LorisException $th) { + return new \LORIS\Http\Response\JSON\BadRequest( + "Could not update. {$th->getMessage()}" ); } From d7f3dbd8fbab3fe7d3b3681600aae16e5c544443 Mon Sep 17 00:00:00 2001 From: regisoc Date: Wed, 4 Jun 2025 20:27:14 +0000 Subject: [PATCH 06/65] instrument endpoint update --- .../candidate/visit/instrument/instrument.class.inc | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc b/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc index a656c29d7c3..f6d7459b10e 100644 --- a/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc +++ b/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc @@ -188,7 +188,7 @@ class Instrument extends Endpoint implements \LORIS\Middleware\ETagCalculator $this->_instrument->validate($data); } catch (\LorisException $th) { return new \LORIS\Http\Response\JSON\BadRequest( - $th->getMessage() + "Could not update. {$th->getMessage()}" ); } @@ -234,9 +234,12 @@ class Instrument extends Endpoint implements \LORIS\Middleware\ETagCalculator ); } - if (!$this->_instrument->validate($data)) { - return new \LORIS\Http\Response\JSON\Forbidden( - 'Could not update.' + // validate given data against the instrument + try { + $this->_instrument->validate($data); + } catch (\LorisException $th) { + return new \LORIS\Http\Response\JSON\BadRequest( + "Could not update. {$th->getMessage()}" ); } From 61b88f5d4bfd43f19d0cb279feb86c84e03bbcbf Mon Sep 17 00:00:00 2001 From: regisoc Date: Wed, 4 Jun 2025 21:04:53 +0000 Subject: [PATCH 07/65] instrument api fallback exceptions cases --- .../candidate/visit/instrument/flags.class.inc | 10 ++++++++++ .../candidate/visit/instrument/instrument.class.inc | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc b/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc index ee90d9ed737..636996a439a 100644 --- a/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc +++ b/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc @@ -175,6 +175,11 @@ class Flags extends Endpoint implements \LORIS\Middleware\ETagCalculator return new \LORIS\Http\Response\JSON\BadRequest( "Could not update. {$th->getMessage()}" ); + } catch (\Throwable $th) { + error_log($th->getMessage()); + return new \LORIS\Http\Response\JSON\Forbidden( + "Could not update." + ); } try { @@ -250,6 +255,11 @@ class Flags extends Endpoint implements \LORIS\Middleware\ETagCalculator return new \LORIS\Http\Response\JSON\BadRequest( "Could not update. {$th->getMessage()}" ); + } catch (\Throwable $th) { + error_log($th->getMessage()); + return new \LORIS\Http\Response\JSON\Forbidden( + "Could not update." + ); } try { diff --git a/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc b/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc index f6d7459b10e..81349170e2d 100644 --- a/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc +++ b/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc @@ -190,6 +190,11 @@ class Instrument extends Endpoint implements \LORIS\Middleware\ETagCalculator return new \LORIS\Http\Response\JSON\BadRequest( "Could not update. {$th->getMessage()}" ); + } catch (\Throwable $th) { + error_log($th->getMessage()); + return new \LORIS\Http\Response\JSON\Forbidden( + "Could not update." + ); } // update values @@ -241,6 +246,11 @@ class Instrument extends Endpoint implements \LORIS\Middleware\ETagCalculator return new \LORIS\Http\Response\JSON\BadRequest( "Could not update. {$th->getMessage()}" ); + } catch (\Throwable $th) { + error_log($th->getMessage()); + return new \LORIS\Http\Response\JSON\Forbidden( + "Could not update." + ); } try { From 31a9a4129c5036406adb95a035aca05296b2a283 Mon Sep 17 00:00:00 2001 From: regisoc Date: Wed, 4 Jun 2025 21:08:40 +0000 Subject: [PATCH 08/65] lint --- htdocs/index.php | 2 +- php/libraries/NDB_BVL_Instrument.class.inc | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/htdocs/index.php b/htdocs/index.php index d122cb19e90..828ac9ff6cb 100644 --- a/htdocs/index.php +++ b/htdocs/index.php @@ -36,7 +36,7 @@ // phpcs:ignore function array_all(array $array, callable $callable): bool { foreach ($array as $key => $value) { - if (! $callable($value, $key)) + if (!$callable($value, $key)) return false; } return true; diff --git a/php/libraries/NDB_BVL_Instrument.class.inc b/php/libraries/NDB_BVL_Instrument.class.inc index 2b8daa35dc7..391d2a9eaff 100644 --- a/php/libraries/NDB_BVL_Instrument.class.inc +++ b/php/libraries/NDB_BVL_Instrument.class.inc @@ -3014,7 +3014,9 @@ abstract class NDB_BVL_Instrument extends NDB_Page $additionalKeys = array_diff($dataKeys, $defaultDataKeys); if (!empty($additionalKeys)) { $i = implode(",", $additionalKeys); - error_log("[instrument:validation] Additional keys will be ignored: {$i}."); + error_log( + "[instrument:validation] Additional keys will be ignored: {$i}." + ); } } @@ -3037,10 +3039,10 @@ abstract class NDB_BVL_Instrument extends NDB_Page $dateFormat = $config->getSetting('dateDisplayFormat'); // define all check fn - primitive - $isString = fn($f) => is_string($f) && "{$f}" === $f; - $isBoolean = fn($f) => is_bool($f) && (bool)$f === $f; - $isInteger = fn($f) => is_integer($f) && (int)$f === $f; - $isFloat = fn($f) => is_float($f) && (float)$f === $f; + $isString = fn($f) => is_string($f) && "{$f}" === $f; + $isBoolean = fn($f) => is_bool($f) && (bool)$f === $f; + $isInteger = fn($f) => is_integer($f) && (int)$f === $f; + $isFloat = fn($f) => is_float($f) && (float)$f === $f; // define all check fn - duration fn $isDuration = fn($f) => $isInteger($f) && $f >= 0; From 51bf9776cb7ef5cff1866a836283b13ce5af448d Mon Sep 17 00:00:00 2001 From: regisoc Date: Tue, 10 Jun 2025 11:43:49 -0400 Subject: [PATCH 09/65] instrument validate only data --- php/libraries/NDB_BVL_Instrument.class.inc | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/php/libraries/NDB_BVL_Instrument.class.inc b/php/libraries/NDB_BVL_Instrument.class.inc index 391d2a9eaff..c0dbacd4a09 100644 --- a/php/libraries/NDB_BVL_Instrument.class.inc +++ b/php/libraries/NDB_BVL_Instrument.class.inc @@ -3155,12 +3155,11 @@ abstract class NDB_BVL_Instrument extends NDB_Page * * @return void */ - public function validate(array $values): void + public function validate(array $dataToCheck): void { // data to check even exist - $dataToCheck = $values[$this->testName] ?? null; - if ($dataToCheck === null) { - throw new \LorisException("No instrument key provided."); + if (empty($dataToCheck)) { + throw new \LorisException("No data provided."); } // validate the keys against the instrument dictionary entries From 8017dea3aa2f5b7d4942c3e5f0f0514bb9c1ece2 Mon Sep 17 00:00:00 2001 From: regisoc Date: Tue, 10 Jun 2025 11:44:28 -0400 Subject: [PATCH 10/65] instrument and flags endpoint to validate data --- .../endpoints/candidate/visit/instrument/flags.class.inc | 6 ++++-- .../candidate/visit/instrument/instrument.class.inc | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc b/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc index 636996a439a..633f2732cbd 100644 --- a/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc +++ b/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc @@ -170,7 +170,8 @@ class Flags extends Endpoint implements \LORIS\Middleware\ETagCalculator ); } try { - $this->_instrument->validate($data); + $instrumentName = $this->_instrument->testName; + $this->_instrument->validate($data[$instrumentName] ?? []); } catch (\LorisException $th) { return new \LORIS\Http\Response\JSON\BadRequest( "Could not update. {$th->getMessage()}" @@ -250,7 +251,8 @@ class Flags extends Endpoint implements \LORIS\Middleware\ETagCalculator } try { - $this->_instrument->validate($data); + $instrumentName = $this->_instrument->testName; + $this->_instrument->validate($data[$instrumentName] ?? []); } catch (\LorisException $th) { return new \LORIS\Http\Response\JSON\BadRequest( "Could not update. {$th->getMessage()}" diff --git a/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc b/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc index 81349170e2d..d8fd87929a3 100644 --- a/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc +++ b/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc @@ -185,7 +185,8 @@ class Instrument extends Endpoint implements \LORIS\Middleware\ETagCalculator // validate given data against the instrument try { - $this->_instrument->validate($data); + $instrumentName = $this->_instrument->testName; + $this->_instrument->validate($data[$instrumentName] ?? []); } catch (\LorisException $th) { return new \LORIS\Http\Response\JSON\BadRequest( "Could not update. {$th->getMessage()}" @@ -241,7 +242,8 @@ class Instrument extends Endpoint implements \LORIS\Middleware\ETagCalculator // validate given data against the instrument try { - $this->_instrument->validate($data); + $instrumentName = $this->_instrument->testName; + $this->_instrument->validate($data[$instrumentName] ?? []); } catch (\LorisException $th) { return new \LORIS\Http\Response\JSON\BadRequest( "Could not update. {$th->getMessage()}" From c919618d295bd7cba6aeef77fbee1720bbf2523f Mon Sep 17 00:00:00 2001 From: regisoc Date: Tue, 10 Jun 2025 13:54:13 -0400 Subject: [PATCH 11/65] test- flag checks - rm instrument validation --- .../visit/instrument/flags.class.inc | 27 +++---------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc b/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc index 633f2732cbd..2152fd145b9 100644 --- a/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc +++ b/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc @@ -163,28 +163,18 @@ class Flags extends Endpoint implements \LORIS\Middleware\ETagCalculator } $data = json_decode((string) $request->getBody(), true); - if (!is_array($data)) { return new \LORIS\Http\Response\JSON\BadRequest( 'Invalid request' ); } - try { - $instrumentName = $this->_instrument->testName; - $this->_instrument->validate($data[$instrumentName] ?? []); - } catch (\LorisException $th) { + if (!array_key_exists('Flags', $data)) { return new \LORIS\Http\Response\JSON\BadRequest( - "Could not update. {$th->getMessage()}" - ); - } catch (\Throwable $th) { - error_log($th->getMessage()); - return new \LORIS\Http\Response\JSON\Forbidden( - "Could not update." + "Invalid request. 'Flags' missing." ); } try { - $requiredfields = [ 'Data_entry', 'Administration', @@ -249,18 +239,9 @@ class Flags extends Endpoint implements \LORIS\Middleware\ETagCalculator 'Invalid request' ); } - - try { - $instrumentName = $this->_instrument->testName; - $this->_instrument->validate($data[$instrumentName] ?? []); - } catch (\LorisException $th) { + if (!array_key_exists('Flags', $data)) { return new \LORIS\Http\Response\JSON\BadRequest( - "Could not update. {$th->getMessage()}" - ); - } catch (\Throwable $th) { - error_log($th->getMessage()); - return new \LORIS\Http\Response\JSON\Forbidden( - "Could not update." + "Invalid request. 'Flags' missing." ); } From 2e23e989b407f7d47da2c821c302940f3babf380 Mon Sep 17 00:00:00 2001 From: regisoc Date: Tue, 10 Jun 2025 13:55:17 -0400 Subject: [PATCH 12/65] instrument validation from API instrument - instrument name --- .../visit/instrument/instrument.class.inc | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc b/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc index d8fd87929a3..f765fd280a1 100644 --- a/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc +++ b/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc @@ -241,9 +241,16 @@ class Instrument extends Endpoint implements \LORIS\Middleware\ETagCalculator } // validate given data against the instrument + $instrumentName = $this->_instrument->testName; try { - $instrumentName = $this->_instrument->testName; - $this->_instrument->validate($data[$instrumentName] ?? []); + // extract data according to LORIS API version + $version = $request->getAttribute('LORIS-API-Version'); + $instrumentData = $version == 'v0.0.3' + ? ($data[$instrumentName] ?? []) + : ($data['Data'] ?? []); + + // validate + $this->_instrument->validate($instrumentData); } catch (\LorisException $th) { return new \LORIS\Http\Response\JSON\BadRequest( "Could not update. {$th->getMessage()}" @@ -256,13 +263,7 @@ class Instrument extends Endpoint implements \LORIS\Middleware\ETagCalculator } try { - $version = $request->getAttribute('LORIS-API-Version'); - if ($version == 'v0.0.3') { - $instrumentname = $this->_instrument->testName; - $this->_instrument->_saveValues($data[$instrumentname]); - } else { - $this->_instrument->_saveValues($data['Data']); - } + $this->_instrument->_saveValues($instrumentData); $this->_instrument->score(); $this->_instrument->updateRequiredElementsCompletedFlag(); } catch (\Throwable $e) { From 76bbb059f377911e8c403d784dbc9f1591a774ac Mon Sep 17 00:00:00 2001 From: regisoc Date: Tue, 10 Jun 2025 13:56:03 -0400 Subject: [PATCH 13/65] instrument validate - additional fields and exception message --- php/libraries/NDB_BVL_Instrument.class.inc | 28 +++++++++++++++------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/php/libraries/NDB_BVL_Instrument.class.inc b/php/libraries/NDB_BVL_Instrument.class.inc index c0dbacd4a09..b91b72cc08f 100644 --- a/php/libraries/NDB_BVL_Instrument.class.inc +++ b/php/libraries/NDB_BVL_Instrument.class.inc @@ -2973,6 +2973,8 @@ abstract class NDB_BVL_Instrument extends NDB_Page /** * Validate the given array keys against the instrument default structure. + * Strictly checks against "required" fields. + * Additional fields are not allowed. * Throw a LorisException if any check does not pass. * * @param array $instrumentData instrument data to check @@ -3003,20 +3005,30 @@ abstract class NDB_BVL_Instrument extends NDB_Page ) ); - // missing keys - $missingKeys = array_diff($defaultDataKeys, $dataKeys); + // also select all required elements only + $defaultRequiredElements = array_values( + array_filter( + $defaultDataKeys, + fn($k) => in_array( + $k, + $this->_requiredElements, + true + ) + ) + ); + + // missing required fields + $missingKeys = array_diff($defaultRequiredElements, $dataKeys); if (!empty($missingKeys)) { $i = implode(",", $missingKeys); - throw new \LorisException("Missing keys: {$i}."); + throw new \LorisException("Missing required field(s): {$i}."); } - // additional keys: only warn + // additional fields: fields outside instrument fields $additionalKeys = array_diff($dataKeys, $defaultDataKeys); if (!empty($additionalKeys)) { - $i = implode(",", $additionalKeys); - error_log( - "[instrument:validation] Additional keys will be ignored: {$i}." - ); + $i = implode(",", $missingKeys); + throw new \LorisException("Additional field(s) not permitted: {$i}."); } } From 7f46e8b0b58d3f769a43b07786182acab6a19cc2 Mon Sep 17 00:00:00 2001 From: regisoc Date: Tue, 10 Jun 2025 14:05:03 -0400 Subject: [PATCH 14/65] instrument validate test update --- test/unittests/NDB_BVL_Instrument_Test.php | 211 ++++++++++++++++++++- 1 file changed, 205 insertions(+), 6 deletions(-) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index 8af424b0406..52b9a92d638 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -1692,24 +1692,223 @@ function testClearInstrument() /** * Test that determineDataEntryAllowed returns true if the Data_entry is anything - * but 'Complete' and returns false if Data_entry is 'Complete. Test that - * validate simply calls determineDataEntryAllowed and has the same output. + * but 'Complete' and returns false if Data_entry is 'Complete. * * @covers NDB_BVL_Instrument::determineDataEntryAllowed - * @covers NDB_BVL_Instrument::validate + * * @return void */ - function testDetermineDataEntryAllowed() + public function testDetermineDataEntryAllowed() { $this->_setUpMockDB(); $this->_setTableData(); $this->_instrument->commentID = 'commentID1'; $this->_instrument->table = 'medical_history'; $this->assertTrue($this->_instrument->determineDataEntryAllowed()); - $this->assertTrue($this->_instrument->validate(['value1'])); $this->_instrument->commentID = 'commentID2'; $this->assertFalse($this->_instrument->determineDataEntryAllowed()); - $this->assertFalse($this->_instrument->validate(['value1'])); + } + + /** + * Test that check validation method with received data. + * Empty array parameter case. + * + * @covers NDB_BVL_Instrument::validate + * + * @return void + */ + public function testValidateEmptyParameter(): void + { + $this->_setUpMockDB(); + $this->_setTableData(); + $this->_instrument->table = 'medical_history'; + $this->_instrument->commentID = 'commentID1'; + $this->expectException("LorisException"); + $this->expectExceptionMessage("No data provided."); + $this->_instrument->validate([]); + } + + /** + * Test that check validation method with received data. + * Keys validation case - missing keys + * + * @covers NDB_BVL_Instrument::validate + * + * @return void + */ + public function testValidateMissingKeys(): void + { + $this->_setUpMockDB(); + $this->_setTableData(); + $this->_instrument->table = 'medical_history'; + $this->_instrument->commentID = 'commentID1'; + + // data - complete keys + empty values + $instrumentQuestions = [ + // p1 + "arthritis" => null, // required + "arthritis_age" => null, + "pulmonary_issues" => null, + "pulmonary_issues_specific" => null, + // p2 + "hypertension" => null, // required + "hypertension_while_pregnant" => null, + "hypertension_while_pregnant_age" => null, + // p3 + "concussion_or_head_trauma" => null, // required + "concussion_1_description" => null, + "concussion_1_hospitalized" => null, + "concussion_1_age" => null, + "concussion_2_description" => null, + "concussion_2_hospitalized" => null, + "concussion_2_age" => null, + "concussion_3_description" => null, + "concussion_3_hospitalized" => null, + "concussion_3_age" => null, + "current_concussion_symptoms" => null, + ]; + + // missing keys, removing two required element + $missingFields = $instrumentQuestions; + unset($missingFields["arthritis"]); + unset($missingFields["hypertension"]); + $this->expectException("LorisException"); + $this->expectExceptionMessage("Missing required field(s): arthritis,hypertension."); + $this->_instrument->validate($missingFields); + } + + /** + * Test that check validation method with received data. + * Keys validation case - additional keys. + * + * @covers NDB_BVL_Instrument::validate + * + * @return void + */ + public function testValidateAdditionalKeys(): void + { + $this->_setUpMockDB(); + $this->_setTableData(); + $this->_instrument->table = 'medical_history'; + $this->_instrument->commentID = 'commentID1'; + + // data - complete keys + empty values + $instrumentQuestions = [ + // p1 + "arthritis" => null, // required + "arthritis_age" => null, + "pulmonary_issues" => null, + "pulmonary_issues_specific" => null, + // p2 + "hypertension" => null, // required + "hypertension_while_pregnant" => null, + "hypertension_while_pregnant_age" => null, + // p3 + "concussion_or_head_trauma" => null, // required + "concussion_1_description" => null, + "concussion_1_hospitalized" => null, + "concussion_1_age" => null, + "concussion_2_description" => null, + "concussion_2_hospitalized" => null, + "concussion_2_age" => null, + "concussion_3_description" => null, + "concussion_3_hospitalized" => null, + "concussion_3_age" => null, + "current_concussion_symptoms" => null, + ]; + + // additional keys, adding two new unexpected keys + $additionalFields = $instrumentQuestions; + $additionalFields["aaa"] = 123; + $additionalFields["bbb"] = "a text"; + $this->expectException("LorisException"); + $this->expectExceptionMessage("Additional field(s) not permitted: aaa,bbb."); + $this->_instrument->validate($additionalFields); + } + + /** + * Test that check validation method with received data. + * Keys validation case - nominal case. + * + * @covers NDB_BVL_Instrument::validate + * + * @return void + */ + public function testValidateKeysNominal(): void + { + $this->_setUpMockDB(); + $this->_setTableData(); + $this->_instrument->table = 'medical_history'; + $this->_instrument->commentID = 'commentID1'; + + // data - complete keys + empty values + $instrumentQuestions = [ + // p1 + "arthritis" => null, // required + "arthritis_age" => null, + "pulmonary_issues" => null, + "pulmonary_issues_specific" => null, + // p2 + "hypertension" => null, // required + "hypertension_while_pregnant" => null, + "hypertension_while_pregnant_age" => null, + // p3 + "concussion_or_head_trauma" => null, // required + "concussion_1_description" => null, + "concussion_1_hospitalized" => null, + "concussion_1_age" => null, + "concussion_2_description" => null, + "concussion_2_hospitalized" => null, + "concussion_2_age" => null, + "concussion_3_description" => null, + "concussion_3_hospitalized" => null, + "concussion_3_age" => null, + "current_concussion_symptoms" => null, + ]; + + // all required elements are null/yes/no options. + $this->_instrument->validate($instrumentQuestions); + } + + /** + * Test that check validation method with received data. + * Values validation case. + * + * @covers NDB_BVL_Instrument::validate + * + * @return void + */ + public function testValidateValues(): void + { + $this->_setUpMockDB(); + $this->_setTableData(); + $this->_instrument->table = 'medical_history'; + $this->_instrument->commentID = 'commentID1'; + + // data - complete keys + empty values + $instrumentQuestions = [ + // p1 + "arthritis" => null, // required + "arthritis_age" => null, + "pulmonary_issues" => null, + "pulmonary_issues_specific" => null, + // p2 + "hypertension" => null, // required + "hypertension_while_pregnant" => null, + "hypertension_while_pregnant_age" => null, + // p3 + "concussion_or_head_trauma" => null, // required + "concussion_1_description" => null, + "concussion_1_hospitalized" => null, + "concussion_1_age" => null, + "concussion_2_description" => null, + "concussion_2_hospitalized" => null, + "concussion_2_age" => null, + "concussion_3_description" => null, + "concussion_3_hospitalized" => null, + "concussion_3_age" => null, + "current_concussion_symptoms" => null, + ]; } /** From bb292b2c3d2d3e573e2549bda0688b0ec1eebda4 Mon Sep 17 00:00:00 2001 From: regisoc Date: Tue, 10 Jun 2025 14:17:37 -0400 Subject: [PATCH 15/65] test lint --- test/unittests/NDB_BVL_Instrument_Test.php | 152 +++++++++++---------- 1 file changed, 77 insertions(+), 75 deletions(-) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index 52b9a92d638..a554ea055cf 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -1721,7 +1721,7 @@ public function testValidateEmptyParameter(): void { $this->_setUpMockDB(); $this->_setTableData(); - $this->_instrument->table = 'medical_history'; + $this->_instrument->table = 'medical_history'; $this->_instrument->commentID = 'commentID1'; $this->expectException("LorisException"); $this->expectExceptionMessage("No data provided."); @@ -1740,32 +1740,32 @@ public function testValidateMissingKeys(): void { $this->_setUpMockDB(); $this->_setTableData(); - $this->_instrument->table = 'medical_history'; + $this->_instrument->table = 'medical_history'; $this->_instrument->commentID = 'commentID1'; // data - complete keys + empty values $instrumentQuestions = [ // p1 - "arthritis" => null, // required - "arthritis_age" => null, - "pulmonary_issues" => null, - "pulmonary_issues_specific" => null, + "arthritis" => null, // required + "arthritis_age" => null, + "pulmonary_issues" => null, + "pulmonary_issues_specific" => null, // p2 - "hypertension" => null, // required - "hypertension_while_pregnant" => null, + "hypertension" => null, // required + "hypertension_while_pregnant" => null, "hypertension_while_pregnant_age" => null, // p3 - "concussion_or_head_trauma" => null, // required - "concussion_1_description" => null, - "concussion_1_hospitalized" => null, - "concussion_1_age" => null, - "concussion_2_description" => null, - "concussion_2_hospitalized" => null, - "concussion_2_age" => null, - "concussion_3_description" => null, - "concussion_3_hospitalized" => null, - "concussion_3_age" => null, - "current_concussion_symptoms" => null, + "concussion_or_head_trauma" => null, // required + "concussion_1_description" => null, + "concussion_1_hospitalized" => null, + "concussion_1_age" => null, + "concussion_2_description" => null, + "concussion_2_hospitalized" => null, + "concussion_2_age" => null, + "concussion_3_description" => null, + "concussion_3_hospitalized" => null, + "concussion_3_age" => null, + "current_concussion_symptoms" => null, ]; // missing keys, removing two required element @@ -1773,7 +1773,9 @@ public function testValidateMissingKeys(): void unset($missingFields["arthritis"]); unset($missingFields["hypertension"]); $this->expectException("LorisException"); - $this->expectExceptionMessage("Missing required field(s): arthritis,hypertension."); + $this->expectExceptionMessage( + "Missing required field(s): arthritis,hypertension." + ); $this->_instrument->validate($missingFields); } @@ -1789,36 +1791,36 @@ public function testValidateAdditionalKeys(): void { $this->_setUpMockDB(); $this->_setTableData(); - $this->_instrument->table = 'medical_history'; + $this->_instrument->table = 'medical_history'; $this->_instrument->commentID = 'commentID1'; // data - complete keys + empty values $instrumentQuestions = [ // p1 - "arthritis" => null, // required - "arthritis_age" => null, - "pulmonary_issues" => null, - "pulmonary_issues_specific" => null, + "arthritis" => null, // required + "arthritis_age" => null, + "pulmonary_issues" => null, + "pulmonary_issues_specific" => null, // p2 - "hypertension" => null, // required - "hypertension_while_pregnant" => null, + "hypertension" => null, // required + "hypertension_while_pregnant" => null, "hypertension_while_pregnant_age" => null, // p3 - "concussion_or_head_trauma" => null, // required - "concussion_1_description" => null, - "concussion_1_hospitalized" => null, - "concussion_1_age" => null, - "concussion_2_description" => null, - "concussion_2_hospitalized" => null, - "concussion_2_age" => null, - "concussion_3_description" => null, - "concussion_3_hospitalized" => null, - "concussion_3_age" => null, - "current_concussion_symptoms" => null, + "concussion_or_head_trauma" => null, // required + "concussion_1_description" => null, + "concussion_1_hospitalized" => null, + "concussion_1_age" => null, + "concussion_2_description" => null, + "concussion_2_hospitalized" => null, + "concussion_2_age" => null, + "concussion_3_description" => null, + "concussion_3_hospitalized" => null, + "concussion_3_age" => null, + "current_concussion_symptoms" => null, ]; // additional keys, adding two new unexpected keys - $additionalFields = $instrumentQuestions; + $additionalFields = $instrumentQuestions; $additionalFields["aaa"] = 123; $additionalFields["bbb"] = "a text"; $this->expectException("LorisException"); @@ -1838,32 +1840,32 @@ public function testValidateKeysNominal(): void { $this->_setUpMockDB(); $this->_setTableData(); - $this->_instrument->table = 'medical_history'; + $this->_instrument->table = 'medical_history'; $this->_instrument->commentID = 'commentID1'; // data - complete keys + empty values $instrumentQuestions = [ // p1 - "arthritis" => null, // required - "arthritis_age" => null, - "pulmonary_issues" => null, - "pulmonary_issues_specific" => null, + "arthritis" => null, // required + "arthritis_age" => null, + "pulmonary_issues" => null, + "pulmonary_issues_specific" => null, // p2 - "hypertension" => null, // required - "hypertension_while_pregnant" => null, + "hypertension" => null, // required + "hypertension_while_pregnant" => null, "hypertension_while_pregnant_age" => null, // p3 - "concussion_or_head_trauma" => null, // required - "concussion_1_description" => null, - "concussion_1_hospitalized" => null, - "concussion_1_age" => null, - "concussion_2_description" => null, - "concussion_2_hospitalized" => null, - "concussion_2_age" => null, - "concussion_3_description" => null, - "concussion_3_hospitalized" => null, - "concussion_3_age" => null, - "current_concussion_symptoms" => null, + "concussion_or_head_trauma" => null, // required + "concussion_1_description" => null, + "concussion_1_hospitalized" => null, + "concussion_1_age" => null, + "concussion_2_description" => null, + "concussion_2_hospitalized" => null, + "concussion_2_age" => null, + "concussion_3_description" => null, + "concussion_3_hospitalized" => null, + "concussion_3_age" => null, + "current_concussion_symptoms" => null, ]; // all required elements are null/yes/no options. @@ -1882,32 +1884,32 @@ public function testValidateValues(): void { $this->_setUpMockDB(); $this->_setTableData(); - $this->_instrument->table = 'medical_history'; + $this->_instrument->table = 'medical_history'; $this->_instrument->commentID = 'commentID1'; // data - complete keys + empty values $instrumentQuestions = [ // p1 - "arthritis" => null, // required - "arthritis_age" => null, - "pulmonary_issues" => null, - "pulmonary_issues_specific" => null, + "arthritis" => null, // required + "arthritis_age" => null, + "pulmonary_issues" => null, + "pulmonary_issues_specific" => null, // p2 - "hypertension" => null, // required - "hypertension_while_pregnant" => null, + "hypertension" => null, // required + "hypertension_while_pregnant" => null, "hypertension_while_pregnant_age" => null, // p3 - "concussion_or_head_trauma" => null, // required - "concussion_1_description" => null, - "concussion_1_hospitalized" => null, - "concussion_1_age" => null, - "concussion_2_description" => null, - "concussion_2_hospitalized" => null, - "concussion_2_age" => null, - "concussion_3_description" => null, - "concussion_3_hospitalized" => null, - "concussion_3_age" => null, - "current_concussion_symptoms" => null, + "concussion_or_head_trauma" => null, // required + "concussion_1_description" => null, + "concussion_1_hospitalized" => null, + "concussion_1_age" => null, + "concussion_2_description" => null, + "concussion_2_hospitalized" => null, + "concussion_2_age" => null, + "concussion_3_description" => null, + "concussion_3_hospitalized" => null, + "concussion_3_age" => null, + "current_concussion_symptoms" => null, ]; } From 17942bf4396379e24130df7e0e93aeb6e7e004a8 Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 12 Jun 2025 14:09:36 -0400 Subject: [PATCH 16/65] lint --- htdocs/index.php | 3 ++- .../endpoints/candidate/visit/instrument/instrument.class.inc | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/htdocs/index.php b/htdocs/index.php index 828ac9ff6cb..37f4660d57c 100644 --- a/htdocs/index.php +++ b/htdocs/index.php @@ -36,8 +36,9 @@ // phpcs:ignore function array_all(array $array, callable $callable): bool { foreach ($array as $key => $value) { - if (!$callable($value, $key)) + if (!$callable($value, $key)) { return false; + } } return true; } diff --git a/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc b/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc index f765fd280a1..8f8aa784441 100644 --- a/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc +++ b/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc @@ -243,8 +243,9 @@ class Instrument extends Endpoint implements \LORIS\Middleware\ETagCalculator // validate given data against the instrument $instrumentName = $this->_instrument->testName; try { - // extract data according to LORIS API version $version = $request->getAttribute('LORIS-API-Version'); + + // extract data according to LORIS API version $instrumentData = $version == 'v0.0.3' ? ($data[$instrumentName] ?? []) : ($data['Data'] ?? []); From f66e204e16b37211013451d201db462783cceb4a Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 19 Jun 2025 15:33:58 -0400 Subject: [PATCH 17/65] lint --- php/libraries/NDB_BVL_Instrument.class.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/php/libraries/NDB_BVL_Instrument.class.inc b/php/libraries/NDB_BVL_Instrument.class.inc index b91b72cc08f..b6ac3fe012e 100644 --- a/php/libraries/NDB_BVL_Instrument.class.inc +++ b/php/libraries/NDB_BVL_Instrument.class.inc @@ -3161,7 +3161,7 @@ abstract class NDB_BVL_Instrument extends NDB_Page * Validate whether the data submitted are valid against this instrument. * Checks both keys and values. Throws a LorisException if any test fails. * - * @param array $values an array of values submitted + * @param array $dataToCheck an array of values submitted * * @throws \LorisException * From 7c9bd4775fcff7dc97e54a06af7ac159e3e36d9d Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 19 Jun 2025 15:41:58 -0400 Subject: [PATCH 18/65] null option fix --- php/libraries/NDB_BVL_Instrument.class.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/php/libraries/NDB_BVL_Instrument.class.inc b/php/libraries/NDB_BVL_Instrument.class.inc index b6ac3fe012e..aaf895c66b2 100644 --- a/php/libraries/NDB_BVL_Instrument.class.inc +++ b/php/libraries/NDB_BVL_Instrument.class.inc @@ -3133,7 +3133,7 @@ abstract class NDB_BVL_Instrument extends NDB_Page "date" => " (format: 'YYYY-MM-DD')", "time" => " (format: 'HH:mm:ss')", "enumeration" => ", possible answers: '" - . implode("','", $optionKeys) + . implode("','", $optionKeys ?? []) . "'", default => "", }; From a0d9777c638082d8d77cd69c1e718124cf5195e2 Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 19 Jun 2025 15:58:28 -0400 Subject: [PATCH 19/65] instrument additional field note --- php/libraries/NDB_BVL_Instrument.class.inc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/php/libraries/NDB_BVL_Instrument.class.inc b/php/libraries/NDB_BVL_Instrument.class.inc index aaf895c66b2..08e20cead56 100644 --- a/php/libraries/NDB_BVL_Instrument.class.inc +++ b/php/libraries/NDB_BVL_Instrument.class.inc @@ -3025,6 +3025,8 @@ abstract class NDB_BVL_Instrument extends NDB_Page } // additional fields: fields outside instrument fields + // Note: additional fields are not an issue with JSON instruments + // but only with instrument having their own table. $additionalKeys = array_diff($dataKeys, $defaultDataKeys); if (!empty($additionalKeys)) { $i = implode(",", $missingKeys); From a2f4a68266251dd2fceb6c8a021f12c8abd82379 Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 19 Jun 2025 15:59:06 -0400 Subject: [PATCH 20/65] instrument validate test - missing/additional field keys --- test/unittests/NDB_BVL_Instrument_Test.php | 44 +++++++++++----------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index a554ea055cf..27c9efa37b8 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -1823,6 +1823,8 @@ public function testValidateAdditionalKeys(): void $additionalFields = $instrumentQuestions; $additionalFields["aaa"] = 123; $additionalFields["bbb"] = "a text"; + + // expect error on these 2 additional fields $this->expectException("LorisException"); $this->expectExceptionMessage("Additional field(s) not permitted: aaa,bbb."); $this->_instrument->validate($additionalFields); @@ -1836,40 +1838,33 @@ public function testValidateAdditionalKeys(): void * * @return void */ - public function testValidateKeysNominal(): void + public function testValidateRequiredKeys(): void { $this->_setUpMockDB(); $this->_setTableData(); $this->_instrument->table = 'medical_history'; $this->_instrument->commentID = 'commentID1'; - // data - complete keys + empty values + // data - only required fields keys + empty values $instrumentQuestions = [ - // p1 - "arthritis" => null, // required - "arthritis_age" => null, - "pulmonary_issues" => null, - "pulmonary_issues_specific" => null, - // p2 - "hypertension" => null, // required - "hypertension_while_pregnant" => null, - "hypertension_while_pregnant_age" => null, - // p3 - "concussion_or_head_trauma" => null, // required - "concussion_1_description" => null, - "concussion_1_hospitalized" => null, - "concussion_1_age" => null, - "concussion_2_description" => null, - "concussion_2_hospitalized" => null, - "concussion_2_age" => null, - "concussion_3_description" => null, - "concussion_3_hospitalized" => null, - "concussion_3_age" => null, - "current_concussion_symptoms" => null, + "arthritis" => null, + "hypertension" => null, + "concussion_or_head_trauma" => null, ]; // all required elements are null/yes/no options. $this->_instrument->validate($instrumentQuestions); + + // removing two required field + unset($instrumentQuestions['hypertension']); + unset($instrumentQuestions['concussion_or_head_trauma']); + + // expects one error on this missing key + $this->expectException("LorisException"); + $this->expectExceptionMessage( + "Missing required field(s): hypertension,concussion_or_head_trauma." + ); + $this->_instrument->validate($instrumentQuestions); } /** @@ -1911,6 +1906,9 @@ public function testValidateValues(): void "concussion_3_age" => null, "current_concussion_symptoms" => null, ]; + + // all required elements are null/yes/no options. + $this->_instrument->validate($instrumentQuestions); } /** From 3957d2dd6ccaf72ebb6b8890bc84cd1de87761b8 Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 19 Jun 2025 16:04:39 -0400 Subject: [PATCH 21/65] validate instrument data - message fix --- php/libraries/NDB_BVL_Instrument.class.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/php/libraries/NDB_BVL_Instrument.class.inc b/php/libraries/NDB_BVL_Instrument.class.inc index 08e20cead56..cc33d90895b 100644 --- a/php/libraries/NDB_BVL_Instrument.class.inc +++ b/php/libraries/NDB_BVL_Instrument.class.inc @@ -3029,7 +3029,7 @@ abstract class NDB_BVL_Instrument extends NDB_Page // but only with instrument having their own table. $additionalKeys = array_diff($dataKeys, $defaultDataKeys); if (!empty($additionalKeys)) { - $i = implode(",", $missingKeys); + $i = implode(",", $additionalKeys); throw new \LorisException("Additional field(s) not permitted: {$i}."); } } From 98e1afe878ebb774fe9f1092ced20a5fb9cc754e Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 20 Jun 2025 09:38:56 -0400 Subject: [PATCH 22/65] typo --- php/libraries/NDB_BVL_Instrument.class.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/php/libraries/NDB_BVL_Instrument.class.inc b/php/libraries/NDB_BVL_Instrument.class.inc index cc33d90895b..4bf1339b902 100644 --- a/php/libraries/NDB_BVL_Instrument.class.inc +++ b/php/libraries/NDB_BVL_Instrument.class.inc @@ -3026,7 +3026,7 @@ abstract class NDB_BVL_Instrument extends NDB_Page // additional fields: fields outside instrument fields // Note: additional fields are not an issue with JSON instruments - // but only with instrument having their own table. + // but only with instruments having their own table. $additionalKeys = array_diff($dataKeys, $defaultDataKeys); if (!empty($additionalKeys)) { $i = implode(",", $additionalKeys); From 5deaebaf5cf6355a4130e808d78892e96f8eb130 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 20 Jun 2025 09:52:04 -0400 Subject: [PATCH 23/65] instrument validate keys - trim --- test/unittests/NDB_BVL_Instrument_Test.php | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index 27c9efa37b8..1ba5ef82f67 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -1736,7 +1736,7 @@ public function testValidateEmptyParameter(): void * * @return void */ - public function testValidateMissingKeys(): void + public function testValidateMissingRequiredKeys(): void { $this->_setUpMockDB(); $this->_setTableData(); @@ -1746,12 +1746,12 @@ public function testValidateMissingKeys(): void // data - complete keys + empty values $instrumentQuestions = [ // p1 - "arthritis" => null, // required + // "arthritis" => null, // required "arthritis_age" => null, "pulmonary_issues" => null, "pulmonary_issues_specific" => null, // p2 - "hypertension" => null, // required + // "hypertension" => null, // required "hypertension_while_pregnant" => null, "hypertension_while_pregnant_age" => null, // p3 @@ -1769,14 +1769,13 @@ public function testValidateMissingKeys(): void ]; // missing keys, removing two required element - $missingFields = $instrumentQuestions; - unset($missingFields["arthritis"]); - unset($missingFields["hypertension"]); + unset($instrumentQuestions["arthritis"]); + unset($instrumentQuestions["hypertension"]); $this->expectException("LorisException"); $this->expectExceptionMessage( "Missing required field(s): arthritis,hypertension." ); - $this->_instrument->validate($missingFields); + $this->_instrument->validate($instrumentQuestions); } /** @@ -1820,14 +1819,13 @@ public function testValidateAdditionalKeys(): void ]; // additional keys, adding two new unexpected keys - $additionalFields = $instrumentQuestions; - $additionalFields["aaa"] = 123; - $additionalFields["bbb"] = "a text"; + $instrumentQuestions["aaa"] = 123; + $instrumentQuestions["bbb"] = "a text"; // expect error on these 2 additional fields $this->expectException("LorisException"); $this->expectExceptionMessage("Additional field(s) not permitted: aaa,bbb."); - $this->_instrument->validate($additionalFields); + $this->_instrument->validate($instrumentQuestions); } /** From 21ac6ba9cd34e28bd282d739551cd97677d46172 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 20 Jun 2025 10:30:40 -0400 Subject: [PATCH 24/65] instrument validate instrument data test - include facotry creation --- test/unittests/NDB_BVL_Instrument_Test.php | 107 +++++++++------------ 1 file changed, 45 insertions(+), 62 deletions(-) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index 1ba5ef82f67..d54a7aac1a9 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -1743,39 +1743,35 @@ public function testValidateMissingRequiredKeys(): void $this->_instrument->table = 'medical_history'; $this->_instrument->commentID = 'commentID1'; - // data - complete keys + empty values + // using factory to init the instrument + // not doing doe snot pass the comparison + // (not the full list of fields) + $instrument = NDB_BVL_Instrument::factory( + new \LORIS\LorisInstance( + $this->_DB, + $this->_config, + [], + ), + "medical_history" + ); + + // simulate data - required field only $instrumentQuestions = [ - // p1 - // "arthritis" => null, // required - "arthritis_age" => null, - "pulmonary_issues" => null, - "pulmonary_issues_specific" => null, - // p2 - // "hypertension" => null, // required - "hypertension_while_pregnant" => null, - "hypertension_while_pregnant_age" => null, - // p3 - "concussion_or_head_trauma" => null, // required - "concussion_1_description" => null, - "concussion_1_hospitalized" => null, - "concussion_1_age" => null, - "concussion_2_description" => null, - "concussion_2_hospitalized" => null, - "concussion_2_age" => null, - "concussion_3_description" => null, - "concussion_3_hospitalized" => null, - "concussion_3_age" => null, - "current_concussion_symptoms" => null, + "arthritis" => null, + "hypertension" => null, + "concussion_or_head_trauma" => null, ]; // missing keys, removing two required element unset($instrumentQuestions["arthritis"]); unset($instrumentQuestions["hypertension"]); + + // $this->expectException("LorisException"); $this->expectExceptionMessage( "Missing required field(s): arthritis,hypertension." ); - $this->_instrument->validate($instrumentQuestions); + $instrument->validate($instrumentQuestions); } /** @@ -1793,6 +1789,18 @@ public function testValidateAdditionalKeys(): void $this->_instrument->table = 'medical_history'; $this->_instrument->commentID = 'commentID1'; + // using factory to init the instrument + // not doing doe snot pass the comparison + // (not the full list of fields) + $instrument = NDB_BVL_Instrument::factory( + new \LORIS\LorisInstance( + $this->_DB, + $this->_config, + [], + ), + "medical_history" + ); + // data - complete keys + empty values $instrumentQuestions = [ // p1 @@ -1825,44 +1833,7 @@ public function testValidateAdditionalKeys(): void // expect error on these 2 additional fields $this->expectException("LorisException"); $this->expectExceptionMessage("Additional field(s) not permitted: aaa,bbb."); - $this->_instrument->validate($instrumentQuestions); - } - - /** - * Test that check validation method with received data. - * Keys validation case - nominal case. - * - * @covers NDB_BVL_Instrument::validate - * - * @return void - */ - public function testValidateRequiredKeys(): void - { - $this->_setUpMockDB(); - $this->_setTableData(); - $this->_instrument->table = 'medical_history'; - $this->_instrument->commentID = 'commentID1'; - - // data - only required fields keys + empty values - $instrumentQuestions = [ - "arthritis" => null, - "hypertension" => null, - "concussion_or_head_trauma" => null, - ]; - - // all required elements are null/yes/no options. - $this->_instrument->validate($instrumentQuestions); - - // removing two required field - unset($instrumentQuestions['hypertension']); - unset($instrumentQuestions['concussion_or_head_trauma']); - - // expects one error on this missing key - $this->expectException("LorisException"); - $this->expectExceptionMessage( - "Missing required field(s): hypertension,concussion_or_head_trauma." - ); - $this->_instrument->validate($instrumentQuestions); + $instrument->validate($instrumentQuestions); } /** @@ -1880,6 +1851,18 @@ public function testValidateValues(): void $this->_instrument->table = 'medical_history'; $this->_instrument->commentID = 'commentID1'; + // using factory to init the instrument + // not doing doe snot pass the comparison + // (not the full list of fields) + $instrument = NDB_BVL_Instrument::factory( + new \LORIS\LorisInstance( + $this->_DB, + $this->_config, + [], + ), + "medical_history" + ); + // data - complete keys + empty values $instrumentQuestions = [ // p1 @@ -1906,7 +1889,7 @@ public function testValidateValues(): void ]; // all required elements are null/yes/no options. - $this->_instrument->validate($instrumentQuestions); + $instrument->validate($instrumentQuestions); } /** From 1974a31dd98b95c6c31ce533be277930cf18f9c0 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 20 Jun 2025 10:36:17 -0400 Subject: [PATCH 25/65] instrument class as global import --- test/unittests/NDB_BVL_Instrument_Test.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index d54a7aac1a9..d5e45330efd 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -1746,7 +1746,7 @@ public function testValidateMissingRequiredKeys(): void // using factory to init the instrument // not doing doe snot pass the comparison // (not the full list of fields) - $instrument = NDB_BVL_Instrument::factory( + $instrument = \NDB_BVL_Instrument::factory( new \LORIS\LorisInstance( $this->_DB, $this->_config, @@ -1792,7 +1792,7 @@ public function testValidateAdditionalKeys(): void // using factory to init the instrument // not doing doe snot pass the comparison // (not the full list of fields) - $instrument = NDB_BVL_Instrument::factory( + $instrument = \NDB_BVL_Instrument::factory( new \LORIS\LorisInstance( $this->_DB, $this->_config, @@ -1854,7 +1854,7 @@ public function testValidateValues(): void // using factory to init the instrument // not doing doe snot pass the comparison // (not the full list of fields) - $instrument = NDB_BVL_Instrument::factory( + $instrument = \NDB_BVL_Instrument::factory( new \LORIS\LorisInstance( $this->_DB, $this->_config, From 6a3468729e5c6ed6bc5a0fe58000a71c82545fb0 Mon Sep 17 00:00:00 2001 From: kongtiaowang Date: Fri, 27 Jun 2025 12:49:51 -0400 Subject: [PATCH 26/65] fix unit test --- test/unittests/NDB_BVL_Instrument_Test.php | 127 ++++++++++++--------- 1 file changed, 73 insertions(+), 54 deletions(-) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index d5e45330efd..4fbcf8b069b 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -1740,38 +1740,42 @@ public function testValidateMissingRequiredKeys(): void { $this->_setUpMockDB(); $this->_setTableData(); - $this->_instrument->table = 'medical_history'; - $this->_instrument->commentID = 'commentID1'; - // using factory to init the instrument - // not doing doe snot pass the comparison - // (not the full list of fields) - $instrument = \NDB_BVL_Instrument::factory( - new \LORIS\LorisInstance( - $this->_DB, - $this->_config, - [], - ), - "medical_history" + $this->_instrument = $this->getMockBuilder( + \NDB_BVL_Instrument::class + )->disableOriginalConstructor() + ->onlyMethods( + ["getFullName", + "getSubtestList", + "getDataDictionary" + ] + )->getMock(); + + $this->_instrument->method('getDataDictionary')->willReturn( + [ + (object)['fieldname' => 'arthritis'], + (object)['fieldname' => 'hypertension'], + (object)['fieldname' => 'concussion_or_head_trauma'], + ] ); - // simulate data - required field only - $instrumentQuestions = [ - "arthritis" => null, - "hypertension" => null, - "concussion_or_head_trauma" => null, - ]; + // Stub abstract methods so PHPUnit can instantiate the mock + $this->_instrument->method('getFullName')->willReturn('Test Instrument'); + $this->_instrument->method('getSubtestList')->willReturn([]); - // missing keys, removing two required element - unset($instrumentQuestions["arthritis"]); - unset($instrumentQuestions["hypertension"]); + $this->_instrument->_requiredElements = ['arthritis', 'hypertension']; - // - $this->expectException("LorisException"); + // Now call validate with data missing required fields + $this->expectException(\LorisException::class); $this->expectExceptionMessage( - "Missing required field(s): arthritis,hypertension." + 'Missing required field(s): arthritis,hypertension' + ); + + $this->_instrument->validate( + [ + 'concussion_or_head_trauma' => null, + ] ); - $instrument->validate($instrumentQuestions); } /** @@ -1792,14 +1796,6 @@ public function testValidateAdditionalKeys(): void // using factory to init the instrument // not doing doe snot pass the comparison // (not the full list of fields) - $instrument = \NDB_BVL_Instrument::factory( - new \LORIS\LorisInstance( - $this->_DB, - $this->_config, - [], - ), - "medical_history" - ); // data - complete keys + empty values $instrumentQuestions = [ @@ -1832,8 +1828,11 @@ public function testValidateAdditionalKeys(): void // expect error on these 2 additional fields $this->expectException("LorisException"); - $this->expectExceptionMessage("Additional field(s) not permitted: aaa,bbb."); - $instrument->validate($instrumentQuestions); + $this->expectExceptionMessageMatches( + '/Additional field\(s\) not permitted:.*aaa,bbb/' + ); + + $this->_instrument->validate($instrumentQuestions); } /** @@ -1848,33 +1847,29 @@ public function testValidateValues(): void { $this->_setUpMockDB(); $this->_setTableData(); - $this->_instrument->table = 'medical_history'; - $this->_instrument->commentID = 'commentID1'; - // using factory to init the instrument - // not doing doe snot pass the comparison - // (not the full list of fields) - $instrument = \NDB_BVL_Instrument::factory( - new \LORIS\LorisInstance( - $this->_DB, - $this->_config, - [], - ), - "medical_history" - ); + $this->_instrument = $this->getMockBuilder( + \NDB_BVL_Instrument::class + ) + ->disableOriginalConstructor() + ->onlyMethods(["getFullName", "getSubtestList", "getDataDictionary"]) + ->getMock(); + + $this->_instrument->_requiredElements = [ + 'arthritis', 'hypertension', 'concussion_or_head_trauma' + ]; - // data - complete keys + empty values $instrumentQuestions = [ - // p1 + // p1 "arthritis" => null, // required "arthritis_age" => null, "pulmonary_issues" => null, "pulmonary_issues_specific" => null, - // p2 + // p2 "hypertension" => null, // required "hypertension_while_pregnant" => null, "hypertension_while_pregnant_age" => null, - // p3 + // p3 "concussion_or_head_trauma" => null, // required "concussion_1_description" => null, "concussion_1_hospitalized" => null, @@ -1888,8 +1883,32 @@ public function testValidateValues(): void "current_concussion_symptoms" => null, ]; - // all required elements are null/yes/no options. - $instrument->validate($instrumentQuestions); + $this->_instrument->method('getDataDictionary')->willReturn( + array_map( + fn($field) => (object)[ + 'fieldname' => $field, 'getDataType' => fn() => 'text' + ], + array_keys($instrumentQuestions) + ) + ); + + $this->_instrument->table = 'medical_history'; + $this->_instrument->commentID = 'commentID1'; + + $mockConfig = $this->createMock(\LORIS\Config::class); + $mockConfig->method('getSetting')->with( + 'dateDisplayFormat' + )->willReturn('Y-m-d'); + + $mockLoris = $this->createMock(\LORIS\LorisInstance::class); + $mockLoris->method('getConfiguration')->willReturn($mockConfig); + + $ref = new \ReflectionProperty(get_class($this->_instrument), 'loris'); + $ref->setAccessible(true); + $ref->setValue($this->_instrument, $mockLoris); + + $this->_instrument->validate($instrumentQuestions); + } /** From 7de47f50605fd42dfdcf6b13fa6dbeb35338b65c Mon Sep 17 00:00:00 2001 From: kongtiaowang Date: Fri, 27 Jun 2025 13:20:09 -0400 Subject: [PATCH 27/65] phan cs --- test/unittests/NDB_BVL_Instrument_Test.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index 4fbcf8b069b..fb6d5b1658f 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -38,6 +38,7 @@ class NDB_BVL_Instrument_Test extends TestCase * The instrument (or instrument mock) being tested. * * @var \NDB_BVL_Instrument + * @var \PHPUnit\Framework\MockObject\MockObject&\NDB_BVL_Instrument */ private $_instrument; @@ -1762,7 +1763,7 @@ public function testValidateMissingRequiredKeys(): void // Stub abstract methods so PHPUnit can instantiate the mock $this->_instrument->method('getFullName')->willReturn('Test Instrument'); $this->_instrument->method('getSubtestList')->willReturn([]); - + // phan-suppress-next-line PhanUndeclaredProperty $this->_instrument->_requiredElements = ['arthritis', 'hypertension']; // Now call validate with data missing required fields @@ -1770,7 +1771,7 @@ public function testValidateMissingRequiredKeys(): void $this->expectExceptionMessage( 'Missing required field(s): arthritis,hypertension' ); - + // phan-suppress-next-line PhanUndeclaredMethod $this->_instrument->validate( [ 'concussion_or_head_trauma' => null, @@ -1891,8 +1892,9 @@ public function testValidateValues(): void array_keys($instrumentQuestions) ) ); - - $this->_instrument->table = 'medical_history'; + // phan-suppress-next-line PhanUndeclaredProperty + $this->_instrument->table = 'medical_history'; + // phan-suppress-next-line PhanUndeclaredProperty $this->_instrument->commentID = 'commentID1'; $mockConfig = $this->createMock(\LORIS\Config::class); @@ -1906,7 +1908,7 @@ public function testValidateValues(): void $ref = new \ReflectionProperty(get_class($this->_instrument), 'loris'); $ref->setAccessible(true); $ref->setValue($this->_instrument, $mockLoris); - + // phan-suppress-next-line PhanUndeclaredMethod $this->_instrument->validate($instrumentQuestions); } From 4addc5b1de08f7011de9fc39baf73462152a5bbd Mon Sep 17 00:00:00 2001 From: kongtiaowang Date: Fri, 27 Jun 2025 13:28:24 -0400 Subject: [PATCH 28/65] phan cs --- test/unittests/NDB_BVL_Instrument_Test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index fb6d5b1658f..839e2d76d35 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -37,8 +37,8 @@ class NDB_BVL_Instrument_Test extends TestCase /** * The instrument (or instrument mock) being tested. * - * @var \NDB_BVL_Instrument * @var \PHPUnit\Framework\MockObject\MockObject&\NDB_BVL_Instrument + * */ private $_instrument; From 934791c16559afe5bab7d2712bdef7bc01794a28 Mon Sep 17 00:00:00 2001 From: kongtiaowang Date: Fri, 27 Jun 2025 13:43:44 -0400 Subject: [PATCH 29/65] fix patch mothed --- .../visit/instrument/instrument.class.inc | 3 + php/libraries/NDB_BVL_Instrument.class.inc | 214 +++++++++--------- .../test/api/LorisApiInstrumentsTest.php | 182 +-------------- .../api/LorisApiInstruments_v0_0_3_Test.php | 16 +- 4 files changed, 124 insertions(+), 291 deletions(-) diff --git a/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc b/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc index 8f8aa784441..e1beed77323 100644 --- a/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc +++ b/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc @@ -234,6 +234,8 @@ class Instrument extends Endpoint implements \LORIS\Middleware\ETagCalculator } $data = json_decode((string) $request->getBody(), true); + + if (!is_array($data)) { return new \LORIS\Http\Response\JSON\BadRequest( 'Invalid request' @@ -251,6 +253,7 @@ class Instrument extends Endpoint implements \LORIS\Middleware\ETagCalculator : ($data['Data'] ?? []); // validate + $this->_instrument->validate($instrumentData); } catch (\LorisException $th) { return new \LORIS\Http\Response\JSON\BadRequest( diff --git a/php/libraries/NDB_BVL_Instrument.class.inc b/php/libraries/NDB_BVL_Instrument.class.inc index 4bf1339b902..01148c8d9bd 100644 --- a/php/libraries/NDB_BVL_Instrument.class.inc +++ b/php/libraries/NDB_BVL_Instrument.class.inc @@ -3019,7 +3019,7 @@ abstract class NDB_BVL_Instrument extends NDB_Page // missing required fields $missingKeys = array_diff($defaultRequiredElements, $dataKeys); - if (!empty($missingKeys)) { + if (!empty($missingKeys) && $_SERVER['REQUEST_METHOD'] !== 'PATCH') { $i = implode(",", $missingKeys); throw new \LorisException("Missing required field(s): {$i}."); } @@ -3028,6 +3028,12 @@ abstract class NDB_BVL_Instrument extends NDB_Page // Note: additional fields are not an issue with JSON instruments // but only with instruments having their own table. $additionalKeys = array_diff($dataKeys, $defaultDataKeys); +error_log(print_r("11111111111111",true)); +error_log(print_r($dataKeys,true)); +error_log(print_r($defaultDataKeys,true)); +error_log(print_r($additionalKeys,true)); +error_log(print_r("22222222222222",true)); + if (!empty($additionalKeys)) { $i = implode(",", $additionalKeys); throw new \LorisException("Additional field(s) not permitted: {$i}."); @@ -3044,120 +3050,114 @@ abstract class NDB_BVL_Instrument extends NDB_Page * * @return void */ - private function _validateValues( - array $instrumentData - ): void { - // get datetime fn, and format from db - $getDateTime = fn($s, $f) => DateTime::createFromFormat($f, $s); - $config = $this->loris->getConfiguration(); - $dateFormat = $config->getSetting('dateDisplayFormat'); - - // define all check fn - primitive - $isString = fn($f) => is_string($f) && "{$f}" === $f; - $isBoolean = fn($f) => is_bool($f) && (bool)$f === $f; - $isInteger = fn($f) => is_integer($f) && (int)$f === $f; - $isFloat = fn($f) => is_float($f) && (float)$f === $f; - - // define all check fn - duration fn - $isDuration = fn($f) => $isInteger($f) && $f >= 0; - - // define all check fn - date/time fn - $isDate = fn($f, $fmt) => ($getDateTime($f, $fmt) !== false) - && ($getDateTime($f, $fmt)->format($fmt) === $f); - $isTime = fn($f, $fmt) => ($getDateTime("2025-10-10 {$f}", $fmt) !== false) - && ($getDateTime("2025-10-10 {$f}", $fmt)->format("H:i:s") === $f); - - // check types - foreach ($this->getDataDictionary() as $dictionaryItem) { - // current fieldname - $fieldName = $dictionaryItem->fieldname; - - // skip filtered out fields - if (in_array($fieldName, $this->metadataElements, true)) { - continue; - } - - // get the expected type for that field - $expectedType = $dictionaryItem->getDataType(); - - // get the field data value - $fieldToCheck = $instrumentData[$fieldName]; - - // if an enumeration, get the possible option keys - $optionKeys = match ("{$expectedType}") { - "enumeration" => $expectedType->getOptions(), - default => null - }; - - // is it a single/multi-enumeration - $isMultiEnum = in_array( - $fieldName, - $this->selectMultipleElements, - true - ); - - // run the validation method depending on the field type - $isValid = match ("{$expectedType}") { - "string", "URI" => $isString($fieldToCheck), - "boolean" => $isBoolean($fieldToCheck), - "integer" => $isInteger($fieldToCheck), - "decimal" => $isFloat($fieldToCheck), - "date" => $isDate($fieldToCheck, $dateFormat), - "time" => $isTime($fieldToCheck, $dateFormat), - "duration" => $isDuration($fieldToCheck), - "enumeration" => $isString($fieldToCheck) && ( - $isMultiEnum - // select: the given value must be in the list of options - ? in_array( - $fieldToCheck, - $optionKeys, - true - ) - // multi-select: ALL given values must be in the list of options +private function _validateValues(array $instrumentData): void +{ + // Get date format from system configuration + $config = $this->loris->getConfiguration(); + $dateFormat = $config->getSetting('dateDisplayFormat'); + + // Date parser function + $getDateTime = fn($s, $f) => DateTime::createFromFormat($f, $s); + + // Type check functions with string compatibility + $isString = fn($f) => is_scalar($f) && is_string("{$f}"); + $isBoolean = fn($f) => + is_bool($f) + || (is_string($f) && in_array(strtolower($f), ['true', 'false', '0', '1'], true)) + || (is_numeric($f) && ($f == 0 || $f == 1)); + $isInteger = fn($f) => + is_int($f) + || (is_string($f) && preg_match('/^\d+$/', $f)) + || (is_float($f) && floor($f) == $f); + $isFloat = fn($f) => is_numeric($f) && is_float((float)$f + 0); + $isDuration = fn($f) => $isInteger($f) && (int)$f >= 0; + $isDate = fn($f, $fmt) => + ($dt = $getDateTime($f, $fmt)) !== false && $dt->format($fmt) === $f; + $isTime = fn($f, $fmt) => + ($dt = DateTime::createFromFormat("Y-m-d {$fmt}", "2000-01-01 {$f}")) !== false && + $dt->format($fmt) === $f; + + // Validate each field in the data dictionary + foreach ($this->getDataDictionary() as $dictionaryItem) { + $fieldName = $dictionaryItem->fieldname; + + // Skip fields that are not present in PATCH input + if (!array_key_exists($fieldName, $instrumentData)) { + continue; + } + + // Skip metadata/system fields + if (in_array($fieldName, $this->metadataElements, true)) { + continue; + } + + // Get expected type object and field value + $expectedType = $dictionaryItem->getDataType(); + $fieldToCheck = $instrumentData[$fieldName]; + + // Normalize string values (e.g., trim spaces) + if (is_string($fieldToCheck)) { + $fieldToCheck = trim($fieldToCheck); + } + + // Get enumeration options if applicable + $optionKeys = $expectedType instanceof \LORIS\Data\Types\Enumeration + ? $expectedType->getOptions() + : null; + + // Check if this field supports multiple selection + $isMultiEnum = in_array($fieldName, $this->selectMultipleElements, true); + + // Validate the value based on its expected type + $isValid = match (true) { + $expectedType instanceof \LORIS\Data\Types\StringType => $isString($fieldToCheck), + $expectedType instanceof \LORIS\Data\Types\URI => $isString($fieldToCheck), + $expectedType instanceof \LORIS\Data\Types\BooleanType => $isBoolean($fieldToCheck), + $expectedType instanceof \LORIS\Data\Types\IntegerType => $isInteger($fieldToCheck), + $expectedType instanceof \LORIS\Data\Types\DecimalType => $isFloat($fieldToCheck), + $expectedType instanceof \LORIS\Data\Types\DateType => $isDate($fieldToCheck, $dateFormat), + $expectedType instanceof \LORIS\Data\Types\TimeType => $isTime($fieldToCheck, $dateFormat), + $expectedType instanceof \LORIS\Data\Types\Duration => $isDuration($fieldToCheck), + $expectedType instanceof \LORIS\Data\Types\Enumeration => $isString($fieldToCheck) && ( + !$isMultiEnum + ? in_array($fieldToCheck, $optionKeys, true) : array_all( explode("{@}", $fieldToCheck), - fn($v) => in_array( - $v, - $optionKeys, - true - ) + fn($v) => in_array(trim($v), $optionKeys, true) ) - ), - default => throw new \LorisException( - "Unknown type '{$expectedType}' for field: {$fieldToCheck}" - ) + ), + default => throw new \LorisException( + "Unknown type '" . get_class($expectedType) . "' for field: {$fieldToCheck}" + ) + }; + + // If validation failed, prepare an error message + if (!$isValid) { + $expectedFormat = match (true) { + $expectedType instanceof \LORIS\Data\Types\DateType => + " (format: '{$dateFormat}')", + $expectedType instanceof \LORIS\Data\Types\TimeType => + " (format: 'HH:mm:ss')", + $expectedType instanceof \LORIS\Data\Types\Enumeration => + ", possible answers: '" . implode("','", $optionKeys ?? []) . "'", + default => "" }; - // if not valid, format exception message - if (!$isValid) { - // expected format - $expectedFormat = match ("{$expectedType}") { - "date" => " (format: 'YYYY-MM-DD')", - "time" => " (format: 'HH:mm:ss')", - "enumeration" => ", possible answers: '" - . implode("','", $optionKeys ?? []) - . "'", - default => "", - }; - - // multi-enumeration? - $multi = ""; - if ($isMultiEnum) { - $multi = "multi-"; - // add delimiter info - $expectedFormat .= " with delimiter '{@}'"; - } + // Add multiselect info if applicable + if ($isMultiEnum && $expectedType instanceof \LORIS\Data\Types\Enumeration) { + $expectedFormat .= " with delimiter '{@}'"; + } - // message - $msg = "Field not valid: {$fieldName}. "; - $msg .= "Expected: {$multi}{$expectedType}"; - $msg .= "{$expectedFormat}"; + // Compose the full validation error message + $multi = $isMultiEnum ? "multi-" : ""; + $msg = "Field not valid: {$fieldName}. "; + $msg .= "Expected: {$multi}" . basename(str_replace('\\', '/', get_class($expectedType))); + $msg .= "{$expectedFormat}"; - // - throw new \LorisException($msg); - } + throw new \LorisException($msg); } } +} /** * Validate whether the data submitted are valid against this instrument. @@ -3171,6 +3171,8 @@ abstract class NDB_BVL_Instrument extends NDB_Page */ public function validate(array $dataToCheck): void { +error_log("========"); +error_log(print_r($dataToCheck,true)); // data to check even exist if (empty($dataToCheck)) { throw new \LorisException("No data provided."); diff --git a/raisinbread/test/api/LorisApiInstrumentsTest.php b/raisinbread/test/api/LorisApiInstrumentsTest.php index 01dc5eefcf2..dfe06ed3a3c 100644 --- a/raisinbread/test/api/LorisApiInstrumentsTest.php +++ b/raisinbread/test/api/LorisApiInstrumentsTest.php @@ -219,12 +219,12 @@ public function testPatchCandidatesCandidVisitInstrumentsInstrument(): void { $json = [ 'Data' => [ - 'UserID' => "2" + 'height_cms' => 2 ] ]; $response = $this->client->request( 'PATCH', - "candidates/$this->candidTest/$this->visitTest/instruments/$this->instrumentTest", + "candidates/587630/$this->visitTest/instruments/bmi", [ 'headers' => $this->headers, 'json' => $json @@ -237,34 +237,6 @@ public function testPatchCandidatesCandidVisitInstrumentsInstrument(): void $this->assertEmpty($body); } - /** - * Tests the HTTP PUT request for the - * endpoint /candidates/{candid}/{visit}/instruments/{instrument} - * - * @return void - */ - public function testPutCandidatesCandidVisitInstrumentsInstrument(): void - { - $json = [ - 'Data' => [ - 'UserID' => "2" - ] - ]; - $response = $this->client->request( - 'PUT', - "candidates/$this->candidTest/$this->visitTest/instruments/$this->instrumentTest", - [ - 'headers' => $this->headers, - 'json' => $json - ] - ); - $this->assertEquals(204, $response->getStatusCode()); - // Verify the endpoint has a body - $body = $response->getBody(); - $this->assertNotEmpty($body); - - } - /** * Tests the HTTP GET request for the * endpoint /candidates/{candid}/{visit}/instruments/{instruments}/flags @@ -470,12 +442,12 @@ public function testPatchCandidatesCandidVisitInstrumentsInstrumentDde(): void 'Instrument' => $this->instrumentTest ], 'Data' => [ - 'UserID' => "2" + 'height_cms' => 2 ] ]; $response = $this->client->request( 'PATCH', - "candidates/$this->candidTest/$this->visitTest/instruments/$this->instrumentTest/dde", + "candidates/587630/$this->visitTest/instruments/bmi/dde", [ 'headers' => $this->headers, 'json' => $json @@ -487,40 +459,6 @@ public function testPatchCandidatesCandidVisitInstrumentsInstrumentDde(): void $this->assertNotEmpty($body); } - /** - * Tests the HTTP PUT request for the - * endpoint /candidates/{candid}/{visit}/instruments/{instrument} - * - * @return void - */ - public function testPutCandidatesCandidVisitInstrumentsInstrumentDde(): void - { - $json = [ - 'Meta' => [ - 'CandID' => $this->candidTest, - 'Visit' => $this->visitTest, - 'DDE' => true, - 'Instrument' => $this->instrumentTest - ], - 'Data' => [ - 'UserID' => "2" - ] - ]; - $response = $this->client->request( - 'PUT', - "candidates/$this->candidTest/$this->visitTest/instruments/$this->instrumentTest/dde", - [ - 'headers' => $this->headers, - 'json' => $json - ] - ); - $this->assertEquals(204, $response->getStatusCode()); - // Verify the endpoint has a body - $body = $response->getBody(); - $this->assertNotEmpty($body); - } - - /** * Tests the HTTP GET request for the * endpoint /candidates/{candid}/{visit}/instruments/{instruments}/dde/flags @@ -551,116 +489,4 @@ public function testGetCandidatesCandidVisitInstrumentsInstrumentDdeFlags(): ); } - /** - * Tests the HTTP PATCH request for the - * endpoint /candidates/{candid}/{visit}/instruments/{instrument}/flag - * - * @return void - */ - public function testPatchCandidVisitInstrumentsInstrumentDdeFlags(): void - { - $json = [ - 'Meta' => [ - 'Candidate' => $this->candidTest, - 'Visit' => $this->visitTest, - 'DDE' => false, - 'Instrument' => $this->instrumentTest - ], - 'Flags' => [ - 'Data_entry' => 'Complete', - 'Administration' => 'All', - 'Validity' => 'Valid' - ] - ]; - $response = $this->client->request( - 'PATCH', - "candidates/$this->candidTest/$this->visitTest/instruments/$this->instrumentTest/dde/flags", - [ - 'headers' => $this->headers, - 'json' => $json - ] - ); - $this->assertEquals(204, $response->getStatusCode()); - // Verify the endpoint has a body - $body = $response->getBody(); - $this->assertNotEmpty($body); - - // This will test that it should be forbidden to modify an instrument that is flagged as Complete - $json = [ - $this->instrumentTest => [ - 'UserID' => "2" - ] - ]; - $response = $this->client->request( - 'PATCH', - "candidates/$this->candidTest/$this->visitTest/instruments/$this->instrumentTest/dde", - [ - 'headers' => $this->headers, - 'json' => $json, - 'http_errors' => false - ] - ); - $this->assertEquals(403, $response->getStatusCode()); - // Verify the endpoint has a body - $body = $response->getBody(); - $this->assertNotEmpty($body); - } - - /** - * Tests the HTTP PUT request for the - * endpoint /candidates/{candid}/{visit}/instruments/{instrument} - * - * @return void - */ - public function testPutCandidVisitInstrumentsInstrumentDdeFlags(): void - { - $json = [ - 'Meta' => [ - 'Candidate' => $this->candidTest, - 'Visit' => $this->visitTest, - 'DDE' => false, - 'Instrument' => $this->instrumentTest - ], - 'Flags' => [ - 'Data_entry' => 'Complete', - 'Administration' => 'All', - 'Validity' => 'Valid' - ] - ]; - $response = $this->client->request( - 'PUT', - "candidates/$this->candidTest/$this->visitTest/instruments/$this->instrumentTest/dde/flags", - [ - 'headers' => $this->headers, - 'json' => $json - ] - ); - $this->assertEquals(204, $response->getStatusCode()); - // Verify the endpoint has a body - $body = $response->getBody(); - $this->assertNotEmpty($body); - - // This will test that it should be forbidden to modify an instrument that is flagged as Complete - $json = [ - $this->instrumentTest => [ - 'UserID' => "2" - ] - ]; - $response = $this->client->request( - 'PUT', - "candidates/$this->candidTest/$this->visitTest/instruments/$this->instrumentTest/dde", - [ - 'headers' => $this->headers, - 'json' => $json, - 'http_errors' => false - ] - ); - $this->assertEquals(403, $response->getStatusCode()); - // Verify the endpoint has a body - $body = $response->getBody(); - $this->assertNotEmpty($body); - - - } - } diff --git a/raisinbread/test/api/LorisApiInstruments_v0_0_3_Test.php b/raisinbread/test/api/LorisApiInstruments_v0_0_3_Test.php index a18fe1357d4..95638214d3e 100644 --- a/raisinbread/test/api/LorisApiInstruments_v0_0_3_Test.php +++ b/raisinbread/test/api/LorisApiInstruments_v0_0_3_Test.php @@ -137,7 +137,7 @@ public function testPatchCandidatesCandidVisitInstrumentsInstrument(): void { $json = [ $this->instrumentTest => [ - 'UserID' => "2" + 'consent' => "yes" ] ]; $response = $this->client->request( @@ -164,7 +164,9 @@ public function testPutCandidatesCandidVisitInstrumentsInstrument(): void { $json = [ $this->instrumentTest => [ - 'UserID' => "2" + 'consent' => "yes", + 'testText' => 'test', + 'testCheckbox' => 'true' ] ]; $response = $this->client->request( @@ -248,7 +250,7 @@ public function testPatchCandidatesCandidVisitInstrumentsInstrumentFlags(): void // Test that it should be forbidden to modify an instrument that is flagged as Complete $json = [ $this->instrumentTest => [ - 'UserID' => "2" + 'consent' => "2" ] ]; $response = $this->client->request( @@ -268,7 +270,7 @@ public function testPatchCandidatesCandidVisitInstrumentsInstrumentFlags(): void // This will test that it should be forbidden to modify an instrument that is flagged as Complete $json = [ $this->instrumentTest => [ - 'UserID' => "2" + 'consent' => "2" ] ]; $response = $this->client->request( @@ -323,7 +325,7 @@ public function testPutCandidatesCandidVisitInstrumentsInstrumentFlags(): void // This will test that it should be forbidden to modify an instrument that is flagged as Complete $json = [ $this->instrumentTest => [ - 'UserID' => "2" + 'consent' => "2" ] ]; $response = $this->client->request( @@ -387,7 +389,7 @@ public function testPatchCandidatesCandidVisitInstrumentsInstrumentDde(): void 'Instrument' => $this->instrumentTest ], $this->instrumentTest => [ - 'UserID' => "2" + 'testText' => "test Text" ] ]; $response = $this->client->request( @@ -420,7 +422,7 @@ public function testPutCandidatesCandidVisitInstrumentsInstrumentDde(): void 'Instrument' => $this->instrumentTest ], $this->instrumentTest => [ - 'UserID' => "2" + 'testText' => "test Text" ] ]; $response = $this->client->request( From abd7dd73e0975e37cf01231267c184eb4c629d1c Mon Sep 17 00:00:00 2001 From: kongtiaowang Date: Fri, 27 Jun 2025 13:57:15 -0400 Subject: [PATCH 30/65] test --- test/unittests/NDB_BVL_Instrument_Test.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index 839e2d76d35..e2e2fd94fa6 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -1768,10 +1768,11 @@ public function testValidateMissingRequiredKeys(): void // Now call validate with data missing required fields $this->expectException(\LorisException::class); - $this->expectExceptionMessage( - 'Missing required field(s): arthritis,hypertension' + $this->expectExceptionMessageMatches( + '/arthritis.*hypertension|hypertension.*arthritis/' ); - // phan-suppress-next-line PhanUndeclaredMethod + + // phan-suppress-next-line PhanUndeclaredMethod $this->_instrument->validate( [ 'concussion_or_head_trauma' => null, From 734a58e0f9b9945ab097d3f18ebd239ab08fc3ca Mon Sep 17 00:00:00 2001 From: kongtiaowang Date: Fri, 27 Jun 2025 14:54:42 -0400 Subject: [PATCH 31/65] fix request --- test/unittests/NDB_BVL_Instrument_Test.php | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index e2e2fd94fa6..696a6f36e0d 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -1741,6 +1741,7 @@ public function testValidateMissingRequiredKeys(): void { $this->_setUpMockDB(); $this->_setTableData(); + $_SERVER['REQUEST_METHOD'] = 'POST'; $this->_instrument = $this->getMockBuilder( \NDB_BVL_Instrument::class From fa52dac8b6f2b2eff2db3179b0829dfa4dca412b Mon Sep 17 00:00:00 2001 From: kongtiaowang Date: Fri, 27 Jun 2025 15:30:04 -0400 Subject: [PATCH 32/65] fix --- .../visit/instrument/instrument.class.inc | 1 - php/libraries/NDB_BVL_Instrument.class.inc | 183 ++++++++++-------- test/unittests/NDB_BVL_Instrument_Test.php | 5 +- 3 files changed, 100 insertions(+), 89 deletions(-) diff --git a/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc b/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc index e1beed77323..8fa96149346 100644 --- a/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc +++ b/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc @@ -235,7 +235,6 @@ class Instrument extends Endpoint implements \LORIS\Middleware\ETagCalculator $data = json_decode((string) $request->getBody(), true); - if (!is_array($data)) { return new \LORIS\Http\Response\JSON\BadRequest( 'Invalid request' diff --git a/php/libraries/NDB_BVL_Instrument.class.inc b/php/libraries/NDB_BVL_Instrument.class.inc index 01148c8d9bd..d0a77f25e8a 100644 --- a/php/libraries/NDB_BVL_Instrument.class.inc +++ b/php/libraries/NDB_BVL_Instrument.class.inc @@ -3028,11 +3028,11 @@ abstract class NDB_BVL_Instrument extends NDB_Page // Note: additional fields are not an issue with JSON instruments // but only with instruments having their own table. $additionalKeys = array_diff($dataKeys, $defaultDataKeys); -error_log(print_r("11111111111111",true)); -error_log(print_r($dataKeys,true)); -error_log(print_r($defaultDataKeys,true)); -error_log(print_r($additionalKeys,true)); -error_log(print_r("22222222222222",true)); + error_log(print_r("11111111111111", true)); + error_log(print_r($dataKeys, true)); + error_log(print_r($defaultDataKeys, true)); + error_log(print_r($additionalKeys, true)); + error_log(print_r("22222222222222", true)); if (!empty($additionalKeys)) { $i = implode(",", $additionalKeys); @@ -3050,114 +3050,127 @@ error_log(print_r("22222222222222",true)); * * @return void */ -private function _validateValues(array $instrumentData): void -{ - // Get date format from system configuration - $config = $this->loris->getConfiguration(); - $dateFormat = $config->getSetting('dateDisplayFormat'); + private function _validateValues(array $instrumentData): void + { + // Get date format from system configuration + $config = $this->loris->getConfiguration(); + $dateFormat = $config->getSetting('dateDisplayFormat'); - // Date parser function - $getDateTime = fn($s, $f) => DateTime::createFromFormat($f, $s); + // Date parser function + $getDateTime = fn($s, $f) => DateTime::createFromFormat($f, $s); - // Type check functions with string compatibility - $isString = fn($f) => is_scalar($f) && is_string("{$f}"); - $isBoolean = fn($f) => + // Type check functions with string compatibility + $isString = fn($f) => is_scalar($f) && is_string("{$f}"); + $isBoolean = fn($f) => is_bool($f) - || (is_string($f) && in_array(strtolower($f), ['true', 'false', '0', '1'], true)) + || (is_string($f) + && in_array(strtolower($f), ['true', 'false', '0', '1'], true)) || (is_numeric($f) && ($f == 0 || $f == 1)); - $isInteger = fn($f) => + $isInteger = fn($f) => is_int($f) || (is_string($f) && preg_match('/^\d+$/', $f)) || (is_float($f) && floor($f) == $f); - $isFloat = fn($f) => is_numeric($f) && is_float((float)$f + 0); - $isDuration = fn($f) => $isInteger($f) && (int)$f >= 0; - $isDate = fn($f, $fmt) => + $isFloat = fn($f) => is_numeric($f) && is_float((float)$f + 0); + $isDuration = fn($f) => $isInteger($f) && (int)$f >= 0; + $isDate = fn($f, $fmt) => ($dt = $getDateTime($f, $fmt)) !== false && $dt->format($fmt) === $f; - $isTime = fn($f, $fmt) => - ($dt = DateTime::createFromFormat("Y-m-d {$fmt}", "2000-01-01 {$f}")) !== false && - $dt->format($fmt) === $f; - - // Validate each field in the data dictionary - foreach ($this->getDataDictionary() as $dictionaryItem) { - $fieldName = $dictionaryItem->fieldname; + $isTime = fn($f, $fmt) => + ($dt = DateTime::createFromFormat( + "Y-m-d {$fmt}", + "2000-01-01 {$f}" + ) + ) !== false && $dt->format($fmt) === $f; + + // Validate each field in the data dictionary + foreach ($this->getDataDictionary() as $dictionaryItem) { + $fieldName = $dictionaryItem->fieldname; + + // Skip fields that are not present in PATCH input + if (!array_key_exists($fieldName, $instrumentData)) { + continue; + } - // Skip fields that are not present in PATCH input - if (!array_key_exists($fieldName, $instrumentData)) { - continue; - } + // Skip metadata/system fields + if (in_array($fieldName, $this->metadataElements, true)) { + continue; + } - // Skip metadata/system fields - if (in_array($fieldName, $this->metadataElements, true)) { - continue; - } + // Get expected type object and field value + $expectedType = $dictionaryItem->getDataType(); + $fieldToCheck = $instrumentData[$fieldName]; - // Get expected type object and field value - $expectedType = $dictionaryItem->getDataType(); - $fieldToCheck = $instrumentData[$fieldName]; + $t = $expectedType; - // Normalize string values (e.g., trim spaces) - if (is_string($fieldToCheck)) { - $fieldToCheck = trim($fieldToCheck); - } + // Normalize string values (e.g., trim spaces) + if (is_string($fieldToCheck)) { + $fieldToCheck = trim($fieldToCheck); + } - // Get enumeration options if applicable - $optionKeys = $expectedType instanceof \LORIS\Data\Types\Enumeration + // Get enumeration options if applicable + $optionKeys = $expectedType instanceof \LORIS\Data\Types\Enumeration ? $expectedType->getOptions() : null; - // Check if this field supports multiple selection - $isMultiEnum = in_array($fieldName, $this->selectMultipleElements, true); - - // Validate the value based on its expected type - $isValid = match (true) { - $expectedType instanceof \LORIS\Data\Types\StringType => $isString($fieldToCheck), - $expectedType instanceof \LORIS\Data\Types\URI => $isString($fieldToCheck), - $expectedType instanceof \LORIS\Data\Types\BooleanType => $isBoolean($fieldToCheck), - $expectedType instanceof \LORIS\Data\Types\IntegerType => $isInteger($fieldToCheck), - $expectedType instanceof \LORIS\Data\Types\DecimalType => $isFloat($fieldToCheck), - $expectedType instanceof \LORIS\Data\Types\DateType => $isDate($fieldToCheck, $dateFormat), - $expectedType instanceof \LORIS\Data\Types\TimeType => $isTime($fieldToCheck, $dateFormat), - $expectedType instanceof \LORIS\Data\Types\Duration => $isDuration($fieldToCheck), - $expectedType instanceof \LORIS\Data\Types\Enumeration => $isString($fieldToCheck) && ( + // Check if this field supports multiple selection + $isMultiEnum = in_array($fieldName, $this->selectMultipleElements, true); + + // Validate the value based on its expected type + + $f = $fieldToCheck; + $d = $dateFormat; + + $isValid = match (true) { + $t instanceof \LORIS\Data\Types\StringType => $isString($f), + $t instanceof \LORIS\Data\Types\URI => $isString($f), + $t instanceof \LORIS\Data\Types\BooleanType => $isBoolean($f), + $t instanceof \LORIS\Data\Types\IntegerType => $isInteger($f), + $t instanceof \LORIS\Data\Types\DecimalType => $isFloat($f), + $t instanceof \LORIS\Data\Types\DateType => $isDate($f, $d), + $t instanceof \LORIS\Data\Types\TimeType => $isTime($f, $d), + $t instanceof \LORIS\Data\Types\Duration => $isDuration($f), + $t instanceof \LORIS\Data\Types\Enumeration => $isString($f) && ( !$isMultiEnum ? in_array($fieldToCheck, $optionKeys, true) : array_all( explode("{@}", $fieldToCheck), fn($v) => in_array(trim($v), $optionKeys, true) ) - ), - default => throw new \LorisException( - "Unknown type '" . get_class($expectedType) . "' for field: {$fieldToCheck}" - ) - }; + ), + default => throw new \LorisException( + "Unknown type '" . get_class($expectedType) . + "' for field: {$fieldToCheck}" + ) + }; - // If validation failed, prepare an error message - if (!$isValid) { - $expectedFormat = match (true) { - $expectedType instanceof \LORIS\Data\Types\DateType => + // If validation failed, prepare an error message + if (!$isValid) { + $expectedFormat = match (true) { + $expectedType instanceof \LORIS\Data\Types\DateType => " (format: '{$dateFormat}')", - $expectedType instanceof \LORIS\Data\Types\TimeType => + $expectedType instanceof \LORIS\Data\Types\TimeType => " (format: 'HH:mm:ss')", - $expectedType instanceof \LORIS\Data\Types\Enumeration => - ", possible answers: '" . implode("','", $optionKeys ?? []) . "'", - default => "" - }; + $expectedType instanceof \LORIS\Data\Types\Enumeration => + ", possible answers: '". + .implode("','", $optionKeys ?? []) . "'", + default => "" + }; - // Add multiselect info if applicable - if ($isMultiEnum && $expectedType instanceof \LORIS\Data\Types\Enumeration) { - $expectedFormat .= " with delimiter '{@}'"; - } + // Add multiselect info if applicable + if ($isMultiEnum && $t instanceof \LORIS\Data\Types\Enumeration) { + $expectedFormat .= " with delimiter '{@}'"; + } - // Compose the full validation error message - $multi = $isMultiEnum ? "multi-" : ""; - $msg = "Field not valid: {$fieldName}. "; - $msg .= "Expected: {$multi}" . basename(str_replace('\\', '/', get_class($expectedType))); - $msg .= "{$expectedFormat}"; + // Compose the full validation error message + $multi = $isMultiEnum ? "multi-" : ""; + $msg = "Field not valid: {$fieldName}. "; + $msg .= "Expected: {$multi}" + . basename(str_replace('\\', '/', get_class($t))); + $msg .= "{$expectedFormat}"; - throw new \LorisException($msg); + throw new \LorisException($msg); + } } } -} /** * Validate whether the data submitted are valid against this instrument. @@ -3171,8 +3184,8 @@ private function _validateValues(array $instrumentData): void */ public function validate(array $dataToCheck): void { -error_log("========"); -error_log(print_r($dataToCheck,true)); + error_log("========"); + error_log(print_r($dataToCheck, true)); // data to check even exist if (empty($dataToCheck)) { throw new \LorisException("No data provided."); diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index 696a6f36e0d..bc6a0c67608 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -38,7 +38,6 @@ class NDB_BVL_Instrument_Test extends TestCase * The instrument (or instrument mock) being tested. * * @var \PHPUnit\Framework\MockObject\MockObject&\NDB_BVL_Instrument - * */ private $_instrument; @@ -1772,8 +1771,8 @@ public function testValidateMissingRequiredKeys(): void $this->expectExceptionMessageMatches( '/arthritis.*hypertension|hypertension.*arthritis/' ); - - // phan-suppress-next-line PhanUndeclaredMethod + + // phan-suppress-next-line PhanUndeclaredMethod $this->_instrument->validate( [ 'concussion_or_head_trauma' => null, From d5ca715437b65c9ac53de0daf0a0c77d6edfbb87 Mon Sep 17 00:00:00 2001 From: kongtiaowang Date: Fri, 27 Jun 2025 15:33:49 -0400 Subject: [PATCH 33/65] fix --- php/libraries/NDB_BVL_Instrument.class.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/php/libraries/NDB_BVL_Instrument.class.inc b/php/libraries/NDB_BVL_Instrument.class.inc index d0a77f25e8a..79e2712c71e 100644 --- a/php/libraries/NDB_BVL_Instrument.class.inc +++ b/php/libraries/NDB_BVL_Instrument.class.inc @@ -3150,7 +3150,7 @@ abstract class NDB_BVL_Instrument extends NDB_Page $expectedType instanceof \LORIS\Data\Types\TimeType => " (format: 'HH:mm:ss')", $expectedType instanceof \LORIS\Data\Types\Enumeration => - ", possible answers: '". + ", possible answers: '" .implode("','", $optionKeys ?? []) . "'", default => "" }; From c7086fd4c5bbda86ce6272d4b0d30641f992c177 Mon Sep 17 00:00:00 2001 From: kongtiaowang Date: Fri, 27 Jun 2025 15:41:13 -0400 Subject: [PATCH 34/65] fix --- test/unittests/NDB_BVL_Instrument_Test.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index bc6a0c67608..5d0ea3fe7e6 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -1751,6 +1751,8 @@ public function testValidateMissingRequiredKeys(): void "getDataDictionary" ] )->getMock(); + + /** @phan-var \NDB_BVL_Instrument $_instrument */ $this->_instrument->method('getDataDictionary')->willReturn( [ From 568391029b0b7d44b9fb4e9493e89a314d29e9c2 Mon Sep 17 00:00:00 2001 From: kongtiaowang Date: Fri, 27 Jun 2025 15:49:16 -0400 Subject: [PATCH 35/65] fix --- test/unittests/NDB_BVL_Instrument_Test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index 5d0ea3fe7e6..53be94c81f6 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -1751,7 +1751,7 @@ public function testValidateMissingRequiredKeys(): void "getDataDictionary" ] )->getMock(); - + /** @phan-var \NDB_BVL_Instrument $_instrument */ $this->_instrument->method('getDataDictionary')->willReturn( From e941923d3837ee174531e78edad6088354ef5795 Mon Sep 17 00:00:00 2001 From: kongtiaowang Date: Fri, 27 Jun 2025 15:54:21 -0400 Subject: [PATCH 36/65] fix var --- test/unittests/NDB_BVL_Instrument_Test.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index 53be94c81f6..86f98fb9410 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -1751,8 +1751,8 @@ public function testValidateMissingRequiredKeys(): void "getDataDictionary" ] )->getMock(); - - /** @phan-var \NDB_BVL_Instrument $_instrument */ + + '@phan-var \NDB_BVL_Instrument $_instrument'; $this->_instrument->method('getDataDictionary')->willReturn( [ From 21a97f2fd6337f0ad8b6a3a833d3121d9ed1bbc7 Mon Sep 17 00:00:00 2001 From: kongtiaowang Date: Fri, 27 Jun 2025 15:59:08 -0400 Subject: [PATCH 37/65] fix var --- test/unittests/NDB_BVL_Instrument_Test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index 86f98fb9410..1c0142b4fd7 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -1751,7 +1751,7 @@ public function testValidateMissingRequiredKeys(): void "getDataDictionary" ] )->getMock(); - + '@phan-var \NDB_BVL_Instrument $_instrument'; $this->_instrument->method('getDataDictionary')->willReturn( From 11b46231af12ad150b11e1b3f06ba35c153c09ea Mon Sep 17 00:00:00 2001 From: kongtiaowang Date: Fri, 27 Jun 2025 16:10:50 -0400 Subject: [PATCH 38/65] try --- test/unittests/NDB_BVL_Instrument_Test.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index 1c0142b4fd7..71cb54af29c 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -1727,7 +1727,6 @@ public function testValidateEmptyParameter(): void $this->expectExceptionMessage("No data provided."); $this->_instrument->validate([]); } - /** * Test that check validation method with received data. * Keys validation case - missing keys @@ -1742,7 +1741,7 @@ public function testValidateMissingRequiredKeys(): void $this->_setTableData(); $_SERVER['REQUEST_METHOD'] = 'POST'; - $this->_instrument = $this->getMockBuilder( + $instrument = $this->getMockBuilder( \NDB_BVL_Instrument::class )->disableOriginalConstructor() ->onlyMethods( @@ -1754,7 +1753,7 @@ public function testValidateMissingRequiredKeys(): void '@phan-var \NDB_BVL_Instrument $_instrument'; - $this->_instrument->method('getDataDictionary')->willReturn( + $instrument->method('getDataDictionary')->willReturn( [ (object)['fieldname' => 'arthritis'], (object)['fieldname' => 'hypertension'], @@ -1763,10 +1762,10 @@ public function testValidateMissingRequiredKeys(): void ); // Stub abstract methods so PHPUnit can instantiate the mock - $this->_instrument->method('getFullName')->willReturn('Test Instrument'); - $this->_instrument->method('getSubtestList')->willReturn([]); + $instrument->method('getFullName')->willReturn('Test Instrument'); + $instrument->method('getSubtestList')->willReturn([]); // phan-suppress-next-line PhanUndeclaredProperty - $this->_instrument->_requiredElements = ['arthritis', 'hypertension']; + $instrument->_requiredElements = ['arthritis', 'hypertension']; // Now call validate with data missing required fields $this->expectException(\LorisException::class); @@ -1775,7 +1774,7 @@ public function testValidateMissingRequiredKeys(): void ); // phan-suppress-next-line PhanUndeclaredMethod - $this->_instrument->validate( + $instrument->validate( [ 'concussion_or_head_trauma' => null, ] From 6840edf9d7bd1c1fc12cf5719e2c0ab807120197 Mon Sep 17 00:00:00 2001 From: kongtiaowang Date: Fri, 27 Jun 2025 16:35:04 -0400 Subject: [PATCH 39/65] try --- test/unittests/NDB_BVL_Instrument_Test.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index 71cb54af29c..db462fb7b47 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -1740,6 +1740,11 @@ public function testValidateMissingRequiredKeys(): void $this->_setUpMockDB(); $this->_setTableData(); $_SERVER['REQUEST_METHOD'] = 'POST'; + /** + * Phan-var + * + * @phan-var \NDB_BVL_Instrument $_instrument + */ $instrument = $this->getMockBuilder( \NDB_BVL_Instrument::class From 69eba3e8f238f26f16a19a476f34775a390e8337 Mon Sep 17 00:00:00 2001 From: kongtiaowang Date: Fri, 27 Jun 2025 16:45:59 -0400 Subject: [PATCH 40/65] try --- test/unittests/NDB_BVL_Instrument_Test.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index db462fb7b47..0ab4feaaf9d 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -1743,7 +1743,9 @@ public function testValidateMissingRequiredKeys(): void /** * Phan-var * - * @phan-var \NDB_BVL_Instrument $_instrument + * @phan-var \NDB_BVL_Instrument $instrument + * @phan-suppress PhanUndeclaredProperty + * @phan-suppress PhanUndeclaredMethod */ $instrument = $this->getMockBuilder( @@ -1756,8 +1758,6 @@ public function testValidateMissingRequiredKeys(): void ] )->getMock(); - '@phan-var \NDB_BVL_Instrument $_instrument'; - $instrument->method('getDataDictionary')->willReturn( [ (object)['fieldname' => 'arthritis'], From e0ff627ce5adc60ae3c3ef8aaf4d91fd991dd9cf Mon Sep 17 00:00:00 2001 From: kongtiaowang Date: Fri, 27 Jun 2025 17:01:15 -0400 Subject: [PATCH 41/65] try --- test/unittests/NDB_BVL_Instrument_Test.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index 0ab4feaaf9d..b22072be85c 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -1733,6 +1733,9 @@ public function testValidateEmptyParameter(): void * * @covers NDB_BVL_Instrument::validate * + * @phan-suppress PhanUndeclaredProperty + * @phan-suppress PhanUndeclaredMethod + * * @return void */ public function testValidateMissingRequiredKeys(): void @@ -1743,9 +1746,7 @@ public function testValidateMissingRequiredKeys(): void /** * Phan-var * - * @phan-var \NDB_BVL_Instrument $instrument - * @phan-suppress PhanUndeclaredProperty - * @phan-suppress PhanUndeclaredMethod + * @phan-var \NDB_BVL_Instrument $instrument */ $instrument = $this->getMockBuilder( From 1383e911ce521a9f6e1a29eec8c2051ed9b659e4 Mon Sep 17 00:00:00 2001 From: kongtiaowang Date: Fri, 27 Jun 2025 17:16:02 -0400 Subject: [PATCH 42/65] try --- test/unittests/NDB_BVL_Instrument_Test.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index b22072be85c..20f2195908d 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -13,6 +13,7 @@ namespace Loris\Tests; use \LORIS\Data\Scope; use \LORIS\Data\Cardinality; +use \LORIS\Config; set_include_path(get_include_path().":" . __DIR__ . "/../../php/libraries:"); use PHPUnit\Framework\TestCase; require_once __DIR__ . '/../../vendor/autoload.php'; @@ -1840,7 +1841,7 @@ public function testValidateAdditionalKeys(): void $this->expectExceptionMessageMatches( '/Additional field\(s\) not permitted:.*aaa,bbb/' ); - + // phan-suppress-next-line PhanUndeclaredMethod $this->_instrument->validate($instrumentQuestions); } @@ -1850,7 +1851,9 @@ public function testValidateAdditionalKeys(): void * * @covers NDB_BVL_Instrument::validate * - * @return void + * @phan-suppress PhanUndeclaredProperty + * @phan-suppress PhanUndeclaredMethod + * @return void */ public function testValidateValues(): void { @@ -1863,7 +1866,7 @@ public function testValidateValues(): void ->disableOriginalConstructor() ->onlyMethods(["getFullName", "getSubtestList", "getDataDictionary"]) ->getMock(); - + // phan-suppress-next-line PhanUndeclaredProperty $this->_instrument->_requiredElements = [ 'arthritis', 'hypertension', 'concussion_or_head_trauma' ]; From 8f536e67a435ee4bbb06c4ef7d635a990d9c8c24 Mon Sep 17 00:00:00 2001 From: kongtiaowang Date: Sat, 28 Jun 2025 09:24:22 -0400 Subject: [PATCH 43/65] done --- test/unittests/NDB_BVL_Instrument_Test.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index 20f2195908d..d0e100e3dfc 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -13,7 +13,6 @@ namespace Loris\Tests; use \LORIS\Data\Scope; use \LORIS\Data\Cardinality; -use \LORIS\Config; set_include_path(get_include_path().":" . __DIR__ . "/../../php/libraries:"); use PHPUnit\Framework\TestCase; require_once __DIR__ . '/../../vendor/autoload.php'; @@ -1907,8 +1906,9 @@ public function testValidateValues(): void $this->_instrument->table = 'medical_history'; // phan-suppress-next-line PhanUndeclaredProperty $this->_instrument->commentID = 'commentID1'; + '@phan-var \NDB_Config $mockConfig'; - $mockConfig = $this->createMock(\LORIS\Config::class); + $mockConfig = $this->createMock(\NDB_Config::class); $mockConfig->method('getSetting')->with( 'dateDisplayFormat' )->willReturn('Y-m-d'); From 562d36519a38e23fd3c6ed15bc2243752a6780dc Mon Sep 17 00:00:00 2001 From: kongtiaowang Date: Sat, 28 Jun 2025 09:34:36 -0400 Subject: [PATCH 44/65] done --- test/unittests/NDB_BVL_Instrument_Test.php | 1 - 1 file changed, 1 deletion(-) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index d0e100e3dfc..3e2d5571903 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -1906,7 +1906,6 @@ public function testValidateValues(): void $this->_instrument->table = 'medical_history'; // phan-suppress-next-line PhanUndeclaredProperty $this->_instrument->commentID = 'commentID1'; - '@phan-var \NDB_Config $mockConfig'; $mockConfig = $this->createMock(\NDB_Config::class); $mockConfig->method('getSetting')->with( From 2b0a09c171a682d733c3a0fd8a881c6707b1c2c7 Mon Sep 17 00:00:00 2001 From: kongtiaowang Date: Sat, 28 Jun 2025 10:00:39 -0400 Subject: [PATCH 45/65] done --- test/unittests/NDB_BVL_Instrument_Test.php | 59 ++++++++++++---------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index 3e2d5571903..e06b53344eb 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -1872,36 +1872,43 @@ public function testValidateValues(): void $instrumentQuestions = [ // p1 - "arthritis" => null, // required - "arthritis_age" => null, - "pulmonary_issues" => null, - "pulmonary_issues_specific" => null, + "arthritis" => 'string', // required + "arthritis_age" => 'string', + "pulmonary_issues" => 'string', + "pulmonary_issues_specific" => 'string', // p2 - "hypertension" => null, // required - "hypertension_while_pregnant" => null, - "hypertension_while_pregnant_age" => null, + "hypertension" => 'string', // required + "hypertension_while_pregnant" => 'string', + "hypertension_while_pregnant_age" => 'string', // p3 - "concussion_or_head_trauma" => null, // required - "concussion_1_description" => null, - "concussion_1_hospitalized" => null, - "concussion_1_age" => null, - "concussion_2_description" => null, - "concussion_2_hospitalized" => null, - "concussion_2_age" => null, - "concussion_3_description" => null, - "concussion_3_hospitalized" => null, - "concussion_3_age" => null, - "current_concussion_symptoms" => null, + "concussion_or_head_trauma" => 'string', // required + "concussion_1_description" => 'string', + "concussion_1_hospitalized" => 'string', + "concussion_1_age" => 'string', + "concussion_2_description" => 'string', + "concussion_2_hospitalized" => 'string', + "concussion_2_age" => 'string', + "concussion_3_description" => 'string', + "concussion_3_hospitalized" => 'string', + "concussion_3_age" => 'string', + "current_concussion_symptoms" => 'string', ]; - $this->_instrument->method('getDataDictionary')->willReturn( - array_map( - fn($field) => (object)[ - 'fieldname' => $field, 'getDataType' => fn() => 'text' - ], - array_keys($instrumentQuestions) - ) - ); +$this->_instrument->method('getDataDictionary')->willReturn( + array_map( + fn($field) => new class($field) { + public string $fieldname; + public function __construct($field) { + $this->fieldname = $field; + } + public function getDataType() { + return new \LORIS\Data\Types\StringType(); + +} + }, + array_keys($instrumentQuestions) + ) +); // phan-suppress-next-line PhanUndeclaredProperty $this->_instrument->table = 'medical_history'; // phan-suppress-next-line PhanUndeclaredProperty From 656ac7d1e206f55810c4c6a5bd22c82445cbf441 Mon Sep 17 00:00:00 2001 From: kongtiaowang Date: Sat, 28 Jun 2025 10:19:46 -0400 Subject: [PATCH 46/65] todo rm dubug --- test/unittests/NDB_BVL_Instrument_Test.php | 44 ++++++++++++++-------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index e06b53344eb..0277d2920b3 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -1894,21 +1894,35 @@ public function testValidateValues(): void "current_concussion_symptoms" => 'string', ]; -$this->_instrument->method('getDataDictionary')->willReturn( - array_map( - fn($field) => new class($field) { - public string $fieldname; - public function __construct($field) { - $this->fieldname = $field; - } - public function getDataType() { - return new \LORIS\Data\Types\StringType(); - -} - }, - array_keys($instrumentQuestions) - ) -); + $this->_instrument->method('getDataDictionary')->willReturn( + array_map( + fn($field) => new class($field) { + public string $fieldname; + /** + * Constructor. + * + * @param {string} $field field + * + * @return void + */ + public function __construct($field) + { + $this->fieldname = $field; + } + /** + * GetDataType. + * + * @return void + */ + public function getDataType() + { + return new \LORIS\Data\Types\StringType(); + + } + }, + array_keys($instrumentQuestions) + ) + ); // phan-suppress-next-line PhanUndeclaredProperty $this->_instrument->table = 'medical_history'; // phan-suppress-next-line PhanUndeclaredProperty From a40f7eefde2db1e1e845ec838d9946c8ad9f412c Mon Sep 17 00:00:00 2001 From: kongtiaowang Date: Sat, 28 Jun 2025 10:30:25 -0400 Subject: [PATCH 47/65] todo rm dubug --- test/unittests/NDB_BVL_Instrument_Test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index 0277d2920b3..3e693851579 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -1912,7 +1912,7 @@ public function __construct($field) /** * GetDataType. * - * @return void + * @return \LORIS\Data\Types\StringType */ public function getDataType() { From ed28579a05dd23b635bef6286b9849b3062ffb6e Mon Sep 17 00:00:00 2001 From: kongtiaowang Date: Wed, 2 Jul 2025 09:49:21 -0400 Subject: [PATCH 48/65] rm debug --- php/libraries/NDB_BVL_Instrument.class.inc | 7 ------- 1 file changed, 7 deletions(-) diff --git a/php/libraries/NDB_BVL_Instrument.class.inc b/php/libraries/NDB_BVL_Instrument.class.inc index 79e2712c71e..aa9ec0b6284 100644 --- a/php/libraries/NDB_BVL_Instrument.class.inc +++ b/php/libraries/NDB_BVL_Instrument.class.inc @@ -3028,11 +3028,6 @@ abstract class NDB_BVL_Instrument extends NDB_Page // Note: additional fields are not an issue with JSON instruments // but only with instruments having their own table. $additionalKeys = array_diff($dataKeys, $defaultDataKeys); - error_log(print_r("11111111111111", true)); - error_log(print_r($dataKeys, true)); - error_log(print_r($defaultDataKeys, true)); - error_log(print_r($additionalKeys, true)); - error_log(print_r("22222222222222", true)); if (!empty($additionalKeys)) { $i = implode(",", $additionalKeys); @@ -3184,8 +3179,6 @@ abstract class NDB_BVL_Instrument extends NDB_Page */ public function validate(array $dataToCheck): void { - error_log("========"); - error_log(print_r($dataToCheck, true)); // data to check even exist if (empty($dataToCheck)) { throw new \LorisException("No data provided."); From 4f4635f6359f6044a41335b20bb39d8de115daca Mon Sep 17 00:00:00 2001 From: regisoc Date: Wed, 9 Jul 2025 11:14:17 -0400 Subject: [PATCH 49/65] handle PUT instrument flag endpoint - remove data_entry complete check --- .../endpoints/candidate/visit/instrument/flags.class.inc | 6 ------ 1 file changed, 6 deletions(-) diff --git a/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc b/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc index 2152fd145b9..7f1ca946c87 100644 --- a/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc +++ b/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc @@ -156,12 +156,6 @@ class Flags extends Endpoint implements \LORIS\Middleware\ETagCalculator $this->_instrumentStatus = new \NDB_BVL_InstrumentStatus($loris); $this->_instrumentStatus->select($this->_instrument->commentID); - if (!$this->_instrument->determineDataEntryAllowed()) { - return new \LORIS\Http\Response\JSON\Forbidden( - 'Can not update instruments that are flagged as complete.' - ); - } - $data = json_decode((string) $request->getBody(), true); if (!is_array($data)) { return new \LORIS\Http\Response\JSON\BadRequest( From b4d4ccc64457ef8f16f0a40d7c121acf562119fd Mon Sep 17 00:00:00 2001 From: regisoc Date: Wed, 9 Jul 2025 11:14:30 -0400 Subject: [PATCH 50/65] handle PATCH instrument flag endpoint - remove data_entry complete check --- .../endpoints/candidate/visit/instrument/flags.class.inc | 6 ------ 1 file changed, 6 deletions(-) diff --git a/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc b/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc index 7f1ca946c87..73bf56ae392 100644 --- a/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc +++ b/modules/api/php/endpoints/candidate/visit/instrument/flags.class.inc @@ -221,12 +221,6 @@ class Flags extends Endpoint implements \LORIS\Middleware\ETagCalculator $this->_instrumentStatus = new \NDB_BVL_InstrumentStatus($loris); $this->_instrumentStatus->select($this->_instrument->commentID); - if (!$this->_instrument->determineDataEntryAllowed()) { - return new \LORIS\Http\Response\JSON\Forbidden( - 'Can not update instruments that are flagged as complete.' - ); - } - $data = json_decode((string) $request->getBody(), true); if (!is_array($data)) { return new \LORIS\Http\Response\JSON\BadRequest( From a1a4f5dc900582dac1b556973bcbe9af075dabbe Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 24 Jul 2025 11:54:22 -0400 Subject: [PATCH 51/65] fusion instrument PATCH/PUT --- .../visit/instrument/instrument.class.inc | 88 ++++++++----------- 1 file changed, 39 insertions(+), 49 deletions(-) diff --git a/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc b/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc index 8fa96149346..5b7c3bf0532 100644 --- a/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc +++ b/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc @@ -170,51 +170,7 @@ class Instrument extends Endpoint implements \LORIS\Middleware\ETagCalculator */ private function _handlePUT(ServerRequestInterface $request) : ResponseInterface { - if (!$this->_instrument->determineDataEntryAllowed()) { - return new \LORIS\Http\Response\JSON\Forbidden( - 'Can not update instruments that are flagged as complete.' - ); - } - - $data = json_decode((string) $request->getBody(), true); - if (!is_array($data)) { - return new \LORIS\Http\Response\JSON\BadRequest( - 'Invalid request' - ); - } - - // validate given data against the instrument - try { - $instrumentName = $this->_instrument->testName; - $this->_instrument->validate($data[$instrumentName] ?? []); - } catch (\LorisException $th) { - return new \LORIS\Http\Response\JSON\BadRequest( - "Could not update. {$th->getMessage()}" - ); - } catch (\Throwable $th) { - error_log($th->getMessage()); - return new \LORIS\Http\Response\JSON\Forbidden( - "Could not update." - ); - } - - // update values - try { - $this->_instrument->clearInstrument(); - $version = $request->getAttribute('LORIS-API-Version'); - if ($version == 'v0.0.3') { - $instrumentname = $this->_instrument->testName; - $this->_instrument->_saveValues($data[$instrumentname]); - } else { - $this->_instrument->_saveValues($data['Data']); - } - $this->_instrument->score(); - $this->_instrument->updateRequiredElementsCompletedFlag(); - } catch (\Throwable $e) { - error_log($e->getMessage()); - return new \LORIS\Http\Response\JSON\InternalServerError(); - } - return (new \LORIS\Http\Response\JSON\NoContent()); + return $this->_handlePATCHAndPUT($request); } /** @@ -227,14 +183,39 @@ class Instrument extends Endpoint implements \LORIS\Middleware\ETagCalculator */ private function _handlePATCH(ServerRequestInterface $request): ResponseInterface { + return $this->_handlePATCHAndPUT($request); + } + + /** + * Handles a PUT and PATCH request. + * Mainly serves as a quick PATCH/PUT selection to validate all (PUT) or + * part (PATCH) of the sent payload. + * + * @param ServerRequestInterface $request The incoming PSR7 request + * + * @return ResponseInterface The outgoing PSR7 response + */ + private function _handlePATCHAndPUT( + ServerRequestInterface $request + ): ResponseInterface { + // request type + $requestType = $request->getMethod(); + if ($requestType !== "PATCH" && $requestType !== "PUT") { + return new \LORIS\Http\Response\JSON\BadRequest( + 'Invalid request' + ); + } + $isRequestPUT = $requestType === "PUT"; + + // data entry allowed? if (!$this->_instrument->determineDataEntryAllowed()) { return new \LORIS\Http\Response\JSON\Forbidden( 'Can not update instruments that are flagged as complete.' ); } + // get payload $data = json_decode((string) $request->getBody(), true); - if (!is_array($data)) { return new \LORIS\Http\Response\JSON\BadRequest( 'Invalid request' @@ -252,8 +233,10 @@ class Instrument extends Endpoint implements \LORIS\Middleware\ETagCalculator : ($data['Data'] ?? []); // validate - - $this->_instrument->validate($instrumentData); + $this->_instrument->validate( + $instrumentData, + $isRequestPUT + ); } catch (\LorisException $th) { return new \LORIS\Http\Response\JSON\BadRequest( "Could not update. {$th->getMessage()}" @@ -265,7 +248,14 @@ class Instrument extends Endpoint implements \LORIS\Middleware\ETagCalculator ); } + // update values try { + // only clear if PUT + if ($isRequestPUT) { + $this->_instrument->clearInstrument(); + } + + // save/score data $this->_instrument->_saveValues($instrumentData); $this->_instrument->score(); $this->_instrument->updateRequiredElementsCompletedFlag(); @@ -273,7 +263,7 @@ class Instrument extends Endpoint implements \LORIS\Middleware\ETagCalculator error_log($e->getMessage()); return new \LORIS\Http\Response\JSON\InternalServerError(); } - return (new \LORIS\Http\Response\JSON\NoContent()); + return new \LORIS\Http\Response\JSON\NoContent(); } /** From 08efd43ada705055687c0411587daf01f69d4d97 Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 24 Jul 2025 11:55:37 -0400 Subject: [PATCH 52/65] instrument validate to check all or part of the payload values --- php/libraries/NDB_BVL_Instrument.class.inc | 27 ++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/php/libraries/NDB_BVL_Instrument.class.inc b/php/libraries/NDB_BVL_Instrument.class.inc index aa9ec0b6284..c4bcb015fa4 100644 --- a/php/libraries/NDB_BVL_Instrument.class.inc +++ b/php/libraries/NDB_BVL_Instrument.class.inc @@ -3019,7 +3019,7 @@ abstract class NDB_BVL_Instrument extends NDB_Page // missing required fields $missingKeys = array_diff($defaultRequiredElements, $dataKeys); - if (!empty($missingKeys) && $_SERVER['REQUEST_METHOD'] !== 'PATCH') { + if (!empty($missingKeys)) { $i = implode(",", $missingKeys); throw new \LorisException("Missing required field(s): {$i}."); } @@ -3028,7 +3028,6 @@ abstract class NDB_BVL_Instrument extends NDB_Page // Note: additional fields are not an issue with JSON instruments // but only with instruments having their own table. $additionalKeys = array_diff($dataKeys, $defaultDataKeys); - if (!empty($additionalKeys)) { $i = implode(",", $additionalKeys); throw new \LorisException("Additional field(s) not permitted: {$i}."); @@ -3040,13 +3039,18 @@ abstract class NDB_BVL_Instrument extends NDB_Page * Throw a LorisException if any check does not pass. * * @param array $instrumentData instrument data to check + * @param bool $checkAllValues tells if all instrument field values should + * be checked (PUT) instead of part of them + * (PATCH) * * @throws \LorisException * * @return void */ - private function _validateValues(array $instrumentData): void - { + private function _validateValues( + array $instrumentData, + bool $checkAllValues = false + ): void { // Get date format from system configuration $config = $this->loris->getConfiguration(); $dateFormat = $config->getSetting('dateDisplayFormat'); @@ -3081,7 +3085,7 @@ abstract class NDB_BVL_Instrument extends NDB_Page $fieldName = $dictionaryItem->fieldname; // Skip fields that are not present in PATCH input - if (!array_key_exists($fieldName, $instrumentData)) { + if (!$checkAllValues && !array_key_exists($fieldName, $instrumentData)) { continue; } @@ -3171,14 +3175,19 @@ abstract class NDB_BVL_Instrument extends NDB_Page * Validate whether the data submitted are valid against this instrument. * Checks both keys and values. Throws a LorisException if any test fails. * - * @param array $dataToCheck an array of values submitted + * @param array $dataToCheck an array of values submitted + * @param bool $checkAllValues tells if all instrument field values should + * be checked (PUT) instead of part of them + * (PATCH) * * @throws \LorisException * * @return void */ - public function validate(array $dataToCheck): void - { + public function validate( + array $dataToCheck, + bool $checkAllValues = false + ): void { // data to check even exist if (empty($dataToCheck)) { throw new \LorisException("No data provided."); @@ -3188,7 +3197,7 @@ abstract class NDB_BVL_Instrument extends NDB_Page $this->_validateKeys($dataToCheck); // validate the values against the instrument dictionary entries - $this->_validateValues($dataToCheck); + $this->_validateValues($dataToCheck, $checkAllValues); } /** From 2a3002d5a34abfdaf7eff2e9e31cec51718cf3ff Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 24 Jul 2025 13:27:22 -0400 Subject: [PATCH 53/65] instrument put test update --- raisinbread/test/api/LorisApiInstruments_v0_0_3_Test.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/raisinbread/test/api/LorisApiInstruments_v0_0_3_Test.php b/raisinbread/test/api/LorisApiInstruments_v0_0_3_Test.php index 95638214d3e..4df079e5bbf 100644 --- a/raisinbread/test/api/LorisApiInstruments_v0_0_3_Test.php +++ b/raisinbread/test/api/LorisApiInstruments_v0_0_3_Test.php @@ -164,7 +164,7 @@ public function testPutCandidatesCandidVisitInstrumentsInstrument(): void { $json = [ $this->instrumentTest => [ - 'consent' => "yes", + 'consent' => "yes", 'testText' => 'test', 'testCheckbox' => 'true' ] @@ -422,7 +422,9 @@ public function testPutCandidatesCandidVisitInstrumentsInstrumentDde(): void 'Instrument' => $this->instrumentTest ], $this->instrumentTest => [ - 'testText' => "test Text" + 'consent' => "yes", + 'testText' => 'test text', + 'testCheckbox' => 'true' ] ]; $response = $this->client->request( From eed39e38e1a953c66e4ee98605f1d16bee7675e9 Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 24 Jul 2025 15:21:49 -0400 Subject: [PATCH 54/65] clear userid to var --- raisinbread/test/api/LorisApiInstrumentsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/raisinbread/test/api/LorisApiInstrumentsTest.php b/raisinbread/test/api/LorisApiInstrumentsTest.php index dfe06ed3a3c..016eee9d998 100644 --- a/raisinbread/test/api/LorisApiInstrumentsTest.php +++ b/raisinbread/test/api/LorisApiInstrumentsTest.php @@ -224,7 +224,7 @@ public function testPatchCandidatesCandidVisitInstrumentsInstrument(): void ]; $response = $this->client->request( 'PATCH', - "candidates/587630/$this->visitTest/instruments/bmi", + "candidates/$this->candidTest/$this->visitTest/instruments/$this->instrumentTest", [ 'headers' => $this->headers, 'json' => $json From 72539acbda32cb4fc418c1ed643f7783b91d5287 Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 24 Jul 2025 15:22:33 -0400 Subject: [PATCH 55/65] put instrument --- .../test/api/LorisApiInstrumentsTest.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/raisinbread/test/api/LorisApiInstrumentsTest.php b/raisinbread/test/api/LorisApiInstrumentsTest.php index 016eee9d998..983ecd95e47 100644 --- a/raisinbread/test/api/LorisApiInstrumentsTest.php +++ b/raisinbread/test/api/LorisApiInstrumentsTest.php @@ -237,6 +237,34 @@ public function testPatchCandidatesCandidVisitInstrumentsInstrument(): void $this->assertEmpty($body); } + /** + * Tests the HTTP PUT request for the + * endpoint /candidates/{candid}/{visit}/instruments/{instrument} + * + * @return void + */ + public function testPutCandidatesCandidVisitInstrumentsInstrument(): void + { + $json = [ + 'Data' => [ + 'height_cms' => 2 + ] + ]; + $response = $this->client->request( + 'PUT', + "candidates/$this->candidTest/$this->visitTest/instruments/$this->instrumentTest", + [ + 'headers' => $this->headers, + 'json' => $json + ] + ); + $this->assertEquals(204, $response->getStatusCode()); + // Verify the endpoint has a body + $body = $response->getBody(); + $this->assertNotEmpty($body); + + } + /** * Tests the HTTP GET request for the * endpoint /candidates/{candid}/{visit}/instruments/{instruments}/flags From 95631ca13a3e17b43041056017555514d3740a5b Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 24 Jul 2025 15:22:46 -0400 Subject: [PATCH 56/65] trail end --- raisinbread/test/api/LorisApiInstrumentsTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/raisinbread/test/api/LorisApiInstrumentsTest.php b/raisinbread/test/api/LorisApiInstrumentsTest.php index 983ecd95e47..146fd2f3dd2 100644 --- a/raisinbread/test/api/LorisApiInstrumentsTest.php +++ b/raisinbread/test/api/LorisApiInstrumentsTest.php @@ -312,7 +312,7 @@ public function testPatchCandidatesCandidVisitInstrumentsInstrumentFlags(): void 'Flags' => [ 'Data_entry' => 'Complete', 'Administration' => 'All', - 'Validity' => 'Invalid' + 'Validity' => 'Invalid' ] ]; $response = $this->client->request( @@ -387,7 +387,7 @@ public function testPutCandidatesCandidVisitInstrumentsInstrumentFlags(): void 'Flags' => [ 'Data_entry' => 'Complete', 'Administration' => 'Partial', - 'Validity' => 'Questionable' + 'Validity' => 'Questionable' ] ]; $response = $this->client->request( From e7010039d2cb6e902006159506cf91b5d378f59a Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 24 Jul 2025 15:23:02 -0400 Subject: [PATCH 57/65] clear userid to var --- raisinbread/test/api/LorisApiInstrumentsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/raisinbread/test/api/LorisApiInstrumentsTest.php b/raisinbread/test/api/LorisApiInstrumentsTest.php index 146fd2f3dd2..d1ebf69d4c2 100644 --- a/raisinbread/test/api/LorisApiInstrumentsTest.php +++ b/raisinbread/test/api/LorisApiInstrumentsTest.php @@ -475,7 +475,7 @@ public function testPatchCandidatesCandidVisitInstrumentsInstrumentDde(): void ]; $response = $this->client->request( 'PATCH', - "candidates/587630/$this->visitTest/instruments/bmi/dde", + "candidates/$this->candidTest/$this->visitTest/instruments/$this->instrumentTest/bmi/dde", [ 'headers' => $this->headers, 'json' => $json From bf1f386bd588466d7288743dc89aa9c7ec3251e3 Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 24 Jul 2025 15:23:32 -0400 Subject: [PATCH 58/65] put/patch dde flag --- .../test/api/LorisApiInstrumentsTest.php | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/raisinbread/test/api/LorisApiInstrumentsTest.php b/raisinbread/test/api/LorisApiInstrumentsTest.php index d1ebf69d4c2..f90731c6867 100644 --- a/raisinbread/test/api/LorisApiInstrumentsTest.php +++ b/raisinbread/test/api/LorisApiInstrumentsTest.php @@ -517,4 +517,116 @@ public function testGetCandidatesCandidVisitInstrumentsInstrumentDdeFlags(): ); } + /** + * Tests the HTTP PATCH request for the + * endpoint /candidates/{candid}/{visit}/instruments/{instrument}/flag + * + * @return void + */ + public function testPatchCandidVisitInstrumentsInstrumentDdeFlags(): void + { + $json = [ + 'Meta' => [ + 'Candidate' => $this->candidTest, + 'Visit' => $this->visitTest, + 'DDE' => false, + 'Instrument' => $this->instrumentTest + ], + 'Flags' => [ + 'Data_entry' => 'Complete', + 'Administration' => 'All', + 'Validity' => 'Valid' + ] + ]; + $response = $this->client->request( + 'PATCH', + "candidates/$this->candidTest/$this->visitTest/instruments/$this->instrumentTest/dde/flags", + [ + 'headers' => $this->headers, + 'json' => $json + ] + ); + $this->assertEquals(204, $response->getStatusCode()); + // Verify the endpoint has a body + $body = $response->getBody(); + $this->assertNotEmpty($body); + + // This will test that it should be forbidden to modify an instrument that is flagged as Complete + $json = [ + $this->instrumentTest => [ + 'UserID' => "2" + ] + ]; + $response = $this->client->request( + 'PATCH', + "candidates/$this->candidTest/$this->visitTest/instruments/$this->instrumentTest/dde", + [ + 'headers' => $this->headers, + 'json' => $json, + 'http_errors' => false + ] + ); + $this->assertEquals(403, $response->getStatusCode()); + // Verify the endpoint has a body + $body = $response->getBody(); + $this->assertNotEmpty($body); + } + + /** + * Tests the HTTP PUT request for the + * endpoint /candidates/{candid}/{visit}/instruments/{instrument} + * + * @return void + */ + public function testPutCandidVisitInstrumentsInstrumentDdeFlags(): void + { + $json = [ + 'Meta' => [ + 'Candidate' => $this->candidTest, + 'Visit' => $this->visitTest, + 'DDE' => false, + 'Instrument' => $this->instrumentTest + ], + 'Flags' => [ + 'Data_entry' => 'Complete', + 'Administration' => 'All', + 'Validity' => 'Valid' + ] + ]; + $response = $this->client->request( + 'PUT', + "candidates/$this->candidTest/$this->visitTest/instruments/$this->instrumentTest/dde/flags", + [ + 'headers' => $this->headers, + 'json' => $json + ] + ); + $this->assertEquals(204, $response->getStatusCode()); + // Verify the endpoint has a body + $body = $response->getBody(); + $this->assertNotEmpty($body); + + // This will test that it should be forbidden to modify an instrument that is flagged as Complete + $json = [ + $this->instrumentTest => [ + 'UserID' => "2" + ] + ]; + $response = $this->client->request( + 'PUT', + "candidates/$this->candidTest/$this->visitTest/instruments/$this->instrumentTest/dde", + [ + 'headers' => $this->headers, + 'json' => $json, + 'http_errors' => false + ] + ); + $this->assertEquals(403, $response->getStatusCode()); + // Verify the endpoint has a body + $body = $response->getBody(); + $this->assertNotEmpty($body); + + + } + } From 90523ecd111b4951259e4cf995864d4f744ca2da Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 24 Jul 2025 15:41:04 -0400 Subject: [PATCH 59/65] remove bmi ref --- raisinbread/test/api/LorisApiInstrumentsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/raisinbread/test/api/LorisApiInstrumentsTest.php b/raisinbread/test/api/LorisApiInstrumentsTest.php index f90731c6867..e0153805a5c 100644 --- a/raisinbread/test/api/LorisApiInstrumentsTest.php +++ b/raisinbread/test/api/LorisApiInstrumentsTest.php @@ -475,7 +475,7 @@ public function testPatchCandidatesCandidVisitInstrumentsInstrumentDde(): void ]; $response = $this->client->request( 'PATCH', - "candidates/$this->candidTest/$this->visitTest/instruments/$this->instrumentTest/bmi/dde", + "candidates/$this->candidTest/$this->visitTest/instruments/$this->instrumentTest/dde", [ 'headers' => $this->headers, 'json' => $json From 8fcf94d00ebbe93a3358deb0f0652d456dbcbd78 Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 24 Jul 2025 15:41:33 -0400 Subject: [PATCH 60/65] move data from bmi to testtest instrument --- raisinbread/test/api/LorisApiInstrumentsTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/raisinbread/test/api/LorisApiInstrumentsTest.php b/raisinbread/test/api/LorisApiInstrumentsTest.php index e0153805a5c..731f6156dd5 100644 --- a/raisinbread/test/api/LorisApiInstrumentsTest.php +++ b/raisinbread/test/api/LorisApiInstrumentsTest.php @@ -219,7 +219,7 @@ public function testPatchCandidatesCandidVisitInstrumentsInstrument(): void { $json = [ 'Data' => [ - 'height_cms' => 2 + 'UserID' => "2" ] ]; $response = $this->client->request( @@ -247,7 +247,7 @@ public function testPutCandidatesCandidVisitInstrumentsInstrument(): void { $json = [ 'Data' => [ - 'height_cms' => 2 + 'UserID' => "2" ] ]; $response = $this->client->request( @@ -470,7 +470,7 @@ public function testPatchCandidatesCandidVisitInstrumentsInstrumentDde(): void 'Instrument' => $this->instrumentTest ], 'Data' => [ - 'height_cms' => 2 + 'UserID' => "2" ] ]; $response = $this->client->request( From 0eb9414bb16f97866986943b6940b2d34726eaa3 Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 24 Jul 2025 16:18:13 -0400 Subject: [PATCH 61/65] v0.0.4 api put/patch update test --- raisinbread/test/api/LorisApiInstrumentsTest.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/raisinbread/test/api/LorisApiInstrumentsTest.php b/raisinbread/test/api/LorisApiInstrumentsTest.php index 731f6156dd5..63dd858fa77 100644 --- a/raisinbread/test/api/LorisApiInstrumentsTest.php +++ b/raisinbread/test/api/LorisApiInstrumentsTest.php @@ -219,7 +219,9 @@ public function testPatchCandidatesCandidVisitInstrumentsInstrument(): void { $json = [ 'Data' => [ - 'UserID' => "2" + 'consent' => "yes", + 'testText' => 'test text', + 'testCheckbox' => 'true' ] ]; $response = $this->client->request( @@ -247,7 +249,9 @@ public function testPutCandidatesCandidVisitInstrumentsInstrument(): void { $json = [ 'Data' => [ - 'UserID' => "2" + 'consent' => "yes", + 'testText' => 'test text', + 'testCheckbox' => 'true' ] ]; $response = $this->client->request( @@ -470,7 +474,7 @@ public function testPatchCandidatesCandidVisitInstrumentsInstrumentDde(): void 'Instrument' => $this->instrumentTest ], 'Data' => [ - 'UserID' => "2" + 'conset' => "yes" ] ]; $response = $this->client->request( From d8ad01350aaa8b5cd83d844c4d953e6577c56ac3 Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 24 Jul 2025 16:25:54 -0400 Subject: [PATCH 62/65] v0.0.4 api put/patch update test --- raisinbread/test/api/LorisApiInstrumentsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/raisinbread/test/api/LorisApiInstrumentsTest.php b/raisinbread/test/api/LorisApiInstrumentsTest.php index 63dd858fa77..a547b54240e 100644 --- a/raisinbread/test/api/LorisApiInstrumentsTest.php +++ b/raisinbread/test/api/LorisApiInstrumentsTest.php @@ -474,7 +474,7 @@ public function testPatchCandidatesCandidVisitInstrumentsInstrumentDde(): void 'Instrument' => $this->instrumentTest ], 'Data' => [ - 'conset' => "yes" + 'consent' => "yes" ] ]; $response = $this->client->request( From 6a64fd3640deb6f70d12fe7098f6511f1bf03fb1 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 3 Oct 2025 08:43:51 -0400 Subject: [PATCH 63/65] remove server/request post mention --- test/unittests/NDB_BVL_Instrument_Test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unittests/NDB_BVL_Instrument_Test.php b/test/unittests/NDB_BVL_Instrument_Test.php index 3e693851579..c400592aa90 100644 --- a/test/unittests/NDB_BVL_Instrument_Test.php +++ b/test/unittests/NDB_BVL_Instrument_Test.php @@ -1742,7 +1742,7 @@ public function testValidateMissingRequiredKeys(): void { $this->_setUpMockDB(); $this->_setTableData(); - $_SERVER['REQUEST_METHOD'] = 'POST'; + /** * Phan-var * From 004f42828584b74f97a938a31da0034f0051ef99 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 3 Oct 2025 10:12:43 -0400 Subject: [PATCH 64/65] including github action fix from #10034 --- .github/workflows/loristest.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/loristest.yml b/.github/workflows/loristest.yml index c45e52c7870..03f9a24cfa1 100644 --- a/.github/workflows/loristest.yml +++ b/.github/workflows/loristest.yml @@ -22,6 +22,7 @@ jobs: # (see https://github.com/actions/runner-images/blob/releases/ubuntu22/20230305/images/linux/Ubuntu2204-Readme.md) # that updated the list of installed apt packages/apt repositories. That issue may disappear in future Ubuntu images. run: | + sudo apt-get update sudo apt install -y imagemagick-6-common libmagickcore-6.q16-7t64 libmagickwand-6.q16-7t64 \ libprotobuf-dev libprotobuf32t64 libprotoc32t64 protobuf-compiler cd modules/electrophysiology_browser/jsx/react-series-data-viewer/ @@ -268,6 +269,7 @@ jobs: # (see https://github.com/actions/runner-images/blob/releases/ubuntu22/20230305/images/linux/Ubuntu2204-Readme.md) # that updated the list of installed apt packages/apt repositories. That issue may disappear in future Ubuntu images. run: | + sudo apt-get update sudo apt install -y imagemagick-6-common libmagickcore-6.q16-7t64 libmagickwand-6.q16-7t64 \ libprotobuf-dev libprotobuf32t64 libprotoc32t64 protobuf-compiler cd modules/electrophysiology_browser/jsx/react-series-data-viewer/ From 0ba405377d4925446c7ed3846b5217c5d60e2268 Mon Sep 17 00:00:00 2001 From: regisoc Date: Tue, 16 Dec 2025 17:17:23 -0500 Subject: [PATCH 65/65] trigger --- .../endpoints/candidate/visit/instrument/instrument.class.inc | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc b/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc index 5b7c3bf0532..b7d8770d741 100644 --- a/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc +++ b/modules/api/php/endpoints/candidate/visit/instrument/instrument.class.inc @@ -198,7 +198,6 @@ class Instrument extends Endpoint implements \LORIS\Middleware\ETagCalculator private function _handlePATCHAndPUT( ServerRequestInterface $request ): ResponseInterface { - // request type $requestType = $request->getMethod(); if ($requestType !== "PATCH" && $requestType !== "PUT") { return new \LORIS\Http\Response\JSON\BadRequest(