Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.foo.rest.examples.spring.openapi.v3.httporacle.failmodification

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.*


@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
@RequestMapping(path = ["/api/resources"])
@RestController
open class FailModificationApplication {

companion object {
@JvmStatic
fun main(args: Array<String>) {
SpringApplication.run(FailModificationApplication::class.java, *args)
}

private val data = mutableMapOf<Int, ResourceData>()

fun reset(){
data.clear()
}
}

data class ResourceData(
var name: String,
var value: Int
)

data class UpdateRequest(
val name: String?,
val value: Int?
)


@PostMapping
open fun create(@RequestBody body: ResourceData): ResponseEntity<ResourceData> {
val id = data.size + 1
data[id] = body.copy()
return ResponseEntity.status(201).body(data[id])
}

@GetMapping(path = ["/{id}"])
open fun get(@PathVariable("id") id: Int): ResponseEntity<ResourceData> {
val resource = data[id]
?: return ResponseEntity.status(404).build()
return ResponseEntity.status(200).body(resource)
}

@PutMapping(path = ["/{id}"])
open fun put(
@PathVariable("id") id: Int,
@RequestBody body: UpdateRequest
): ResponseEntity<Any> {

val resource = data[id]
?: return ResponseEntity.status(404).build()

// bug: modifies data even though it will return 4xx
if(body.name != null) {
resource.name = body.name
}
if(body.value != null) {
resource.value = body.value
}

// returns 400 Bad Request, but the data was already modified above
return ResponseEntity.status(400).body("Invalid request")
}

@PatchMapping(path = ["/{id}"])
open fun patch(
@PathVariable("id") id: Int,
@RequestBody body: UpdateRequest
): ResponseEntity<Any> {

val resource = data[id]
?: return ResponseEntity.status(404).build()

// correct: validation first, reject without modifying
if(body.name == null && body.value == null) {
return ResponseEntity.status(400).body("No fields to update")
}

// correct: does NOT modify data, just returns 4xx
return ResponseEntity.status(403).body("Forbidden")
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.foo.rest.examples.spring.openapi.v3.httporacle.failmodification

import com.foo.rest.examples.spring.openapi.v3.SpringController


class FailModificationController: SpringController(FailModificationApplication::class.java){

override fun resetStateOfSUT() {
FailModificationApplication.reset()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.evomaster.e2etests.spring.openapi.v3.httporacle.failmodification

import com.foo.rest.examples.spring.openapi.v3.httporacle.failmodification.FailModificationController
import org.evomaster.core.problem.enterprise.DetectedFaultUtils
import org.evomaster.core.problem.enterprise.ExperimentalFaultCategory
import org.evomaster.core.problem.rest.data.HttpVerb
import org.evomaster.e2etests.spring.openapi.v3.SpringTestBase
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.Test

class FailModificationEMTest : SpringTestBase(){

companion object {
@BeforeAll
@JvmStatic
fun init() {
initClass(FailModificationController())
}
}


@Test
fun testRunEM() {

runTestHandlingFlakyAndCompilation(
"FailedModificationEM",
500
) { args: MutableList<String> ->

setOption(args, "security", "false")
setOption(args, "schemaOracles", "false")
setOption(args, "httpOracles", "true")
setOption(args, "useExperimentalOracles", "true")

val solution = initAndRun(args)

assertTrue(solution.individuals.size >= 1)

val faults = DetectedFaultUtils.getDetectedFaultCategories(solution)
assertEquals(1, faults.size)
assertEquals(ExperimentalFaultCategory.HTTP_SIDE_EFFECTS_FAILED_MODIFICATION, faults.first())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ enum class ExperimentalFaultCategory(
"TODO"),
HTTP_REPEATED_CREATE_PUT(914, "Repeated PUT Creates Resource With 201", "repeatedCreatePut",
"TODO"),
HTTP_SIDE_EFFECTS_FAILED_MODIFICATION(915, "A failed PUT or PATCH must not change the resource", "sideEffectsFailedModification",
"TODO"),


//3xx: GraphQL
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package org.evomaster.core.problem.rest.oracle

import com.google.gson.JsonParser
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.param.BodyParam
import org.evomaster.core.problem.rest.StatusGroup
import org.evomaster.core.search.action.ActionResult
import org.evomaster.core.search.gene.ObjectGene

object HttpSemanticsOracle {

Expand Down Expand Up @@ -103,4 +107,141 @@ object HttpSemanticsOracle {

return NonWorkingDeleteResult(checkingDelete, nonWorking, delete.getName(), actions.size - 2)
}
}

fun hasSideEffectFailedModification(individual: RestIndividual,
actionResults: List<ActionResult>
): Boolean{

if(individual.size() < 3){
return false
}

val actions = individual.seeMainExecutableActions()

val before = actions[actions.size - 3] // GET (before state)
val modify = actions[actions.size - 2] // PUT or PATCH (failed modification)
val after = actions[actions.size - 1] // GET (after state)

// check verbs: GET, PUT|PATCH, GET
if(before.verb != HttpVerb.GET) {
return false
}
if(modify.verb != HttpVerb.PUT && modify.verb != HttpVerb.PATCH) {
return false
}
if(after.verb != HttpVerb.GET) {
return false
}

// all three must be on the same resolved path
if(!before.usingSameResolvedPath(modify) || !after.usingSameResolvedPath(modify)) {
return false
}

// auth should be consistent
if(before.auth.isDifferentFrom(modify.auth) || after.auth.isDifferentFrom(modify.auth)) {
return false
}

val resBefore = actionResults.find { it.sourceLocalId == before.getLocalId() } as RestCallResult?
?: return false
val resModify = actionResults.find { it.sourceLocalId == modify.getLocalId() } as RestCallResult?
?: return false
val resAfter = actionResults.find { it.sourceLocalId == after.getLocalId() } as RestCallResult?
?: return false

// before GET must be 2xx
if(!StatusGroup.G_2xx.isInGroup(resBefore.getStatusCode())) {
return false
}

// PUT/PATCH must have failed with 4xx
if(!StatusGroup.G_4xx.isInGroup(resModify.getStatusCode())) {
return false
}

// after GET must be 2xx
if(!StatusGroup.G_2xx.isInGroup(resAfter.getStatusCode())) {
return false
}

val bodyBefore = resBefore.getBody()
val bodyAfter = resAfter.getBody()

// if both are null/empty, no side-effect detected
if(bodyBefore.isNullOrEmpty() && bodyAfter.isNullOrEmpty()) {
return false
}

// extract the field names sent in the PUT/PATCH request body
val modifiedFieldNames = extractModifiedFieldNames(modify)

// if we can identify specific fields, compare only those to avoid false positives from timestamps etc.
if(modifiedFieldNames.isNotEmpty()
&& !bodyBefore.isNullOrEmpty()
&& !bodyAfter.isNullOrEmpty()) {
return hasChangedModifiedFields(bodyBefore, bodyAfter, modifiedFieldNames)
}

// otherwise compare entire bodies
return bodyBefore != bodyAfter
}

/**
* Extract field names from the PUT/PATCH request body.
* These are the fields that the client attempted to modify.
*/
private fun extractModifiedFieldNames(modify: RestCallAction): Set<String> {

val bodyParam = modify.parameters.find { it is BodyParam } as BodyParam?
?: return emptySet()

val gene = bodyParam.primaryGene()
val objectGene = gene.getWrappedGene(ObjectGene::class.java) as ObjectGene?
?: if (gene is ObjectGene) gene else null

if(objectGene == null){
return emptySet()
}

return objectGene.fields.map { it.name }.toSet()
}

/**
* Compare only the fields that were sent in the PUT/PATCH request.
* Returns true if any of those fields changed between before and after GET responses.
*/
internal fun hasChangedModifiedFields(
bodyBefore: String,
bodyAfter: String,
fieldNames: Set<String>
): Boolean {

try {
val jsonBefore = JsonParser.parseString(bodyBefore)
val jsonAfter = JsonParser.parseString(bodyAfter)

if(!jsonBefore.isJsonObject || !jsonAfter.isJsonObject){
// not JSON objects, fallback to full comparison
return bodyBefore != bodyAfter
}

val objBefore = jsonBefore.asJsonObject
val objAfter = jsonAfter.asJsonObject

for(field in fieldNames){
val valueBefore = objBefore.get(field)
val valueAfter = objAfter.get(field)

if(valueBefore != valueAfter){
return true
}
}

return false
} catch (e: Exception) {
// JSON parsing failed, fallback to full comparison
return bodyBefore != bodyAfter
}
}
}
Loading