From 471ed5636935b14144f33f7183aab44095585034 Mon Sep 17 00:00:00 2001 From: arcuri82 Date: Tue, 14 Jul 2026 22:20:37 +0200 Subject: [PATCH 1/6] starting with dynamic resource resolution refactoring --- .../core/problem/rest/data/RestCallAction.kt | 37 +++++++++++++++++-- .../core/problem/rest/data/RestPath.kt | 2 +- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestCallAction.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestCallAction.kt index 90a9d41a18..f263ae1816 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestCallAction.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestCallAction.kt @@ -166,7 +166,9 @@ class RestCallAction( /** * Make sure that the path params are resolved to the same concrete values of "other". - * Note: "this" can be just an ancestor of "other" + * Note: "this" can be just an ancestor of "other". + * This function takes care when path elements are dynamically handled based on + * results of previous calls (eg a POST creating a resource). * * **/ fun bindToSamePathResolution(other: RestCallAction) { @@ -189,11 +191,38 @@ class RestCallAction( g.forceNewTaints() } } + if(this.path.isEquivalent(other.path)) { + //if pointing to the same resource, make sure to handle dynamic resource creation + //TODO does it make sense to do it even for ancestor paths??? likely not... but not 100% sure + this.usePreviousLocationId = other.usePreviousLocationId + this.weakReference = other.weakReference + } } - fun usingSameResolvedPath(other: RestCallAction) = - //FIXME this does not consider dynamic fields? - this.resolvedOnlyPath() == other.resolvedOnlyPath() + /** + * Check if the resulting path of this action is the same of [other], taking into account dynamic info + */ + fun usingSameResolvedPath(other: RestCallAction) : Boolean{ + if(this.path.levels() != other.path.levels()){ + return false + } + + /* + TODO Is this really correct? what about cases of + 1) /items/{id}/foo + 2) /items/{id}/bar + ??? + TODO we need to handle the possible non-shared suffix + */ + if(this.usePreviousLocationId != null && this.usePreviousLocationId == other.usePreviousLocationId){ + return true + } + if(this.weakReference != null && this.weakReference == other.weakReference){ + return true + } + + return this.resolvedOnlyPath() == other.resolvedOnlyPath() + } /** * When the URL path of this endpoint is resolved, would it be a (strict) parent from the other action diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestPath.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestPath.kt index 4e1439b4e6..6ca918471a 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestPath.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestPath.kt @@ -263,7 +263,7 @@ class RestPath(path: String) { return false } - return (0 until this.elements.size).none { other.elements[it] != this.elements[it] } + return this.elements.indices.none { other.elements[it] != this.elements[it] } } From ab5a138a58ed4cb64155de005614cb6a3bbb3839 Mon Sep 17 00:00:00 2001 From: arcuri82 Date: Tue, 4 Aug 2026 12:25:49 +0200 Subject: [PATCH 2/6] fixed issue in path resolution check --- .../core/problem/rest/data/RestCallAction.kt | 11 +++++------ .../org/evomaster/core/problem/rest/data/RestPath.kt | 4 ++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestCallAction.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestCallAction.kt index f263ae1816..0955eb2e80 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestCallAction.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestCallAction.kt @@ -203,16 +203,15 @@ class RestCallAction( * Check if the resulting path of this action is the same of [other], taking into account dynamic info */ fun usingSameResolvedPath(other: RestCallAction) : Boolean{ - if(this.path.levels() != other.path.levels()){ + if(!this.path.isEquivalent(other.path)){ return false } /* - TODO Is this really correct? what about cases of - 1) /items/{id}/foo - 2) /items/{id}/bar - ??? - TODO we need to handle the possible non-shared suffix + Consider + 1) /items/{id}/{x=a} + 1) /items/{id}/{x=b} + TODO this should result in different, even if sharing same resource {id} */ if(this.usePreviousLocationId != null && this.usePreviousLocationId == other.usePreviousLocationId){ return true diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestPath.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestPath.kt index 6ca918471a..c6101ca893 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestPath.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestPath.kt @@ -173,6 +173,10 @@ class RestPath(path: String) { return elements.flatMap { it.tokens }.any { it.isParameter } } + /** + * Check if having the exact same structure, base only on static information. + * Ie, variable resolution could lead to different resolved paths + */ fun isEquivalent(other: RestPath): Boolean { if (this.elements.size != other.elements.size) { return false From 105b21ed5cc87b721022227d967f4fb913f7289c Mon Sep 17 00:00:00 2001 From: arcuri82 Date: Tue, 4 Aug 2026 12:58:08 +0200 Subject: [PATCH 3/6] refactoring --- .../SecurityForbiddenOperationTest.kt | 9 +- .../rest/builder/CreateResourceUtils.kt | 96 ----------- .../problem/rest/builder/DynamicPathUtils.kt | 153 ++++++++++++++++++ .../core/problem/rest/data/RestCallAction.kt | 72 +-------- .../rest/oracle/HttpSemanticsOracle.kt | 23 +-- .../problem/rest/oracle/RestSecurityOracle.kt | 8 +- .../problem/rest/resource/RestResourceNode.kt | 9 +- .../rest/service/HttpSemanticsService.kt | 9 +- .../rest/service/RestIndividualBuilder.kt | 13 +- .../rest/service/RestSecurityBuilder.kt | 8 +- .../service/fitness/BlackBoxRestFitness.kt | 9 +- .../problem/rest/RestActionBuilderV3Test.kt | 7 +- 12 files changed, 199 insertions(+), 217 deletions(-) delete mode 100644 core/src/main/kotlin/org/evomaster/core/problem/rest/builder/CreateResourceUtils.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/problem/rest/builder/DynamicPathUtils.kt diff --git a/core-tests/integration-tests/core-it/src/test/kotlin/org/evomaster/core/problem/rest/securityrestoracle/SecurityForbiddenOperationTest.kt b/core-tests/integration-tests/core-it/src/test/kotlin/org/evomaster/core/problem/rest/securityrestoracle/SecurityForbiddenOperationTest.kt index fe6fe5755b..ad08792b74 100644 --- a/core-tests/integration-tests/core-it/src/test/kotlin/org/evomaster/core/problem/rest/securityrestoracle/SecurityForbiddenOperationTest.kt +++ b/core-tests/integration-tests/core-it/src/test/kotlin/org/evomaster/core/problem/rest/securityrestoracle/SecurityForbiddenOperationTest.kt @@ -3,17 +3,14 @@ package org.evomaster.core.problem.rest.securityrestoracle import bar.examples.it.spring.securityforbiddenoperation.SecurityForbiddenOperationApplication import bar.examples.it.spring.securityforbiddenoperation.SecurityForbiddenOperationController import com.webfuzzing.commons.faults.DefinedFaultCategory -import com.webfuzzing.commons.faults.FaultCategory import org.evomaster.core.JdkIssue import org.evomaster.core.problem.enterprise.DetectedFaultUtils -import org.evomaster.core.problem.enterprise.ExperimentalFaultCategory import org.evomaster.core.problem.enterprise.SampleType import org.evomaster.core.problem.httpws.auth.HttpWsAuthenticationInfo import org.evomaster.core.problem.rest.* -import org.evomaster.core.problem.rest.builder.CreateResourceUtils +import org.evomaster.core.problem.rest.builder.DynamicPathUtils import org.evomaster.core.problem.rest.data.HttpVerb import org.evomaster.core.problem.rest.data.RestCallResult -import org.evomaster.core.problem.rest.oracle.RestSecurityOracle import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeAll @@ -81,9 +78,9 @@ class SecurityForbiddenOperationTest : IntegrationTestRestBase() { val a = pirTest.fromVerbPath("POST", "/api/resources")!! val b = pirTest.fromVerbPath("DELETE", "/api/resources/1234")!! - CreateResourceUtils.linkDynamicCreateResource(a,b)//FIXME should be in PirToRest + DynamicPathUtils.linkDynamicCreateResource(a,b)//FIXME should be in PirToRest val c = pirTest.fromVerbPath("PUT", "/api/resources/333")!! - CreateResourceUtils.linkDynamicCreateResource(a,c)//FIXME should be in PirToRest + DynamicPathUtils.linkDynamicCreateResource(a,c)//FIXME should be in PirToRest val auth = controller.getInfoForAuthentication() val foo = HttpWsAuthenticationInfo.fromDto(auth.find { it.name == "FOO" }!!) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/CreateResourceUtils.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/CreateResourceUtils.kt deleted file mode 100644 index 0795aa6412..0000000000 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/CreateResourceUtils.kt +++ /dev/null @@ -1,96 +0,0 @@ -package org.evomaster.core.problem.rest.builder - -import org.evomaster.core.problem.rest.data.HttpVerb -import org.evomaster.core.problem.rest.data.RestCallAction - - -/** - * POST/PUT operations can create new resources. - * What are the ids of these newly created resources? - * Typically, 2 options: - * 1) returned in a HTTP Location header - * 2) in a field of body response - * Either way, such info is dynamically generated, and it would not - * be known before executing the test. - * - * Once a test is executed, the needed info to make such a decision will be - * stored in [RestCallResult.HEURISTICS_FOR_CHAINED_LOCATION] - */ -object CreateResourceUtils { - - /** - * Given two actions in sequence, [before] and [after], setup a creation link. - * This means that the POST [before] is supposed to create a resource dynamically, which is then used - * by [after]. - * eg: - * before: POST /products - * after: DELETE /products/{id} - * - * In case the two actions are on the same path, the [after] is linked to the creator of [before], - * if any - */ - fun linkDynamicCreateResource( - before: RestCallAction, - after: RestCallAction - ) { - if(before.verb != HttpVerb.POST && before.verb != HttpVerb.PUT){ - throw IllegalArgumentException("Before action is neither a POST nor a PUT. It is a ${before.verb}") - } - - if (!before.path.isEquivalent(after.path)) { - /* - eg - POST /x - GET /x/{id} - */ - before.saveAndLinkLocationTo(after) - } else { - /* - eg - POST /x - POST /x/{id}/y - GET /x/{id}/y - not need to save the position of last POST, as same as target - - however, might also be in the case of: - PUT /x/{id} - GET /x/{id} - */ - /* - removing the flag here was a mistake. - even if after is not using the resource path, between "before" and "after" - there could be other calls that need it, eg: - - PUT /x/{a} - PUT /x/{a}/y/{b} - DELETE /x/{a} - */ - //before.saveCreatedResourceLocation = false - - // the target (eg GET) needs to use the location of first POST, or more correctly - // the same location used for the last POST (in case there is a deeper chain) - after.usePreviousLocationId = before.usePreviousLocationId - } - } - - - /** - * Check if two actions are on same resource. - * This is not necessarily simple, as path resolution might depend on dynamic info - * coming from previous actions (e.g., a POST create) - */ - fun doesResolveToSamePath(a: RestCallAction, b: RestCallAction) : Boolean { - - if(a.usePreviousLocationId == null && b.usePreviousLocationId == null) { - return a.resolvedOnlyPath() == b.resolvedOnlyPath() - } - - if(a.usePreviousLocationId != b.usePreviousLocationId) { - //different dynamic info - return false - } - - //TODO this might need more thinking... eg, how handled variables resolutions in chained calls??? - return true - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/DynamicPathUtils.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/DynamicPathUtils.kt new file mode 100644 index 0000000000..9927809f85 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/DynamicPathUtils.kt @@ -0,0 +1,153 @@ +package org.evomaster.core.problem.rest.builder + +import org.evomaster.core.problem.rest.data.HttpVerb +import org.evomaster.core.problem.rest.data.RestCallAction +import org.evomaster.core.problem.rest.param.PathParam + + +/** + * POST/PUT operations can create new resources. + * What are the ids of these newly created resources? + * Typically, 2 options: + * 1) returned in a HTTP Location header + * 2) in a field of body response + * Either way, such info is dynamically generated, and it would not + * be known before executing the test. + * + * Once a test is executed, the needed info to make such a decision will be + * stored in RestCallResult. + */ +object DynamicPathUtils { + + /** + * Given two actions in sequence, [before] and [after], setup a creation link. + * This means that the POST [before] is supposed to create a resource dynamically, which is then used + * by [after]. + * eg: + * before: POST /products + * after: DELETE /products/{id} + * + * In case the two actions are on the same path, the [after] is linked to the creator of [before], + * if any + */ + fun linkDynamicCreateResource( + before: RestCallAction, + after: RestCallAction + ) { + if(before.verb != HttpVerb.POST && before.verb != HttpVerb.PUT){ + throw IllegalArgumentException("Before action is neither a POST nor a PUT. It is a ${before.verb}") + } + + if (!before.path.isEquivalent(after.path)) { + /* + eg + POST /x + GET /x/{id} + */ + before.saveAndLinkLocationTo(after) + } else { + /* + eg + POST /x + POST /x/{id}/y + GET /x/{id}/y + not need to save the position of last POST, as same as target + + however, might also be in the case of: + PUT /x/{id} + GET /x/{id} + */ + /* + removing the flag here was a mistake. + even if after is not using the resource path, between "before" and "after" + there could be other calls that need it, eg: + + PUT /x/{a} + PUT /x/{a}/y/{b} + DELETE /x/{a} + */ + //before.saveCreatedResourceLocation = false + + // the target (eg GET) needs to use the location of first POST, or more correctly + // the same location used for the last POST (in case there is a deeper chain) + after.usePreviousLocationId = before.usePreviousLocationId + } + } + + + /** + * Check if two actions are on same resource. + * This is not necessarily simple, as path resolution might depend on dynamic info + * coming from previous actions (e.g., a POST create) + */ + fun doesResolveToSamePath(a: RestCallAction, b: RestCallAction) : Boolean { + + if(!a.path.isEquivalent(b.path)){ + return false + } + + /* + Consider + 1) /items/{id}/{x=a} + 1) /items/{id}/{x=b} + TODO this should result in different, even if sharing same resource {id} + */ + if(a.usePreviousLocationId != null && a.usePreviousLocationId == b.usePreviousLocationId){ + return true + } + if(a.weakReference != null && a.weakReference == b.weakReference){ + return true + } + + return a.resolvedOnlyPath() == b.resolvedOnlyPath() + } + + /** + * Make sure that the path params are resolved to the same concrete values of "other". + * Note: "this" can be just an ancestor of "other". + * This function takes care when path elements are dynamically handled based on + * results of previous calls (eg a POST creating a resource). * + * + **/ + fun bindToSamePathResolution(a: RestCallAction, b: RestCallAction) { + if (!a.path.isSameOrAncestorOf(b.path)) { + throw IllegalArgumentException("Cannot bind 2 different unrelated paths to the same path resolution: " + + "${a.path} vs ${b.path}") + } + for (i in a.parameters.indices) { + val target = a.parameters[i] + if (target is PathParam) { + val k = b.parameters.find { p -> p is PathParam && p.name == target.name }!! + /* + Note: even if they are referring to same path variable, it does not mean that + necessarily they are represented with the same type of gene, eg., typically a StringGene. + For example, they could be a ChoiceGene when dealing with "examples" or Regex when having patterns + only defined on some endpoints + */ + val g = a.parameters[i].primaryGene() + g.copyValueFrom(k.primaryGene()) + g.forceNewTaints() + } + } + if(a.path.isEquivalent(b.path)) { + //if pointing to the same resource, make sure to handle dynamic resource creation + //TODO does it make sense to do it even for ancestor paths??? likely not... but not 100% sure + a.usePreviousLocationId = b.usePreviousLocationId + a.weakReference = b.weakReference + } + } + + /** + * When the URL path of this endpoint is resolved, would it be a (strict) parent from the other action + */ + fun isResolvedParentPath(a: RestCallAction, b: RestCallAction): Boolean { + + val parent = a.resolvedOnlyPath() // TODO deal with dynamic info + val child = b.resolvedOnlyPath() + + if(parent.length >= child.length) { + return false + } + return child.startsWith(parent) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestCallAction.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestCallAction.kt index 0955eb2e80..b431b3f92d 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestCallAction.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestCallAction.kt @@ -73,7 +73,7 @@ class RestCallAction( * * TODO check if it could be used to handle issue in BackwardLinkReference */ - private var weakReference: RestCallAction? = null + var weakReference: RestCallAction? = null ) : HttpWsAction(auth, isCleanUp, parameters) { companion object{ @@ -164,78 +164,8 @@ class RestCallAction( return path.resolveOnlyPath(parameters) } - /** - * Make sure that the path params are resolved to the same concrete values of "other". - * Note: "this" can be just an ancestor of "other". - * This function takes care when path elements are dynamically handled based on - * results of previous calls (eg a POST creating a resource). * - * - **/ - fun bindToSamePathResolution(other: RestCallAction) { - if (!this.path.isSameOrAncestorOf(other.path)) { - throw IllegalArgumentException("Cannot bind 2 different unrelated paths to the same path resolution: " + - "${this.path} vs ${other.path}") - } - for (i in parameters.indices) { - val target = parameters[i] - if (target is PathParam) { - val k = other.parameters.find { p -> p is PathParam && p.name == target.name }!! - /* - Note: even if they are referring to same path variable, it does not mean that - necessarily they are represented with the same type of gene, eg., typically a StringGene. - For example, they could be a ChoiceGene when dealing with "examples" or Regex when having patterns - only defined on some endpoints - */ - val g = parameters[i].primaryGene() - g.copyValueFrom(k.primaryGene()) - g.forceNewTaints() - } - } - if(this.path.isEquivalent(other.path)) { - //if pointing to the same resource, make sure to handle dynamic resource creation - //TODO does it make sense to do it even for ancestor paths??? likely not... but not 100% sure - this.usePreviousLocationId = other.usePreviousLocationId - this.weakReference = other.weakReference - } - } - - /** - * Check if the resulting path of this action is the same of [other], taking into account dynamic info - */ - fun usingSameResolvedPath(other: RestCallAction) : Boolean{ - if(!this.path.isEquivalent(other.path)){ - return false - } - - /* - Consider - 1) /items/{id}/{x=a} - 1) /items/{id}/{x=b} - TODO this should result in different, even if sharing same resource {id} - */ - if(this.usePreviousLocationId != null && this.usePreviousLocationId == other.usePreviousLocationId){ - return true - } - if(this.weakReference != null && this.weakReference == other.weakReference){ - return true - } - - return this.resolvedOnlyPath() == other.resolvedOnlyPath() - } - - /** - * When the URL path of this endpoint is resolved, would it be a (strict) parent from the other action - */ - fun isResolvedParentPath(other: RestCallAction): Boolean { - val parent = this.resolvedOnlyPath() // TODO deal with dynamic info - val child = other.resolvedOnlyPath() - if(parent.length >= child.length) { - return false - } - return child.startsWith(parent) - } /** diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/HttpSemanticsOracle.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/HttpSemanticsOracle.kt index 35f0caadfe..b3a323cddc 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/HttpSemanticsOracle.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/HttpSemanticsOracle.kt @@ -9,6 +9,7 @@ import org.evomaster.core.problem.rest.param.BodyParam import org.evomaster.core.problem.rest.schema.RestSchema import org.evomaster.core.problem.rest.schema.SchemaUtils import org.evomaster.core.problem.rest.StatusGroup +import org.evomaster.core.problem.rest.builder.DynamicPathUtils import org.evomaster.core.search.action.ActionResult import org.evomaster.core.search.gene.ObjectGene import org.evomaster.core.search.gene.utils.GeneUtils @@ -35,7 +36,7 @@ object HttpSemanticsOracle { } //on same resource - if(! first.usingSameResolvedPath(second)){ + if(! DynamicPathUtils.doesResolveToSamePath(first,second)){ return false } @@ -93,7 +94,8 @@ object HttpSemanticsOracle { } //check path resolution - if(!before.usingSameResolvedPath(delete) || !after.usingSameResolvedPath(delete)) { + if(!DynamicPathUtils.doesResolveToSamePath(before,delete) + || !DynamicPathUtils.doesResolveToSamePath(after, delete)) { return NonWorkingDeleteResult() } @@ -139,7 +141,8 @@ object HttpSemanticsOracle { } // all three must be on the same resolved path - if(!before.usingSameResolvedPath(modify) || !after.usingSameResolvedPath(modify)) { + if(!DynamicPathUtils.doesResolveToSamePath(before,modify) + || !DynamicPathUtils.doesResolveToSamePath(after,modify)) { return false } @@ -235,7 +238,8 @@ object HttpSemanticsOracle { if(modify.verb != HttpVerb.PUT && modify.verb != HttpVerb.PATCH) return false if(after.verb != HttpVerb.GET) return false - if(!before.usingSameResolvedPath(modify) || !after.usingSameResolvedPath(modify)) return false + if(!DynamicPathUtils.doesResolveToSamePath(before,modify) + || !DynamicPathUtils.doesResolveToSamePath(after,modify)) return false val resBefore = actionResults.find { it.sourceLocalId == before.getLocalId() } as RestCallResult? ?: return false @@ -337,7 +341,7 @@ object HttpSemanticsOracle { if (put.verb != HttpVerb.PUT) return null if (get.verb != HttpVerb.GET) return null - if (!put.usingSameResolvedPath(get)) return null + if (!DynamicPathUtils.doesResolveToSamePath(put,get)) return null if (put.auth.isDifferentFrom(get.auth)) return null val resPut = actionResults.find { it.sourceLocalId == put.getLocalId() } as RestCallResult? @@ -594,7 +598,7 @@ object HttpSemanticsOracle { if (get.verb != HttpVerb.GET) return false if (put.verb != HttpVerb.PUT) return false - if (!get.usingSameResolvedPath(put)) return false + if (!DynamicPathUtils.doesResolveToSamePath(get,put)) return false if (get.auth.isDifferentFrom(put.auth)) return false @@ -638,11 +642,11 @@ object HttpSemanticsOracle { if (get1.verb != HttpVerb.GET || get2.verb != HttpVerb.GET) return false // both PUTs on same resolved path with same auth - if (!put1.usingSameResolvedPath(put2)) return false + if (!DynamicPathUtils.doesResolveToSamePath(put1,put2)) return false if (put1.auth.isDifferentFrom(put2.auth)) return false // both GETs on same resolved path with same auth - if (!get1.usingSameResolvedPath(get2)) return false + if (!DynamicPathUtils.doesResolveToSamePath(get1,get2)) return false if (get1.auth.isDifferentFrom(get2.auth)) return false val resPut1 = actionResults.find { it.sourceLocalId == put1.getLocalId() } as RestCallResult? @@ -755,7 +759,8 @@ object HttpSemanticsOracle { if (patch.verb != HttpVerb.PATCH) return false if (after.verb != HttpVerb.GET) return false - if (!before.usingSameResolvedPath(patch) || !after.usingSameResolvedPath(patch)) return false + if (!DynamicPathUtils.doesResolveToSamePath(before,patch) + || !DynamicPathUtils.doesResolveToSamePath(after,patch)) return false // the two GETs must use the same auth for a meaningful state comparison if (before.auth.isDifferentFrom(after.auth)) return false diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/RestSecurityOracle.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/RestSecurityOracle.kt index e0be7528e9..6223cc2ff1 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/RestSecurityOracle.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/RestSecurityOracle.kt @@ -5,12 +5,10 @@ import com.webfuzzing.commons.faults.FaultCategory import org.apache.http.HttpStatus import org.evomaster.core.EMConfig import org.evomaster.core.problem.enterprise.DetectedFault -import org.evomaster.core.problem.enterprise.ExperimentalFaultCategory import org.evomaster.core.problem.enterprise.SampleType -import org.evomaster.core.problem.enterprise.auth.NoAuth import org.evomaster.core.problem.httpws.HttpWsCallResult import org.evomaster.core.problem.rest.* -import org.evomaster.core.problem.rest.builder.CreateResourceUtils +import org.evomaster.core.problem.rest.builder.DynamicPathUtils import org.evomaster.core.problem.rest.data.* import org.evomaster.core.problem.rest.service.CallGraphService import org.evomaster.core.problem.rest.service.RestSecurityBuilder @@ -771,7 +769,7 @@ class RestSecurityOracle { //FIXME i don't think it is correct, as ignoring dynamic info? //TODO need tests for it val matching = verifiers.filter { - it.isResolvedParentPath(notfound) + DynamicPathUtils.isResolvedParentPath(it,notfound) && ! notfound.auth.isDifferentFrom(it.auth) } @@ -845,7 +843,7 @@ class RestSecurityOracle { // first check that they all refer to the same endpoint val conditionForEndpointEquivalence = - CreateResourceUtils.doesResolveToSamePath(lastAction, secondLastAction) + DynamicPathUtils.doesResolveToSamePath(lastAction, secondLastAction) if (!conditionForEndpointEquivalence) { return false diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/resource/RestResourceNode.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/resource/RestResourceNode.kt index c74c362d49..19aac9fc02 100755 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/resource/RestResourceNode.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/resource/RestResourceNode.kt @@ -3,11 +3,10 @@ package org.evomaster.core.problem.rest.resource import org.evomaster.core.Lazy import org.evomaster.core.sql.SqlAction import org.evomaster.core.logging.LoggingUtil -import org.evomaster.core.problem.rest.* import org.evomaster.core.problem.rest.param.BodyParam import org.evomaster.core.problem.api.param.Param import org.evomaster.core.problem.enterprise.EnterpriseActionGroup -import org.evomaster.core.problem.rest.builder.CreateResourceUtils +import org.evomaster.core.problem.rest.builder.DynamicPathUtils import org.evomaster.core.problem.rest.data.HttpVerb import org.evomaster.core.problem.rest.data.RestCallAction import org.evomaster.core.problem.rest.data.RestCallResult @@ -477,7 +476,7 @@ open class RestResourceNode( if (actions.size == 1) return actions.first() (1 until actions.size).forEach { i-> - CreateResourceUtils.linkDynamicCreateResource(actions[i-1], actions[i]) + DynamicPathUtils.linkDynamicCreateResource(actions[i-1], actions[i]) } return actions.last() @@ -517,7 +516,7 @@ open class RestResourceNode( if (ats.size == 2){ val action = createActionByVerb(ats[1], randomness) if (lastPost != null) - CreateResourceUtils.linkDynamicCreateResource(lastPost, action) + DynamicPathUtils.linkDynamicCreateResource(lastPost, action) results.add(action) }else if (ats.size > 2){ throw IllegalStateException("the size of action with $template should be less than 2, but it is ${ats.size}") @@ -527,7 +526,7 @@ open class RestResourceNode( if (ats.last() == HttpVerb.PATCH && results.size +1 <= maxTestSize && randomness.nextBoolean(PROB_EXTRA_PATCH)){ val second = results.last().copyKeepingSameWeakRef() if (lastPost != null) - CreateResourceUtils.linkDynamicCreateResource(lastPost, second) + DynamicPathUtils.linkDynamicCreateResource(lastPost, second) results.add(second) } diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/HttpSemanticsService.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/HttpSemanticsService.kt index d473aedac0..f39569ed87 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/HttpSemanticsService.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/HttpSemanticsService.kt @@ -8,6 +8,7 @@ import org.evomaster.core.problem.enterprise.SampleType import org.evomaster.core.problem.httpws.auth.HttpWsAuthenticationInfo import org.evomaster.core.problem.httpws.auth.HttpWsNoAuth import org.evomaster.core.problem.rest.* +import org.evomaster.core.problem.rest.builder.DynamicPathUtils import org.evomaster.core.problem.rest.builder.RestIndividualSelectorUtils import org.evomaster.core.problem.rest.data.HttpVerb import org.evomaster.core.problem.rest.data.RestCallAction @@ -274,7 +275,9 @@ class HttpSemanticsService : TimeBoxedPhase{ //does it have a previous GET call on it in previous action? val hasPreviousGet = okDelete.size() > 1 && actions[actions.size - 2].let { - it.verb == HttpVerb.GET && it.path == del.path && it.usingSameResolvedPath(last) + it.verb == HttpVerb.GET + && it.path == del.path + && DynamicPathUtils.doesResolveToSamePath(it,last) && ! it.auth.isDifferentFrom(last.auth) } @@ -284,7 +287,7 @@ class HttpSemanticsService : TimeBoxedPhase{ val getOp = getDef.copy() as RestCallAction getOp.doInitialize(randomness) getOp.forceNewTaints() - getOp.bindToSamePathResolution(last) + DynamicPathUtils.bindToSamePathResolution(getOp,last) getOp.auth = last.auth //TODO: what if the GET needs WM handling? okDelete.addMainActionInEmptyEnterpriseGroup(actions.size - 1, getOp) @@ -619,7 +622,7 @@ class HttpSemanticsService : TimeBoxedPhase{ putAction.resetLocalIdRecursively() putAction.forceNewTaints() putAction.auth = getAction.auth - putAction.bindToSamePathResolution(getAction) + DynamicPathUtils.bindToSamePathResolution(putAction, getAction) ind.addMainActionInEmptyEnterpriseGroup(-1, putAction) prepareEvaluateAndSave(ind) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/RestIndividualBuilder.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/RestIndividualBuilder.kt index 4a094dc2dd..b3abee388a 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/RestIndividualBuilder.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/RestIndividualBuilder.kt @@ -1,9 +1,8 @@ package org.evomaster.core.problem.rest.service import com.google.inject.Inject -import org.evomaster.core.problem.enterprise.EnterpriseActionGroup import org.evomaster.core.problem.rest.* -import org.evomaster.core.problem.rest.builder.CreateResourceUtils +import org.evomaster.core.problem.rest.builder.DynamicPathUtils import org.evomaster.core.problem.rest.builder.RestIndividualSelectorUtils import org.evomaster.core.problem.rest.data.HttpVerb import org.evomaster.core.problem.rest.data.RestCallAction @@ -11,10 +10,8 @@ import org.evomaster.core.problem.rest.data.RestIndividual import org.evomaster.core.problem.rest.data.RestPath import org.evomaster.core.problem.rest.service.sampler.AbstractRestSampler import org.evomaster.core.search.EvaluatedIndividual -import org.evomaster.core.search.action.EnvironmentAction import org.evomaster.core.search.service.Randomness import org.evomaster.core.sql.SqlAction -import javax.ws.rs.POST /** @@ -207,7 +204,7 @@ class RestIndividualBuilder { } res.auth = target.auth res.forceNewTaints() - res.bindToSamePathResolution(target) + DynamicPathUtils.bindToSamePathResolution(res, target) return res } @@ -245,9 +242,9 @@ class RestIndividualBuilder { res.auth = previous.auth res.forceNewTaints() if(res.path.isEquivalent(previous.path)) { - res.bindToSamePathResolution(previous) + DynamicPathUtils.bindToSamePathResolution(res,previous) } - CreateResourceUtils.linkDynamicCreateResource(previous, res) + DynamicPathUtils.linkDynamicCreateResource(previous, res) return res } @@ -387,7 +384,7 @@ class RestIndividualBuilder { Once the create is fully initialized, need to fix links with target */ - CreateResourceUtils.linkDynamicCreateResource(create, target) + DynamicPathUtils.linkDynamicCreateResource(create, target) return true } diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/RestSecurityBuilder.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/RestSecurityBuilder.kt index 19170a8f44..f971edc087 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/RestSecurityBuilder.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/RestSecurityBuilder.kt @@ -7,16 +7,14 @@ import javax.annotation.PostConstruct import org.evomaster.core.logging.LoggingUtil import org.evomaster.core.problem.enterprise.DetectedFaultUtils -import org.evomaster.core.problem.enterprise.ExperimentalFaultCategory import org.evomaster.core.problem.enterprise.SampleType import org.evomaster.core.problem.enterprise.auth.AuthSettings -import org.evomaster.core.problem.enterprise.auth.NoAuth import org.evomaster.core.problem.externalservice.HostnameResolutionAction import org.evomaster.core.problem.httpws.HttpWsCallResult import org.evomaster.core.problem.httpws.auth.HttpWsAuthenticationInfo import org.evomaster.core.problem.httpws.auth.HttpWsNoAuth import org.evomaster.core.problem.rest.* -import org.evomaster.core.problem.rest.builder.CreateResourceUtils +import org.evomaster.core.problem.rest.builder.DynamicPathUtils import org.evomaster.core.problem.rest.builder.RestIndividualSelectorUtils import org.evomaster.core.problem.rest.data.* import org.evomaster.core.problem.rest.oracle.RestSecurityOracle.Companion.SQLI_PAYLOADS @@ -899,7 +897,7 @@ class RestSecurityBuilder : TimeBoxedPhase { parentGetAction.auth = lastAuth // Bind to the same path params from the 404 action to ensure same IDs //FIXME this would currently not work for dynamic parameters - parentGetAction.bindToSamePathResolution(action404) + DynamicPathUtils.bindToSamePathResolution(parentGetAction,action404) final.addResourceCall( restCalls = RestResourceCalls( @@ -1517,7 +1515,7 @@ class RestSecurityBuilder : TimeBoxedPhase { creationAction: RestCallAction, targetAction: RestCallAction ) { - CreateResourceUtils.linkDynamicCreateResource(creationAction, targetAction) + DynamicPathUtils.linkDynamicCreateResource(creationAction, targetAction) if (creationAction.path.isEquivalent(targetAction.path)) { targetAction.bindBasedOn(creationAction.path, creationAction.parameters.filterIsInstance(), null) } diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/BlackBoxRestFitness.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/BlackBoxRestFitness.kt index 5e4aeee0c5..3532f60f73 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/BlackBoxRestFitness.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/BlackBoxRestFitness.kt @@ -4,21 +4,18 @@ import org.evomaster.client.java.controller.api.dto.AdditionalInfoDto import org.evomaster.core.problem.httpws.HttpWsCallResult import org.evomaster.core.problem.httpws.auth.AuthUtils import org.evomaster.core.problem.rest.StatusGroup -import org.evomaster.core.problem.rest.builder.CreateResourceUtils +import org.evomaster.core.problem.rest.builder.DynamicPathUtils import org.evomaster.core.problem.rest.builder.RestIndividualSelectorUtils import org.evomaster.core.problem.rest.data.HttpVerb import org.evomaster.core.problem.rest.data.RestCallAction import org.evomaster.core.problem.rest.data.RestCallResult import org.evomaster.core.problem.rest.data.RestIndividual -import org.evomaster.core.problem.rest.service.CallGraphService import org.evomaster.core.search.action.ActionResult import org.evomaster.core.search.EvaluatedIndividual import org.evomaster.core.search.FitnessValue -import org.evomaster.core.search.StructuralElement import org.evomaster.core.utils.CollectionUtils import org.slf4j.Logger import org.slf4j.LoggerFactory -import javax.inject.Inject import javax.ws.rs.core.NewCookie @@ -117,7 +114,7 @@ class BlackBoxRestFitness : RestFitness() { val toHandle = createWithPost.plus( CollectionUtils.deDuplicate(createWithPut){x,y -> //if more than 1 PUT resolve to same location, just need to handle it once - CreateResourceUtils.doesResolveToSamePath(x.action as RestCallAction, y.action as RestCallAction) + DynamicPathUtils.doesResolveToSamePath(x.action as RestCallAction, y.action as RestCallAction) } ) @@ -141,7 +138,7 @@ class BlackBoxRestFitness : RestFitness() { val existing = mainActions.filterIndexed { i, a -> i > index && a.verb == HttpVerb.DELETE && a.path.isEquivalent(delete.path) - && CreateResourceUtils.doesResolveToSamePath(a,delete) + && DynamicPathUtils.doesResolveToSamePath(a,delete) } if(existing.isNotEmpty()){ continue diff --git a/core/src/test/kotlin/org/evomaster/core/problem/rest/RestActionBuilderV3Test.kt b/core/src/test/kotlin/org/evomaster/core/problem/rest/RestActionBuilderV3Test.kt index a5358e0ee1..56ac900bb6 100644 --- a/core/src/test/kotlin/org/evomaster/core/problem/rest/RestActionBuilderV3Test.kt +++ b/core/src/test/kotlin/org/evomaster/core/problem/rest/RestActionBuilderV3Test.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import io.swagger.parser.OpenAPIParser import org.evomaster.client.java.instrumentation.shared.ClassToSchemaUtils.OPENAPI_REF_PATH import org.evomaster.core.EMConfig +import org.evomaster.core.problem.rest.builder.DynamicPathUtils import org.evomaster.core.problem.rest.builder.RestActionBuilderV3 import org.evomaster.core.problem.rest.data.HttpVerb import org.evomaster.core.problem.rest.data.RestCallAction @@ -2026,7 +2027,7 @@ class RestActionBuilderV3Test{ // only 1 option in the enum assertEquals("/v2/api/foo/data", child.resolvedPath()) - parent.bindToSamePathResolution(child) + DynamicPathUtils.bindToSamePathResolution(parent, child) assertEquals("/v2/api/foo", parent.resolvedPath()) } @@ -2046,7 +2047,7 @@ class RestActionBuilderV3Test{ val isSet = x.unsafeSetFromStringValue(target) assertTrue(isSet) - parent.bindToSamePathResolution(child) + DynamicPathUtils.bindToSamePathResolution(parent, child) assertEquals("/v2/api/$target", parent.resolvedPath()) } @@ -2065,7 +2066,7 @@ class RestActionBuilderV3Test{ val isSet = x.unsafeSetFromStringValue(target) assertTrue(isSet) - parent.bindToSamePathResolution(child) + DynamicPathUtils.bindToSamePathResolution(parent, child) assertEquals("/v2/api/$target", parent.resolvedPath()) } From 75f933c328049a98b76cf967679ade9fdebee146 Mon Sep 17 00:00:00 2001 From: arcuri82 Date: Tue, 4 Aug 2026 12:58:56 +0200 Subject: [PATCH 4/6] fixed missing edge case --- .../org/evomaster/core/problem/rest/builder/DynamicPathUtils.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/DynamicPathUtils.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/DynamicPathUtils.kt index 9927809f85..a55682c67f 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/DynamicPathUtils.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/DynamicPathUtils.kt @@ -71,6 +71,7 @@ object DynamicPathUtils { // the target (eg GET) needs to use the location of first POST, or more correctly // the same location used for the last POST (in case there is a deeper chain) after.usePreviousLocationId = before.usePreviousLocationId + after.weakReference = before.weakReference } } From 89618299d13a571b34f2a2b75999fe74aa291351 Mon Sep 17 00:00:00 2001 From: arcuri82 Date: Tue, 4 Aug 2026 13:51:18 +0200 Subject: [PATCH 5/6] fixing use of query params in added GETs --- .../problem/rest/builder/DynamicPathUtils.kt | 25 +++++++++++++++++++ .../rest/service/HttpSemanticsService.kt | 11 ++++++++ .../core/search/gene/wrapper/OptionalGene.kt | 4 +-- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/DynamicPathUtils.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/DynamicPathUtils.kt index a55682c67f..4dd893f01b 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/DynamicPathUtils.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/DynamicPathUtils.kt @@ -3,6 +3,8 @@ package org.evomaster.core.problem.rest.builder import org.evomaster.core.problem.rest.data.HttpVerb import org.evomaster.core.problem.rest.data.RestCallAction import org.evomaster.core.problem.rest.param.PathParam +import org.evomaster.core.problem.rest.param.QueryParam +import org.evomaster.core.search.gene.wrapper.OptionalGene /** @@ -151,4 +153,27 @@ object DynamicPathUtils { } return child.startsWith(parent) } + + /** + * Try to force [current] to use the same query params as [other], if possible. + * Note that the 2 actions could be on DIFFERENT endpoints. + * Equivalence is based on query param names. + */ + fun forceSameQueryParams(current: RestCallAction, other: RestCallAction) { + + val x = current.parameters.filterIsInstance() + val y = other.parameters.filterIsInstance() + + x.forEach { p -> + val k = y.find { p.name == it.name } + if(k == null || !k.isActive()) { + p.primaryGene().getWrappedGene(OptionalGene::class.java)?.forbidSelection() + } else { + val copied = p.primaryGene().copyValueFrom(k.primaryGene()) + if(!copied){ + p.primaryGene().getWrappedGene(OptionalGene::class.java)?.forbidSelection() + } + } + } + } } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/HttpSemanticsService.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/HttpSemanticsService.kt index f39569ed87..76a38db5cb 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/HttpSemanticsService.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/HttpSemanticsService.kt @@ -295,6 +295,7 @@ class HttpSemanticsService : TimeBoxedPhase{ } else { actions[actions.size - 2] } + DynamicPathUtils.forceSameQueryParams(previous, last) //we want to have same GET call before and after the 2xx DELETE val after = previous.copy() as RestCallAction @@ -378,9 +379,11 @@ class HttpSemanticsService : TimeBoxedPhase{ val last = actions.last() // the PUT/PATCH [404] val getBefore = builder.createBoundActionFor(getDef, last) + DynamicPathUtils.forceSameQueryParams(getBefore, last) ind.addMainActionInEmptyEnterpriseGroup(actions.size - 1, getBefore) val getAfter = builder.createBoundActionFor(getDef, last) + DynamicPathUtils.forceSameQueryParams(getAfter, last) ind.addMainActionInEmptyEnterpriseGroup(-1, getAfter) prepareEvaluateAndSave(ind) @@ -469,6 +472,9 @@ class HttpSemanticsService : TimeBoxedPhase{ val getAfter = builder.createBoundActionFor(getDef, getAction) + DynamicPathUtils.forceSameQueryParams(modifyCopy, getAction) + DynamicPathUtils.forceSameQueryParams(getAfter, getAction) + ind.addMainActionInEmptyEnterpriseGroup(action = modifyCopy) ind.addMainActionInEmptyEnterpriseGroup(action = getAfter) @@ -511,6 +517,7 @@ class HttpSemanticsService : TimeBoxedPhase{ val last = ind.seeMainExecutableActions().last() // the PUT 2xx val getAfter = builder.createBoundActionFor(getDef, last) + DynamicPathUtils.forceSameQueryParams(getAfter, last) ind.addMainActionInEmptyEnterpriseGroup(-1, getAfter) prepareEvaluateAndSave(ind) @@ -560,10 +567,12 @@ class HttpSemanticsService : TimeBoxedPhase{ } val getBefore = builder.createBoundActionFor(getDef, patch) creator?.saveAndLinkLocationTo(getBefore) + DynamicPathUtils.forceSameQueryParams(getBefore, patch) ind.addMainActionInEmptyEnterpriseGroup(size - 1, getBefore) val getAfter = builder.createBoundActionFor(getDef, patch) creator?.saveAndLinkLocationTo(getAfter) + DynamicPathUtils.forceSameQueryParams(getAfter, patch) ind.addMainActionInEmptyEnterpriseGroup(-1, getAfter) val ei = prepareEvaluateAndSave(ind) @@ -672,6 +681,7 @@ class HttpSemanticsService : TimeBoxedPhase{ // GET after the 1st PUT: bound to firstPut's resolved path and auth val get1 = builder.createBoundActionFor(getDef, firstPut) + DynamicPathUtils.forceSameQueryParams(get1, firstPut) // 2nd PUT: exact copy of the 1st PUT (same body) to test idempotency of that request val secondPut = firstPut.copy() as RestCallAction @@ -679,6 +689,7 @@ class HttpSemanticsService : TimeBoxedPhase{ // GET after the 2nd PUT val get2 = builder.createBoundActionFor(getDef, firstPut) + DynamicPathUtils.forceSameQueryParams(get2, firstPut) ind.addMainActionInEmptyEnterpriseGroup(-1, get1) ind.addMainActionInEmptyEnterpriseGroup(-1, secondPut) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/wrapper/OptionalGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/wrapper/OptionalGene.kt index bfddef59db..e4500842cb 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/wrapper/OptionalGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/wrapper/OptionalGene.kt @@ -45,7 +45,7 @@ class OptionalGene(name: String, init { - if(searchPercentageActive < 0 || searchPercentageActive > 1){ + if(searchPercentageActive !in 0.0..1.0){ throw IllegalArgumentException("Invalid searchPercentageActive value: $searchPercentageActive") } } @@ -116,7 +116,7 @@ class OptionalGene(name: String, return randomness.nextBoolean(INACTIVE) } - if (additionalGeneMutationInfo?.impact is OptionalGeneImpact){ + if (additionalGeneMutationInfo.impact is OptionalGeneImpact){ //we only set 'active' false from true when the mutated times is more than 5 and its impact times of a falseValue is more than 1.5 times of a trueValue. val inactive = additionalGeneMutationInfo.impact.activeImpact.determinateSelect( minManipulatedTimes = 5, From 5404e0357d6b13f9bcee351a2b67b8420b8b7f10 Mon Sep 17 00:00:00 2001 From: arcuri82 Date: Wed, 5 Aug 2026 12:15:42 +0200 Subject: [PATCH 6/6] test case for forceSameQueryParams --- .../dynamicpath/DynamicPathApplication.kt | 46 ++++++++++++++++ .../dynamicpath/DynamicPathController.kt | 6 +++ .../rest/dynamicpath/DynamicPathTest.kt | 52 +++++++++++++++++++ .../problem/rest/builder/DynamicPathUtils.kt | 22 ++++---- 4 files changed, 115 insertions(+), 11 deletions(-) create mode 100644 core-tests/integration-tests/core-it/src/main/kotlin/bar/examples/it/spring/dynamicpath/DynamicPathApplication.kt create mode 100644 core-tests/integration-tests/core-it/src/test/kotlin/bar/examples/it/spring/dynamicpath/DynamicPathController.kt create mode 100644 core-tests/integration-tests/core-it/src/test/kotlin/org/evomaster/core/problem/rest/dynamicpath/DynamicPathTest.kt diff --git a/core-tests/integration-tests/core-it/src/main/kotlin/bar/examples/it/spring/dynamicpath/DynamicPathApplication.kt b/core-tests/integration-tests/core-it/src/main/kotlin/bar/examples/it/spring/dynamicpath/DynamicPathApplication.kt new file mode 100644 index 0000000000..c4e5c699dd --- /dev/null +++ b/core-tests/integration-tests/core-it/src/main/kotlin/bar/examples/it/spring/dynamicpath/DynamicPathApplication.kt @@ -0,0 +1,46 @@ +package bar.examples.it.spring.dynamicpath + +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RequestMapping(path = ["/api/dynamicpath"]) +@RestController +open class DynamicPathApplication { + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(DynamicPathApplication::class.java, *args) + } + } + + + @PutMapping(path = ["/x/{id}"]) + fun putX(@RequestBody body: String, + @PathVariable id: String, + @RequestParam(required = false) foo: String? + ): ResponseEntity { + + return ResponseEntity.ok().body("OK") + } + + @GetMapping(path = ["/x/{id}"]) + fun getX(@PathVariable id: String, + @RequestParam(required = false) bar: Boolean?, + @RequestParam(required = false) foo: String?, + @RequestParam(required = true) k: Boolean + ): ResponseEntity{ + + return ResponseEntity.ok().body("OK") + } +} \ No newline at end of file diff --git a/core-tests/integration-tests/core-it/src/test/kotlin/bar/examples/it/spring/dynamicpath/DynamicPathController.kt b/core-tests/integration-tests/core-it/src/test/kotlin/bar/examples/it/spring/dynamicpath/DynamicPathController.kt new file mode 100644 index 0000000000..eacea0424b --- /dev/null +++ b/core-tests/integration-tests/core-it/src/test/kotlin/bar/examples/it/spring/dynamicpath/DynamicPathController.kt @@ -0,0 +1,6 @@ +package bar.examples.it.spring.dynamicpath + +import bar.examples.it.spring.SpringController + + +class DynamicPathController : SpringController(DynamicPathApplication::class.java) diff --git a/core-tests/integration-tests/core-it/src/test/kotlin/org/evomaster/core/problem/rest/dynamicpath/DynamicPathTest.kt b/core-tests/integration-tests/core-it/src/test/kotlin/org/evomaster/core/problem/rest/dynamicpath/DynamicPathTest.kt new file mode 100644 index 0000000000..0184eb032f --- /dev/null +++ b/core-tests/integration-tests/core-it/src/test/kotlin/org/evomaster/core/problem/rest/dynamicpath/DynamicPathTest.kt @@ -0,0 +1,52 @@ +package org.evomaster.core.problem.rest.dynamicpath + +import bar.examples.it.spring.body.BodyController +import bar.examples.it.spring.dynamicpath.DynamicPathController +import com.fasterxml.jackson.databind.ObjectMapper +import org.evomaster.core.problem.rest.IntegrationTestRestBase +import org.evomaster.core.problem.rest.builder.DynamicPathUtils +import org.evomaster.core.problem.rest.data.RestCallResult +import org.evomaster.core.problem.rest.param.BodyParam +import org.evomaster.core.search.gene.ObjectGene +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test + +class DynamicPathTest : IntegrationTestRestBase() { + + + companion object { + @BeforeAll + @JvmStatic + fun init() { + initClass(DynamicPathController()) + } + } + + + @Test + fun testForceSameQueryParams() { + + val pirTest = getPirToRest() + + val put = pirTest.fromVerbPath( + "put", "/api/dynamicpath/x/42", queryParams = mapOf("foo" to "abc") + )!! + + val get = pirTest.fromVerbPath( + "get", "/api/dynamicpath/x/77", queryParams = mapOf("foo" to "wrong","bar" to "true", "k" to "true") + )!! + + DynamicPathUtils.bindToSamePathResolution(get, put) + DynamicPathUtils.forceSameQueryParams(get, put) + + val x = put.resolvedPath() + val y = get.resolvedPath() + + assertEquals("/api/dynamicpath/x/42?foo=abc", x) + assertEquals("/api/dynamicpath/x/42?foo=abc&k=true", y) + } + +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/DynamicPathUtils.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/DynamicPathUtils.kt index 4dd893f01b..a4da162f65 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/DynamicPathUtils.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/DynamicPathUtils.kt @@ -106,37 +106,37 @@ object DynamicPathUtils { } /** - * Make sure that the path params are resolved to the same concrete values of "other". + * Make sure that the path params are of "this" [x] resolve to the same concrete values of "other" [y]. * Note: "this" can be just an ancestor of "other". * This function takes care when path elements are dynamically handled based on * results of previous calls (eg a POST creating a resource). * * **/ - fun bindToSamePathResolution(a: RestCallAction, b: RestCallAction) { - if (!a.path.isSameOrAncestorOf(b.path)) { + fun bindToSamePathResolution(x: RestCallAction, y: RestCallAction) { + if (!x.path.isSameOrAncestorOf(y.path)) { throw IllegalArgumentException("Cannot bind 2 different unrelated paths to the same path resolution: " + - "${a.path} vs ${b.path}") + "${x.path} vs ${y.path}") } - for (i in a.parameters.indices) { - val target = a.parameters[i] + for (i in x.parameters.indices) { + val target = x.parameters[i] if (target is PathParam) { - val k = b.parameters.find { p -> p is PathParam && p.name == target.name }!! + val k = y.parameters.find { p -> p is PathParam && p.name == target.name }!! /* Note: even if they are referring to same path variable, it does not mean that necessarily they are represented with the same type of gene, eg., typically a StringGene. For example, they could be a ChoiceGene when dealing with "examples" or Regex when having patterns only defined on some endpoints */ - val g = a.parameters[i].primaryGene() + val g = x.parameters[i].primaryGene() g.copyValueFrom(k.primaryGene()) g.forceNewTaints() } } - if(a.path.isEquivalent(b.path)) { + if(x.path.isEquivalent(y.path)) { //if pointing to the same resource, make sure to handle dynamic resource creation //TODO does it make sense to do it even for ancestor paths??? likely not... but not 100% sure - a.usePreviousLocationId = b.usePreviousLocationId - a.weakReference = b.weakReference + x.usePreviousLocationId = y.usePreviousLocationId + x.weakReference = y.weakReference } }