Skip to content

Commit c09bdff

Browse files
committed
refactor and detect null or empty fields
1 parent df244d5 commit c09bdff

2 files changed

Lines changed: 244 additions & 91 deletions

File tree

core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/HttpSemanticsOracle.kt

Lines changed: 117 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -278,25 +278,11 @@ object HttpSemanticsOracle {
278278
schema: RestSchema? = null
279279
): Boolean {
280280

281-
if (individual.size() < 2) return false
282-
283-
val actions = individual.seeMainExecutableActions()
284-
val put = actions[actions.size - 2]
285-
val get = actions[actions.size - 1]
286-
287-
if (put.verb != HttpVerb.PUT) return false
288-
if (get.verb != HttpVerb.GET) return false
289-
290-
if (!put.usingSameResolvedPath(get)) return false
291-
292-
if (put.auth.isDifferentFrom(get.auth)) return false
293-
294-
val resPut = actionResults.find { it.sourceLocalId == put.getLocalId() } as RestCallResult?
295-
?: return false
296-
val resGet = actionResults.find { it.sourceLocalId == get.getLocalId() } as RestCallResult?
297-
?: return false
281+
val (put, get, resPut, resGet) = findPutGetPair(individual, actionResults) ?: return false
298282

299283
if (!StatusGroup.G_2xx.isInGroup(resPut.getStatusCode())) return false
284+
// if put returned 2xx but entity does not exist afterwards
285+
if (resGet.getStatusCode() == 404) return true
300286
if (!StatusGroup.G_2xx.isInGroup(resGet.getStatusCode())) return false
301287

302288
val bodyParam = put.parameters.find { it is BodyParam } as BodyParam?
@@ -307,33 +293,75 @@ object HttpSemanticsOracle {
307293
val putBody = extractRequestBody(put)
308294
val getBody = resGet.getBody()
309295

310-
if (putBody.isNullOrEmpty() && !getBody.isNullOrEmpty()) return true
311-
312-
// if putBody is not empty but getBody is.
296+
// PUT sent content but GET body is empty -> sent fields definitely missing
313297
if (!putBody.isNullOrEmpty() && getBody.isNullOrEmpty()) return true
314-
315-
if (putBody.isNullOrEmpty() || getBody.isNullOrEmpty()) return false
298+
if (getBody.isNullOrEmpty()) return false
316299

317300
val sentFields = extractSentFieldNames(put)
318-
val allPutSchemaFields = extractModifiedFieldNames(put)
319-
if (sentFields.isEmpty() && allPutSchemaFields.isEmpty()) return false
320-
321-
// Wiped = in PUT schema but not sent. PUT semantics say the server must
322-
// clear these. We only check wiped fields the GET schema actually exposes,
323-
// otherwise write-only fields (e.g. passwords) would cause false positives.
324-
val wipedCandidates = allPutSchemaFields - sentFields
325-
val wipedFields = if (schema != null && wipedCandidates.isNotEmpty()) {
326-
val getSchemaFields = extractGetResponseSchemaFields(schema, get)
327-
if (getSchemaFields.isEmpty()) emptySet() else wipedCandidates intersect getSchemaFields
328-
} else {
329-
emptySet()
301+
val allPutSchemaFields = extractModifiedFieldNames(put).ifEmpty {
302+
schema?.let { extractPutRequestSchemaFields(it, put) } ?: emptySet()
330303
}
304+
if (sentFields.isEmpty() && allPutSchemaFields.isEmpty()) {
305+
// no information to verify against; flag only when PUT sent nothing either
306+
return putBody.isNullOrEmpty()
307+
}
308+
309+
val wipedFields = computeWipedFields(allPutSchemaFields - sentFields, schema, get)
331310

332-
return hasMismatchedPutFields(putBody, getBody, sentFields, wipedFields, bodyParam)
311+
return hasMismatchedPutFields(putBody ?: "", getBody, sentFields, wipedFields, bodyParam)
333312
}
334313

314+
private data class PutGetPair(
315+
val put: RestCallAction,
316+
val get: RestCallAction,
317+
val resPut: RestCallResult,
318+
val resGet: RestCallResult
319+
)
320+
335321
/**
336-
* Dispatches the field-level checks based on the request body content type.
322+
* Validates and extracts the trailing PUT/GET pair from the individual.
323+
* Returns null if any structural or authorization precondition fails.
324+
*/
325+
private fun findPutGetPair(
326+
individual: RestIndividual,
327+
actionResults: List<ActionResult>
328+
): PutGetPair? {
329+
if (individual.size() < 2) return null
330+
331+
val actions = individual.seeMainExecutableActions()
332+
val put = actions[actions.size - 2]
333+
val get = actions[actions.size - 1]
334+
335+
if (put.verb != HttpVerb.PUT) return null
336+
if (get.verb != HttpVerb.GET) return null
337+
if (!put.usingSameResolvedPath(get)) return null
338+
if (put.auth.isDifferentFrom(get.auth)) return null
339+
340+
val resPut = actionResults.find { it.sourceLocalId == put.getLocalId() } as RestCallResult?
341+
?: return null
342+
val resGet = actionResults.find { it.sourceLocalId == get.getLocalId() } as RestCallResult?
343+
?: return null
344+
345+
return PutGetPair(put, get, resPut, resGet)
346+
}
347+
348+
/**
349+
* Wiped candidates are restricted to fields the GET schema actually exposes, otherwise
350+
* write-only fields (e.g. passwords) would cause false positives.
351+
*/
352+
private fun computeWipedFields(
353+
candidates: Set<String>,
354+
schema: RestSchema?,
355+
get: RestCallAction
356+
): Set<String> {
357+
if (candidates.isEmpty() || schema == null) return emptySet()
358+
val getSchemaFields = extractGetResponseSchemaFields(schema, get)
359+
if (getSchemaFields.isEmpty()) return emptySet()
360+
return candidates intersect getSchemaFields
361+
}
362+
363+
/**
364+
* Unified field-level comparison for JSON, XML and form-encoded PUT bodies.
337365
*
338366
* @param sentFields fields whose values must match between PUT and GET
339367
* @param wipedFields fields that must be absent (or null) in the GET response
@@ -345,40 +373,22 @@ object HttpSemanticsOracle {
345373
wipedFields: Set<String>,
346374
bodyParam: BodyParam? = null
347375
): Boolean {
348-
return when {
349-
bodyParam == null || bodyParam.isJson() ->
350-
hasMismatchedPutFieldsStructured(OutputFormatter.JSON_FORMATTER, putBody, getBody, sentFields, wipedFields)
351-
bodyParam.isXml() ->
352-
hasMismatchedPutFieldsStructured(OutputFormatter.XML_FORMATTER, putBody, getBody, sentFields, wipedFields)
353-
bodyParam.isForm() ->
354-
hasMismatchedPutFieldsForm(putBody, getBody, sentFields, wipedFields)
355-
else -> false
356-
}
357-
}
358-
359-
private fun hasMismatchedPutFieldsStructured(
360-
formatter: OutputFormatter,
361-
putBody: String,
362-
getBody: String,
363-
sentFields: Set<String>,
364-
wipedFields: Set<String>
365-
): Boolean {
366376

367377
// sent fields: PUT value must equal GET value
368378
if (sentFields.isNotEmpty()) {
369-
val fieldsPut = formatter.readFields(putBody, sentFields) ?: return false
379+
val fieldsPut = readPutFields(putBody, bodyParam, sentFields) ?: return false
370380
if (fieldsPut.isNotEmpty()) {
371-
val fieldsGet = formatter.readFields(getBody, fieldsPut.keys.toSet()) ?: return false
381+
val fieldsGet = readGetFields(getBody, fieldsPut.keys) ?: return true
372382
for ((field, valuePut) in fieldsPut) {
373-
val valueGet = fieldsGet[field] ?: return true // sent but missing in GET
383+
val valueGet = fieldsGet[field] ?: return true
374384
if (valuePut != valueGet) return true
375385
}
376386
}
377387
}
378388

379389
// wiped fields: must be absent or null in GET
380390
if (wipedFields.isNotEmpty()) {
381-
val getWiped = formatter.readFields(getBody, wipedFields) ?: return false
391+
val getWiped = readGetFields(getBody, wipedFields) ?: return false
382392
for (field in wipedFields) {
383393
if (isWipedFieldStillPresent(getWiped[field])) return true
384394
}
@@ -388,39 +398,36 @@ object HttpSemanticsOracle {
388398
}
389399

390400
/**
391-
* Handles the case where the PUT request body is form-encoded.
392-
* The GET response format is auto-detected by trying JSON then XML.
401+
* Extracts field values from a PUT request body according to its content type.
402+
* Returns null if the body cannot be parsed by the chosen reader.
393403
*/
394-
private fun hasMismatchedPutFieldsForm(
404+
private fun readPutFields(
395405
putBody: String,
396-
getBody: String,
397-
sentFields: Set<String>,
398-
wipedFields: Set<String>
399-
): Boolean {
400-
val formFields = parseFormBody(putBody)
401-
if (formFields.isEmpty() && sentFields.isNotEmpty()) return false
402-
403-
for (formatter in listOf(OutputFormatter.JSON_FORMATTER, OutputFormatter.XML_FORMATTER)) {
404-
val fieldsGetSent = if (sentFields.isEmpty()) emptyMap()
405-
else formatter.readFields(getBody, sentFields) ?: continue
406-
407-
// sent fields
408-
for (field in sentFields) {
409-
val valuePut = formFields[field] ?: continue
410-
val valueGet = fieldsGetSent[field] ?: return true
411-
if (valuePut != valueGet) return true
412-
}
413-
414-
// wiped fields
415-
if (wipedFields.isNotEmpty()) {
416-
val getWiped = formatter.readFields(getBody, wipedFields) ?: return false
417-
for (field in wipedFields) {
418-
if (isWipedFieldStillPresent(getWiped[field])) return true
419-
}
420-
}
421-
return false
406+
bodyParam: BodyParam?,
407+
fieldNames: Set<String>
408+
): Map<String, String>? = when {
409+
bodyParam == null || bodyParam.isJson() ->
410+
OutputFormatter.JSON_FORMATTER.readFields(putBody, fieldNames)
411+
bodyParam.isXml() ->
412+
OutputFormatter.XML_FORMATTER.readFields(putBody, fieldNames)
413+
bodyParam.isForm() -> {
414+
val parsed = parseFormBody(putBody)
415+
if (parsed.isEmpty()) null
416+
else fieldNames.mapNotNull { f -> parsed[f]?.let { f to it } }.toMap()
422417
}
423-
return false
418+
else -> null
419+
}
420+
421+
/**
422+
* Extracts field values from a GET response body, auto-detecting JSON or XML.
423+
* Returns null if neither formatter can parse the body.
424+
*/
425+
private fun readGetFields(
426+
getBody: String,
427+
fieldNames: Set<String>
428+
): Map<String, String>? {
429+
return OutputFormatter.JSON_FORMATTER.readFields(getBody, fieldNames)
430+
?: OutputFormatter.XML_FORMATTER.readFields(getBody, fieldNames)
424431
}
425432

426433
/**
@@ -562,6 +569,28 @@ object HttpSemanticsOracle {
562569
return false
563570
}
564571

572+
/**
573+
* Returns the property names from the PUT request body schema in the OpenAPI spec.
574+
* Used as a fallback to determine writable fields when no BodyParam is present on the action.
575+
*/
576+
internal fun extractPutRequestSchemaFields(
577+
schema: RestSchema,
578+
put: RestCallAction
579+
): Set<String> {
580+
581+
val openAPI = schema.main.schemaParsed
582+
val pathItem = openAPI.paths?.get(put.path.toString()) ?: return emptySet()
583+
val op = pathItem.put ?: return emptySet()
584+
val requestBody = op.requestBody ?: return emptySet()
585+
val mediaType = requestBody.content?.values?.firstOrNull() ?: return emptySet()
586+
val rawSchema = mediaType.schema ?: return emptySet()
587+
val resolved = rawSchema.`$ref`?.let {
588+
SchemaUtils.getReferenceSchema(schema, schema.main, it, mutableListOf())
589+
} ?: rawSchema
590+
591+
return resolved.properties?.keys?.toSet() ?: emptySet()
592+
}
593+
565594
/**
566595
* Returns the property names from the GET 2xx response schema in the OpenAPI spec.
567596
* Empty set if unresolvable, which makes callers skip wiped-field checks.

0 commit comments

Comments
 (0)