add test security attribute type handling

* added an attribute type enum which converts the `String` `value` into various types of objects
* use the converter when setting up the security identity for tests
* added some documentation about how to use `@SecurityAttribute` inside the `@TestSecurity` annotation

Signed-off-by:Nathan Erwin <nathan.d.erwin@gmail.com>
This commit is contained in:
Nathan Erwin 2024-05-02 22:21:21 -04:00
parent 016bfb50c3
commit bc206d7449
6 changed files with 144 additions and 1 deletions

View File

@ -93,6 +93,24 @@ identity to be present.
See xref:security-oidc-bearer-token-authentication.adoc#integration-testing-security-annotation[OpenID Connect Bearer Token Integration testing], xref:security-oidc-code-flow-authentication.adoc#integration-testing-security-annotation[OpenID Connect Authorization Code Flow Integration testing] and xref:security-jwt.adoc#integration-testing-security-annotation[SmallRye JWT Integration testing] for more details about testing the endpoint code which depends on the injected `JsonWebToken`.
Additionally, you can specify attributes for the identity, perhaps custom items that were added with identity augmentation:
[source,java]
----
@Inject
SecurityIdentity identity;
@Test
@TestSecurity(user = "testUser", "roles = {"admin, "user"}, attributes = {
@SecurityAttribute(key = "answer", value = "42", type = AttributeType.LONG) }
void someTestMethod() {
Long answer = identity.<Long>getAttribute("answer");
...
}
----
This will run the test with an identity with an attribute of type `Long` named `answer`.
[WARNING]
====
The feature is only available for `@QuarkusTest` and will **not** work on a `@QuarkusIntegrationTest`.

View File

@ -1,7 +1,10 @@
package io.quarkus.it.resteasy.elytron;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import java.util.stream.Stream;
@ -13,6 +16,7 @@ import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.AttributeType;
import io.quarkus.test.security.SecurityAttribute;
import io.quarkus.test.security.TestSecurity;
@ -94,6 +98,50 @@ class TestSecurityTestCase {
.body(is("foo=bar"));
}
@Test
@TestSecurity(user = "testUser", roles = "user", attributes = {
@SecurityAttribute(key = "foo", value = "9223372036854775807", type = AttributeType.LONG) })
void testLongAttributes() {
given()
.when()
.get("/attributes")
.then()
.statusCode(200)
.body(is("foo=" + Long.MAX_VALUE));
}
@Test
@TestSecurity(user = "testUser", roles = "user", attributes = {
@SecurityAttribute(key = "foo", value = "[\"A\",\"B\",\"C\"]", type = AttributeType.JSON_ARRAY) })
void testJsonArrayAttributes() {
given()
.when()
.get("/attributes")
.then()
.statusCode(200)
.body(is("foo=[\"A\",\"B\",\"C\"]"));
}
@Test
@TestSecurity(user = "testUser", roles = "user", attributes = {
@SecurityAttribute(key = "foo", value = "\"A\",\"B\",\"C\"", type = AttributeType.STRING_SET) })
void testStringSetAttributes() {
given()
.when()
.get("/attributes")
.then()
.statusCode(200)
.body(startsWith("foo=["))
.and()
.body(endsWith("]"))
.and()
.body(containsString("\"A\""))
.and()
.body(containsString("\"B\""))
.and()
.body(containsString("\"C\""));
}
static Stream<Arguments> arrayParams() {
return Stream.of(
arguments(new int[] { 1, 2 }, new String[] { "hello", "world" }));

View File

@ -32,6 +32,12 @@
<artifactId>junit-jupiter</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jsonp</artifactId>
<scope>compile</scope>
<optional>true</optional>
</dependency>
</dependencies>
<build>

View File

@ -0,0 +1,69 @@
package io.quarkus.test.security;
import java.io.StringReader;
import java.util.Set;
import jakarta.json.Json;
import jakarta.json.JsonReader;
public enum AttributeType {
LONG {
@Override
Object convert(String value) {
return Long.valueOf(value);
}
},
INTEGER {
@Override
Object convert(String value) {
return Integer.valueOf(value);
}
},
BOOLEAN {
@Override
Object convert(String value) {
return Boolean.valueOf(value);
}
},
STRING {
@Override
Object convert(String value) {
return value;
}
},
STRING_SET {
/**
* Returns a Set of String values, parsed from the given value.
*
* @param value a comma separated list of values
*/
@Override
Object convert(String value) {
return Set.of(value.split(","));
}
},
JSON_ARRAY {
@Override
Object convert(String value) {
try (JsonReader reader = Json.createReader(new StringReader(value))) {
return reader.readArray();
}
}
},
JSON_OBJECT {
@Override
Object convert(String value) {
try (JsonReader reader = Json.createReader(new StringReader(value))) {
return reader.readObject();
}
}
},
DEFAULT {
@Override
Object convert(String value) {
return value;
}
};
abstract Object convert(String value);
}

View File

@ -60,7 +60,7 @@ public class QuarkusSecurityTestExtension implements QuarkusTestBeforeEachCallba
if (testSecurity.attributes() != null) {
user.addAttributes(Arrays.stream(testSecurity.attributes())
.collect(Collectors.toMap(s -> s.key(), s -> s.value())));
.collect(Collectors.toMap(s -> s.key(), s -> s.type().convert(s.value()))));
}
SecurityIdentity userIdentity = augment(user.build(), allAnnotations);

View File

@ -10,4 +10,6 @@ public @interface SecurityAttribute {
String key();
String value();
AttributeType type() default AttributeType.DEFAULT;
}