diff --git a/client-java/test-utils-java/src/main/java/org/evomaster/test/utils/EMTestUtils.java b/client-java/test-utils-java/src/main/java/org/evomaster/test/utils/EMTestUtils.java index e106cbecce..664032249f 100644 --- a/client-java/test-utils-java/src/main/java/org/evomaster/test/utils/EMTestUtils.java +++ b/client-java/test-utils-java/src/main/java/org/evomaster/test/utils/EMTestUtils.java @@ -22,6 +22,79 @@ is used in the EvoMaster Core (eg, when making HTTP calls) and */ public class EMTestUtils { + /** + * Loaded only once at class loading. + * Seed is still going to incremented with ++ at each use. + * The idea is to force each value unique during a session, even when generating hundreds of thousands of tests. + * However, when running again in generated test suite, a new starting seed might reduce chances of clashes, + * albeit cannot guarantee removal of them + */ + private static long seed = System.currentTimeMillis(); + + /** + * + * @param minLength Optional minimum length of the generated string + * @param maxLength Optional maximum length of the generated string + * @param prefix Optional fixed prefix shared by all generated strings + * @param postfix Optional fixed postfix shared by all generated strings + * @return + */ + public static String createString(Integer minLength, Integer maxLength, String prefix, String postfix){ + + if(minLength != null && minLength < 0){ + throw new IllegalArgumentException("Negative minimum length: " + minLength); + } + if(maxLength != null && maxLength < 0){ + throw new IllegalArgumentException("Negative maximum length: " + maxLength); + } + + int min = 0; + if(minLength != null){ + min = minLength; + } + int len = 0; + if(prefix != null){ + len += prefix.length(); + } + if(postfix != null){ + len += postfix.length(); + } + min = Math.max(min, len); + + //actual check on inputs + if(maxLength != null && maxLength < len){ + throw new IllegalArgumentException("Maximum length " + maxLength + " does not cover minimum prefix+postfix length: "+prefix+postfix); + } + + //recompute with default values if not specified + if(prefix == null){ + prefix = "u"; + } + if(postfix == null){ + postfix = ""; + } + len = prefix.length() + postfix.length(); + + int maxDigits = 6; // 999 999 values + if(maxDigits + len < min){ + maxDigits = min - len; + } + if(maxLength != null && maxDigits + len > maxLength ){ + maxDigits = maxLength - len; + } + + int mask = 1; + for(int i = 0; i < maxDigits; i++){ + mask = mask * 10; + } + + long value = seed % mask; + seed++; + + return prefix + value + postfix; + } + + /** * * @param locationHeader a URI-reference, coming from a "location" header. See RFC 7231. diff --git a/client-java/test-utils-java/src/test/java/org/evomaster/test/utils/EMTestUtilsTest.java b/client-java/test-utils-java/src/test/java/org/evomaster/test/utils/EMTestUtilsTest.java index dd3fe17261..4f3bb05c63 100644 --- a/client-java/test-utils-java/src/test/java/org/evomaster/test/utils/EMTestUtilsTest.java +++ b/client-java/test-utils-java/src/test/java/org/evomaster/test/utils/EMTestUtilsTest.java @@ -1,13 +1,35 @@ package org.evomaster.test.utils; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + public class EMTestUtilsTest { + @Test + public void testCreateString(){ + + String prefix = "foo"; + String postfix = "bar"; + int min = 5; + int max = 10; + + String first = EMTestUtils.createString(min, max, prefix, postfix); + assertTrue(first.startsWith(prefix)); + assertTrue(first.endsWith(postfix)); + assertTrue(first.length() >= min); + assertTrue(first.length() <= max); + + String second = EMTestUtils.createString(min, max, prefix, postfix); + assertTrue(second.startsWith(prefix)); + assertTrue(second.endsWith(postfix)); + assertTrue(second.length() >= min); + assertTrue(second.length() <= max); + + assertNotEquals(first, second); + } + + @Test public void testEmptyPath(){ diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/authcreateusers/AuthDto.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/authcreateusers/AuthDto.kt new file mode 100644 index 0000000000..28a38a4716 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/authcreateusers/AuthDto.kt @@ -0,0 +1,6 @@ +package com.foo.rest.examples.bb.authcreateusers + +class AuthDto( + var email : String? = null, + var token: TokenDto? = null +) diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/authcreateusers/BBAuthCreateUsersApplication.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/authcreateusers/BBAuthCreateUsersApplication.kt new file mode 100644 index 0000000000..cb8b03b02a --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/authcreateusers/BBAuthCreateUsersApplication.kt @@ -0,0 +1,17 @@ +package com.foo.rest.examples.bb.authcreateusers + +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration + + +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +open class BBAuthCreateUsersApplication { + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(BBAuthCreateUsersApplication::class.java, *args) + } + } +} \ No newline at end of file diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/authcreateusers/BBAuthCreateUsersRest.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/authcreateusers/BBAuthCreateUsersRest.kt new file mode 100644 index 0000000000..04081f8891 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/authcreateusers/BBAuthCreateUsersRest.kt @@ -0,0 +1,72 @@ +package com.foo.rest.examples.bb.authcreateusers + +import org.evomaster.e2etests.utils.CoveredTargets +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping(path = ["/api/authcreateusers"]) +class BBAuthCreateUsersRest { + + private val SECRET = "a complex secret - " + + private val users = mutableMapOf() + + private val tokens = mutableMapOf() + + + @PostMapping("/users") + fun createUser(@RequestBody user: CreateUserDto): ResponseEntity { + + if(user.email == null || user.username == null || user.password == null) { + return ResponseEntity.status(400).build() + } + + if(!user.email!!.contains("@") || !user.email!!.contains(".")) { + return ResponseEntity.status(400).build() + } + + if(user.password != user.repeatPassword) { + return ResponseEntity.status(400).build() + } + + if(users.containsKey(user.email)){ + return ResponseEntity.status(403).build() + } + + users[user.email!!] = user + return ResponseEntity.status(201).build() + } + + + @PostMapping(path = ["/users/login"], consumes = [MediaType.APPLICATION_JSON_VALUE]) + fun login(@RequestBody login : LoginDto) : ResponseEntity{ + + val user = users[login.email!!] + ?: return ResponseEntity.status(404).build() + + if(login.password != user.password){ + return ResponseEntity.status(400).build() + } + + val secret = "$SECRET${System.currentTimeMillis()}" + + tokens[secret] = user.email!! + + return ResponseEntity.ok(AuthDto(user.email, TokenDto(secret))) + } + + @GetMapping(path = ["/check"]) + fun check(@RequestHeader("Authorization") authorization: String?) : ResponseEntity{ + + val secret = authorization!!.substring("Bearer ".length) + + if(tokens.containsKey(secret)){ + CoveredTargets.cover("CHECK") + return ResponseEntity.ok("OK") + } + + return ResponseEntity.status(401).build() + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/authcreateusers/CreateUserDto.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/authcreateusers/CreateUserDto.kt new file mode 100644 index 0000000000..1e768bf878 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/authcreateusers/CreateUserDto.kt @@ -0,0 +1,8 @@ +package com.foo.rest.examples.bb.authcreateusers + +class CreateUserDto( + var email: String? = null, + var password: String? = null, + var repeatPassword: String? = null, + var username: String? = null +) diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/authcreateusers/LoginDto.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/authcreateusers/LoginDto.kt new file mode 100644 index 0000000000..dbdedac31a --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/authcreateusers/LoginDto.kt @@ -0,0 +1,6 @@ +package com.foo.rest.examples.bb.authcreateusers + +class LoginDto( + var email: String? = null, + var password: String? = null +) \ No newline at end of file diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/authcreateusers/TokenDto.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/authcreateusers/TokenDto.kt new file mode 100644 index 0000000000..74c134ccad --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/authcreateusers/TokenDto.kt @@ -0,0 +1,5 @@ +package com.foo.rest.examples.bb.authcreateusers + +class TokenDto( + var authToken : String? = null +) \ No newline at end of file diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/authcreateusers/AuthCreateUsersController.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/authcreateusers/AuthCreateUsersController.kt new file mode 100644 index 0000000000..cb3d52b451 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/authcreateusers/AuthCreateUsersController.kt @@ -0,0 +1,8 @@ +package com.foo.rest.examples.bb.authcreateusers + +import com.foo.rest.examples.bb.SpringController +import com.foo.rest.examples.bb.authcookie.CookieLoginApplication +import org.evomaster.client.java.controller.problem.ProblemInfo +import org.evomaster.client.java.controller.problem.RestProblem + +class AuthCreateUsersController : SpringController(BBAuthCreateUsersApplication::class.java) \ No newline at end of file diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/authcreateusers/BBAuthCreateUsersEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/authcreateusers/BBAuthCreateUsersEMTest.kt new file mode 100644 index 0000000000..0e8dee74cd --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/authcreateusers/BBAuthCreateUsersEMTest.kt @@ -0,0 +1,48 @@ +package org.evomaster.e2etests.spring.rest.bb.authcreateusers + +import com.foo.rest.examples.bb.authcookie.CookieLoginController +import com.foo.rest.examples.bb.authcreateusers.AuthCreateUsersController +import org.evomaster.core.output.OutputFormat +import org.evomaster.core.problem.rest.data.HttpVerb +import org.evomaster.e2etests.spring.rest.bb.SpringTestBase +import org.evomaster.e2etests.utils.EnterpriseTestBase +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource + +class BBAuthCreateUsersEMTest : SpringTestBase() { + + companion object { + init { + shouldApplyInstrumentation = false + } + + @BeforeAll + @JvmStatic + fun init() { + initClass(AuthCreateUsersController()) + } + } + + @ParameterizedTest + @EnumSource + fun testBlackBoxOutput(outputFormat: OutputFormat) { + + executeAndEvaluateBBTest( + outputFormat, + "authcreateusers", + 50, + 3, + "CHECK" + ){ args: MutableList -> + + setOption(args, "configPath", "src/test/resources/config/authcreateusers.yaml") + + val solution = initAndRun(args) + + assertTrue(solution.individuals.size >= 1) + assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/api/authcreateusers/check", "OK") + } + } +} \ No newline at end of file diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/resources/config/authcreateusers.yaml b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/resources/config/authcreateusers.yaml new file mode 100644 index 0000000000..9bc398a34c --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/resources/config/authcreateusers.yaml @@ -0,0 +1,24 @@ +auth: + - name: "CreatedUser" + createUsers: + endpoint: "/api/authcreateusers/users" + contentType: "application/json" + verb: "POST" + payloadRaw: '{"email": "{$username}@example.com", "password": "123456", "repeatPassword": "123456", "username": "{$username}"}' + generators: + - placeHolder: "{$username}" + minLength: 8 + maxLength: 30 + prefix: "user_" + postfix: "" + loginEndpointAuth: + endpoint: "/api/authcreateusers/users/login" + verb: "POST" + contentType: "application/json" + payloadRaw: '{"email": "{$username}@example.com", "password": "123456"}' + token: + extractFrom: "body" + extractSelector: "/token/authToken" + sendIn: "header" + sendName: "Authorization" + sendTemplate: "Bearer {token}" diff --git a/core-tests/integration-tests/core-it/src/test/kotlin/org/evomaster/core/problem/rest/selectorutils/RestIndividualSelectorUtilsTest.kt b/core-tests/integration-tests/core-it/src/test/kotlin/org/evomaster/core/problem/rest/selectorutils/RestIndividualSelectorUtilsTest.kt index e91f368dbd..d0da2f9816 100644 --- a/core-tests/integration-tests/core-it/src/test/kotlin/org/evomaster/core/problem/rest/selectorutils/RestIndividualSelectorUtilsTest.kt +++ b/core-tests/integration-tests/core-it/src/test/kotlin/org/evomaster/core/problem/rest/selectorutils/RestIndividualSelectorUtilsTest.kt @@ -82,7 +82,7 @@ class RestIndividualSelectorUtilsTest : IntegrationTestRestBase() { val action4Ind2 = pirTest.fromVerbPath("DELETE", "/api/endpoint5/8000")!! action4Ind2.auth = HttpWsAuthenticationInfo("action4Ind2", listOf(AuthenticationHeader("name", "authentication")), - null, false) + null, false, null) val action5Ind2 = pirTest.fromVerbPath("GET", "/api/endpoint2/setStatus/403")!! val individual2 = createIndividual(listOf(action1Ind2, action2Ind2, action3Ind2, action4Ind2, action5Ind2)) @@ -96,7 +96,7 @@ class RestIndividualSelectorUtilsTest : IntegrationTestRestBase() { val action4Ind3 = pirTest.fromVerbPath("DELETE", "/api/endpoint5/8700")!! action1Ind3.auth = HttpWsAuthenticationInfo("action1Ind3", listOf(AuthenticationHeader("name", "authentication")), - null, false) + null, false, null) val action5Ind3 = pirTest.fromVerbPath("GET", "/api/endpoint4/setStatus/415")!! val individual3 = createIndividual(listOf(action1Ind3, action2Ind3, action3Ind3, action4Ind3, action5Ind3)) @@ -109,7 +109,7 @@ class RestIndividualSelectorUtilsTest : IntegrationTestRestBase() { val action4Ind4 = pirTest.fromVerbPath("DELETE", "/api/endpoint3/1700")!! action1Ind4.auth = HttpWsAuthenticationInfo("action1Ind3", listOf(AuthenticationHeader("name", "authentication")), - null, false) + null, false, null) val action5Ind4 = pirTest.fromVerbPath("GET", "/api/endpoint4/setStatus/404")!! val individual4 = createIndividual(listOf(action1Ind4, action2Ind4, action3Ind4, action4Ind4, action5Ind4)) diff --git a/core/src/main/kotlin/org/evomaster/core/output/auth/AuthWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/auth/AuthWriter.kt index 6bc5ce2200..7ae8dea3b9 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/auth/AuthWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/auth/AuthWriter.kt @@ -4,7 +4,9 @@ import org.evomaster.core.output.Lines import org.evomaster.core.output.OutputFormat import org.evomaster.core.output.service.HttpWsTestCaseWriter import org.evomaster.core.problem.httpws.auth.CallToEndpoint +import org.evomaster.core.problem.httpws.auth.PlaceHolderResolver import org.evomaster.core.problem.rest.data.ContentType +import org.evomaster.core.search.gene.utils.GeneUtils object AuthWriter { @@ -25,7 +27,8 @@ object AuthWriter { testCaseWriter: HttpWsTestCaseWriter, format: OutputFormat, baseUrlOfSut: String, - targetVariable: String? + targetVariable: String?, + placeHolderResolver: PlaceHolderResolver? ) { if(format.isJavaScript()) { @@ -63,6 +66,20 @@ object AuthWriter { throw IllegalStateException("Currently not supporting yet ${k.contentType} in login") } } + + if(placeHolderResolver != null) { + if (!format.isPython()) { + //easier to just remove the closing ')' then hunting down all places in which it is added + lines.replaceInCurrent(Regex("\\)\\s*$"), "") + } + placeHolderResolver.placeHolders.entries.forEach { + val placeholder = GeneUtils.applyEscapes(it.key, mode = GeneUtils.EscapeMode.BODY, format) + lines.append(".replace(\"${placeholder}\", ${it.value})") + } + if (!format.isPython()) { + lines.append(")") + } + } } for(header in k.headers) { diff --git a/core/src/main/kotlin/org/evomaster/core/output/auth/CookieWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/auth/CookieWriter.kt index 4193276bc6..8648fe2481 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/auth/CookieWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/auth/CookieWriter.kt @@ -46,6 +46,8 @@ object CookieWriter { for (k in cookiesInfo) { + val resolver = CreateUsersWriter.handleCreateUsers(k.name, ind.individual, format, lines, testCaseWriter, baseUrlOfSut) + when { format.isJava() -> lines.add("final Map ${cookiesName(k)} = ") format.isKotlin() -> lines.add("val ${cookiesName(k)} : Map = ") @@ -68,7 +70,7 @@ object CookieWriter { else -> cookiesName(k) } - AuthWriter.addBodyOfCallCommand(lines, k.call, testCaseWriter, format, baseUrlOfSut, targetCookieVariable) + AuthWriter.addBodyOfCallCommand(lines, k.call, testCaseWriter, format, baseUrlOfSut, targetCookieVariable, resolver) when { format.isJavaOrKotlin() -> lines.add(".then().extract().cookies()") diff --git a/core/src/main/kotlin/org/evomaster/core/output/auth/CreateUsersWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/auth/CreateUsersWriter.kt new file mode 100644 index 0000000000..c19c5cb69a --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/output/auth/CreateUsersWriter.kt @@ -0,0 +1,120 @@ +package org.evomaster.core.output.auth + +import org.evomaster.core.output.Lines +import org.evomaster.core.output.OutputFormat +import org.evomaster.core.output.TestWriterUtils +import org.evomaster.core.output.service.HttpWsTestCaseWriter +import org.evomaster.core.output.service.TestSuiteWriter +import org.evomaster.core.problem.httpws.HttpWsAction +import org.evomaster.core.problem.httpws.auth.CreateUsers +import org.evomaster.core.problem.httpws.auth.Generator +import org.evomaster.core.problem.httpws.auth.PlaceHolderResolver +import org.evomaster.core.search.Individual + + +object CreateUsersWriter { + + fun generatorName(name: String, g: Generator): String = + TestWriterUtils.safeVariableName("generator_${name}_${g.placeHolder}") + + fun responseName(name: String) : String = + TestWriterUtils.safeVariableName("res_create_user_${name}") + + fun getCreateUsersForNamedAuth(name: String, ind: Individual): CreateUsers? { + + return ind.seeAllActions() + .filterIsInstance() + .find { it.auth.name == name } + ?.auth?.createUsers + } + + fun getCreateUsersAuth(ind: Individual) = ind.seeAllActions() + .filterIsInstance() + .filter { it.auth.createUsers != null } + .distinctBy { it.auth.name } + .map { it.auth.createUsers!! } + + + /** + * If needed, make call to create a new user. + */ + fun handleCreateUsers( + name: String, + ind: Individual, + format: OutputFormat, + lines: Lines, + testCaseWriter: HttpWsTestCaseWriter, + baseUrlOfSut: String + ) : PlaceHolderResolver? { + + val user = getCreateUsersForNamedAuth(name, ind) + ?: return null + + val resolverData = mutableMapOf() + + for(g in user.generators) { + + val variableName = generatorName(user.name, g) + resolverData[g.placeHolder] = variableName + + when { + format.isJava() -> lines.add("final String $variableName = ") + format.isKotlin() -> lines.add("val $variableName : String = ") + format.isJavaScript() -> lines.add("const $variableName = ") + format.isPython() -> lines.add("$variableName = ") + } + + val min = g.minLength + val max = g.maxLength + val prefix = if(g.prefix == null) null else "\"${g.prefix}\"" + val postfix = if(g.postfix == null) null else "\"${g.postfix}\"" + + if (format.isJavaScript()) { + lines.append("${TestSuiteWriter.jsImport}.") + } + when{ + // function in EMTestUtils + format.isPython() -> lines.append("create_string") + else -> lines.append("createString") + } + lines.append("($min, $max, $prefix, $postfix)") + lines.appendSemicolon() + } + lines.addEmpty() + + val infoMsg = "Create new user dynamically for ${user.name}" + + if(! format.isPython()){ + lines.addEmpty() + testCaseWriter.startRequest(lines) + lines.appendSingleCommentLine(infoMsg) + lines.indent(2) + } else { + lines.addSingleCommentLine(infoMsg) + } + + val resName = responseName(name) + + AuthWriter.addBodyOfCallCommand(lines, user.call, testCaseWriter, format, baseUrlOfSut, resName, null) + + //need to add check that call was 2xx success or 3xx + + if (! format.isPython()){ + if(format.isJavaOrKotlin()){ + lines.add(".then()") + lines.add(".statusCode(both(greaterThanOrEqualTo(200)).and(Matchers.lessThan(400)))") + } + if(format.isJavaScript()){ + lines.add(".ok(res => res.status >= 200 && res.status < 400)") + } + lines.appendSemicolon() + lines.deindent(2) + } else { + lines.add("assert $resName.status_code >= 200 and $resName.status_code < 400") + } + + lines.addEmpty() + + return PlaceHolderResolver(user.name, resolverData) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/output/auth/TokenWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/auth/TokenWriter.kt index 5500c0df83..5192549ca3 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/auth/TokenWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/auth/TokenWriter.kt @@ -47,6 +47,8 @@ object TokenWriter { for (k in tokensInfo) { + val resolver = CreateUsersWriter.handleCreateUsers(k.name, ind.individual, format, lines, testCaseWriter, baseUrlOfSut) + val token = k.token!! when { @@ -69,7 +71,7 @@ object TokenWriter { lines.indent(2) } - AuthWriter.addBodyOfCallCommand(lines,k.call,testCaseWriter,format,baseUrlOfSut, responseName(k)) + AuthWriter.addBodyOfCallCommand(lines,k.call,testCaseWriter,format,baseUrlOfSut, responseName(k), resolver) var path = token.extractSelector.substring(1).replace("/",".") diff --git a/core/src/main/kotlin/org/evomaster/core/problem/graphql/service/GraphQLFitness.kt b/core/src/main/kotlin/org/evomaster/core/problem/graphql/service/GraphQLFitness.kt index e1d1876ed2..da881fc358 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/graphql/service/GraphQLFitness.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/graphql/service/GraphQLFitness.kt @@ -42,8 +42,9 @@ open class GraphQLFitness : HttpWsFitness() { goingToStartExecutingNewTest() - val cookies = AuthUtils.getCookies(client, getBaseUrl(), individual) - val tokens = AuthUtils.getTokens(client, getBaseUrl(), individual) + val placeholders = AuthUtils.createUsers(client, getBaseUrl(), individual) + val cookies = AuthUtils.getCookies(client, getBaseUrl(), individual, placeholders) + val tokens = AuthUtils.getTokens(client, getBaseUrl(), individual, placeholders) val actionResults: MutableList = mutableListOf() diff --git a/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/AuthUtils.kt b/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/AuthUtils.kt index e4db52fb83..1758a16df1 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/AuthUtils.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/AuthUtils.kt @@ -1,8 +1,11 @@ package org.evomaster.core.problem.httpws.auth import com.fasterxml.jackson.databind.ObjectMapper +import org.checkerframework.checker.units.qual.g +import org.evomaster.core.Lazy import org.evomaster.core.logging.LoggingUtil import org.evomaster.core.output.auth.CookieWriter +import org.evomaster.core.output.auth.CreateUsersWriter import org.evomaster.core.output.auth.TokenWriter import org.evomaster.core.problem.enterprise.auth.NoAuth import org.evomaster.core.problem.graphql.GraphQLAction @@ -11,6 +14,7 @@ import org.evomaster.core.problem.rest.data.ContentType import org.evomaster.core.problem.rest.data.HttpVerb import org.evomaster.core.problem.rest.data.RestCallAction import org.evomaster.core.search.Individual +import org.evomaster.test.utils.EMTestUtils import org.slf4j.Logger import org.slf4j.LoggerFactory import javax.ws.rs.client.Client @@ -25,33 +29,33 @@ object AuthUtils { private val log: Logger = LoggerFactory.getLogger(AuthUtils::class.java) - fun getTokens(client: Client, baseUrl: String, ind: Individual): Map{ + fun getTokens(client: Client, baseUrl: String, ind: Individual, placeholders: List): Map { val tokensLogin = TokenWriter.getTokenLoginAuth(ind) - return getTokens(client, baseUrl, tokensLogin) + return getTokens(client, baseUrl, tokensLogin, placeholders) } /** * If any action needs auth based on tokens via JSON, do a "login" before * running the actions, and store the tokens */ - fun getTokens(client: Client, baseUrl: String, tokensLogin: List): Map{ + fun getTokens(client: Client, baseUrl: String, tokensLogin: List, placeholders: List): Map { //from userId to Token val map = mutableMapOf() - for(tl in tokensLogin){ + for (tl in tokensLogin) { - if(tl.expectsCookie()){ + if (tl.expectsCookie()) { throw IllegalArgumentException("Token based login does not expect cookies") } val data = tl.token ?: throw IllegalArgumentException("Token based login requires token definition") - val response = makeCall(client, tl.name, tl.call, baseUrl) + val response = makeCall(client, tl.name, tl.call, baseUrl,placeholders) ?: continue - var token = when(data.extractFrom){ + var token = when (data.extractFrom) { TokenHandling.ExtractFrom.BODY -> { - if(! response.hasEntity()){ + if (!response.hasEntity()) { log.warn("Login request failed, with no body response from which to extract the auth token") continue } @@ -62,16 +66,17 @@ object AuthUtils { val jackson = ObjectMapper() val tree = jackson.readTree(body) val token = tree.at(tl.token!!.extractSelector).asText() - if(token == null || token.isEmpty()){ + if (token == null || token.isEmpty()) { log.warn("Failed login. Cannot extract token '${data.extractSelector}' from response: $body") continue } token } + TokenHandling.ExtractFrom.HEADER -> { val header = response.getHeaderString(data.extractSelector) response.close() - if(header == null || header.isEmpty()){ + if (header == null || header.isEmpty()) { log.warn("Failed login. No token to extract from header '${data.extractSelector}'") continue } @@ -79,8 +84,8 @@ object AuthUtils { } } - if(data.sendTemplate.isNotEmpty()){ - token = data.sendTemplate.replace("{token}", token) + if (data.sendTemplate.isNotEmpty()) { + token = data.sendTemplate.replace("{token}", token) } map[tl.name] = token @@ -90,10 +95,9 @@ object AuthUtils { } - fun getCookies(client: Client, baseUrl: String, ind: Individual): Map> { - + fun getCookies(client: Client, baseUrl: String, ind: Individual, placeholders: List): Map> { val cookieLogins = CookieWriter.getCookieLoginAuth(ind) - return getCookies(client, baseUrl, cookieLogins) + return getCookies(client, baseUrl, cookieLogins, placeholders) } /** @@ -102,18 +106,22 @@ object AuthUtils { * * @return a map from username to auth cookie for those users */ - fun getCookies(client: Client, baseUrl: String, cookieLogins: List): Map> { + fun getCookies( + client: Client, + baseUrl: String, + cookieLogins: List, + placeholders: List + ): Map> { val map: MutableMap> = mutableMapOf() for (cl in cookieLogins) { - if(!cl.expectsCookie()){ + if (!cl.expectsCookie()) { throw IllegalArgumentException("Cookie based login expects cookies") } - - val response = makeCall(client, cl.name, cl.call, baseUrl) + val response = makeCall(client, cl.name, cl.call, baseUrl, placeholders) ?: continue response.close() @@ -129,32 +137,132 @@ object AuthUtils { } + fun createUsers( + client: Client, + baseUrl: String, + individual: Individual + ) : List { + return createUsers(client, baseUrl, CreateUsersWriter.getCreateUsersAuth(individual)) + } + + /** + * Make calls to create new users. + * Each user will be chosen with info based on the declared generators. + */ + fun createUsers( + client: Client, + baseUrl: String, + createUsersList: List + ): List { + + val results = mutableListOf() + + for(c in createUsersList) { + + var payload = c.call.payload!! //TODO will need to handle headers, where payload could be null + val placeHolders = mutableMapOf() + + for(g in c.generators) { + val generated = EMTestUtils.createString(g.minLength, g.maxLength, g.prefix, g.postfix) + placeHolders[g.placeHolder] = generated + payload = payload.replace(g.placeHolder, generated) + } + + val response = makeCall( + client, + baseUrl, + c.name, + c.call.verb, + c.call.contentType, + payload, + listOf(), + c.call.endpoint, + c.call.externalEndpointURL) + ?: continue + response.close() + + results.add(PlaceHolderResolver(c.name, placeHolders)) + } + + return results + } + + fun constructUrl(baseUrl: String, endpoint: String?, externalEndpointURL: String?): String { - private fun makeCall(client: Client, name: String, x: CallToEndpoint, baseUrl: String) : Response?{ + if (externalEndpointURL != null) { + return externalEndpointURL + } + + val s = baseUrl.trim() + + if (!s.startsWith("http://", true) && !s.startsWith("https://")) { + throw IllegalArgumentException("baseUrl should use HTTP(S): $baseUrl") + } + + if(endpoint == null || !endpoint.startsWith("/")) { + throw IllegalArgumentException("Invalid endpoint when externalEndpointURL is null -> $endpoint") + } + + return if (s.endsWith("/")) { + s.substring(0, s.length - 1) + endpoint + } else { + s + endpoint + } + } + + + private fun makeCall(client: Client, name: String, x: CallToEndpoint, baseUrl: String, placeholders: List): Response?{ + + val resolver = placeholders.firstOrNull { it.name == name } + val payload = if(resolver == null){ + x.payload + } else { + var modified = x.payload + if(modified != null) { + for (p in resolver.placeHolders.entries) { + modified = modified!!.replace(p.key, p.value) + } + } + modified + } + + return makeCall(client, baseUrl, name, x.verb, x.contentType, payload, x.headers, x.endpoint, x.externalEndpointURL) + } - val mediaType = when (x.contentType) { + private fun makeCall( + client: Client, + baseUrl: String, + name: String, + verb: HttpVerb, + contentType: ContentType?, + payload: String?, + headers: List, + endpoint: String?, + externalEndpointURL: String? + ) : Response?{ + + val mediaType = when (contentType) { ContentType.X_WWW_FORM_URLENCODED -> MediaType.APPLICATION_FORM_URLENCODED_TYPE ContentType.JSON -> MediaType.APPLICATION_JSON_TYPE null -> null } val bodyEntity = if(mediaType != null) { - Entity.entity(x.payload, mediaType) + Entity.entity(payload, mediaType) } else { null } - val builder = client.target(x.getUrl(baseUrl)).request() + val builder = client.target(constructUrl(baseUrl,endpoint,externalEndpointURL)).request() - x.headers.forEach { builder.header(it.name, it.value) } + headers.forEach { builder.header(it.name, it.value) } if(mediaType!=null){ builder.header("Content-Type", mediaType) } - //TODO duplicated code, should put in a utility val invocation = if(bodyEntity != null) { - when (x.verb) { + when (verb) { HttpVerb.GET -> builder.buildGet() HttpVerb.DELETE -> builder.build("DELETE", bodyEntity) HttpVerb.POST -> builder.buildPost(bodyEntity) @@ -165,7 +273,7 @@ object AuthUtils { HttpVerb.TRACE -> builder.build("TRACE") } } else { - builder.build(x.verb.toString()) + builder.build(verb.toString()) } val response = try { @@ -188,12 +296,12 @@ object AuthUtils { if (response.statusInfo.family == Response.Status.Family.REDIRECTION) { val location = response.getHeaderString("location") if (location != null && (location.contains("error", true) || location.contains("login", true))) { - log.warn("Login request failed with ${response.status} redirection toward $location") + log.warn("Auth request failed with ${response.status} redirection toward $location") response.close() return null } } else { - log.warn("Login request failed with status ${response.status}") + log.warn("Auth request failed with status ${response.status}") response.close() return null } diff --git a/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/CreateUsers.kt b/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/CreateUsers.kt new file mode 100644 index 0000000000..5e30d317e6 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/CreateUsers.kt @@ -0,0 +1,46 @@ +package org.evomaster.core.problem.httpws.auth + +import com.webfuzzing.commons.auth.CreateUsers +import org.evomaster.core.problem.rest.data.ContentType +import org.evomaster.core.problem.rest.data.HttpVerb + + +class CreateUsers( + + val name: String, + + val call: CallToEndpoint, + + val generators: List +){ + init { + + val payload = call.payload + //TODO could handle payload in headers in future, and so this could be null + ?: throw IllegalArgumentException("No payload provided") + + for (generator in generators) { + val placeholder = generator.placeHolder + if(!payload.contains(placeholder)) { + throw IllegalArgumentException("Payload does not contain the placeholder '$placeholder': $payload") + } + } + } + + companion object { + fun fromDto(name: String, dto: CreateUsers) : org.evomaster.core.problem.httpws.auth.CreateUsers{ + return CreateUsers( + name = name, + call = CallToEndpoint( + endpoint = dto.endpoint, + externalEndpointURL = dto.externalEndpointURL, + payload = dto.payloadRaw, + verb = HttpVerb.valueOf(dto.verb.toString()), + contentType = dto.contentType.let { ContentType.from(it)}, + headers = listOf() // TODO + ), + generators = dto.generators.map {Generator.fromDto(it)} + ) + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/EndpointCallLogin.kt b/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/EndpointCallLogin.kt index 7e9226b682..2b0a469f7a 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/EndpointCallLogin.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/EndpointCallLogin.kt @@ -110,4 +110,10 @@ class EndpointCallLogin( } fun expectsCookie() = token == null + + fun getUrl(baseUrl: String): String { + return AuthUtils.constructUrl(baseUrl, call.endpoint, call.externalEndpointURL) + } + + } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/Generator.kt b/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/Generator.kt new file mode 100644 index 0000000000..e4386bd2b7 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/Generator.kt @@ -0,0 +1,76 @@ +package org.evomaster.core.problem.httpws.auth + +import com.webfuzzing.commons.auth.Generator + +class Generator( + + /** + * Placeholder tag used to represent a value generated with this generator. \ + * String interpolation will be applied to the raw payloads to replace any found instance of \ + * this placeholder with the generated value. + */ + val placeHolder: String, + + /** + * Minimum length of the generated string + */ + val minLength: Int?, + + /** + * Maximum length of the generated string + */ + val maxLength: Int?, + + /** + * Fixed prefix shared by all generated strings + */ + val prefix: String?, + + /** + * Fixed postfix shared by all generated strings + */ + val postfix: String? +){ + + init{ + if(placeHolder.isEmpty()){ + throw IllegalArgumentException("Placeholder can not be empty") + } + if(minLength != null && minLength < 0){ + throw IllegalArgumentException("Minimum length must be greater than or equal to 0, but was $minLength") + } + if(maxLength != null && maxLength < 0){ + throw IllegalArgumentException("Maximum length must be greater than or equal to 0, but was $maxLength") + } + + if(maxLength != null) { + var length = 0 + if (prefix != null) { + length += prefix.length + } + if (postfix != null) { + length += postfix.length + } + if (length >= maxLength) { + throw IllegalArgumentException("If specified, maximum length must be greater than prefix+postfix") + } + } + } + + + companion object { + + fun fromDto(dto: Generator) : org.evomaster.core.problem.httpws.auth.Generator{ + + return Generator( + placeHolder = dto.placeHolder, + minLength = dto.minLength, + maxLength = dto.maxLength, + prefix = dto.prefix, + postfix = dto.postfix + ) + } + + } + +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/HttpWsAuthenticationInfo.kt b/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/HttpWsAuthenticationInfo.kt index 318c0a0e2b..989bf207ee 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/HttpWsAuthenticationInfo.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/HttpWsAuthenticationInfo.kt @@ -24,7 +24,11 @@ open class HttpWsAuthenticationInfo( * for auth in following requests. */ val endpointCallLogin: EndpointCallLogin?, - val requireMockHandling: Boolean + val requireMockHandling: Boolean, + /** + * Represent information on to create new users on-the-fly + */ + val createUsers: CreateUsers? ): AuthenticationInfo(name) { init { @@ -71,7 +75,17 @@ open class HttpWsAuthenticationInfo( val requireMockHandling = dto.requireMockHandling != null && dto.requireMockHandling - return HttpWsAuthenticationInfo(dto.name.trim(), headers, endpointCallLogin, requireMockHandling) + val createUsers = if (dto.createUsers != null){ + try { + CreateUsers.fromDto(dto.name, dto.createUsers) + }catch (e: Exception){ + throw IllegalArgumentException("Issue when parsing auth info for '${dto.name}': ${e.message}") + } + } else { + null + } + + return HttpWsAuthenticationInfo(dto.name.trim(), headers, endpointCallLogin, requireMockHandling, createUsers) } } diff --git a/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/HttpWsNoAuth.kt b/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/HttpWsNoAuth.kt index fdb4df9322..6ec5f46aa1 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/HttpWsNoAuth.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/HttpWsNoAuth.kt @@ -3,4 +3,4 @@ package org.evomaster.core.problem.httpws.auth import org.evomaster.core.problem.enterprise.auth.NoAuth -class HttpWsNoAuth : HttpWsAuthenticationInfo("NoAuth", listOf(), null, false), NoAuth \ No newline at end of file +class HttpWsNoAuth : HttpWsAuthenticationInfo("NoAuth", listOf(), null, false, null), NoAuth \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/PlaceHolderResolver.kt b/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/PlaceHolderResolver.kt new file mode 100644 index 0000000000..a8d26674c3 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/problem/httpws/auth/PlaceHolderResolver.kt @@ -0,0 +1,10 @@ +package org.evomaster.core.problem.httpws.auth + +class PlaceHolderResolver( + val name: String, + /** + * Map from (key) placeholder to (value) newly created string that will replace it (or name of variable + * in the generated code) + */ + val placeHolders: Map +) \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/schema/OpenApiAccess.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/schema/OpenApiAccess.kt index 5432418a83..f44aecc0f6 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/schema/OpenApiAccess.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/schema/OpenApiAccess.kt @@ -212,16 +212,20 @@ object OpenApiAccess { */ val baseUrl = "${url.protocol}://${url.host}:${url.port}" + //TODO should handle CreateUsers here? + val cookies = if(ecl != null && ecl.expectsCookie()) AuthUtils.getCookies( client, baseUrl, - listOf(ecl) + listOf(ecl), + listOf() ) else mapOf() val tokens = if(ecl != null && !ecl.expectsCookie()) AuthUtils.getTokens( client, baseUrl, - listOf(ecl) + listOf(ecl), + listOf() ) else mapOf() 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 bc93bafb9a..5e4aeee0c5 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 @@ -49,8 +49,9 @@ class BlackBoxRestFitness : RestFitness() { rc.resetSUT() } - cookies.putAll(AuthUtils.getCookies(client, getBaseUrl(), individual)) - tokens.putAll(AuthUtils.getTokens(client, getBaseUrl(), individual)) + val placeholders = AuthUtils.createUsers(client, getBaseUrl(), individual) + cookies.putAll(AuthUtils.getCookies(client, getBaseUrl(), individual, placeholders)) + tokens.putAll(AuthUtils.getTokens(client, getBaseUrl(), individual, placeholders)) val fv = FitnessValue(individual.size().toDouble()) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/ResourceRestFitness.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/ResourceRestFitness.kt index d8e6abec88..f7887da108 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/ResourceRestFitness.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/ResourceRestFitness.kt @@ -60,8 +60,9 @@ class ResourceRestFitness : AbstractRestFitness() { which prevents the retrieval of token or cookie values. */ - val cookies = AuthUtils.getCookies(client, getBaseUrl(), individual) - val tokens = AuthUtils.getTokens(client, getBaseUrl(), individual) + val placeholders = AuthUtils.createUsers(client, getBaseUrl(), individual) + val cookies = AuthUtils.getCookies(client, getBaseUrl(), individual, placeholders) + val tokens = AuthUtils.getTokens(client, getBaseUrl(), individual, placeholders) /* there might some dbaction between rest actions. diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/RestFitness.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/RestFitness.kt index bf4a9a4e11..83555c0ae9 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/RestFitness.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/RestFitness.kt @@ -31,8 +31,9 @@ open class RestFitness : AbstractRestFitness() { rc.resetSUT() goingToStartExecutingNewTest() - val cookies = AuthUtils.getCookies(client, getBaseUrl(), individual) - val tokens = AuthUtils.getTokens(client, getBaseUrl(), individual) + val placeholders = AuthUtils.createUsers(client, getBaseUrl(), individual) + val cookies = AuthUtils.getCookies(client, getBaseUrl(), individual, placeholders) + val tokens = AuthUtils.getTokens(client, getBaseUrl(), individual, placeholders) if (log.isTraceEnabled){ log.trace("do evaluate the individual, which contains {} dbactions and {} rest actions", diff --git a/pom.xml b/pom.xml index 190cc0f09c..edb4ff0f5a 100644 --- a/pom.xml +++ b/pom.xml @@ -217,7 +217,7 @@ 17 2.2.20 true - 0.5.1 + 0.5.2-SNAPSHOT 5.14.2 1.14.2 3.1.5 diff --git a/test-utils/test-utils-js/src/main/resources/EMTestUtils.js b/test-utils/test-utils-js/src/main/resources/EMTestUtils.js index a580e99d66..734dcad839 100644 --- a/test-utils/test-utils-js/src/main/resources/EMTestUtils.js +++ b/test-utils/test-utils-js/src/main/resources/EMTestUtils.js @@ -7,6 +7,82 @@ const URI = require("urijs"); module.exports = class EMTestUtils { + + /** + * Loaded only once at module loading. + * Seed is still going to incremented with ++ at each use. + * The idea is to force each value unique during a session, even when generating hundreds of thousands of tests. + * However, when running again in generated test suite, a new starting seed might reduce chances of clashes, + * albeit cannot guarantee removal of them + */ + static seed = Date.now(); + + /** + * + * @param {number|null} minLength - Optional minimum length of the generated string + * @param {number|null} maxLength - Optional maximum length of the generated string + * @param {string|null} prefix - Optional fixed prefix shared by all generated strings + * @param {string|null} postfix - Optional fixed postfix shared by all generated strings + * @returns {string} + */ + static createString(minLength = null, maxLength = null, prefix = null, postfix = null) { + + if (minLength !== null && minLength < 0) { + throw new Error(`Negative minimum length: ${minLength}`); + } + if (maxLength !== null && maxLength < 0) { + throw new Error(`Negative maximum length: ${maxLength}`); + } + + let min = 0; + if (minLength !== null) { + min = minLength; + } + + let len = 0; + if (prefix !== null) { + len += prefix.length; + } + if (postfix !== null) { + len += postfix.length; + } + min = Math.max(min, len); + + // Actual check on inputs + if (maxLength !== null && maxLength < len) { + throw new Error( + `Maximum length ${maxLength} does not cover minimum prefix+postfix length: ${prefix}${postfix}` + ); + } + + // Recompute with default values if not specified + if (prefix === null) { + prefix = "u"; + } + if (postfix === null) { + postfix = ""; + } + len = prefix.length + postfix.length; + + let maxDigits = 6; // 999 999 values + if (maxDigits + len < min) { + maxDigits = min - len; + } + if (maxLength !== null && maxDigits + len > maxLength) { + maxDigits = maxLength - len; + } + + let mask = 1; + for (let i = 0; i < maxDigits; i++) { + mask = mask * 10; + } + + let value = EMTestUtils.seed % mask; + EMTestUtils.seed++; + + return `${prefix}${value}${postfix}`; + } + /** * * @param locationHeader a URI-reference, coming from a "location" header. See RFC 7231. diff --git a/test-utils/test-utils-js/src/test/EMTestUtils-test.js b/test-utils/test-utils-js/src/test/EMTestUtils-test.js index b51db1a09a..b8695b493b 100644 --- a/test-utils/test-utils-js/src/test/EMTestUtils-test.js +++ b/test-utils/test-utils-js/src/test/EMTestUtils-test.js @@ -1,5 +1,28 @@ const EMTestUtils = require("../main/resources/EMTestUtils"); + + test('should create string with prefix, postfix, and length constraints', () => { + const prefix = "foo"; + const postfix = "bar"; + const min = 5; + const max = 10; + + const first = EMTestUtils.createString(min, max, prefix, postfix); + expect(first.startsWith(prefix)).toBe(true); + expect(first.endsWith(postfix)).toBe(true); + expect(first.length).toBeGreaterThanOrEqual(min); + expect(first.length).toBeLessThanOrEqual(max); + + const second = EMTestUtils.createString(min, max, prefix, postfix); + expect(second.startsWith(prefix)).toBe(true); + expect(second.endsWith(postfix)).toBe(true); + expect(second.length).toBeGreaterThanOrEqual(min); + expect(second.length).toBeLessThanOrEqual(max); + + expect(first).not.toEqual(second); + }) + + test("testResolveLocation_direct", () => { const template = "http://localhost:12345/a/{id}"; diff --git a/test-utils/test-utils-py/src/main/resources/em_test_utils.py b/test-utils/test-utils-py/src/main/resources/em_test_utils.py index 5110a62aa0..20d2bb5479 100644 --- a/test-utils/test-utils-py/src/main/resources/em_test_utils.py +++ b/test-utils/test-utils-py/src/main/resources/em_test_utils.py @@ -1,5 +1,75 @@ from urllib.parse import urlparse, quote from rfc3986 import validators, uri_reference +import time +import math + +# Loaded only once at module loading. +# Seed is still going to incremented with += 1 at each use. +# The idea is to force each value unique during a session, even when generating hundreds of thousands of tests. +# However, when running again in generated test suite, a new starting seed might reduce chances of clashes, +# albeit cannot guarantee removal of them +_seed = int(time.time() * 1000) + + +def create_string(min_length=None, max_length=None, prefix=None, postfix=None): + """ + Generate a unique string with optional constraints. + + Args: + min_length: Optional minimum length of the generated string + max_length: Optional maximum length of the generated string + prefix: Optional fixed prefix shared by all generated strings + postfix: Optional fixed postfix shared by all generated strings + + Returns: + Generated string + """ + global _seed + + if min_length is not None and min_length < 0: + raise ValueError(f"Negative minimum length: {min_length}") + if max_length is not None and max_length < 0: + raise ValueError(f"Negative maximum length: {max_length}") + + min_val = 0 + if min_length is not None: + min_val = min_length + + length = 0 + if prefix is not None: + length += len(prefix) + if postfix is not None: + length += len(postfix) + min_val = max(min_val, length) + + # Actual check on inputs + if max_length is not None and max_length < length: + raise ValueError( + f"Maximum length {max_length} does not cover minimum prefix+postfix length: {prefix}{postfix}" + ) + + # Recompute with default values if not specified + if prefix is None: + prefix = "u" + if postfix is None: + postfix = "" + length = len(prefix) + len(postfix) + + max_digits = 6 # 999 999 values + if max_digits + length < min_val: + max_digits = min_val - length + if max_length is not None and max_digits + length > max_length: + max_digits = max_length - length + + mask = 1 + for i in range(max_digits): + mask = mask * 10 + + value = _seed % mask + _seed += 1 + + return f"{prefix}{value}{postfix}" + def resolve_location(location_header: str, expected_template: str) -> str: if not location_header: diff --git a/test-utils/test-utils-py/src/test/em_test_utils_test.py b/test-utils/test-utils-py/src/test/em_test_utils_test.py index 4b7fcd7fa7..5427b42b60 100644 --- a/test-utils/test-utils-py/src/test/em_test_utils_test.py +++ b/test-utils/test-utils-py/src/test/em_test_utils_test.py @@ -5,6 +5,27 @@ class EvoMaster_EM_Test_Utils_Test(unittest.TestCase): + def test_create_string(self): + prefix = "foo" + postfix = "bar" + min_val = 5 + max_val = 10 + + first = create_string(min_val, max_val, prefix, postfix) + self.assertTrue(first.startswith(prefix)) + self.assertTrue(first.endswith(postfix)) + self.assertTrue(len(first) >= min_val) + self.assertTrue(len(first) <= max_val) + + second = create_string(min_val, max_val, prefix, postfix) + self.assertTrue(second.startswith(prefix)) + self.assertTrue(second.endswith(postfix)) + self.assertTrue(len(second) >= min_val) + self.assertTrue(len(second) <= max_val) + + self.assertNotEqual(first, second) + + def test_resolve_location_direct(self): template = "http://localhost:12345/a/{id}" location = "/a/5"