From a6a355d36c70aac74276065925137c877e04f465 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Sat, 13 Dec 2025 21:34:30 +0100 Subject: [PATCH 1/3] webui_props delete v6.0.0 --- .../scala/code/api/v6_0_0/APIMethods600.scala | 177 ++++++++- .../webuiprops/MappedWebUiPropsProvider.scala | 1 + .../scala/code/webuiprops/WebUiProps.scala | 4 + .../code/api/v6_0_0/WebUiPropsTest.scala | 338 +++++++++++++++++- 4 files changed, 512 insertions(+), 8 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index c592809a1..e8f7392a6 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -40,7 +40,7 @@ import code.util.Helper import code.util.Helper.{MdcLoggable, ObpS, SILENCE_IS_GOLDEN} import code.views.Views import code.views.system.ViewDefinition -import code.webuiprops.{MappedWebUiPropsProvider, WebUiPropsCommons} +import code.webuiprops.{MappedWebUiPropsProvider, WebUiPropsCommons, WebUiPropsPutJsonV600} import code.dynamicEntity.DynamicEntityCommons import code.DynamicData.{DynamicData, DynamicDataProvider} import com.github.dwickern.macros.NameOf.nameOf @@ -3634,6 +3634,181 @@ trait APIMethods600 { } } + staticResourceDocs += ResourceDoc( + createOrUpdateWebUiProps, + implementedInApiVersion, + nameOf(createOrUpdateWebUiProps), + "PUT", + "/management/webui_props/WEBUI_PROP_NAME", + "Create or Update WebUiProps", + s"""Create or Update a WebUiProps. + | + |${userAuthenticationMessage(true)} + | + |This endpoint is idempotent - it will create the property if it doesn't exist, or update it if it does. + |The property is identified by WEBUI_PROP_NAME in the URL path. + | + |Explanation of Fields: + | + |* WEBUI_PROP_NAME in URL path (must start with `webui_`, contain only alphanumeric characters, underscore, and dot, not exceed 255 characters, and will be converted to lowercase) + |* value is required String value in request body + | + |The line break and double quotations should be escaped, example: + | + |``` + | + |{"name": "webui_some", "value": "this value + |have "line break" and double quotations."} + | + |``` + |should be escaped like this: + | + |``` + | + |{"name": "webui_some", "value": "this value\\nhave \\"line break\\" and double quotations."} + | + |``` + | + |Insert image examples: + | + |``` + |// set width=100 and height=50 + |{"name": "webui_some_pic", "value": "here is a picture ![hello](http://somedomain.com/images/pic.png =100x50)"} + | + |// only set height=50 + |{"name": "webui_some_pic", "value": "here is a picture ![hello](http://somedomain.com/images/pic.png =x50)"} + | + |// only width=20% + |{"name": "webui_some_pic", "value": "here is a picture ![hello](http://somedomain.com/images/pic.png =20%x)"} + | + |``` + | + |""", + WebUiPropsPutJsonV600("https://apiexplorer.openbankproject.com"), + WebUiPropsCommons("webui_api_explorer_url", "https://apiexplorer.openbankproject.com", Some("some-web-ui-props-id")), + List( + UserNotLoggedIn, + UserHasMissingRoles, + InvalidJsonFormat, + InvalidWebUiProps, + UnknownError + ), + List(apiTagWebUiProps), + Some(List(canCreateWebUiProps)) + ) + + lazy val createOrUpdateWebUiProps: OBPEndpoint = { + case "management" :: "webui_props" :: webUiPropName :: Nil JsonPut json -> _ => { + cc => implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- authenticatedAccess(cc) + _ <- NewStyle.function.hasEntitlement("", u.userId, canCreateWebUiProps, callContext) + // Convert name to lowercase + webUiPropNameLower = webUiPropName.toLowerCase + invalidMsg = s"""$InvalidWebUiProps name must start with webui_, but current name is: ${webUiPropNameLower} """ + _ <- NewStyle.function.tryons(invalidMsg, 400, callContext) { + require(webUiPropNameLower.startsWith("webui_")) + } + invalidCharsMsg = s"""$InvalidWebUiProps name must contain only alphanumeric characters, underscore, and dot. Current name: ${webUiPropNameLower} """ + _ <- NewStyle.function.tryons(invalidCharsMsg, 400, callContext) { + require(webUiPropNameLower.matches("^[a-zA-Z0-9_.]+$")) + } + invalidLengthMsg = s"""$InvalidWebUiProps name must not exceed 255 characters. Current length: ${webUiPropNameLower.length} """ + _ <- NewStyle.function.tryons(invalidLengthMsg, 400, callContext) { + require(webUiPropNameLower.length <= 255) + } + // Check if resource already exists to determine status code + existingProp <- Future { MappedWebUiPropsProvider.getByName(webUiPropNameLower) } + resourceExists = existingProp.isDefined + failMsg = s"$InvalidJsonFormat The Json body should contain a value field" + valueJson <- NewStyle.function.tryons(failMsg, 400, callContext) { + json.extract[WebUiPropsPutJsonV600] + } + webUiPropsData = WebUiPropsCommons(webUiPropNameLower, valueJson.value) + Full(webUiProps) <- Future { MappedWebUiPropsProvider.createOrUpdate(webUiPropsData) } + } yield { + val commonsData: WebUiPropsCommons = webUiProps + val statusCode = if (resourceExists) HttpCode.`200`(callContext) else HttpCode.`201`(callContext) + (commonsData, statusCode) + } + } + } + + staticResourceDocs += ResourceDoc( + deleteWebUiProps, + implementedInApiVersion, + nameOf(deleteWebUiProps), + "DELETE", + "/management/webui_props/WEBUI_PROP_NAME", + "Delete WebUiProps", + s"""Delete a WebUiProps specified by WEBUI_PROP_NAME. + | + |${userAuthenticationMessage(true)} + | + |The property name will be converted to lowercase before deletion. + | + |Returns 204 No Content on successful deletion. + | + |This endpoint is idempotent - if the property does not exist, it still returns 204 No Content. + | + |Requires the $canDeleteWebUiProps role. + | + |""", + EmptyBody, + EmptyBody, + List( + UserNotLoggedIn, + UserHasMissingRoles, + InvalidWebUiProps, + UnknownError + ), + List(apiTagWebUiProps), + Some(List(canDeleteWebUiProps)) + ) + + lazy val deleteWebUiProps: OBPEndpoint = { + case "management" :: "webui_props" :: webUiPropName :: Nil JsonDelete _ => { + cc => implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- authenticatedAccess(cc) + _ <- NewStyle.function.hasEntitlement("", u.userId, canDeleteWebUiProps, callContext) + // Convert name to lowercase + webUiPropNameLower = webUiPropName.toLowerCase + invalidMsg = s"""$InvalidWebUiProps name must start with webui_, but current name is: ${webUiPropNameLower} """ + _ <- NewStyle.function.tryons(invalidMsg, 400, callContext) { + require(webUiPropNameLower.startsWith("webui_")) + } + invalidCharsMsg = s"""$InvalidWebUiProps name must contain only alphanumeric characters, underscore, and dot. Current name: ${webUiPropNameLower} """ + _ <- NewStyle.function.tryons(invalidCharsMsg, 400, callContext) { + require(webUiPropNameLower.matches("^[a-zA-Z0-9_.]+$")) + } + invalidLengthMsg = s"""$InvalidWebUiProps name must not exceed 255 characters. Current length: ${webUiPropNameLower.length} """ + _ <- NewStyle.function.tryons(invalidLengthMsg, 400, callContext) { + require(webUiPropNameLower.length <= 255) + } + // Check if resource exists + existingProp <- Future { MappedWebUiPropsProvider.getByName(webUiPropNameLower) } + _ <- existingProp match { + case Full(prop) => + // Property exists - delete it + Future { MappedWebUiPropsProvider.delete(prop.webUiPropsId.getOrElse("")) } map { + case Full(true) => Full(()) + case Full(false) => ObpApiFailure(s"$UnknownError Cannot delete WebUI prop", 500, callContext) + case Empty => ObpApiFailure(s"$UnknownError Cannot delete WebUI prop", 500, callContext) + case Failure(msg, _, _) => ObpApiFailure(msg, 500, callContext) + } + case Empty => + // Property not found - idempotent delete returns success + Future.successful(Full(())) + case Failure(msg, _, _) => + Future.failed(new Exception(msg)) + } + } yield { + (EmptyBody, HttpCode.`204`(callContext)) + } + } + } + staticResourceDocs += ResourceDoc( getSystemDynamicEntities, implementedInApiVersion, diff --git a/obp-api/src/main/scala/code/webuiprops/MappedWebUiPropsProvider.scala b/obp-api/src/main/scala/code/webuiprops/MappedWebUiPropsProvider.scala index 85e77cb5d..cec35e7c0 100644 --- a/obp-api/src/main/scala/code/webuiprops/MappedWebUiPropsProvider.scala +++ b/obp-api/src/main/scala/code/webuiprops/MappedWebUiPropsProvider.scala @@ -19,6 +19,7 @@ object MappedWebUiPropsProvider extends WebUiPropsProvider { override def getAll(): List[WebUiPropsT] = WebUiProps.findAll() + override def getByName(name: String): Box[WebUiPropsT] = WebUiProps.find(By(WebUiProps.Name, name)) override def createOrUpdate(webUiProps: WebUiPropsT): Box[WebUiPropsT] = { WebUiProps.find(By(WebUiProps.Name, webUiProps.name)) diff --git a/obp-api/src/main/scala/code/webuiprops/WebUiProps.scala b/obp-api/src/main/scala/code/webuiprops/WebUiProps.scala index cd1763017..02c6a5872 100644 --- a/obp-api/src/main/scala/code/webuiprops/WebUiProps.scala +++ b/obp-api/src/main/scala/code/webuiprops/WebUiProps.scala @@ -19,9 +19,13 @@ case class WebUiPropsCommons(name: String, object WebUiPropsCommons extends Converter[WebUiPropsT, WebUiPropsCommons] +case class WebUiPropsPutJsonV600(value: String) extends JsonFieldReName + trait WebUiPropsProvider { def getAll(): List[WebUiPropsT] + def getByName(name: String): Box[WebUiPropsT] + def createOrUpdate(webUiProps: WebUiPropsT): Box[WebUiPropsT] def delete(webUiPropsId: String):Box[Boolean] diff --git a/obp-api/src/test/scala/code/api/v6_0_0/WebUiPropsTest.scala b/obp-api/src/test/scala/code/api/v6_0_0/WebUiPropsTest.scala index a4a2f0353..ed23f1b16 100644 --- a/obp-api/src/test/scala/code/api/v6_0_0/WebUiPropsTest.scala +++ b/obp-api/src/test/scala/code/api/v6_0_0/WebUiPropsTest.scala @@ -48,7 +48,9 @@ class WebUiPropsTest extends V600ServerSetup { * This is made possible by the scalatest maven plugin */ object VersionOfApi extends Tag(ApiVersion.v6_0_0.toString) - object ApiEndpoint extends Tag(nameOf(Implementations6_0_0.getWebUiProp)) + object ApiEndpoint1 extends Tag(nameOf(Implementations6_0_0.getWebUiProp)) + object ApiEndpoint2 extends Tag(nameOf(Implementations6_0_0.createOrUpdateWebUiProps)) + object ApiEndpoint3 extends Tag(nameOf(Implementations6_0_0.deleteWebUiProps)) val rightEntity = WebUiPropsCommons("webui_api_explorer_url", "https://apiexplorer.openbankproject.com") val anotherEntity = WebUiPropsCommons("webui_api_manager_url", "https://apimanager.openbankproject.com") @@ -57,7 +59,7 @@ class WebUiPropsTest extends V600ServerSetup { feature("Get Single WebUiProp by Name v6.0.0") { - scenario("Get WebUiProp - successful case with explicit prop from database", VersionOfApi, ApiEndpoint) { + scenario("Get WebUiProp - successful case with explicit prop from database", VersionOfApi, ApiEndpoint1) { // First create a webui prop Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString) When("We create a webui prop") @@ -76,7 +78,7 @@ class WebUiPropsTest extends V600ServerSetup { webUiPropJson.value should equal(rightEntity.value) } - scenario("Get WebUiProp - successful case with active=true returns explicit prop", VersionOfApi, ApiEndpoint) { + scenario("Get WebUiProp - successful case with active=true returns explicit prop", VersionOfApi, ApiEndpoint1) { // First create a webui prop Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString) When("We create a webui prop") @@ -95,7 +97,7 @@ class WebUiPropsTest extends V600ServerSetup { webUiPropJson.value should equal(anotherEntity.value) } - scenario("Get WebUiProp - not found without active flag", VersionOfApi, ApiEndpoint) { + scenario("Get WebUiProp - not found without active flag", VersionOfApi, ApiEndpoint1) { When("We get a non-existent webui prop by name without active flag") val requestGet = (v6_0_0_Request / "webui-props" / "webui_non_existent_prop").GET val responseGet = makeGetRequest(requestGet) @@ -105,7 +107,7 @@ class WebUiPropsTest extends V600ServerSetup { error.message should include(WebUiPropsNotFoundByName) } - scenario("Get WebUiProp - with active=true returns implicit prop from config", VersionOfApi, ApiEndpoint) { + scenario("Get WebUiProp - with active=true returns implicit prop from config", VersionOfApi, ApiEndpoint1) { // Test that we can get implicit props from sample.props.template when active=true When("We get a webui prop by name with active=true that exists in config but not in DB") // Use a prop that should exist in sample.props.template like webui_sandbox_introduction @@ -118,7 +120,7 @@ class WebUiPropsTest extends V600ServerSetup { webUiPropJson.webUiPropsId should equal(Some("default")) } - scenario("Get WebUiProp - invalid active parameter", VersionOfApi, ApiEndpoint) { + scenario("Get WebUiProp - invalid active parameter", VersionOfApi, ApiEndpoint1) { When("We get a webui prop with invalid active parameter") val requestGet = (v6_0_0_Request / "webui-props" / "webui_api_explorer_url").GET.addQueryParameter("active", "invalid") val responseGet = makeGetRequest(requestGet) @@ -128,7 +130,7 @@ class WebUiPropsTest extends V600ServerSetup { error.message should include(InvalidFilterParameterFormat) } - scenario("Get WebUiProp - database prop takes precedence over config prop when active=true", VersionOfApi, ApiEndpoint) { + scenario("Get WebUiProp - database prop takes precedence over config prop when active=true", VersionOfApi, ApiEndpoint1) { // Create a webui prop that overrides a config value Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString) val customValue = WebUiPropsCommons("webui_get_started_text", "Custom Get Started Text") @@ -149,5 +151,327 @@ class WebUiPropsTest extends V600ServerSetup { webUiPropJson.webUiPropsId should not equal(Some("default")) } } + + feature("Create or Update WebUiProp (PUT) v6.0.0") { + + scenario("PUT WebUiProp - create new property successfully", VersionOfApi, ApiEndpoint2) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString) + When("We create a new webui prop using PUT") + val putValue = """{"value": "https://new-api-explorer.com"}""" + val requestPut = (v6_0_0_Request / "management" / "webui_props" / "webui_test_new_prop").PUT <@(user1) + val responsePut = makePutRequest(requestPut, putValue) + Then("We should get a 201 Created") + responsePut.code should equal(201) + val webUiProp = responsePut.body.extract[WebUiPropsCommons] + webUiProp.name should equal("webui_test_new_prop") + webUiProp.value should equal("https://new-api-explorer.com") + webUiProp.webUiPropsId.isDefined should equal(true) + } + + scenario("PUT WebUiProp - update existing property successfully", VersionOfApi, ApiEndpoint2) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString) + When("We create a webui prop") + val putValue1 = """{"value": "original value"}""" + val requestPut1 = (v6_0_0_Request / "management" / "webui_props" / "webui_test_update_prop").PUT <@(user1) + val responsePut1 = makePutRequest(requestPut1, putValue1) + Then("We should get a 201 Created") + responsePut1.code should equal(201) + + When("We update the same webui prop") + val putValue2 = """{"value": "updated value"}""" + val requestPut2 = (v6_0_0_Request / "management" / "webui_props" / "webui_test_update_prop").PUT <@(user1) + val responsePut2 = makePutRequest(requestPut2, putValue2) + Then("We should get a 200 OK") + responsePut2.code should equal(200) + val webUiProp = responsePut2.body.extract[WebUiPropsCommons] + webUiProp.name should equal("webui_test_update_prop") + webUiProp.value should equal("updated value") + } + + scenario("PUT WebUiProp - idempotent create (same value twice)", VersionOfApi, ApiEndpoint2) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString) + val putValue = """{"value": "idempotent value"}""" + + When("We create a webui prop") + val requestPut1 = (v6_0_0_Request / "management" / "webui_props" / "webui_test_idempotent").PUT <@(user1) + val responsePut1 = makePutRequest(requestPut1, putValue) + Then("We should get a 201 Created") + responsePut1.code should equal(201) + val webUiPropsId1 = responsePut1.body.extract[WebUiPropsCommons].webUiPropsId + + When("We PUT the same value again") + val requestPut2 = (v6_0_0_Request / "management" / "webui_props" / "webui_test_idempotent").PUT <@(user1) + val responsePut2 = makePutRequest(requestPut2, putValue) + Then("We should get a 200 OK with same ID") + responsePut2.code should equal(200) + val webUiPropsId2 = responsePut2.body.extract[WebUiPropsCommons].webUiPropsId + webUiPropsId1 should equal(webUiPropsId2) + } + + scenario("PUT WebUiProp - name converted to lowercase", VersionOfApi, ApiEndpoint2) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString) + When("We create a webui prop with UPPERCASE name") + val putValue = """{"value": "test value"}""" + val requestPut = (v6_0_0_Request / "management" / "webui_props" / "WEBUI_UPPERCASE_TEST").PUT <@(user1) + val responsePut = makePutRequest(requestPut, putValue) + Then("We should get a 201 and name should be lowercase") + responsePut.code should equal(201) + val webUiProp = responsePut.body.extract[WebUiPropsCommons] + webUiProp.name should equal("webui_uppercase_test") + } + + scenario("PUT WebUiProp - dot allowed in name", VersionOfApi, ApiEndpoint2) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString) + When("We create a webui prop with dots in name") + val putValue = """{"value": "https://api.v1.example.com"}""" + val requestPut = (v6_0_0_Request / "management" / "webui_props" / "webui_api.v1.endpoint").PUT <@(user1) + val responsePut = makePutRequest(requestPut, putValue) + Then("We should get a 201") + responsePut.code should equal(201) + val webUiProp = responsePut.body.extract[WebUiPropsCommons] + webUiProp.name should equal("webui_api.v1.endpoint") + } + + scenario("PUT WebUiProp - fail without webui_ prefix", VersionOfApi, ApiEndpoint2) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString) + When("We create a webui prop without webui_ prefix") + val putValue = """{"value": "test value"}""" + val requestPut = (v6_0_0_Request / "management" / "webui_props" / "invalid_name").PUT <@(user1) + val responsePut = makePutRequest(requestPut, putValue) + Then("We should get a 400") + responsePut.code should equal(400) + val error = responsePut.body.extract[ErrorMessage] + error.message should include(InvalidWebUiProps) + error.message should include("must start with webui_") + } + + scenario("PUT WebUiProp - fail with hyphen in name", VersionOfApi, ApiEndpoint2) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString) + When("We create a webui prop with hyphen") + val putValue = """{"value": "test value"}""" + val requestPut = (v6_0_0_Request / "management" / "webui_props" / "webui_api-explorer").PUT <@(user1) + val responsePut = makePutRequest(requestPut, putValue) + Then("We should get a 400") + responsePut.code should equal(400) + val error = responsePut.body.extract[ErrorMessage] + error.message should include(InvalidWebUiProps) + error.message should include("alphanumeric characters, underscore, and dot") + } + + scenario("PUT WebUiProp - fail with space in name", VersionOfApi, ApiEndpoint2) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString) + When("We create a webui prop with space") + val putValue = """{"value": "test value"}""" + val requestPut = (v6_0_0_Request / "management" / "webui_props" / "webui_invalid name").PUT <@(user1) + val responsePut = makePutRequest(requestPut, putValue) + Then("We should get a 400") + responsePut.code should equal(400) + val error = responsePut.body.extract[ErrorMessage] + error.message should include(InvalidWebUiProps) + } + + scenario("PUT WebUiProp - fail without authentication", VersionOfApi, ApiEndpoint2) { + When("We try to PUT without authentication") + val putValue = """{"value": "test value"}""" + val requestPut = (v6_0_0_Request / "management" / "webui_props" / "webui_test_noauth").PUT + val responsePut = makePutRequest(requestPut, putValue) + Then("We should get a 401") + responsePut.code should equal(401) + } + + scenario("PUT WebUiProp - fail without CanCreateWebUiProps role", VersionOfApi, ApiEndpoint2) { + When("We try to PUT without proper role") + val putValue = """{"value": "test value"}""" + val requestPut = (v6_0_0_Request / "management" / "webui_props" / "webui_test_norole").PUT <@(user1) + val responsePut = makePutRequest(requestPut, putValue) + Then("We should get a 403") + responsePut.code should equal(403) + val error = responsePut.body.extract[ErrorMessage] + error.message should include(UserHasMissingRoles) + } + + scenario("PUT WebUiProp - fail with invalid JSON body", VersionOfApi, ApiEndpoint2) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString) + When("We PUT with invalid JSON") + val putValue = """{"invalid": "no value field"}""" + val requestPut = (v6_0_0_Request / "management" / "webui_props" / "webui_test_invalid_json").PUT <@(user1) + val responsePut = makePutRequest(requestPut, putValue) + Then("We should get a 400") + responsePut.code should equal(400) + val error = responsePut.body.extract[ErrorMessage] + error.message should include(InvalidJsonFormat) + } + + scenario("PUT WebUiProp - fail with name exceeding 255 characters", VersionOfApi, ApiEndpoint2) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString) + When("We create a webui prop with name exceeding 255 chars") + val longName = "webui_" + ("a" * 250) // 256 chars total + val putValue = """{"value": "test value"}""" + val requestPut = (v6_0_0_Request / "management" / "webui_props" / longName).PUT <@(user1) + val responsePut = makePutRequest(requestPut, putValue) + Then("We should get a 400") + responsePut.code should equal(400) + val error = responsePut.body.extract[ErrorMessage] + error.message should include(InvalidWebUiProps) + error.message should include("255 characters") + } + } + + feature("Delete WebUiProp (DELETE) v6.0.0") { + + scenario("DELETE WebUiProp - delete existing property successfully", VersionOfApi, ApiEndpoint3) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanDeleteWebUiProps.toString) + + When("We create a webui prop") + val putValue = """{"value": "to be deleted"}""" + val requestPut = (v6_0_0_Request / "management" / "webui_props" / "webui_test_delete").PUT <@(user1) + val responsePut = makePutRequest(requestPut, putValue) + Then("We should get a 201") + responsePut.code should equal(201) + + When("We delete the webui prop") + val requestDelete = (v6_0_0_Request / "management" / "webui_props" / "webui_test_delete").DELETE <@(user1) + val responseDelete = makeDeleteRequest(requestDelete) + Then("We should get a 204 No Content") + responseDelete.code should equal(204) + responseDelete.body.toString should equal("{}") + } + + scenario("DELETE WebUiProp - idempotent delete (delete twice)", VersionOfApi, ApiEndpoint3) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanDeleteWebUiProps.toString) + + When("We create a webui prop") + val putValue = """{"value": "to be deleted twice"}""" + val requestPut = (v6_0_0_Request / "management" / "webui_props" / "webui_test_delete_twice").PUT <@(user1) + val responsePut = makePutRequest(requestPut, putValue) + responsePut.code should equal(201) + + When("We delete the webui prop first time") + val requestDelete1 = (v6_0_0_Request / "management" / "webui_props" / "webui_test_delete_twice").DELETE <@(user1) + val responseDelete1 = makeDeleteRequest(requestDelete1) + Then("We should get a 204") + responseDelete1.code should equal(204) + + When("We delete the same webui prop again (idempotent)") + val requestDelete2 = (v6_0_0_Request / "management" / "webui_props" / "webui_test_delete_twice").DELETE <@(user1) + val responseDelete2 = makeDeleteRequest(requestDelete2) + Then("We should still get a 204") + responseDelete2.code should equal(204) + } + + scenario("DELETE WebUiProp - delete non-existent property (idempotent)", VersionOfApi, ApiEndpoint3) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanDeleteWebUiProps.toString) + When("We delete a non-existent webui prop") + val requestDelete = (v6_0_0_Request / "management" / "webui_props" / "webui_never_existed").DELETE <@(user1) + val responseDelete = makeDeleteRequest(requestDelete) + Then("We should get a 204 (idempotent)") + responseDelete.code should equal(204) + } + + scenario("DELETE WebUiProp - name converted to lowercase", VersionOfApi, ApiEndpoint3) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanDeleteWebUiProps.toString) + + When("We create a webui prop with lowercase name") + val putValue = """{"value": "test value"}""" + val requestPut = (v6_0_0_Request / "management" / "webui_props" / "webui_delete_uppercase").PUT <@(user1) + val responsePut = makePutRequest(requestPut, putValue) + responsePut.code should equal(201) + + When("We delete using UPPERCASE name") + val requestDelete = (v6_0_0_Request / "management" / "webui_props" / "WEBUI_DELETE_UPPERCASE").DELETE <@(user1) + val responseDelete = makeDeleteRequest(requestDelete) + Then("We should get a 204 (lowercase conversion works)") + responseDelete.code should equal(204) + } + + scenario("DELETE WebUiProp - fail without webui_ prefix", VersionOfApi, ApiEndpoint3) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanDeleteWebUiProps.toString) + When("We try to delete with invalid name") + val requestDelete = (v6_0_0_Request / "management" / "webui_props" / "invalid_name").DELETE <@(user1) + val responseDelete = makeDeleteRequest(requestDelete) + Then("We should get a 400") + responseDelete.code should equal(400) + val error = responseDelete.body.extract[ErrorMessage] + error.message should include(InvalidWebUiProps) + error.message should include("must start with webui_") + } + + scenario("DELETE WebUiProp - fail with hyphen in name", VersionOfApi, ApiEndpoint3) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanDeleteWebUiProps.toString) + When("We try to delete with hyphen in name") + val requestDelete = (v6_0_0_Request / "management" / "webui_props" / "webui_api-explorer").DELETE <@(user1) + val responseDelete = makeDeleteRequest(requestDelete) + Then("We should get a 400") + responseDelete.code should equal(400) + val error = responseDelete.body.extract[ErrorMessage] + error.message should include(InvalidWebUiProps) + } + + scenario("DELETE WebUiProp - fail without authentication", VersionOfApi, ApiEndpoint3) { + When("We try to DELETE without authentication") + val requestDelete = (v6_0_0_Request / "management" / "webui_props" / "webui_test_noauth").DELETE + val responseDelete = makeDeleteRequest(requestDelete) + Then("We should get a 401") + responseDelete.code should equal(401) + } + + scenario("DELETE WebUiProp - fail without CanDeleteWebUiProps role", VersionOfApi, ApiEndpoint3) { + When("We try to DELETE without proper role") + val requestDelete = (v6_0_0_Request / "management" / "webui_props" / "webui_test_norole").DELETE <@(user1) + val responseDelete = makeDeleteRequest(requestDelete) + Then("We should get a 403") + responseDelete.code should equal(403) + val error = responseDelete.body.extract[ErrorMessage] + error.message should include(UserHasMissingRoles) + } + + scenario("DELETE WebUiProp - complete CRUD workflow", VersionOfApi, ApiEndpoint3) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanDeleteWebUiProps.toString) + + When("We create a webui prop") + val putValue1 = """{"value": "initial value"}""" + val requestPut1 = (v6_0_0_Request / "management" / "webui_props" / "webui_test_crud").PUT <@(user1) + val responsePut1 = makePutRequest(requestPut1, putValue1) + Then("We should get a 201") + responsePut1.code should equal(201) + + When("We read the webui prop") + val requestGet1 = (v6_0_0_Request / "webui-props" / "webui_test_crud").GET + val responseGet1 = makeGetRequest(requestGet1) + Then("We should get a 200 with correct value") + responseGet1.code should equal(200) + responseGet1.body.extract[WebUiPropsCommons].value should equal("initial value") + + When("We update the webui prop") + val putValue2 = """{"value": "updated value"}""" + val requestPut2 = (v6_0_0_Request / "management" / "webui_props" / "webui_test_crud").PUT <@(user1) + val responsePut2 = makePutRequest(requestPut2, putValue2) + Then("We should get a 200") + responsePut2.code should equal(200) + + When("We read the updated webui prop") + val requestGet2 = (v6_0_0_Request / "webui-props" / "webui_test_crud").GET + val responseGet2 = makeGetRequest(requestGet2) + Then("We should get the updated value") + responseGet2.code should equal(200) + responseGet2.body.extract[WebUiPropsCommons].value should equal("updated value") + + When("We delete the webui prop") + val requestDelete = (v6_0_0_Request / "management" / "webui_props" / "webui_test_crud").DELETE <@(user1) + val responseDelete = makeDeleteRequest(requestDelete) + Then("We should get a 204") + responseDelete.code should equal(204) + + When("We try to read the deleted webui prop") + val requestGet3 = (v6_0_0_Request / "webui-props" / "webui_test_crud").GET + val responseGet3 = makeGetRequest(requestGet3) + Then("We should get a 400 (not found in database)") + responseGet3.code should equal(400) + } + } } \ No newline at end of file From f84d2e757b818d28cbb69cf79e6ad04bbf006446 Mon Sep 17 00:00:00 2001 From: Wander Madeira Date: Sat, 13 Dec 2025 20:53:28 +0000 Subject: [PATCH 2/3] Test Alpha --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index a63ff2930..b70c3749e 100644 --- a/README.md +++ b/README.md @@ -823,3 +823,5 @@ Steps to add Spanish language: - add file `lift-core_es_ES.properties` at the folder `/resources/i18n` Please note that default translation file is `lift-core.properties` + +Testing VS Code Workspace Web From 85dad3d598beab5274155c3bacd8d207df72105d Mon Sep 17 00:00:00 2001 From: Wander Madeira Date: Sun, 14 Dec 2025 13:55:50 +0000 Subject: [PATCH 3/3] Usefull tips --- .vscode/settings.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..ef83f48f5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "githubPullRequests.ignoredPullRequestBranches": [ + "develop" + ] +} \ No newline at end of file