Skip to content

Commit b6726ec

Browse files
authored
Merge pull request #1543 from WebFuzzing/non-idempotent-put
Non idempotent put
2 parents ee82d5b + e7b59fb commit b6726ec

16 files changed

Lines changed: 715 additions & 16 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package com.foo.rest.examples.spring.openapi.v3.httporacle.nonidempotentput.json
2+
3+
import org.springframework.boot.SpringApplication
4+
import org.springframework.boot.autoconfigure.SpringBootApplication
5+
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
6+
import org.springframework.http.ResponseEntity
7+
import org.springframework.web.bind.annotation.GetMapping
8+
import org.springframework.web.bind.annotation.PathVariable
9+
import org.springframework.web.bind.annotation.PostMapping
10+
import org.springframework.web.bind.annotation.PutMapping
11+
import org.springframework.web.bind.annotation.RequestBody
12+
import org.springframework.web.bind.annotation.RequestMapping
13+
import org.springframework.web.bind.annotation.RestController
14+
15+
16+
@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
17+
@RequestMapping(path = ["/api/accounts"])
18+
@RestController
19+
open class HttpNonIdempotentPutApplication {
20+
21+
companion object {
22+
@JvmStatic
23+
fun main(args: Array<String>) {
24+
SpringApplication.run(HttpNonIdempotentPutApplication::class.java, *args)
25+
}
26+
27+
private val data = mutableMapOf<Int, AccountData>()
28+
29+
fun reset(){
30+
data.clear()
31+
}
32+
}
33+
34+
data class AccountData(
35+
var balance: Int
36+
)
37+
38+
data class DepositRequest(
39+
val amount: Int
40+
)
41+
42+
43+
@PostMapping()
44+
open fun create(@RequestBody body: AccountData): ResponseEntity<AccountData> {
45+
val id = data.size + 1
46+
data[id] = body.copy()
47+
return ResponseEntity.status(201).body(data[id])
48+
}
49+
50+
@GetMapping(path = ["/{id}"])
51+
open fun get(@PathVariable("id") id: Int): ResponseEntity<AccountData> {
52+
val resource = data[id]
53+
?: return ResponseEntity.status(404).build()
54+
return ResponseEntity.status(200).body(resource)
55+
}
56+
57+
@PutMapping(path = ["/{id}/deposit"])
58+
open fun deposit(
59+
@PathVariable("id") id: Int,
60+
@RequestBody body: DepositRequest
61+
): ResponseEntity<AccountData> {
62+
63+
val resource = data[id]
64+
?: return ResponseEntity.status(404).build()
65+
66+
// wrong: PUT must be idempotent, but each call accumulates the deposit
67+
resource.balance += body.amount
68+
69+
return ResponseEntity.status(200).body(resource)
70+
}
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.foo.rest.examples.spring.openapi.v3.httporacle.nonidempotentput.urlencoded
2+
3+
import org.springframework.boot.SpringApplication
4+
import org.springframework.boot.autoconfigure.SpringBootApplication
5+
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
6+
import org.springframework.http.MediaType
7+
import org.springframework.http.ResponseEntity
8+
import org.springframework.web.bind.annotation.GetMapping
9+
import org.springframework.web.bind.annotation.ModelAttribute
10+
import org.springframework.web.bind.annotation.PathVariable
11+
import org.springframework.web.bind.annotation.PostMapping
12+
import org.springframework.web.bind.annotation.PutMapping
13+
import org.springframework.web.bind.annotation.RequestMapping
14+
import org.springframework.web.bind.annotation.RestController
15+
16+
17+
@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
18+
@RequestMapping(path = ["/api/accounts"])
19+
@RestController
20+
open class HttpNonIdempotentPutUrlEncodedApplication {
21+
22+
companion object {
23+
@JvmStatic
24+
fun main(args: Array<String>) {
25+
SpringApplication.run(HttpNonIdempotentPutUrlEncodedApplication::class.java, *args)
26+
}
27+
28+
private val data = mutableMapOf<Int, AccountData>()
29+
30+
fun reset(){
31+
data.clear()
32+
}
33+
}
34+
35+
open class AccountData(
36+
var balance: Int = 0
37+
)
38+
39+
open class DepositRequest(
40+
var amount: Int = 0
41+
)
42+
43+
44+
@PostMapping(
45+
consumes = [MediaType.APPLICATION_FORM_URLENCODED_VALUE],
46+
produces = [MediaType.APPLICATION_JSON_VALUE]
47+
)
48+
open fun create(@ModelAttribute body: AccountData): ResponseEntity<AccountData> {
49+
val id = data.size + 1
50+
data[id] = AccountData(balance = body.balance)
51+
return ResponseEntity.status(201).body(data[id])
52+
}
53+
54+
@GetMapping(
55+
path = ["/{id}"],
56+
produces = [MediaType.APPLICATION_JSON_VALUE]
57+
)
58+
open fun get(@PathVariable("id") id: Int): ResponseEntity<AccountData> {
59+
val resource = data[id]
60+
?: return ResponseEntity.status(404).build()
61+
return ResponseEntity.status(200).body(resource)
62+
}
63+
64+
@PutMapping(
65+
path = ["/{id}/deposit"],
66+
consumes = [MediaType.APPLICATION_FORM_URLENCODED_VALUE],
67+
produces = [MediaType.APPLICATION_JSON_VALUE]
68+
)
69+
open fun deposit(
70+
@PathVariable("id") id: Int,
71+
@ModelAttribute body: DepositRequest
72+
): ResponseEntity<AccountData> {
73+
74+
val resource = data[id]
75+
?: return ResponseEntity.status(404).build()
76+
77+
// wrong: PUT must be idempotent, but each call accumulates the deposit
78+
resource.balance += body.amount
79+
80+
return ResponseEntity.status(200).body(resource)
81+
}
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package com.foo.rest.examples.spring.openapi.v3.httporacle.nonidempotentput.xml
2+
3+
import org.springframework.boot.SpringApplication
4+
import org.springframework.boot.autoconfigure.SpringBootApplication
5+
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
6+
import org.springframework.http.MediaType
7+
import org.springframework.http.ResponseEntity
8+
import org.springframework.web.bind.annotation.GetMapping
9+
import org.springframework.web.bind.annotation.PathVariable
10+
import org.springframework.web.bind.annotation.PostMapping
11+
import org.springframework.web.bind.annotation.PutMapping
12+
import org.springframework.web.bind.annotation.RequestBody
13+
import org.springframework.web.bind.annotation.RequestMapping
14+
import org.springframework.web.bind.annotation.RestController
15+
import javax.xml.bind.annotation.XmlAccessType
16+
import javax.xml.bind.annotation.XmlAccessorType
17+
import javax.xml.bind.annotation.XmlRootElement
18+
19+
20+
@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
21+
@RequestMapping(path = ["/api/accounts"])
22+
@RestController
23+
open class HttpNonIdempotentPutXMLApplication {
24+
25+
companion object {
26+
@JvmStatic
27+
fun main(args: Array<String>) {
28+
SpringApplication.run(HttpNonIdempotentPutXMLApplication::class.java, *args)
29+
}
30+
31+
private val data = mutableMapOf<Int, AccountData>()
32+
33+
fun reset(){
34+
data.clear()
35+
}
36+
}
37+
38+
@XmlRootElement(name = "accountData")
39+
@XmlAccessorType(XmlAccessType.FIELD)
40+
open class AccountData(
41+
var balance: Int = 0
42+
)
43+
44+
@XmlRootElement(name = "depositRequest")
45+
@XmlAccessorType(XmlAccessType.FIELD)
46+
open class DepositRequest(
47+
var amount: Int = 0
48+
)
49+
50+
51+
@PostMapping(
52+
consumes = [MediaType.APPLICATION_XML_VALUE],
53+
produces = [MediaType.APPLICATION_XML_VALUE]
54+
)
55+
open fun create(@RequestBody body: AccountData): ResponseEntity<AccountData> {
56+
val id = data.size + 1
57+
data[id] = AccountData(balance = body.balance)
58+
return ResponseEntity.status(201).body(data[id])
59+
}
60+
61+
@GetMapping(
62+
path = ["/{id}"],
63+
produces = [MediaType.APPLICATION_XML_VALUE]
64+
)
65+
open fun get(@PathVariable("id") id: Int): ResponseEntity<AccountData> {
66+
val resource = data[id]
67+
?: return ResponseEntity.status(404).build()
68+
return ResponseEntity.status(200).body(resource)
69+
}
70+
71+
@PutMapping(
72+
path = ["/{id}/deposit"],
73+
consumes = [MediaType.APPLICATION_XML_VALUE],
74+
produces = [MediaType.APPLICATION_XML_VALUE]
75+
)
76+
open fun deposit(
77+
@PathVariable("id") id: Int,
78+
@RequestBody body: DepositRequest
79+
): ResponseEntity<AccountData> {
80+
81+
val resource = data[id]
82+
?: return ResponseEntity.status(404).build()
83+
84+
// wrong: PUT must be idempotent, but each call accumulates the deposit
85+
resource.balance += body.amount
86+
87+
return ResponseEntity.status(200).body(resource)
88+
}
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.foo.rest.examples.spring.openapi.v3.httporacle.nonidempotentput.json
2+
3+
import com.foo.rest.examples.spring.openapi.v3.SpringController
4+
5+
6+
class HttpNonIdempotentPutController: SpringController(HttpNonIdempotentPutApplication::class.java){
7+
8+
override fun resetStateOfSUT() {
9+
HttpNonIdempotentPutApplication.reset()
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.foo.rest.examples.spring.openapi.v3.httporacle.nonidempotentput.urlencoded
2+
3+
import com.foo.rest.examples.spring.openapi.v3.SpringController
4+
import com.foo.rest.examples.spring.openapi.v3.httporacle.nonidempotentput.xml.HttpNonIdempotentPutXMLApplication
5+
6+
7+
class HttpNonIdempotentPutUrlencodedController: SpringController(HttpNonIdempotentPutUrlEncodedApplication::class.java){
8+
9+
override fun resetStateOfSUT() {
10+
HttpNonIdempotentPutUrlEncodedApplication.reset()
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.foo.rest.examples.spring.openapi.v3.httporacle.nonidempotentput.xml
2+
3+
import com.foo.rest.examples.spring.openapi.v3.SpringController
4+
import com.foo.rest.examples.spring.openapi.v3.httporacle.nonidempotentput.xml.HttpNonIdempotentPutXMLApplication
5+
6+
7+
class HttpNonIdempotentPutXMLController: SpringController(HttpNonIdempotentPutXMLApplication::class.java){
8+
9+
override fun resetStateOfSUT() {
10+
HttpNonIdempotentPutXMLApplication.reset()
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.evomaster.e2etests.spring.openapi.v3.httporacle.nonidempotentput
2+
3+
import com.foo.rest.examples.spring.openapi.v3.httporacle.nonidempotentput.json.HttpNonIdempotentPutController
4+
import org.evomaster.core.problem.enterprise.DetectedFaultUtils
5+
import org.evomaster.core.problem.enterprise.ExperimentalFaultCategory
6+
import org.evomaster.e2etests.spring.openapi.v3.SpringTestBase
7+
import org.junit.jupiter.api.Assertions.assertEquals
8+
import org.junit.jupiter.api.Assertions.assertTrue
9+
import org.junit.jupiter.api.BeforeAll
10+
import org.junit.jupiter.api.Test
11+
12+
class HttpNonIdempotentPutEMTest : SpringTestBase(){
13+
14+
companion object {
15+
@BeforeAll
16+
@JvmStatic
17+
fun init() {
18+
initClass(HttpNonIdempotentPutController())
19+
}
20+
}
21+
22+
23+
@Test
24+
fun testRunEM() {
25+
26+
runTestHandlingFlakyAndCompilation(
27+
"HttpNonIdempotentPutEM",
28+
500
29+
) { args: MutableList<String> ->
30+
31+
setOption(args, "security", "false")
32+
setOption(args, "schemaOracles", "false")
33+
setOption(args, "httpOracles", "true")
34+
setOption(args, "useExperimentalOracles", "true")
35+
36+
val solution = initAndRun(args)
37+
38+
assertTrue(solution.individuals.size >= 1)
39+
40+
val faults = DetectedFaultUtils.getDetectedFaultCategories(solution)
41+
assertEquals(1, faults.size)
42+
assertEquals(ExperimentalFaultCategory.HTTP_NON_IDEMPOTENT_PUT, faults.first())
43+
}
44+
}
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.evomaster.e2etests.spring.openapi.v3.httporacle.nonidempotentput
2+
3+
import com.foo.rest.examples.spring.openapi.v3.httporacle.nonidempotentput.json.HttpNonIdempotentPutController
4+
import com.foo.rest.examples.spring.openapi.v3.httporacle.nonidempotentput.urlencoded.HttpNonIdempotentPutUrlencodedController
5+
import com.foo.rest.examples.spring.openapi.v3.httporacle.nonidempotentput.xml.HttpNonIdempotentPutXMLController
6+
import org.evomaster.core.problem.enterprise.DetectedFaultUtils
7+
import org.evomaster.core.problem.enterprise.ExperimentalFaultCategory
8+
import org.evomaster.e2etests.spring.openapi.v3.SpringTestBase
9+
import org.junit.jupiter.api.Assertions.assertEquals
10+
import org.junit.jupiter.api.Assertions.assertTrue
11+
import org.junit.jupiter.api.BeforeAll
12+
import org.junit.jupiter.api.Test
13+
14+
class HttpNonIdempotentPutUrlencodedEMTest : SpringTestBase(){
15+
16+
companion object {
17+
@BeforeAll
18+
@JvmStatic
19+
fun init() {
20+
initClass(HttpNonIdempotentPutUrlencodedController())
21+
}
22+
}
23+
24+
25+
@Test
26+
fun testRunEM() {
27+
28+
runTestHandlingFlakyAndCompilation(
29+
"HttpNonIdempotentPutUrlencodedEM",
30+
1000
31+
) { args: MutableList<String> ->
32+
33+
setOption(args, "security", "false")
34+
setOption(args, "schemaOracles", "false")
35+
setOption(args, "httpOracles", "true")
36+
setOption(args, "useExperimentalOracles", "true")
37+
38+
val solution = initAndRun(args)
39+
40+
assertTrue(solution.individuals.size >= 1)
41+
42+
val faults = DetectedFaultUtils.getDetectedFaultCategories(solution)
43+
assertEquals(1, faults.size)
44+
assertEquals(ExperimentalFaultCategory.HTTP_NON_IDEMPOTENT_PUT, faults.first())
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)