diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c2916b8..56a4b9a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [2.125.3](https://github.com/open-horizon/exchange-api/pull/730) - 2024-11-04 +- issue 594: Rework unit-test for ApiUtilsSuite. +- Split ApiUtilsSuite in two tests: TestApiUtilsTime and TestNodeAgbotTokenValidation. +- Extend number of tests for TestApiUtilsTime and TestNodeAgbotTokenValidation. +- Add validation for invalid time format. + ## [2.125.2](https://github.com/open-horizon/exchange-api/pull/726) - 2024-10-31 - issue 607: Rework unit-test for version. - Increased the number of test cases to cover corner cases for Version. diff --git a/src/main/resources/version.txt b/src/main/resources/version.txt index 394b36fa..01a32c8b 100644 --- a/src/main/resources/version.txt +++ b/src/main/resources/version.txt @@ -1 +1 @@ -2.125.2 +2.125.3 diff --git a/src/main/scala/org/openhorizon/exchangeapi/utility/ApiTime.scala b/src/main/scala/org/openhorizon/exchangeapi/utility/ApiTime.scala index e6a4cb56..be330481 100644 --- a/src/main/scala/org/openhorizon/exchangeapi/utility/ApiTime.scala +++ b/src/main/scala/org/openhorizon/exchangeapi/utility/ApiTime.scala @@ -1,4 +1,5 @@ package org.openhorizon.exchangeapi.utility +import java.time.format.DateTimeParseException import java.time.{Instant, ZoneId, ZonedDateTime} @@ -48,7 +49,21 @@ object ApiTime { (nowSeconds - thenTime >= secondsStale) } + /** Validates time format */ + def isValidTimeFormat(time: String): Boolean = { + try { + ZonedDateTime.parse(time) + true + } catch { + case _: DateTimeParseException => false + case _: Exception => false + } + } + def fixFormatting(time: String): String = { + if (time.isEmpty || !isValidTimeFormat(time)) { + return "Invalid format" + } val timeLength: Int = time.length /* This implementation uses length of the string instead of a regex to make it as fast as possible diff --git a/src/test/scala/org/openhorizon/exchangeapi/ApiUtilsSuite.scala b/src/test/scala/org/openhorizon/exchangeapi/ApiUtilsSuite.scala deleted file mode 100644 index f4cd4826..00000000 --- a/src/test/scala/org/openhorizon/exchangeapi/ApiUtilsSuite.scala +++ /dev/null @@ -1,80 +0,0 @@ -package org.openhorizon.exchangeapi - -import org.junit.runner.RunWith -import org.openhorizon.exchangeapi.utility.{ApiTime, NodeAgbotTokenValidation} -import org.scalatest.funsuite.AnyFunSuite -import org.scalatestplus.junit.JUnitRunner - -@RunWith(classOf[JUnitRunner]) -class ApiUtilsSuite extends AnyFunSuite{ - - test("ApiTime fixFormatting no milliseconds"){ - val timeNoMilliseconds = "2019-06-17T21:24:55Z[UTC]" - info(ApiTime.fixFormatting(timeNoMilliseconds) + " , " + "2019-06-17T21:24:55.000Z[UTC]") - assert(ApiTime.fixFormatting(timeNoMilliseconds) == "2019-06-17T21:24:55.000Z[UTC]") - } - - test("ApiTime fixFormatting no seconds and nomilliseconds"){ - val timeNoSeconds = "2019-06-17T21:24Z[UTC]" - info(ApiTime.fixFormatting(timeNoSeconds) + " , " + "2019-06-17T21:24:00.000Z[UTC]") - assert(ApiTime.fixFormatting(timeNoSeconds) == "2019-06-17T21:24:00.000Z[UTC]") - } - - test("ApiTime.nowUTC is always right length"){ - info(ApiTime.nowUTC) - assert(ApiTime.nowUTC.length >= 29) - } - - test("ApiTime.thenUTC is always right time and length"){ - info(ApiTime.thenUTC(1615406509)+ " , 2021-03-10T20:01:49.000Z[UTC]") - assert(ApiTime.thenUTC(1615406509) == "2021-03-10T20:01:49.000Z[UTC]") - assert(ApiTime.thenUTC(1615406509).length >= 29) - } - - test("ApiTime.thenUTC is always right time and length test 2"){ - info(ApiTime.thenUTC(1615406355)+ " , 2021-03-10T19:59:15.000Z[UTC]") - assert(ApiTime.thenUTC(1615406355) == "2021-03-10T19:59:15.000Z[UTC]") - assert(ApiTime.thenUTC(1615406355).length >= 29) - } - - test("ApiTime.pastUTC is always right length"){ - info(ApiTime.pastUTC(10)) - assert(ApiTime.pastUTC(10).length >= 29) - } - - test("ApiTime.futureUTC is always right length"){ - info(ApiTime.futureUTC(10)) - assert(ApiTime.futureUTC(10).length >= 29) - } - - test("ApiTime.beginningUTC is always correct and right length"){ - info(ApiTime.beginningUTC + " , " +"1970-01-01T00:00:00.000Z[UTC]") - assert(ApiTime.beginningUTC == "1970-01-01T00:00:00.000Z[UTC]") - assert(ApiTime.beginningUTC.length >= 29) - } - - test("NodeAgbotTokenValidation.isValid correctly identifies if password is too short, <15 chars"){ - info("NodeAgbotTokenValidation.isValid(\"1Abbb\")"+ " , " + NodeAgbotTokenValidation.isValid("1Abbb").toString()) - assert(NodeAgbotTokenValidation.isValid("1Abbb") == false) - } - - test("NodeAgbotTokenValidation.isValid correctly identifies if password does not contain digit"){ - info("NodeAgbotTokenValidation.isValid(\"aaaaaaaaaaaaaaaB\")"+ " , " + NodeAgbotTokenValidation.isValid("aaaaaaaaaaaaaaaB").toString()) - assert(NodeAgbotTokenValidation.isValid("aaaaaaaaaaaaaaaB") == false) - } - - test("NodeAgbotTokenValidation.isValid correctly identifies if password does not contain uppercase letter"){ - info("NodeAgbotTokenValidation.isValid(\"aaaaaaaaaaaaaaa1\")"+ " , " + NodeAgbotTokenValidation.isValid("aaaaaaaaaaaaaaa1").toString()) - assert(NodeAgbotTokenValidation.isValid("aaaaaaaaaaaaaaa1") == false) - } - - test("NodeAgbotTokenValidation.isValid correctly identifies if password does not contain lowercase letter"){ - info("NodeAgbotTokenValidation.isValid(\"AAAAAAAAAAAAAB1\")"+ " , " + NodeAgbotTokenValidation.isValid("AAAAAAAAAAAAAB1").toString()) - assert(NodeAgbotTokenValidation.isValid("AAAAAAAAAAAAAB1") == false) - } - - test("NodeAgbotTokenValidation.isValid correctly identifies valid password"){ - info("NodeAgbotTokenValidation.isValid(\"AAAAAAAAAAAAaB1\")"+ " , " + NodeAgbotTokenValidation.isValid("AAAAAAAAAAAAaB1").toString()) - assert(NodeAgbotTokenValidation.isValid("AAAAAAAAAAAAaB1") == true) - } -} diff --git a/src/test/scala/org/openhorizon/exchangeapi/utility/apiutils/TestApiUtilsTime.scala b/src/test/scala/org/openhorizon/exchangeapi/utility/apiutils/TestApiUtilsTime.scala new file mode 100644 index 00000000..be80e65a --- /dev/null +++ b/src/test/scala/org/openhorizon/exchangeapi/utility/apiutils/TestApiUtilsTime.scala @@ -0,0 +1,100 @@ +package org.openhorizon.exchangeapi + +import org.openhorizon.exchangeapi.utility.{ApiTime} +import org.scalatest.funsuite.AnyFunSuite + +object DateTimeConstants { + val DateTimePattern: String = """\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{1,9}Z\[UTC\]""" + val TimeLength: Int = 29 +} + +class TestApiUtilsTime extends AnyFunSuite { + test("ApiTime fixFormatting no milliseconds") { + val timeNoMilliseconds = "2019-06-17T21:24:55Z[UTC]" + info(ApiTime.fixFormatting(timeNoMilliseconds) + " , " + "2019-06-17T21:24:55.000Z[UTC]") + assert(ApiTime.fixFormatting(timeNoMilliseconds) == "2019-06-17T21:24:55.000Z[UTC]") + } + + test("ApiTime fixFormatting no seconds and no milliseconds") { + val timeNoSeconds = "2019-06-17T21:24Z[UTC]" + info(ApiTime.fixFormatting(timeNoSeconds) + " , " + "2019-06-17T21:24:00.000Z[UTC]") + assert(ApiTime.fixFormatting(timeNoSeconds) == "2019-06-17T21:24:00.000Z[UTC]") + } + + test("ApiTime fixFormatting handles invalid format") { + val invalidTime = "invalid-time-format" + info(ApiTime.fixFormatting(invalidTime) + " , " + "Invalid format") + assert(ApiTime.fixFormatting(invalidTime) == "Invalid format") + } + + test("ApiTime.nowUTC is always right length") { + info(ApiTime.nowUTC) + assert(ApiTime.nowUTC.length >= DateTimeConstants.TimeLength) + } + + test("ApiTime.nowUTC returns current time in valid format") { + val currentTime = ApiTime.nowUTC + info(currentTime) + assert(currentTime.matches(DateTimeConstants.DateTimePattern)) + } + + test("ApiTime.thenUTC is always right time and length") { + info(ApiTime.thenUTC(1615406509) + " , 2021-03-10T20:01:49.000Z[UTC]") + assert(ApiTime.thenUTC(1615406509) == "2021-03-10T20:01:49.000Z[UTC]") + assert(ApiTime.thenUTC(1615406509).length >= DateTimeConstants.TimeLength) + } + + test("ApiTime.thenUTC is always right time and length test 2"){ + info(ApiTime.thenUTC(1615406355)+ " , 2021-03-10T19:59:15.000Z[UTC]") + assert(ApiTime.thenUTC(1615406355) == "2021-03-10T19:59:15.000Z[UTC]") + assert(ApiTime.thenUTC(1615406355).length >= DateTimeConstants.TimeLength) + } + + test("ApiTime.thenUTC handles future timestamps") { + val futureTimestamp = System.currentTimeMillis() / 1000 + 10000 + val futureTime = ApiTime.thenUTC(futureTimestamp) + info(futureTime) + assert(futureTime.length >= DateTimeConstants.TimeLength) + } + + test("ApiTime.thenUTC handles past timestamps") { + val pastTimestamp = System.currentTimeMillis() / 1000 - 10000 + val pastTime = ApiTime.thenUTC(pastTimestamp) + info(pastTime) + assert(pastTime.length >= DateTimeConstants.TimeLength) + } + + test("ApiTime.pastUTC is always right length") { + info(ApiTime.pastUTC(10)) + assert(ApiTime.pastUTC(10).length >= DateTimeConstants.TimeLength) + } + + test("ApiTime.pastUTC returns correct time") { + val tenSecondsAgo = ApiTime.pastUTC(10) + info(tenSecondsAgo) + assert(tenSecondsAgo.matches(DateTimeConstants.DateTimePattern)) + } + + test("ApiTime.futureUTC is always right length") { + info(ApiTime.futureUTC(10)) + assert(ApiTime.futureUTC(10).length >= DateTimeConstants.TimeLength) + } + + test("ApiTime.futureUTC returns correct time") { + val tenSecondsLater = ApiTime.futureUTC(10) + info(tenSecondsLater) + assert(tenSecondsLater.matches(DateTimeConstants.DateTimePattern)) + } + + test("ApiTime.beginningUTC is always correct and right length") { + info(ApiTime.beginningUTC + " , " + "1970-01-01T00:00:00.000Z[UTC]") + assert(ApiTime.beginningUTC == "1970-01-01T00:00:00.000Z[UTC]") + assert(ApiTime.beginningUTC.length >= DateTimeConstants.TimeLength) + } + + test("ApiTime.beginningUTC is formatted correctly") { + val beginningTime = ApiTime.beginningUTC + info(beginningTime) + assert(beginningTime.matches(DateTimeConstants.DateTimePattern)) + } +} diff --git a/src/test/scala/org/openhorizon/exchangeapi/utility/tokenvalidation/TestNodeAgbotTokenValidation.scala b/src/test/scala/org/openhorizon/exchangeapi/utility/tokenvalidation/TestNodeAgbotTokenValidation.scala new file mode 100644 index 00000000..5c5ca6c1 --- /dev/null +++ b/src/test/scala/org/openhorizon/exchangeapi/utility/tokenvalidation/TestNodeAgbotTokenValidation.scala @@ -0,0 +1,63 @@ +package org.openhorizon.exchangeapi + +import org.openhorizon.exchangeapi.utility.{NodeAgbotTokenValidation} +import org.scalatest.funsuite.AnyFunSuite + + +class TestNodeAgbotTokenValidation extends AnyFunSuite { + test("NodeAgbotTokenValidation.isValid correctly identifies if password is too short, <15 chars"){ + info("NodeAgbotTokenValidation.isValid(\"1Abbb\")"+ " , " + NodeAgbotTokenValidation.isValid("1Abbb").toString()) + assert(NodeAgbotTokenValidation.isValid("1Abbb") == false) + info("NodeAgbotTokenValidation.isValid(\"1Abcdefghijkl\")"+ " , " + NodeAgbotTokenValidation.isValid("1Abcdefghijkl").toString()) + assert(NodeAgbotTokenValidation.isValid("1Abcdefghijkl") == false) + } + + test("NodeAgbotTokenValidation.isValid correctly identifies if password does not contain digit"){ + info("NodeAgbotTokenValidation.isValid(\"aaaaaaaaaaaaaaaB\")"+ " , " + NodeAgbotTokenValidation.isValid("aaaaaaaaaaaaaaaB").toString()) + assert(NodeAgbotTokenValidation.isValid("aaaaaaaaaaaaaaaB") == false) + info("NodeAgbotTokenValidation.isValid(\"abcdefghijklmno\")"+ " , " + NodeAgbotTokenValidation.isValid("abcdefghijklmno").toString()) + assert(NodeAgbotTokenValidation.isValid("abcdefghijklmno") == false) + } + + test("NodeAgbotTokenValidation.isValid correctly identifies if password does not contain uppercase letter"){ + info("NodeAgbotTokenValidation.isValid(\"aaaaaaaaaaaaaaa1\")"+ " , " + NodeAgbotTokenValidation.isValid("aaaaaaaaaaaaaaa1").toString()) + assert(NodeAgbotTokenValidation.isValid("aaaaaaaaaaaaaaa1") == false) + info("NodeAgbotTokenValidation.isValid(\"abcdefghijklmno1\")"+ " , " + NodeAgbotTokenValidation.isValid("abcdefghijklmno1").toString()) + assert(NodeAgbotTokenValidation.isValid("abcdefghijklmno1") == false) + } + + test("NodeAgbotTokenValidation.isValid correctly identifies if password does not contain lowercase letter"){ + info("NodeAgbotTokenValidation.isValid(\"AAAAAAAAAAAAAB1\")"+ " , " + NodeAgbotTokenValidation.isValid("AAAAAAAAAAAAAB1").toString()) + assert(NodeAgbotTokenValidation.isValid("AAAAAAAAAAAAAB1") == false) + info("NodeAgbotTokenValidation.isValid(\"ABCDEFGHIJKLMNO1\")"+ " , " + NodeAgbotTokenValidation.isValid("ABCDEFGHIJKLMNO1").toString()) + assert(NodeAgbotTokenValidation.isValid("ABCDEFGHIJKLMNO1") == false) + } + + test("NodeAgbotTokenValidation.isValid correctly identifies valid password"){ + info("NodeAgbotTokenValidation.isValid(\"AAAAAAAAAAAAaB1\")"+ " , " + NodeAgbotTokenValidation.isValid("AAAAAAAAAAAAaB1").toString()) + assert(NodeAgbotTokenValidation.isValid("AAAAAAAAAAAAaB1") == true) + info("NodeAgbotTokenValidation.isValid(\"ValidPassword1!\")"+ " , " + NodeAgbotTokenValidation.isValid("ValidPassword1!").toString()) + assert(NodeAgbotTokenValidation.isValid("ValidPassword1!") == true) + } + + test("NodeAgbotTokenValidation.isValid correctly identifies valid password with digits only") { + info("NodeAgbotTokenValidation.isValid(\"123456789012345\")"+ " , " + NodeAgbotTokenValidation.isValid("123456789012345").toString()) + assert(NodeAgbotTokenValidation.isValid("123456789012345") == false) + } + + test("NodeAgbotTokenValidation.isValid correctly identifies password with exactly 15 characters") { + info("NodeAgbotTokenValidation.isValid(\"Aa1aaaaaaaaaaaa\")"+ " , " + NodeAgbotTokenValidation.isValid("Aa1aaaaaaaaaaaa").toString()) + assert(NodeAgbotTokenValidation.isValid("Aa1" + "a" * 12) == true) // valid + info("NodeAgbotTokenValidation.isValid(\"Aaaaaaaabbbbbbb\")"+ " , " + NodeAgbotTokenValidation.isValid("Aaaaaaaabbbbbbb").toString()) + assert(NodeAgbotTokenValidation.isValid("Aaaaaaaabbbbbbb") == false) // invalid + info("NodeAgbotTokenValidation.isValid(\"A1abbbbbbbbbbbb\")"+ " , " + NodeAgbotTokenValidation.isValid("A1abbbbbbbbbbbb").toString()) + assert(NodeAgbotTokenValidation.isValid("A1a" + "b" * 12) == true) // valid + } + + test("NodeAgbotTokenValidation.isValid correctly identifies password with special characters") { + info("NodeAgbotTokenValidation.isValid(\"A1!abbbbbbbbbbb\")"+ " , " + NodeAgbotTokenValidation.isValid("A1!abbbbbbbbbbb").toString()) + assert(NodeAgbotTokenValidation.isValid("A1!a" + "b" * 11) == true) + info("NodeAgbotTokenValidation.isValid(\"!A1abbbbbbbbbbb\")"+ " , " + NodeAgbotTokenValidation.isValid("!A1abbbbbbbbbbb").toString()) + assert(NodeAgbotTokenValidation.isValid("!A1a" + "b" * 11) == true) + } +}