This is an automated email from the ASF dual-hosted git repository. btellier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 81880375289776284c9fbc9f524dc1f5d3b22dab Author: duc91 <[email protected]> AuthorDate: Tue May 26 14:54:14 2020 +0700 JAMES-3093 Add BasicAuthenticationStrategy for JMAP RFC-8621 --- .../DistributedBasicAuthenticationTest.java | 51 +++++++ .../rfc8621/contract/AuthenticationContract.scala | 151 +++++++++++++++++++++ .../jmap/rfc8621/contract/EchoMethodContract.scala | 83 +++-------- .../james/jmap/rfc8621/contract/Fixture.scala | 94 +++++++++++++ .../rfc8621/memory/MemoryAuthenticationTest.java | 38 ++++++ server/protocols/jmap-rfc-8621/pom.xml | 60 +++++--- .../jmap/http/BasicAuthenticationStrategy.scala | 109 +++++++++++++++ .../org/apache/james/jmap/http/SessionRoutes.scala | 7 +- .../apache/james/jmap/routes/JMAPApiRoutes.scala | 34 +++-- .../james/jmap/http/UserCredentialParserTest.scala | 135 ++++++++++++++++++ .../james/jmap/routes/JMAPApiRoutesTest.scala | 130 +++++++++++++----- 11 files changed, 760 insertions(+), 132 deletions(-) diff --git a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedBasicAuthenticationTest.java b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedBasicAuthenticationTest.java new file mode 100644 index 0000000..1d7932b --- /dev/null +++ b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedBasicAuthenticationTest.java @@ -0,0 +1,51 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.jmap.rfc8621.distributed; + +import org.apache.james.CassandraExtension; +import org.apache.james.CassandraRabbitMQJamesConfiguration; +import org.apache.james.CassandraRabbitMQJamesServerMain; +import org.apache.james.DockerElasticSearchExtension; +import org.apache.james.GuiceJamesServer; +import org.apache.james.JamesServerBuilder; +import org.apache.james.JamesServerExtension; +import org.apache.james.jmap.rfc8621.contract.AuthenticationContract; +import org.apache.james.modules.AwsS3BlobStoreExtension; +import org.apache.james.modules.RabbitMQExtension; +import org.apache.james.modules.TestJMAPServerModule; +import org.apache.james.modules.blobstore.BlobStoreConfiguration; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class DistributedBasicAuthenticationTest implements AuthenticationContract { + @RegisterExtension + static JamesServerExtension testExtension = new JamesServerBuilder<CassandraRabbitMQJamesConfiguration>(tmpDir -> + CassandraRabbitMQJamesConfiguration.builder() + .workingDirectory(tmpDir) + .configurationFromClasspath() + .blobStore(BlobStoreConfiguration.objectStorage().disableCache()) + .build()) + .extension(new DockerElasticSearchExtension()) + .extension(new CassandraExtension()) + .extension(new RabbitMQExtension()) + .extension(new AwsS3BlobStoreExtension()) + .server(configuration -> CassandraRabbitMQJamesServerMain.createServer(configuration) + .overrideWith(new TestJMAPServerModule())) + .build(); +} diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/AuthenticationContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/AuthenticationContract.scala new file mode 100644 index 0000000..6716c1a --- /dev/null +++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/AuthenticationContract.scala @@ -0,0 +1,151 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.jmap.rfc8621.contract + +import java.nio.charset.StandardCharsets.UTF_8 +import java.util.Base64 + +import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT +import io.restassured.RestAssured._ +import io.restassured.authentication.NoAuthScheme +import io.restassured.http.{Header, Headers} +import org.apache.http.HttpStatus.{SC_OK, SC_UNAUTHORIZED} +import org.apache.james.GuiceJamesServer +import org.apache.james.jmap.rfc8621.contract.AuthenticationContract._ +import org.apache.james.jmap.rfc8621.contract.Fixture._ +import org.apache.james.jmap.rfc8621.contract.tags.CategoryTags +import org.apache.james.utils.DataProbeImpl +import org.junit.jupiter.api.{BeforeEach, Tag, Test} + +object AuthenticationContract { + private val AUTHORIZATION_HEADER: String = "Authorization" +} + +trait AuthenticationContract { + @BeforeEach + def setUp(server: GuiceJamesServer): Unit = { + server.getProbe(classOf[DataProbeImpl]) + .fluent() + .addDomain(DOMAIN.asString()) + .addUser(BOB.asString(), BOB_PASSWORD) + .addDomain(_2_DOT_DOMAIN.asString()) + .addUser(ALICE.asString(), ALICE_PASSWORD) + + requestSpecification = baseRequestSpecBuilder(server) + .setAuth(new NoAuthScheme()) + .build + } + + @Test + def postShouldRespondUnauthorizedWhenNoAuthorizationHeader(): Unit = { + `given`() + .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .body(ECHO_REQUEST_OBJECT) + .when() + .post() + .then + .statusCode(SC_UNAUTHORIZED) + } + + @Test + @Tag(CategoryTags.BASIC_FEATURE) + def postShouldRespond200WhenHasCredentials(): Unit = { + val authHeader: Header = new Header(AUTHORIZATION_HEADER, s"Basic ${toBase64(s"${BOB.asString}:$BOB_PASSWORD")}") + `given`() + .headers(getHeadersWith(authHeader)) + .body(ECHO_REQUEST_OBJECT) + .when() + .post() + .then + .statusCode(SC_OK) + } + + @Test + def postShouldRespond401WhenCredentialsWithInvalidUser(): Unit = { + val authHeader: Header = new Header(AUTHORIZATION_HEADER, s"Basic ${toBase64(s"${BOB.getLocalPart}@@$DOMAIN:$BOB_PASSWORD")}") + `given`() + .headers(getHeadersWith(authHeader)) + .body(ECHO_REQUEST_OBJECT) + .when() + .post() + .then + .statusCode(SC_UNAUTHORIZED) + } + + @Test + def postShouldRespondOKWhenCredentialsWith2DotDomain(): Unit = { + val authHeader: Header = new Header(AUTHORIZATION_HEADER, s"Basic ${toBase64(s"${ALICE.asString}:$ALICE_PASSWORD")}") + `given`() + .headers(getHeadersWith(authHeader)) + .body(ECHO_REQUEST_OBJECT) + .when() + .post() + .then + .statusCode(SC_OK) + } + + @Test + def postShouldRespond401WhenCredentialsWithSpaceDomain(): Unit = { + val authHeader: Header = new Header(AUTHORIZATION_HEADER, s"Basic ${toBase64(s"${BOB.getLocalPart}@$DOMAIN_WITH_SPACE:$BOB_PASSWORD")}") + `given`() + .headers(getHeadersWith(authHeader)) + .body(ECHO_REQUEST_OBJECT) + .when() + .post() + .then + .statusCode(SC_UNAUTHORIZED) + } + + @Test + def postShouldRespond401WhenUserNotFound(): Unit = { + val authHeader: Header = new Header(AUTHORIZATION_HEADER, s"Basic ${toBase64(s"usernotfound@$DOMAIN:$BOB_PASSWORD")}") + `given`() + .headers(getHeadersWith(authHeader)) + .body(ECHO_REQUEST_OBJECT) + .when() + .post() + .then + .statusCode(SC_UNAUTHORIZED) + } + + @Test + @Tag(CategoryTags.BASIC_FEATURE) + def postShouldRespond401WhenWrongPassword(): Unit = { + val authHeader: Header = new Header(AUTHORIZATION_HEADER, s"Basic ${toBase64(s"${BOB.asString}:WRONG_PASSWORD")}") + `given`() + .headers(getHeadersWith(authHeader)) + .body(ECHO_REQUEST_OBJECT) + .when() + .post() + .then + .statusCode(SC_UNAUTHORIZED) + } + + private def getHeadersWith(authHeader: Header): Headers = { + new Headers( + new Header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER), + authHeader + ) + } + + private def toBase64(stringValue: String): String = { + Base64.getEncoder.encodeToString(stringValue.getBytes(UTF_8)) + } +} diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EchoMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EchoMethodContract.scala index 79578fc..509106d 100644 --- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EchoMethodContract.scala +++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EchoMethodContract.scala @@ -18,41 +18,20 @@ ****************************************************************/ package org.apache.james.jmap.rfc8621.contract -import java.nio.charset.StandardCharsets - import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT -import io.restassured.RestAssured -import io.restassured.builder.RequestSpecBuilder -import io.restassured.config.EncoderConfig.encoderConfig -import io.restassured.config.RestAssuredConfig.newConfig -import io.restassured.http.ContentType +import io.restassured.RestAssured._ +import io.restassured.http.ContentType.JSON import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson -import org.apache.http.HttpStatus +import org.apache.http.HttpStatus.SC_OK import org.apache.james.GuiceJamesServer -import org.apache.james.jmap.JMAPUrls.JMAP -import org.apache.james.jmap.draft.JmapGuiceProbe +import org.apache.james.jmap.http.UserCredential import org.apache.james.jmap.rfc8621.contract.EchoMethodContract._ +import org.apache.james.jmap.rfc8621.contract.Fixture._ import org.apache.james.jmap.rfc8621.contract.tags.CategoryTags +import org.apache.james.utils.DataProbeImpl import org.junit.jupiter.api.{BeforeEach, Tag, Test} object EchoMethodContract { - - private val REQUEST_OBJECT: String = - """{ - | "using": [ - | "urn:ietf:params:jmap:core" - | ], - | "methodCalls": [ - | [ - | "Core/echo", - | { - | "arg1": "arg1data", - | "arg2": "arg2data" - | }, - | "c1" - | ] - | ] - |}""".stripMargin private val REQUEST_OBJECT_WITH_UNSUPPORTED_METHOD: String = """{ | "using": [ @@ -77,20 +56,6 @@ object EchoMethodContract { | ] |}""".stripMargin - private val RESPONSE_OBJECT: String = - """{ - | "sessionState": "75128aab4b1b", - | "methodResponses": [ - | [ - | "Core/echo", - | { - | "arg1": "arg1data", - | "arg2": "arg2data" - | }, - | "c1" - | ] - | ] - |}""".stripMargin private val RESPONSE_OBJECT_WITH_UNSUPPORTED_METHOD: String = """{ | "sessionState": "75128aab4b1b", @@ -112,55 +77,51 @@ object EchoMethodContract { | ] | ] |}""".stripMargin - - private val ACCEPT_RFC8621_VERSION_HEADER: String = """application/json; jmapVersion=rfc-8621""" } trait EchoMethodContract { @BeforeEach def setUp(server: GuiceJamesServer): Unit = { - RestAssured.requestSpecification = new RequestSpecBuilder() - .setContentType(ContentType.JSON) - .setAccept(ContentType.JSON) - .setConfig(newConfig.encoderConfig(encoderConfig.defaultContentCharset(StandardCharsets.UTF_8))) - .setPort(server.getProbe(classOf[JmapGuiceProbe]) - .getJmapPort - .getValue) - .setBasePath(JMAP) + server.getProbe(classOf[DataProbeImpl]) + .fluent() + .addDomain(DOMAIN.asString()) + .addUser(BOB.asString(), BOB_PASSWORD) + + requestSpecification = baseRequestSpecBuilder(server) + .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD))) .build } @Test @Tag(CategoryTags.BASIC_FEATURE) def echoMethodShouldRespondOKWithRFC8621VersionAndSupportedMethod(): Unit = { - val response: String = RestAssured - .`given`() + + val response: String = `given`() .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) - .body(REQUEST_OBJECT) + .body(ECHO_REQUEST_OBJECT) .when() .post() .then - .statusCode(HttpStatus.SC_OK) - .contentType(ContentType.JSON) + .statusCode(SC_OK) + .contentType(JSON) .extract() .body() .asString() - assertThatJson(response).isEqualTo(RESPONSE_OBJECT) + assertThatJson(response).isEqualTo(ECHO_RESPONSE_OBJECT) } @Test def echoMethodShouldRespondWithRFC8621VersionAndUnsupportedMethod(): Unit = { - val response: String = RestAssured - .`given`() + val response: String = `given`() .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) .body(REQUEST_OBJECT_WITH_UNSUPPORTED_METHOD) .when() .post() .then - .statusCode(HttpStatus.SC_OK) - .contentType(ContentType.JSON) + .statusCode(SC_OK) + .contentType(JSON) .extract() .body() .asString() diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/Fixture.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/Fixture.scala new file mode 100644 index 0000000..56be582 --- /dev/null +++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/Fixture.scala @@ -0,0 +1,94 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.jmap.rfc8621.contract + +import java.nio.charset.StandardCharsets + +import io.restassured.authentication.PreemptiveBasicAuthScheme +import io.restassured.builder.RequestSpecBuilder +import io.restassured.config.EncoderConfig.encoderConfig +import io.restassured.config.RestAssuredConfig.newConfig +import io.restassured.http.ContentType +import org.apache.james.GuiceJamesServer +import org.apache.james.core.{Domain, Username} +import org.apache.james.jmap.JMAPUrls.JMAP +import org.apache.james.jmap.draft.JmapGuiceProbe +import org.apache.james.jmap.http.UserCredential + +object Fixture { + def baseRequestSpecBuilder(server: GuiceJamesServer) = new RequestSpecBuilder() + .setContentType(ContentType.JSON) + .setAccept(ContentType.JSON) + .setConfig(newConfig.encoderConfig(encoderConfig.defaultContentCharset(StandardCharsets.UTF_8))) + .setPort(server.getProbe(classOf[JmapGuiceProbe]) + .getJmapPort + .getValue) + .setBasePath(JMAP) + + def authScheme(userCredential: UserCredential): PreemptiveBasicAuthScheme = { + val authScheme: PreemptiveBasicAuthScheme = new PreemptiveBasicAuthScheme + authScheme.setUserName(userCredential.username.asString()) + authScheme.setPassword(userCredential.password) + + authScheme + } + + val DOMAIN: Domain = Domain.of("domain.tld") + val DOMAIN_WITH_SPACE: Domain = Domain.of("dom ain.tld") + val _2_DOT_DOMAIN: Domain = Domain.of("do.main.tld") + val BOB: Username = Username.fromLocalPartWithDomain("bob", DOMAIN) + val ALICE: Username = Username.fromLocalPartWithDomain("alice", _2_DOT_DOMAIN) + val BOB_PASSWORD: String = "bobpassword" + val ALICE_PASSWORD: String = "alicepassword" + + val ECHO_REQUEST_OBJECT: String = + """{ + | "using": [ + | "urn:ietf:params:jmap:core" + | ], + | "methodCalls": [ + | [ + | "Core/echo", + | { + | "arg1": "arg1data", + | "arg2": "arg2data" + | }, + | "c1" + | ] + | ] + |}""".stripMargin + + val ECHO_RESPONSE_OBJECT: String = + """{ + | "sessionState": "75128aab4b1b", + | "methodResponses": [ + | [ + | "Core/echo", + | { + | "arg1": "arg1data", + | "arg2": "arg2data" + | }, + | "c1" + | ] + | ] + |}""".stripMargin + + val ACCEPT_RFC8621_VERSION_HEADER: String = "application/json; jmapVersion=rfc-8621" +} diff --git a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryAuthenticationTest.java b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryAuthenticationTest.java new file mode 100644 index 0000000..c99bf0f --- /dev/null +++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryAuthenticationTest.java @@ -0,0 +1,38 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.jmap.rfc8621.memory; + +import static org.apache.james.MemoryJamesServerMain.IN_MEMORY_SERVER_AGGREGATE_MODULE; + +import org.apache.james.GuiceJamesServer; +import org.apache.james.JamesServerBuilder; +import org.apache.james.JamesServerExtension; +import org.apache.james.jmap.rfc8621.contract.AuthenticationContract; +import org.apache.james.modules.TestJMAPServerModule; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class MemoryAuthenticationTest implements AuthenticationContract { + @RegisterExtension + static JamesServerExtension testExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider()) + .server(configuration -> GuiceJamesServer.forConfiguration(configuration) + .combineWith(IN_MEMORY_SERVER_AGGREGATE_MODULE) + .overrideWith(new TestJMAPServerModule())) + .build(); +} diff --git a/server/protocols/jmap-rfc-8621/pom.xml b/server/protocols/jmap-rfc-8621/pom.xml index ba30cd2..feccb0c 100644 --- a/server/protocols/jmap-rfc-8621/pom.xml +++ b/server/protocols/jmap-rfc-8621/pom.xml @@ -34,26 +34,25 @@ <dependencies> <dependency> - <groupId>com.typesafe.play</groupId> - <artifactId>play-json_${scala.base}</artifactId> - </dependency> - <dependency> - <groupId>eu.timepit</groupId> - <artifactId>refined_${scala.base}</artifactId> - <version>0.9.13</version> + <groupId>${james.groupId}</groupId> + <artifactId>apache-james-mailbox-api</artifactId> </dependency> <dependency> - <groupId>io.rest-assured</groupId> - <artifactId>rest-assured</artifactId> + <groupId>${james.groupId}</groupId> + <artifactId>apache-james-mailbox-api</artifactId> + <type>test-jar</type> <scope>test</scope> </dependency> <dependency> - <groupId>io.projectreactor.netty</groupId> - <artifactId>reactor-netty</artifactId> + <groupId>${james.groupId}</groupId> + <artifactId>apache-james-mailbox-memory</artifactId> + <scope>test</scope> </dependency> <dependency> - <groupId>io.projectreactor</groupId> - <artifactId>reactor-scala-extensions_${scala.base}</artifactId> + <groupId>${james.groupId}</groupId> + <artifactId>apache-james-mailbox-memory</artifactId> + <scope>test</scope> + <type>test-jar</type> </dependency> <dependency> <groupId>${james.groupId}</groupId> @@ -61,26 +60,47 @@ </dependency> <dependency> <groupId>${james.groupId}</groupId> - <artifactId>james-server-jmap</artifactId> + <artifactId>james-server-data-api</artifactId> </dependency> <dependency> <groupId>${james.groupId}</groupId> - <artifactId>testing-base</artifactId> + <artifactId>james-server-data-memory</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>${james.groupId}</groupId> - <artifactId>apache-james-mailbox-api</artifactId> + <artifactId>james-server-jmap</artifactId> </dependency> <dependency> <groupId>${james.groupId}</groupId> - <artifactId>apache-james-mailbox-api</artifactId> - <type>test-jar</type> + <artifactId>metrics-tests</artifactId> <scope>test</scope> </dependency> <dependency> - <groupId>net.javacrumbs.json-unit</groupId> - <artifactId>json-unit-assertj</artifactId> + <groupId>${james.groupId}</groupId> + <artifactId>testing-base</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.typesafe.play</groupId> + <artifactId>play-json_${scala.base}</artifactId> + </dependency> + <dependency> + <groupId>eu.timepit</groupId> + <artifactId>refined_${scala.base}</artifactId> + <version>0.9.13</version> + </dependency> + <dependency> + <groupId>io.projectreactor</groupId> + <artifactId>reactor-scala-extensions_${scala.base}</artifactId> + </dependency> + <dependency> + <groupId>io.projectreactor.netty</groupId> + <artifactId>reactor-netty</artifactId> + </dependency> + <dependency> + <groupId>io.rest-assured</groupId> + <artifactId>rest-assured</artifactId> <scope>test</scope> </dependency> <dependency> diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/BasicAuthenticationStrategy.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/BasicAuthenticationStrategy.scala new file mode 100644 index 0000000..52d9127 --- /dev/null +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/BasicAuthenticationStrategy.scala @@ -0,0 +1,109 @@ +/** ************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + * ***************************************************************/ + +package org.apache.james.jmap.http + +import java.util.Base64 + +import eu.timepit.refined.api.Refined +import eu.timepit.refined.auto._ +import eu.timepit.refined.refineV +import eu.timepit.refined.string.MatchesRegex +import javax.inject.Inject +import org.apache.james.core.Username +import org.apache.james.jmap.http.UserCredential._ +import org.apache.james.mailbox.{MailboxManager, MailboxSession} +import org.apache.james.user.api.UsersRepository +import org.slf4j.LoggerFactory +import reactor.core.publisher.Mono +import reactor.core.scala.publisher.SFlux +import reactor.netty.http.server.HttpServerRequest + +import scala.compat.java8.StreamConverters._ +import scala.util.{Failure, Success, Try} + +object UserCredential { + type BasicAuthenticationHeaderValue = String Refined MatchesRegex["Basic [\\d\\w=]++"] + type CredentialsAsString = String Refined MatchesRegex[".*:.*"] + + private val logger = LoggerFactory.getLogger(classOf[UserCredential]) + private val BASIC_AUTHENTICATION_PREFIX: String = "Basic " + + def parseUserCredentials(token: String): Option[UserCredential] = { + val refinedValue: Either[String, BasicAuthenticationHeaderValue] = refineV(token) + + refinedValue match { + // Ignore Authentication headers not being Basic Auth + case Left(_) => None + case Right(value) => extractUserCredentialsAsString(value) + } + } + + private def extractUserCredentialsAsString(token: BasicAuthenticationHeaderValue): Option[UserCredential] = { + val encodedCredentials = token.replace(BASIC_AUTHENTICATION_PREFIX, "") + val decodedCredentialsString = new String(Base64.getDecoder.decode(encodedCredentials)) + val refinedValue: Either[String, CredentialsAsString] = refineV(decodedCredentialsString) + + refinedValue match { + case Left(errorMessage: String) => + logger.info(s"Supplied basic authentication credentials do not match expected format. $errorMessage") + None + case Right(value) => toCredential(value) + } + } + + private def toCredential(token: CredentialsAsString): Option[UserCredential] = { + val partSeparatorIndex: Int = token.indexOf(':') + val usernameString: String = token.substring(0, partSeparatorIndex) + val passwordString: String = token.substring(partSeparatorIndex + 1) + + Try(UserCredential(Username.of(usernameString), passwordString)) match { + case Success(credential) => Some(credential) + case Failure(throwable:IllegalArgumentException) => + logger.info("Username is not valid", throwable) + None + case Failure(unexpectedException) => + logger.error("Unexpected Exception", unexpectedException) + None + } + } +} + +case class UserCredential(username: Username, password: String) + +class BasicAuthenticationStrategy @Inject()(val usersRepository: UsersRepository, + val mailboxManager: MailboxManager) extends AuthenticationStrategy { + + override def createMailboxSession(httpRequest: HttpServerRequest): Mono[MailboxSession] = { + SFlux.fromStream(() => authHeaders(httpRequest).toScala[Stream]) + .map(parseUserCredentials) + .handle(publishNext) + .filter(isValid) + .map(_.username) + .map(mailboxManager.createSystemSession) + .singleOrEmpty() + .asJava() + } + + private def publishNext[T]: (Option[T], reactor.core.publisher.SynchronousSink[T]) => Unit = + (maybeT, sink) => maybeT.foreach(t => sink.next(t)) + + private def isValid(userCredential: UserCredential): Boolean = + usersRepository.test(userCredential.username, userCredential.password) +} diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala index 465c212..a7efeab 100644 --- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala @@ -24,12 +24,13 @@ import java.util.stream.Stream import io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE import io.netty.handler.codec.http.HttpMethod import io.netty.handler.codec.http.HttpResponseStatus.OK -import javax.inject.Inject +import javax.inject.{Inject, Named} import org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE_UTF8 import org.apache.james.jmap.JMAPRoutes.CORS_CONTROL import org.apache.james.jmap.JMAPUrls.AUTHENTICATION import org.apache.james.jmap.exceptions.UnauthorizedException import org.apache.james.jmap.http.SessionRoutes.{JMAP_SESSION, LOGGER} +import org.apache.james.jmap.http.rfc8621.InjectionKeys import org.apache.james.jmap.json.Serializer import org.apache.james.jmap.model.Session import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes} @@ -46,8 +47,8 @@ object SessionRoutes { } @Inject -class SessionRoutes(val serializer: Serializer, - val authenticator: Authenticator, +class SessionRoutes(@Named(InjectionKeys.RFC_8621) val authenticator: Authenticator, + val serializer: Serializer, val sessionSupplier: SessionSupplier = new SessionSupplier()) extends JMAPRoutes { private val generateSession: JMAPRoute.Action = diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala index 8ea7002..410ad77 100644 --- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala @@ -27,22 +27,31 @@ import eu.timepit.refined.auto._ import io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE import io.netty.handler.codec.http.HttpMethod import io.netty.handler.codec.http.HttpResponseStatus.OK -import javax.inject.Inject +import javax.inject.{Inject, Named} import org.apache.http.HttpStatus.SC_BAD_REQUEST import org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE import org.apache.james.jmap.JMAPUrls.JMAP +import org.apache.james.jmap.exceptions.UnauthorizedException +import org.apache.james.jmap.http.Authenticator +import org.apache.james.jmap.http.rfc8621.InjectionKeys import org.apache.james.jmap.json.Serializer import org.apache.james.jmap.method.CoreEcho import org.apache.james.jmap.model.Invocation.{Arguments, MethodName} import org.apache.james.jmap.model.{Invocation, RequestObject, ResponseObject} import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes} +import org.slf4j.{Logger, LoggerFactory} import play.api.libs.json.{JsError, JsSuccess, Json} import reactor.core.publisher.Mono import reactor.core.scala.publisher.{SFlux, SMono} import reactor.core.scheduler.Schedulers import reactor.netty.http.server.{HttpServerRequest, HttpServerResponse} -class JMAPApiRoutes @Inject() (serializer: Serializer) extends JMAPRoutes { +object JMAPApiRoutes { + val LOGGER: Logger = LoggerFactory.getLogger(classOf[JMAPApiRoutes]) +} + +class JMAPApiRoutes @Inject() (@Named(InjectionKeys.RFC_8621) val authenticator: Authenticator, + val serializer: Serializer) extends JMAPRoutes { private val coreEcho = new CoreEcho override def routes(): stream.Stream[JMAPRoute] = Stream.of( @@ -56,8 +65,9 @@ class JMAPApiRoutes @Inject() (serializer: Serializer) extends JMAPRoutes { .corsHeaders()) private def post(httpServerRequest: HttpServerRequest, httpServerResponse: HttpServerResponse): Mono[Void] = - this.requestAsJsonStream(httpServerRequest) - .flatMap(requestObject => this.process(requestObject, httpServerResponse)) + SMono(authenticator.authenticate(httpServerRequest)) + .flatMap(_ => this.requestAsJsonStream(httpServerRequest) + .flatMap(requestObject => this.process(requestObject, httpServerResponse))) .onErrorResume(throwable => handleError(throwable, httpServerResponse)) .subscribeOn(Schedulers.elastic) .asJava() @@ -101,14 +111,12 @@ class JMAPApiRoutes @Inject() (serializer: Serializer) extends JMAPRoutes { invocation.methodCallId)) } - private def handleError(throwable: Throwable, httpServerResponse: HttpServerResponse): SMono[Void] = { - if (throwable.isInstanceOf[IllegalArgumentException]) { - return SMono.fromPublisher(httpServerResponse.status(SC_BAD_REQUEST) - .header(CONTENT_TYPE, JSON_CONTENT_TYPE) - .sendString(SMono.fromCallable(() => throwable.getMessage), StandardCharsets.UTF_8) - .`then`()) - } - - SMono.fromPublisher(handleInternalError(httpServerResponse, throwable)) + private def handleError(throwable: Throwable, httpServerResponse: HttpServerResponse): SMono[Void] = throwable match { + case exception: IllegalArgumentException => SMono.fromPublisher(httpServerResponse.status(SC_BAD_REQUEST) + .header(CONTENT_TYPE, JSON_CONTENT_TYPE) + .sendString(SMono.fromCallable(() => exception.getMessage), StandardCharsets.UTF_8) + .`then`()) + case exception: UnauthorizedException => SMono(handleAuthenticationFailure(httpServerResponse, JMAPApiRoutes.LOGGER, exception)) + case _ => SMono.fromPublisher(handleInternalError(httpServerResponse, throwable)) } } diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/UserCredentialParserTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/UserCredentialParserTest.scala new file mode 100644 index 0000000..8ac66c8 --- /dev/null +++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/UserCredentialParserTest.scala @@ -0,0 +1,135 @@ +/** ************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + * ***************************************************************/ + +package org.apache.james.jmap.http + +import java.nio.charset.StandardCharsets +import java.util.Base64 + +import org.apache.james.core.Username +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class UserCredentialParserTest { + @Test + def shouldReturnCredentialWhenUsernamePasswordToken(): Unit = { + val token: String = "Basic " + toBase64("user1:password") + + assertThat(UserCredential.parseUserCredentials(token)) + .isEqualTo(Some(UserCredential(Username.of("user1"), "password"))) + } + + @Test + def shouldAcceptPartSeparatorAsPartOfPassword(): Unit = { + val token: String = "Basic " + toBase64("user1:pass:word") + + assertThat(UserCredential.parseUserCredentials(token)) + .isEqualTo(Some(UserCredential(Username.of("user1"), "pass:word"))) + } + + @Test + def shouldReturnCredentialWhenRandomSpecialCharacterInUsernameToken(): Unit = { + val token: String = "Basic " + toBase64("fd2*#jk:password") + + assertThat(UserCredential.parseUserCredentials(token)) + .isEqualTo(Some(UserCredential(Username.of("fd2*#jk"), "password"))) + } + + @Test + def shouldReturnCredentialsWhenRandomSpecialCharacterInBothUsernamePasswoedToken(): Unit = { + val token: String = "Basic " + toBase64("fd2*#jk:password@fd23*&^$%") + + assertThat(UserCredential.parseUserCredentials(token)) + .isEqualTo(Some(UserCredential(Username.of("fd2*#jk"), "password@fd23*&^$%"))) + } + + @Test + def shouldReturnCredentialWhenUsernameDomainPasswordToken(): Unit = { + val token: String = "Basic " + toBase64("[email protected]:password") + + assertThat(UserCredential.parseUserCredentials(token)) + .isEqualTo(Some(UserCredential(Username.of("[email protected]"), "password"))) + } + + @Test + def shouldReturnCredentialWhenUsernameDomainNoPasswordToken(): Unit = { + val token: String = "Basic " + toBase64("[email protected]:") + + assertThat(UserCredential.parseUserCredentials(token)) + .isEqualTo(Some(UserCredential(Username.of("[email protected]"), ""))) + } + + @Test + def shouldReturnNoneWhenPayloadIsNotBase64(): Unit = { + val token: String = "Basic user1:password" + + assertThat(UserCredential.parseUserCredentials(token)) + .isEqualTo(None) + } + + @Test + def shouldReturnNoneWhenEmptyToken(): Unit = { + assertThat(UserCredential.parseUserCredentials("")) + .isEqualTo(None) + } + + @Test + def shouldReturnNoneWhenWrongFormatCredential(): Unit = { + val token: String = "Basic " + toBase64("user1@password") + + assertThat(UserCredential.parseUserCredentials(token)) + .isEqualTo(None) + } + + @Test + def shouldReturnNoneWhenUpperCaseToken(): Unit = { + val token: String = "BASIC " + toBase64("user1@password") + + assertThat(UserCredential.parseUserCredentials(token)) + .isEqualTo(None) + } + + @Test + def shouldReturnNoneWhenLowerCaseToken(): Unit = { + val token: String = "basic " + toBase64("user1:password") + + assertThat(UserCredential.parseUserCredentials(token)) + .isEqualTo(None) + } + + @Test + def shouldReturnNoneWhenCredentialWithNoPassword(): Unit = { + val token: String = "Basic " + toBase64("user1:") + + assertThat(UserCredential.parseUserCredentials(token)) + .isEqualTo(Some(UserCredential(Username.of("user1"), ""))) + } + + @Test + def shouldReturnEmptyWhenCredentialWithNoUsername(): Unit = { + val token: String = "Basic " + toBase64(":pass") + + assertThat(UserCredential.parseUserCredentials(token)) + .isEqualTo(None) + } + + private def toBase64(stringValue: String): String = { + Base64.getEncoder.encodeToString(stringValue.getBytes(StandardCharsets.UTF_8)) + } +} diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala index 9bf7ba8..f23e835 100644 --- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala +++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala @@ -19,6 +19,7 @@ package org.apache.james.jmap.routes import java.nio.charset.StandardCharsets +import java.util.Base64 import com.google.common.collect.ImmutableSet import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT @@ -26,28 +27,51 @@ import io.restassured.RestAssured import io.restassured.builder.RequestSpecBuilder import io.restassured.config.EncoderConfig.encoderConfig import io.restassured.config.RestAssuredConfig.newConfig -import io.restassured.http.ContentType +import io.restassured.http.{ContentType, Header, Headers} import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson import org.apache.http.HttpStatus +import org.apache.james.core.{Domain, Username} +import org.apache.james.dnsservice.api.DNSService +import org.apache.james.domainlist.memory.MemoryDomainList import org.apache.james.jmap.JMAPUrls.JMAP -import org.apache.james.jmap.json.Serializer import org.apache.james.jmap._ +import org.apache.james.jmap.http.{Authenticator, BasicAuthenticationStrategy} +import org.apache.james.jmap.json.Serializer +import org.apache.james.jmap.routes.JMAPApiRoutesTest._ +import org.apache.james.mailbox.MailboxManager +import org.apache.james.mailbox.extension.PreDeletionHook +import org.apache.james.mailbox.inmemory.MemoryMailboxManagerProvider import org.apache.james.mailbox.model.TestId +import org.apache.james.metrics.tests.RecordingMetricFactory +import org.apache.james.user.memory.MemoryUsersRepository +import org.mockito.Mockito.mock import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers -class JMAPApiRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers { +object JMAPApiRoutesTest { private val SERIALIZER: Serializer = new Serializer(new TestId.Factory) - private val TEST_CONFIGURATION: JMAPConfiguration = JMAPConfiguration.builder().enable().randomPort().build() private val ACCEPT_JMAP_VERSION_HEADER = "application/json; jmapVersion=" private val ACCEPT_DRAFT_VERSION_HEADER = ACCEPT_JMAP_VERSION_HEADER + Version.DRAFT.asString() private val ACCEPT_RFC8621_VERSION_HEADER = ACCEPT_JMAP_VERSION_HEADER + Version.RFC8621.asString() - private val JMAP_API_ROUTE: JMAPApiRoutes = new JMAPApiRoutes(SERIALIZER) + private val empty_set: ImmutableSet[PreDeletionHook] = ImmutableSet.of() + private val dnsService = mock(classOf[DNSService]) + private val domainList = new MemoryDomainList(dnsService) + domainList.addDomain(Domain.of("james.org")) + + private val usersRepository = MemoryUsersRepository.withoutVirtualHosting(domainList) + usersRepository.addUser(Username.of("user1"), "password") + + private val mailboxManager: MailboxManager = MemoryMailboxManagerProvider.provideMailboxManager(empty_set) + private val authenticationStrategy: BasicAuthenticationStrategy = new BasicAuthenticationStrategy(usersRepository, mailboxManager) + private val AUTHENTICATOR: Authenticator = Authenticator.of(new RecordingMetricFactory, authenticationStrategy) + private val JMAP_API_ROUTE: JMAPApiRoutes = new JMAPApiRoutes(AUTHENTICATOR, SERIALIZER) private val ROUTES_HANDLER: ImmutableSet[JMAPRoutesHandler] = ImmutableSet.of(new JMAPRoutesHandler(Version.RFC8621, JMAP_API_ROUTE)) + private val userBase64String: String = Base64.getEncoder.encodeToString("user1:password".getBytes(StandardCharsets.UTF_8)) + private val REQUEST_OBJECT: String = """{ | "using": [ @@ -136,6 +160,9 @@ class JMAPApiRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers { | } |} |""".stripMargin +} + +class JMAPApiRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers { var jmapServer: JMAPServer = _ @@ -158,88 +185,121 @@ class JMAPApiRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers { } "RFC-8621 version, GET" should "not supported and return 404 status" in { + val headers: Headers = Headers.headers( + new Header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER), + new Header("Authorization", s"Basic ${userBase64String}") + ) + RestAssured .`given`() - .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .headers(headers) .when() - .get + .get .then - .statusCode(HttpStatus.SC_NOT_FOUND) + .statusCode(HttpStatus.SC_NOT_FOUND) } "RFC-8621 version, POST, without body" should "return 200 status" in { + val headers: Headers = Headers.headers( + new Header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER), + new Header("Authorization", s"Basic ${userBase64String}") + ) + RestAssured .`given`() - .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .headers(headers) .when() - .post + .post .then - .statusCode(HttpStatus.SC_OK) + .statusCode(HttpStatus.SC_OK) } "RFC-8621 version, POST, methods include supported" should "return OK status" in { + val headers: Headers = Headers.headers( + new Header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER), + new Header("Authorization", s"Basic ${userBase64String}") + ) + val response = RestAssured .`given`() - .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) - .body(REQUEST_OBJECT) + .headers(headers) + .body(REQUEST_OBJECT) .when() - .post() + .post() .then - .statusCode(HttpStatus.SC_OK) - .contentType(ContentType.JSON) + .statusCode(HttpStatus.SC_OK) + .contentType(ContentType.JSON) .extract() - .body() - .asString() + .body() + .asString() assertThatJson(response).isEqualTo(RESPONSE_OBJECT) } "RFC-8621 version, POST, with methods" should "return OK status, ResponseObject depend on method" in { + val headers: Headers = Headers.headers( + new Header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER), + new Header("Authorization", s"Basic ${userBase64String}") + ) + val response = RestAssured .`given`() - .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) - .body(REQUEST_OBJECT_WITH_UNSUPPORTED_METHOD) + .headers(headers) + .body(REQUEST_OBJECT_WITH_UNSUPPORTED_METHOD) .when() - .post() + .post() .then - .statusCode(HttpStatus.SC_OK) - .contentType(ContentType.JSON) + .statusCode(HttpStatus.SC_OK) + .contentType(ContentType.JSON) .extract() - .body() - .asString() + .body() + .asString() assertThatJson(response).isEqualTo(RESPONSE_OBJECT_WITH_UNSUPPORTED_METHOD) } "Draft version, GET" should "return 404 status" in { + val headers: Headers = Headers.headers( + new Header(ACCEPT.toString, ACCEPT_DRAFT_VERSION_HEADER), + new Header("Authorization", s"Basic ${userBase64String}") + ) + RestAssured .`given`() - .header(ACCEPT.toString, ACCEPT_DRAFT_VERSION_HEADER) + .headers(headers) .when() - .get + .get .then - .statusCode(HttpStatus.SC_NOT_FOUND) + .statusCode(HttpStatus.SC_NOT_FOUND) } "Draft version, POST, without body" should "return 400 status" in { + val headers: Headers = Headers.headers( + new Header(ACCEPT.toString, ACCEPT_DRAFT_VERSION_HEADER), + new Header("Authorization", s"Basic ${userBase64String}") + ) RestAssured .`given`() - .header(ACCEPT.toString, ACCEPT_DRAFT_VERSION_HEADER) + .headers(headers) .when() - .post + .post .then - .statusCode(HttpStatus.SC_NOT_FOUND) + .statusCode(HttpStatus.SC_NOT_FOUND) } "RFC-8621 version, POST, with wrong requestObject body" should "return 400 status" in { + val headers: Headers = Headers.headers( + new Header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER), + new Header("Authorization", s"Basic ${userBase64String}") + ) RestAssured .`given`() - .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) - .body(WRONG_OBJECT_REQUEST) + .headers(headers) + .body(WRONG_OBJECT_REQUEST) .when() - .post + .post .then - .statusCode(HttpStatus.SC_BAD_REQUEST) + .statusCode(HttpStatus.SC_BAD_REQUEST) } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
