diff --git a/core/src/main/resources/swagger/api-docs.yaml b/core/src/main/resources/swagger/api-docs.yaml index 856adc29b3..44e808a2bc 100644 --- a/core/src/main/resources/swagger/api-docs.yaml +++ b/core/src/main/resources/swagger/api-docs.yaml @@ -710,6 +710,36 @@ paths: $ref: '#/components/schemas/ErrorReport' 500: $ref: '#/components/responses/RawlsInternalError' + /api/admin/billing/{projectId}: + get: + tags: + - admin + summary: get support summary information for billing project + description: get support summary information for billing project + operationId: adminGetBillingProject + parameters: + - $ref: '#/components/parameters/billingProjectIdPathParam' + responses: + 200: + description: Successful Request + content: + 'application/json': + schema: + $ref: '#/components/schemas/BillingProjectAdminResponse' + 403: + description: You must be an admin to call this API + content: + 'application/json': + schema: + $ref: '#/components/schemas/ErrorReport' + 404: + description: Billing project not found + content: + 'application/json': + schema: + $ref: '#/components/schemas/ErrorReport' + 500: + $ref: '#/components/responses/RawlsInternalError' /api/admin/submissions: get: tags: @@ -6736,6 +6766,74 @@ components: $ref: '#/components/schemas/ProjectRole' description: the role of the user in the project description: an element of a list of project users and their role + BillingProjectAdminResponse: + required: + - billingProject + - workspaces + type: object + properties: + billingProject: + $ref: '#/components/schemas/RawlsBillingProject' + workspaces: + type: object + additionalProperties: + type: string + format: uuid + example: + "workspace1": "00000000-0000-0000-0000-000000000000" + "workspace2": "11111111-1111-1111-1111-111111111111" + description: "workspaces in the billing project with their IDs" + RawlsBillingProject: + required: + - projectName + - status + - invalidBillingAccount + type: object + description: internal representation of a billing project, returned by admin API only + properties: + projectName: + type: string + description: the name of the project + billingAccount: + type: string + description: the billing account to use in google projects (cloudPlatform GCP only) + invalidBillingAccount: + type: boolean + description: whether or not the billing account is usable by Terra + status: + $ref: '#/components/schemas/BillingProjectStatus' + message: + type: string + description: informational message about the project + azureManagedAppCoordinates: + $ref: '#/components/schemas/AzureManagedAppCoordinates' + cloudPlatform: + $ref: '#/components/schemas/BillingProjectCloudPlatform' + landingZoneId: + type: string + format: uuid + description: the UUID of the landing zone associated with the project (cloudPlatform AZURE only) + billingProfileId: + type: string + description: the billing profile ID associated with the project + cromwellBackend: + type: string + description: the cromwell backend to use for this billing project + servicePerimeter: + type: string + description: the name of the service perimeter for this billing project (cloudPlatform GCP only) + googleProjectNumber: + type: string + description: the google project number for this billing project (cloudPlatform GCP only) + spendReportDataset: + type: string + description: the name of the BigQuery dataset containing project spend data (cloudPlatform GCP only) + spendReportTable: + type: string + description: the name of the BigQuery table containing project spend data (cloudPlatform GCP only) + spendReportDatasetGoogleProject: + type: string + description: the name of the Google Project where the BigQuery dataset resides (cloudPlatform GCP only) RawlsBillingProjectResponse: required: - projectName @@ -6761,27 +6859,14 @@ components: $ref: '#/components/schemas/ProjectRole' description: the roles the caller has on the project status: - type: string - enum: - - Creating - - Ready - - Error - - Deleting - - DeletionFailed - - AddingToPerimeter - - CreatingLandingZone - description: the status of allocating the billing project's resources. + $ref: '#/components/schemas/BillingProjectStatus' message: type: string description: informational message about the project azureManagedAppCoordinates: $ref: '#/components/schemas/AzureManagedAppCoordinates' cloudPlatform: - type: string - enum: - - GCP - - AZURE - - UNKNOWN + $ref: '#/components/schemas/BillingProjectCloudPlatform' landingZoneId: type: string format: uuid @@ -6794,6 +6879,24 @@ components: description: whether this billing project supports protected data (cloudPlatform AZURE only) organization: $ref: '#/components/schemas/RawlsBillingProjectOrganization' + BillingProjectStatus: + type: string + enum: + - Creating + - Ready + - Error + - Deleting + - DeletionFailed + - AddingToPerimeter + - CreatingLandingZone + description: the status of allocating the billing project's resources. + BillingProjectCloudPlatform: + type: string + enum: + - GCP + - AZURE + - UNKNOWN + description: the cloud platform of the billing project AzureManagedAppCoordinates: required: - tenantId diff --git a/core/src/main/scala/org/broadinstitute/dsde/rawls/Boot.scala b/core/src/main/scala/org/broadinstitute/dsde/rawls/Boot.scala index d6ff9cd143..6615b7d605 100644 --- a/core/src/main/scala/org/broadinstitute/dsde/rawls/Boot.scala +++ b/core/src/main/scala/org/broadinstitute/dsde/rawls/Boot.scala @@ -518,6 +518,9 @@ object Boot extends IOApp with LazyLogging { spendReportingServiceConfig ) + val billingAdminServiceConstructor: RawlsRequestContext => BillingAdminService = + new BillingAdminService(samDAO, billingRepository, workspaceRepository, _) + val bucketMigrationServiceConstructor: RawlsRequestContext => BucketMigrationService = BucketMigrationServiceFactory.createBucketMigrationService(appConfigManager, slickDataSource, samDAO, gcsDAO) @@ -532,6 +535,7 @@ object Boot extends IOApp with LazyLogging { workspaceSettingServiceConstructor, entityServiceConstructor, userServiceConstructor, + billingAdminServiceConstructor, genomicsServiceConstructor, snapshotServiceConstructor, spendReportingServiceConstructor, diff --git a/core/src/main/scala/org/broadinstitute/dsde/rawls/billing/BillingAdminService.scala b/core/src/main/scala/org/broadinstitute/dsde/rawls/billing/BillingAdminService.scala new file mode 100644 index 0000000000..a87671b350 --- /dev/null +++ b/core/src/main/scala/org/broadinstitute/dsde/rawls/billing/BillingAdminService.scala @@ -0,0 +1,47 @@ +package org.broadinstitute.dsde.rawls.billing + +import akka.http.scaladsl.model.StatusCodes +import com.typesafe.scalalogging.LazyLogging +import org.broadinstitute.dsde.rawls.RawlsExceptionWithErrorReport +import org.broadinstitute.dsde.rawls.dataaccess.SamDAO +import org.broadinstitute.dsde.rawls.model.{ + BillingProjectAdminResponse, + ErrorReport, + RawlsBillingProjectName, + RawlsRequestContext, + SamResourceTypeAdminActions, + SamResourceTypeNames +} +import org.broadinstitute.dsde.rawls.workspace.WorkspaceRepository + +import scala.concurrent.{ExecutionContext, Future} + +class BillingAdminService(samDAO: SamDAO, + billingRepository: BillingRepository, + workspaceRepository: WorkspaceRepository, + ctx: RawlsRequestContext +)(implicit protected val ec: ExecutionContext) + extends LazyLogging { + + def getBillingProjectSupportSummary( + billingProjectName: RawlsBillingProjectName + ): Future[BillingProjectAdminResponse] = + for { + userIsAdmin <- samDAO.admin.userHasResourceTypeAdminPermission(SamResourceTypeNames.billingProject, + SamResourceTypeAdminActions.readSummaryInformation, + ctx + ) + _ = if (!userIsAdmin) + throw new RawlsExceptionWithErrorReport( + ErrorReport(StatusCodes.Forbidden, "You must be an admin to call this API.") + ) + + billingProjectOpt <- billingRepository.getBillingProject(billingProjectName) + billingProject = billingProjectOpt.getOrElse( + throw new RawlsExceptionWithErrorReport( + ErrorReport(StatusCodes.NotFound, s"Billing project ${billingProjectName.value} not found.") + ) + ) + workspaces <- workspaceRepository.listWorkspacesByBillingProject(billingProjectName) + } yield BillingProjectAdminResponse(billingProject, workspaces.map(ws => (ws.name, ws.workspaceIdAsUUID)).toMap) +} diff --git a/core/src/main/scala/org/broadinstitute/dsde/rawls/model/UserAuth.scala b/core/src/main/scala/org/broadinstitute/dsde/rawls/model/UserAuth.scala index 6b54e95255..7b2d900bf9 100644 --- a/core/src/main/scala/org/broadinstitute/dsde/rawls/model/UserAuth.scala +++ b/core/src/main/scala/org/broadinstitute/dsde/rawls/model/UserAuth.scala @@ -11,6 +11,7 @@ import org.broadinstitute.dsde.workbench.model.google.GoogleModelJsonSupport._ import org.broadinstitute.dsde.workbench.model.google.{BigQueryDatasetName, BigQueryTableName, GoogleProject} import spray.json._ +import java.util.UUID import scala.language.implicitConversions case class RawlsBillingProjectMembership(projectName: RawlsBillingProjectName, @@ -133,6 +134,8 @@ object RawlsBillingProjectResponse { ) } +case class BillingProjectAdminResponse(billingProject: RawlsBillingProject, workspaces: Map[String, UUID]) + case class RawlsBillingProjectTransfer(project: String, bucket: String, newOwnerEmail: String, newOwnerToken: String) case class ProjectAccessUpdate(email: String, role: ProjectRole) @@ -331,6 +334,10 @@ class UserAuthJsonSupport extends JsonSupport { RawlsBillingProjectOrganization.apply ) + implicit val billingProjectAdminResponse: RootJsonFormat[BillingProjectAdminResponse] = jsonFormat2( + BillingProjectAdminResponse + ) + implicit val RawlsBillingProjectResponseFormat: RootJsonFormat[RawlsBillingProjectResponse] = jsonFormat13(RawlsBillingProjectResponse.apply) } diff --git a/core/src/main/scala/org/broadinstitute/dsde/rawls/webservice/AdminApiService.scala b/core/src/main/scala/org/broadinstitute/dsde/rawls/webservice/AdminApiService.scala index 02cdd8c702..b6b9b7b28f 100644 --- a/core/src/main/scala/org/broadinstitute/dsde/rawls/webservice/AdminApiService.scala +++ b/core/src/main/scala/org/broadinstitute/dsde/rawls/webservice/AdminApiService.scala @@ -9,6 +9,7 @@ import akka.http.scaladsl.server import akka.http.scaladsl.server.Directives._ import io.opentelemetry.context.Context import org.broadinstitute.dsde.rawls.RawlsException +import org.broadinstitute.dsde.rawls.billing.BillingAdminService import org.broadinstitute.dsde.rawls.bucketMigration.{BucketMigrationService, BucketMigrationServiceImpl} import org.broadinstitute.dsde.rawls.model.ExecutionJsonSupport._ import org.broadinstitute.dsde.rawls.model.WorkspaceJsonSupport._ @@ -34,20 +35,29 @@ trait AdminApiService extends UserInfoDirectives { val submissionsServiceConstructor: RawlsRequestContext => SubmissionsService val userServiceConstructor: RawlsRequestContext => UserService val bucketMigrationServiceConstructor: RawlsRequestContext => BucketMigrationService + val billingAdminServiceConstructor: RawlsRequestContext => BillingAdminService def adminRoutes(otelContext: Context = Context.root()): server.Route = { requireUserInfo(Option(otelContext)) { userInfo => val ctx = RawlsRequestContext(userInfo, Option(otelContext)) path("admin" / "billing" / Segment) { projectId => - delete { - entity(as[Map[String, String]]) { ownerInfo => - complete { - userServiceConstructor(ctx) - .adminDeleteBillingProject(RawlsBillingProjectName(projectId), ownerInfo) - .map(_ => StatusCodes.NoContent) + val billingProjectName = RawlsBillingProjectName(projectId) + get { + complete { + billingAdminServiceConstructor(ctx) + .getBillingProjectSupportSummary(billingProjectName) + .map(StatusCodes.OK -> _) + } + } ~ + delete { + entity(as[Map[String, String]]) { ownerInfo => + complete { + userServiceConstructor(ctx) + .adminDeleteBillingProject(billingProjectName, ownerInfo) + .map(_ => StatusCodes.NoContent) + } } } - } } ~ path("admin" / "submissions") { get { diff --git a/core/src/main/scala/org/broadinstitute/dsde/rawls/webservice/RawlsApiService.scala b/core/src/main/scala/org/broadinstitute/dsde/rawls/webservice/RawlsApiService.scala index 1cc0b2574a..d1aa4c73fc 100644 --- a/core/src/main/scala/org/broadinstitute/dsde/rawls/webservice/RawlsApiService.scala +++ b/core/src/main/scala/org/broadinstitute/dsde/rawls/webservice/RawlsApiService.scala @@ -17,7 +17,7 @@ import com.typesafe.scalalogging.LazyLogging import io.opentelemetry.context.Context import io.sentry.Sentry import org.broadinstitute.dsde.rawls.RawlsExceptionWithErrorReport -import org.broadinstitute.dsde.rawls.billing.BillingProjectOrchestrator +import org.broadinstitute.dsde.rawls.billing.{BillingAdminService, BillingProjectOrchestrator} import org.broadinstitute.dsde.rawls.bucketMigration.BucketMigrationService import org.broadinstitute.dsde.rawls.dataaccess.{ExecutionServiceCluster, SamDAO} import org.broadinstitute.dsde.rawls.entities.EntityService @@ -32,12 +32,7 @@ import org.broadinstitute.dsde.rawls.spendreporting.SpendReportingService import org.broadinstitute.dsde.rawls.status.StatusService import org.broadinstitute.dsde.rawls.submissions.SubmissionsService import org.broadinstitute.dsde.rawls.user.UserService -import org.broadinstitute.dsde.rawls.workspace.{ - MultiCloudWorkspaceService, - WorkspaceAdminService, - WorkspaceService, - WorkspaceSettingService -} +import org.broadinstitute.dsde.rawls.workspace.{MultiCloudWorkspaceService, WorkspaceAdminService, WorkspaceService, WorkspaceSettingService} import org.broadinstitute.dsde.workbench.oauth2.OpenIDConnectConfiguration import java.sql.{SQLException, SQLTransactionRollbackException} @@ -221,6 +216,7 @@ class RawlsApiServiceImpl(val multiCloudWorkspaceServiceConstructor: RawlsReques val workspaceSettingServiceConstructor: RawlsRequestContext => WorkspaceSettingService, val entityServiceConstructor: RawlsRequestContext => EntityService, val userServiceConstructor: RawlsRequestContext => UserService, + val billingAdminServiceConstructor: RawlsRequestContext => BillingAdminService, val genomicsServiceConstructor: RawlsRequestContext => GenomicsService, val snapshotServiceConstructor: RawlsRequestContext => SnapshotService, val spendReportingConstructor: RawlsRequestContext => SpendReportingService, diff --git a/core/src/main/scala/org/broadinstitute/dsde/rawls/workspace/WorkspaceRepository.scala b/core/src/main/scala/org/broadinstitute/dsde/rawls/workspace/WorkspaceRepository.scala index 48ba88b21c..f4c521e83e 100644 --- a/core/src/main/scala/org/broadinstitute/dsde/rawls/workspace/WorkspaceRepository.scala +++ b/core/src/main/scala/org/broadinstitute/dsde/rawls/workspace/WorkspaceRepository.scala @@ -5,17 +5,7 @@ import org.broadinstitute.dsde.rawls.RawlsExceptionWithErrorReport import org.broadinstitute.dsde.rawls.dataaccess.SlickDataSource import org.broadinstitute.dsde.rawls.dataaccess.slick.PendingBucketDeletionRecord import org.broadinstitute.dsde.rawls.model.Attributable.AttributeMap -import org.broadinstitute.dsde.rawls.model.{ - ErrorReport, - PendingCloneWorkspaceFileTransfer, - RawlsRequestContext, - Workspace, - WorkspaceAttributeSpecs, - WorkspaceName, - WorkspaceState, - WorkspaceSubmissionStats, - WorkspaceTag -} +import org.broadinstitute.dsde.rawls.model.{ErrorReport, PendingCloneWorkspaceFileTransfer, RawlsBillingProjectName, RawlsRequestContext, Workspace, WorkspaceAttributeSpecs, WorkspaceName, WorkspaceState, WorkspaceSubmissionStats, WorkspaceTag} import org.broadinstitute.dsde.rawls.model.WorkspaceState.WorkspaceState import org.broadinstitute.dsde.rawls.util.TracingUtils.traceDBIOWithParent import org.joda.time.DateTime @@ -57,6 +47,10 @@ class WorkspaceRepository(dataSource: SlickDataSource) { _.workspaceQuery.listV2WorkspacesByIds(workspaceIds, attributeSpecs) } + def listWorkspacesByBillingProject(billingProjectName: RawlsBillingProjectName): Future[Seq[Workspace]] = dataSource.inTransaction { + _.workspaceQuery.listWithBillingProject(billingProjectName) + } + def createWorkspace(workspace: Workspace): Future[Workspace] = dataSource.inTransaction { access => access.workspaceQuery.createOrUpdate(workspace) diff --git a/core/src/test/scala/org/broadinstitute/dsde/rawls/billing/BillingAdminServiceUnitTests.scala b/core/src/test/scala/org/broadinstitute/dsde/rawls/billing/BillingAdminServiceUnitTests.scala new file mode 100644 index 0000000000..aed1645ed0 --- /dev/null +++ b/core/src/test/scala/org/broadinstitute/dsde/rawls/billing/BillingAdminServiceUnitTests.scala @@ -0,0 +1,157 @@ +package org.broadinstitute.dsde.rawls.billing + +import akka.http.scaladsl.model.StatusCodes +import akka.http.scaladsl.model.headers.OAuth2BearerToken +import org.broadinstitute.dsde.rawls.dataaccess.{GoogleServicesDAO, SamAdminDAO, SamDAO, SlickDataSource} +import org.broadinstitute.dsde.rawls.model.{ + BillingProjectAdminResponse, + CreationStatuses, + RawlsBillingAccountName, + RawlsBillingProject, + RawlsBillingProjectName, + RawlsRequestContext, + RawlsUserEmail, + RawlsUserSubjectId, + SamResourceTypeAdminActions, + SamResourceTypeNames, + UserInfo, + Workspace, + WorkspaceAdminResponse, + WorkspaceDetails +} +import org.broadinstitute.dsde.rawls.util.MockitoTestUtils +import org.broadinstitute.dsde.rawls.workspace.WorkspaceRepository +import org.broadinstitute.dsde.rawls.{NoSuchWorkspaceException, RawlsExceptionWithErrorReport} +import org.joda.time.DateTime +import org.mockito.{ArgumentMatchers, Mockito} +import org.mockito.Mockito.{when, RETURNS_SMART_NULLS} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers.convertToAnyShouldWrapper + +import java.util.UUID +import scala.concurrent.duration.Duration +import scala.concurrent.{Await, Future} + +class BillingAdminServiceUnitTests extends AnyFlatSpec with MockitoTestUtils { + + implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global + + val defaultRequestContext: RawlsRequestContext = + RawlsRequestContext( + UserInfo(RawlsUserEmail("test"), OAuth2BearerToken("Bearer 123"), 123, RawlsUserSubjectId("abc")) + ) + + def billingAdminServiceConstructor( + samDAO: SamDAO = mock[SamDAO](RETURNS_SMART_NULLS), + billingRepository: BillingRepository = mock[BillingRepository]( + RETURNS_SMART_NULLS + ), + workspaceRepository: WorkspaceRepository = mock[WorkspaceRepository](RETURNS_SMART_NULLS), + ctx: RawlsRequestContext = defaultRequestContext + ): BillingAdminService = + new BillingAdminService( + samDAO, + billingRepository, + workspaceRepository, + ctx + ) + + "getBillingProject" should "return the billing project with a list of its workspaces" in { + val billingProject: RawlsBillingProject = RawlsBillingProject(RawlsBillingProjectName("project"), + CreationStatuses.Ready, + Option(RawlsBillingAccountName("account")), + None + ) + + val billingRepository = mock[BillingRepository] + when(billingRepository.getBillingProject(billingProject.projectName)) + .thenReturn(Future.successful(Option(billingProject))) + + val workspace = Workspace( + "billingAdminNamespace", + "billingAdminWorkspace", + UUID.randomUUID.toString, + "bucketName", + Some("workflowCollection"), + new DateTime(), + new DateTime(), + "creator", + Map.empty + ) + val workspaceRepository = mock[WorkspaceRepository] + when(workspaceRepository.listWorkspacesByBillingProject(billingProject.projectName)) + .thenReturn(Future.successful(Seq(workspace))) + + val samAdminDAO = mock[SamAdminDAO] + when( + samAdminDAO.userHasResourceTypeAdminPermission( + ArgumentMatchers.eq(SamResourceTypeNames.billingProject), + ArgumentMatchers.eq(SamResourceTypeAdminActions.readSummaryInformation), + ArgumentMatchers.any() + ) + ).thenReturn(Future.successful(true)) + val samDAO = mock[SamDAO] + when(samDAO.admin).thenReturn(samAdminDAO) + + val service = + billingAdminServiceConstructor( + samDAO = samDAO, + billingRepository = billingRepository, + workspaceRepository = workspaceRepository + ) + + val returnedBillingProject = + Await.result(service.getBillingProjectSupportSummary(billingProject.projectName), Duration.Inf) + returnedBillingProject shouldEqual BillingProjectAdminResponse(billingProject, + Map(workspace.name -> workspace.workspaceIdAsUUID) + ) + } + + it should "throw if the user is not an admin" in { + val samAdminDAO = mock[SamAdminDAO] + when( + samAdminDAO.userHasResourceTypeAdminPermission( + ArgumentMatchers.eq(SamResourceTypeNames.billingProject), + ArgumentMatchers.eq(SamResourceTypeAdminActions.readSummaryInformation), + ArgumentMatchers.any() + ) + ).thenReturn(Future.successful(false)) + val samDAO = mock[SamDAO] + when(samDAO.admin).thenReturn(samAdminDAO) + + val service = billingAdminServiceConstructor(samDAO = samDAO) + + val exception = intercept[RawlsExceptionWithErrorReport] { + Await.result(service.getBillingProjectSupportSummary(RawlsBillingProjectName("project")), Duration.Inf) + } + exception.errorReport.statusCode shouldEqual Option(StatusCodes.Forbidden) + } + + it should "throw if the billing project is not found" in { + val projectName = RawlsBillingProjectName("project") + val billingRepository = mock[BillingRepository] + when(billingRepository.getBillingProject(projectName)).thenReturn(Future.successful(None)) + + val samAdminDAO = mock[SamAdminDAO] + when( + samAdminDAO.userHasResourceTypeAdminPermission( + ArgumentMatchers.eq(SamResourceTypeNames.billingProject), + ArgumentMatchers.eq(SamResourceTypeAdminActions.readSummaryInformation), + ArgumentMatchers.any() + ) + ).thenReturn(Future.successful(true)) + val samDAO = mock[SamDAO] + when(samDAO.admin).thenReturn(samAdminDAO) + + val service = + billingAdminServiceConstructor( + samDAO = samDAO, + billingRepository = billingRepository + ) + + val exception = intercept[RawlsExceptionWithErrorReport] { + Await.result(service.getBillingProjectSupportSummary(projectName), Duration.Inf) + } + exception.errorReport.statusCode shouldEqual Option(StatusCodes.NotFound) + } +} diff --git a/core/src/test/scala/org/broadinstitute/dsde/rawls/webservice/AdminApiServiceSpec.scala b/core/src/test/scala/org/broadinstitute/dsde/rawls/webservice/AdminApiServiceSpec.scala index 62cb83519b..6c643bf117 100644 --- a/core/src/test/scala/org/broadinstitute/dsde/rawls/webservice/AdminApiServiceSpec.scala +++ b/core/src/test/scala/org/broadinstitute/dsde/rawls/webservice/AdminApiServiceSpec.scala @@ -3,6 +3,7 @@ package org.broadinstitute.dsde.rawls.webservice import akka.http.scaladsl.model._ import akka.http.scaladsl.model.headers.OAuth2BearerToken import akka.http.scaladsl.server.Route.{seal => sealRoute} +import org.broadinstitute.dsde.rawls.billing.BillingAdminService import org.broadinstitute.dsde.rawls.dataaccess._ import org.broadinstitute.dsde.rawls.google.MockGooglePubSubDAO import org.broadinstitute.dsde.rawls.workspace.WorkspaceAdminService @@ -340,4 +341,31 @@ class AdminApiServiceSpec extends ApiServiceSpec { verify(workspaceAdminService).getWorkspaceById(workspaceId) } + + it should "get a billing project with a list of its workspaces" in { + val billingProjectName = RawlsBillingProjectName("project") + val billingAdminService = mock[BillingAdminService] + + when(billingAdminService.getBillingProjectSupportSummary(billingProjectName)).thenReturn( + Future.successful( + BillingProjectAdminResponse( + RawlsBillingProject(billingProjectName, + CreationStatuses.Ready, + Option(RawlsBillingAccountName("account")), + None + ), + Map("ws1" -> UUID.randomUUID, "ws2" -> UUID.randomUUID) + ) + ) + ) + val service = new MockApiService(billingAdminServiceConstructor = _ => billingAdminService) + + Get( + s"/admin/billing/${billingProjectName.value}" + ) ~> service.testRoutes ~> check { + assertResult(StatusCodes.OK)(status) + } + + verify(billingAdminService).getBillingProjectSupportSummary(billingProjectName) + } } diff --git a/core/src/test/scala/org/broadinstitute/dsde/rawls/webservice/ApiServiceSpec.scala b/core/src/test/scala/org/broadinstitute/dsde/rawls/webservice/ApiServiceSpec.scala index fe81b8a205..21361f5924 100644 --- a/core/src/test/scala/org/broadinstitute/dsde/rawls/webservice/ApiServiceSpec.scala +++ b/core/src/test/scala/org/broadinstitute/dsde/rawls/webservice/ApiServiceSpec.scala @@ -286,6 +286,11 @@ trait ApiServiceSpec spendReportingServiceConfig ) + override val billingAdminServiceConstructor: RawlsRequestContext => BillingAdminService = + new BillingAdminService(samDAO, billingRepository, new WorkspaceRepository(slickDataSource), _)( + testExecutionContext + ) + override val bucketMigrationServiceConstructor: RawlsRequestContext => BucketMigrationService = BucketMigrationServiceImpl.constructor(slickDataSource, samDAO, gcsDAO) diff --git a/core/src/test/scala/org/broadinstitute/dsde/rawls/webservice/MockApiService.scala b/core/src/test/scala/org/broadinstitute/dsde/rawls/webservice/MockApiService.scala index 0577e23898..21384eac6e 100644 --- a/core/src/test/scala/org/broadinstitute/dsde/rawls/webservice/MockApiService.scala +++ b/core/src/test/scala/org/broadinstitute/dsde/rawls/webservice/MockApiService.scala @@ -75,7 +75,8 @@ class MockApiService( mock[WorkspaceSettingService](RETURNS_SMART_NULLS), override val bucketMigrationServiceConstructor: RawlsRequestContext => BucketMigrationService = _ => mock[BucketMigrationService](RETURNS_SMART_NULLS), - override val userServiceConstructor: RawlsRequestContext => UserService = _ => mock[UserService](RETURNS_SMART_NULLS) + override val userServiceConstructor: RawlsRequestContext => UserService = _ => mock[UserService](RETURNS_SMART_NULLS), + override val billingAdminServiceConstructor: RawlsRequestContext => BillingAdminService = _ => mock[BillingAdminService](RETURNS_SMART_NULLS) )(implicit val executionContext: ExecutionContext) extends RawlsApiService with AdminApiService diff --git a/pact4s/src/test/scala/org/broadinstitute/dsde/rawls/provider/RawlsProviderSpec.scala b/pact4s/src/test/scala/org/broadinstitute/dsde/rawls/provider/RawlsProviderSpec.scala index 5502464e6a..4101bf8756 100644 --- a/pact4s/src/test/scala/org/broadinstitute/dsde/rawls/provider/RawlsProviderSpec.scala +++ b/pact4s/src/test/scala/org/broadinstitute/dsde/rawls/provider/RawlsProviderSpec.scala @@ -8,7 +8,7 @@ import cats.effect.IO import cats.effect.unsafe.implicits.global import io.opentelemetry.context.Context import org.broadinstitute.dsde.rawls.TestExecutionContext.testExecutionContext -import org.broadinstitute.dsde.rawls.billing.BillingProjectOrchestrator +import org.broadinstitute.dsde.rawls.billing.{BillingAdminService, BillingProjectOrchestrator} import org.broadinstitute.dsde.rawls.bucketMigration.BucketMigrationService import org.broadinstitute.dsde.rawls.dataaccess.{ExecutionServiceCluster, SamDAO} import org.broadinstitute.dsde.rawls.entities.EntityService @@ -117,6 +117,10 @@ class RawlsProviderSpec extends AnyFlatSpec with BeforeAndAfterAll with PactVeri lazy val mockUserService: UserService = mock[UserService] _ => mockUserService } + val mockBillingAdminServiceConstructor: RawlsRequestContext => BillingAdminService = { + lazy val mockBillingAdminService: BillingAdminService = mock[BillingAdminService] + _ => mockBillingAdminService + } val mockGenomicsServiceConstructor: RawlsRequestContext => GenomicsService = { lazy val mockGenomicsService: GenomicsService = mock[GenomicsService] _ => mockGenomicsService @@ -155,6 +159,7 @@ class RawlsProviderSpec extends AnyFlatSpec with BeforeAndAfterAll with PactVeri mockWorkspaceSettingServiceConstructor, mockEntityServiceConstructor, mockUserServiceConstructor, + mockBillingAdminServiceConstructor, mockGenomicsServiceConstructor, mockSnapshotServiceConstructor, mockSpendReportingConstructor,