diff --git a/app/controllers/SessionDataController.scala b/app/controllers/SessionDataController.scala new file mode 100644 index 0000000..e8e5e6e --- /dev/null +++ b/app/controllers/SessionDataController.scala @@ -0,0 +1,70 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed 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 controllers + +import common.Extractors +import play.api.Logging +import play.api.libs.json.JsValue +import play.api.mvc.{Action, AnyContent, ControllerComponents} +import services.{AuthService, SessionDataService} +import uk.gov.hmrc.play.bootstrap.backend.controller.BackendController + +import javax.inject.{Inject, Singleton} +import scala.concurrent.ExecutionContext + +@Singleton +class SessionDataController @Inject()(authService: AuthService, + sessionDataService: SessionDataService, + cc: ControllerComponents) + (implicit ec: ExecutionContext) extends BackendController(cc) with Logging with Extractors { + + + def getAllSessionData: Action[AnyContent] = Action.async { implicit request => + authService.authorised() { + sessionDataService.getAllSessionData.map { + case Some(data) => Ok(data) + case None => NoContent + } + } + } + + def retrieveSessionData(id: String): Action[AnyContent] = Action.async { implicit request => + authService.authorised() { + sessionDataService.getSessionData(id).map { + case Some(data) => Ok(data) + case None => NoContent + } + } + } + + def insertSessionData(id: String): Action[JsValue] = Action.async(parse.json) { implicit request => + authService.authorised() { + sessionDataService.insertSessionData( + dataId = id, + request.body + ).map(_ => Ok) + } + } + + def deleteSessionData(id: String): Action[AnyContent] = Action.async { implicit request => + authService.authorised() { + sessionDataService.deleteSessionData( + dataId = id + ).map(_ => Ok) + } + } +} diff --git a/app/services/SessionDataService.scala b/app/services/SessionDataService.scala new file mode 100644 index 0000000..ef64f58 --- /dev/null +++ b/app/services/SessionDataService.scala @@ -0,0 +1,59 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed 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 services + +import config.AppConfig +import config.featureswitch.FeatureSwitching +import play.api.libs.json.JsValue +import repositories.SessionDataRepository +import uk.gov.hmrc.http.{HeaderCarrier, InternalServerException} + +import javax.inject.{Inject, Singleton} +import scala.concurrent.{ExecutionContext, Future} + +@Singleton +class SessionDataService @Inject()(sessionDataRepository: SessionDataRepository, val appConfig: AppConfig) + (implicit ec: ExecutionContext) extends FeatureSwitching { + + + def getAllSessionData(implicit hc:HeaderCarrier): Future[Option[JsValue]] = + sessionDataRepository.getSessionData(sessionId = sessionIdFromHC) + + def getSessionData(dataId: String)(implicit hc:HeaderCarrier): Future[Option[JsValue]] = + sessionDataRepository.getDataFromSession(sessionId = sessionIdFromHC, dataId = dataId) + + def insertSessionData(dataId: String, data: JsValue)(implicit hc:HeaderCarrier): Future[Option[JsValue]] = + sessionDataRepository.insertDataWithSession( + sessionId = sessionIdFromHC, + dataId = dataId, + data = data + ) + + def deleteSessionData(dataId: String)(implicit hc:HeaderCarrier): Future[Option[JsValue]] = + sessionDataRepository.deleteDataWithSession( + sessionId = sessionIdFromHC, + dataId = dataId + ) + + private[services] def sessionIdFromHC(implicit hc: HeaderCarrier): String = { + hc.sessionId.fold( + throw new InternalServerException("[SessionDataService][sessionIdFromHC] - No session id in header carrier") + )(_.value) + } +} + + diff --git a/conf/subscription.routes b/conf/subscription.routes index 605331d..f299a99 100644 --- a/conf/subscription.routes +++ b/conf/subscription.routes @@ -13,6 +13,12 @@ POST /subscription-data controllers.Subscription DELETE /subscription-data/:reference/id/:id controllers.SubscriptionDataController.deleteSubscriptionData(reference: String, id: String) +#Session Data +GET /session-data/all controllers.SessionDataController.getAllSessionData +GET /session-data/id/:id controllers.SessionDataController.retrieveSessionData(id: String) +POST /session-data/id/:id controllers.SessionDataController.insertSessionData(id: String) +DELETE /session-data/id/:id controllers.SessionDataController.deleteSessionData(id: String) + POST /mis/sign-up/:nino/:taxYear controllers.SignUpController.signUp(nino: String, taxYear: String) POST /mis/create/:mtdbsaRef controllers.BusinessIncomeSourcesController.createIncomeSource(mtdbsaRef: String) diff --git a/it/test/controllers/SessionDataControllerISpec.scala b/it/test/controllers/SessionDataControllerISpec.scala new file mode 100644 index 0000000..dd6af0c --- /dev/null +++ b/it/test/controllers/SessionDataControllerISpec.scala @@ -0,0 +1,180 @@ +/* + * Copyright 2018 HM Revenue & Customs + * + * Licensed 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 controllers + +import config.AppConfig +import config.featureswitch.FeatureSwitching +import helpers.ComponentSpecBase +import helpers.servicemocks.AuthStub +import play.api.http.Status._ +import play.api.libs.json.{JsObject, Json} +import play.api.test.Helpers.{await, defaultAwaitTimeout} +import repositories.SessionDataRepository + +class SessionDataControllerISpec extends ComponentSpecBase with FeatureSwitching { + + val appConfig: AppConfig = app.injector.instanceOf[AppConfig] + val repository: SessionDataRepository = app.injector.instanceOf[SessionDataRepository] + + + val testJson: JsObject = Json.obj("testDataIdKey" -> "testDataIdValue") + val testDocument: JsObject = Json.obj( + "session-id" -> "testSessionId", + "testDataId" -> Json.obj( + "testDataIdKey" -> "testDataIdValue", + "testDataIdKey2" -> 1 + ) + ) + + val testDocumentAll: JsObject = Json.obj( + "session-id" -> "testSessionId", + "testDataId" -> Json.obj( + "testDataIdKey" -> "testDataIdValue", + "testDataIdKey2" -> 1 + ), + "testDataId2" -> Json.obj( + "testDataId2Key" -> "testDataId2Value", + "testDataId2Key2" -> 2 + ) + ) + + override def beforeEach(): Unit = { + await(repository.drop()) + super.beforeEach() + } + + + s"GET ${controllers.routes.SessionDataController.getAllSessionData.url}" should { + "return OK with all the data related to the user in mongo" when { + "the sessionId exists in mongo for the user" in { + + AuthStub.stubAuthSuccess() + await(repository.insert(testDocumentAll)) + + IncomeTaxSubscription.getAllSessionData should have( + httpStatus(OK), + jsonBodyOf(testDocumentAll) + ) + } + } + "return NO_CONTENT" when { + "the user's sessionId could not be found in mongo" in { + + AuthStub.stubAuthSuccess() + + IncomeTaxSubscription.getAllSessionData should have( + httpStatus(NO_CONTENT), + emptyBody + ) + } + } + "return unauthorised" when { + "the user is not authorised" in { + + AuthStub.stubAuthFailure() + + IncomeTaxSubscription.getAllSessionData should have( + httpStatus(UNAUTHORIZED) + ) + } + } + } + + s"GET ${controllers.routes.SessionDataController.retrieveSessionData("testDataId").url}" should { + "return OK with the data related to the key in mongo" when { + "the data exists in mongo for the user" in { + + AuthStub.stubAuthSuccess() + await(repository.insert(testDocument)) + + IncomeTaxSubscription.retrieveSessionData("testDataId") should have( + httpStatus(OK), + jsonBodyOf(Json.obj( + "testDataIdKey" -> "testDataIdValue", + "testDataIdKey2" -> 1 + )) + ) + } + } + "return NO_CONTENT" when { + "the data could not be retrieved from mongo" in { + + AuthStub.stubAuthSuccess() + + IncomeTaxSubscription.retrieveSessionData("testDataId") should have( + httpStatus(NO_CONTENT), + emptyBody + ) + } + } + "return unauthorised" when { + "the user is not authorised" in { + + AuthStub.stubAuthFailure() + + IncomeTaxSubscription.retrieveSessionData("testDataId") should have( + httpStatus(UNAUTHORIZED) + ) + } + } + } + + s"POST ${controllers.routes.SessionDataController.insertSessionData("testDataId").url}" should { + "return OK to upsert the data in mongo" when { + "the session document already existed for the user" in { + + AuthStub.stubAuthSuccess() + await(repository.insert(testDocument)) + + IncomeTaxSubscription.insertSessionData("testDataId", testJson) should have( + httpStatus(OK) + ) + } + "the session document did not exist for the user" in { + + AuthStub.stubAuthSuccess() + + IncomeTaxSubscription.insertSessionData("testDataId", testJson) should have( + httpStatus(OK) + ) + } + } + + "return unauthorised" when { + "the user is not authorised" in { + + AuthStub.stubAuthFailure() + + val res = IncomeTaxSubscription.insertSessionData("testDataId", testJson) + + res should have( + httpStatus(UNAUTHORIZED) + ) + } + } + } + + s"DELETE ${controllers.routes.SessionDataController.deleteSessionData("testDataId").url}" should { + "return OK and remove select data related to the user in mongo" when { + "the sessionId exists in mongo for the user" in { + AuthStub.stubAuthSuccess() + await(repository.insert(testDocumentAll)) + IncomeTaxSubscription.deleteSessionData(id = "testDataId") should have(httpStatus(OK)) + } + } + } +} diff --git a/it/test/helpers/ComponentSpecBase.scala b/it/test/helpers/ComponentSpecBase.scala index d50e4ca..a71bc82 100644 --- a/it/test/helpers/ComponentSpecBase.scala +++ b/it/test/helpers/ComponentSpecBase.scala @@ -26,7 +26,7 @@ import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike import org.scalatestplus.play.guice.GuiceOneServerPerSuite import play.api.inject.guice.GuiceApplicationBuilder -import play.api.libs.json.{JsValue, Json, Writes} +import play.api.libs.json.{JsObject, JsValue, Json, Writes} import play.api.libs.ws.WSResponse import play.api.{Application, Environment, Mode} import uk.gov.hmrc.http.HeaderCarrier @@ -149,6 +149,19 @@ trait ComponentSpecBase extends AnyWordSpecLike .get() .futureValue + + def getAllSessionData: WSResponse = + authorisedClient(s"/session-data/all").get().futureValue + + def retrieveSessionData(id:String): WSResponse = + authorisedClient(s"/session-data/id/$id").get().futureValue + + def deleteSessionData(id:String): WSResponse = + authorisedClient(s"/session-data/id/$id").delete().futureValue + + def insertSessionData(id:String, body: JsObject): WSResponse = + authorisedClient(s"/session-data/id/$id", "Content-Type" -> "application/json").post(body.toString()).futureValue + def post[T](uri: String, body: T)(implicit writes: Writes[T]): WSResponse = buildClient(uri) .withHttpHeaders( diff --git a/test/services/SessionDataServiceSpec.scala b/test/services/SessionDataServiceSpec.scala new file mode 100644 index 0000000..22e9495 --- /dev/null +++ b/test/services/SessionDataServiceSpec.scala @@ -0,0 +1,116 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed 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 services + +import com.mongodb.client.result.DeleteResult +import common.CommonSpec +import config.MicroserviceAppConfig +import config.featureswitch.FeatureSwitching +import org.mockito.ArgumentMatchers +import org.mockito.Mockito._ +import org.scalatest.BeforeAndAfterEach +import org.scalatestplus.mockito.MockitoSugar +import play.api.Configuration +import play.api.libs.json.{JsObject, Json} +import play.api.test.Helpers.{await, defaultAwaitTimeout} +import repositories.SessionDataRepository +import services.SubscriptionDataService.{Created, Existence, Existing} +import uk.gov.hmrc.http.{HeaderCarrier, InternalServerException, SessionId} +import uk.gov.hmrc.play.bootstrap.config.ServicesConfig + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future + +class SessionDataServiceSpec extends CommonSpec with MockitoSugar with FeatureSwitching with BeforeAndAfterEach { + + val mockServicesConfig: ServicesConfig = mock[ServicesConfig] + val mockConfiguration: Configuration = mock[Configuration] + val appConfig = new MicroserviceAppConfig(mockServicesConfig, mockConfiguration) + + trait Setup { + val mockSessionDataRepository: SessionDataRepository = mock[SessionDataRepository] + val service = new SessionDataService(mockSessionDataRepository, appConfig) + } + + val testJson: JsObject = Json.obj( + "testDataIdKey" -> "testDataIdValue" + ) + val testSessionId: String = "sessionId" + val testDataId: String = "dataId" + + val headerCarrier = HeaderCarrier(sessionId = Some(SessionId(testSessionId))) + val headerCarrierWithOutSessionId = HeaderCarrier() + val errorMessage = "[SessionDataService][sessionIdFromHC] - No session id in header carrier" + + "getAllSessionData" should { + "retrieve all data using the sessionId" in new Setup { + when(mockSessionDataRepository.getSessionData(ArgumentMatchers.eq(testSessionId))) + .thenReturn(Future.successful(Some(testJson))) + + await(service.getAllSessionData(headerCarrier)) shouldBe Some(testJson) + } + "throw an internal server exception when no session id exists" in new Setup { + intercept[InternalServerException]( + await(service.getAllSessionData(headerCarrierWithOutSessionId)) + ).message shouldBe errorMessage + } + } + + "retrieveSessionData" should { + "retrieve the data using session and data id" in new Setup { + when(mockSessionDataRepository.getDataFromSession(ArgumentMatchers.eq(testSessionId), ArgumentMatchers.eq(testDataId))) + .thenReturn(Future.successful(Some(testJson))) + + await(service.getSessionData(testDataId)(headerCarrier)) shouldBe Some(testJson) + } + "throw an internal server exception when no session id exists" in new Setup { + intercept[InternalServerException]( + await(service.getSessionData(testDataId)(headerCarrierWithOutSessionId)) + ).message shouldBe errorMessage + } + } + + "insertSessionData" should { + "insert the data using the session id" in new Setup { + when(mockSessionDataRepository.insertDataWithSession( + ArgumentMatchers.eq(testSessionId), ArgumentMatchers.eq(testDataId), ArgumentMatchers.eq(testJson)) + ) thenReturn Future.successful(Some(testJson)) + + await(service.insertSessionData(testDataId, testJson)(headerCarrier)) shouldBe Some(testJson) + } + "throw an internal server exception when no session id exists" in new Setup { + intercept[InternalServerException]( + await(service.insertSessionData(testDataId, testJson)(headerCarrierWithOutSessionId)) + ).message shouldBe errorMessage + } + } + + "deleteSessionData" should { + "delete dataId using sessionData" in new Setup { + when(mockSessionDataRepository.deleteDataWithSession(ArgumentMatchers.eq(testSessionId), ArgumentMatchers.eq(testDataId))) + .thenReturn(Future.successful(Some(testJson))) + + await(service.deleteSessionData(testDataId)(headerCarrier)) shouldBe Some(testJson) + } + "throw an internal server exception when no session id exists" in new Setup { + intercept[InternalServerException]( + await(service.deleteSessionData(testDataId)(headerCarrierWithOutSessionId)) + ).message shouldBe errorMessage + } + } + +}