diff --git a/src/main/resources/org/broadinstitute/dsde/sam/liquibase/changelog.xml b/src/main/resources/org/broadinstitute/dsde/sam/liquibase/changelog.xml
index 578a27770..17575edf0 100644
--- a/src/main/resources/org/broadinstitute/dsde/sam/liquibase/changelog.xml
+++ b/src/main/resources/org/broadinstitute/dsde/sam/liquibase/changelog.xml
@@ -31,4 +31,5 @@
+
diff --git a/src/main/resources/org/broadinstitute/dsde/sam/liquibase/changesets/20240809_sam_user_favorite_resources_table.xml b/src/main/resources/org/broadinstitute/dsde/sam/liquibase/changesets/20240809_sam_user_favorite_resources_table.xml
new file mode 100644
index 000000000..24c3e4ddf
--- /dev/null
+++ b/src/main/resources/org/broadinstitute/dsde/sam/liquibase/changesets/20240809_sam_user_favorite_resources_table.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/swagger/api-docs.yaml b/src/main/resources/swagger/api-docs.yaml
index e7ef83d59..3f60bd5e0 100755
--- a/src/main/resources/swagger/api-docs.yaml
+++ b/src/main/resources/swagger/api-docs.yaml
@@ -3239,6 +3239,106 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorReport'
+ /api/users/v2/self/favoriteResources:
+ get:
+ tags:
+ - Users
+ summary: Gets a user's favorite resources.
+ operationId: getFavoriteResources
+ responses:
+ 200:
+ description: list of the user's favorite resources
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/FullyQualifiedResourceId'
+ 403:
+ description: user not found or is not allowed to use Terra
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorReport'
+ /api/users/v2/self/favoriteResources/{resourceTypeName}:
+ get:
+ tags:
+ - Users
+ summary: Gets a user's favorite resources of a given resource type.
+ operationId: getFavoriteResourcesOfType
+ parameters:
+ - name: resourceTypeName
+ in: path
+ description: Resource type to narrow favorite resources to
+ required: true
+ schema:
+ type: string
+ responses:
+ 200:
+ description: list of the user's favorite resources of a specific resource type
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/FullyQualifiedResourceId'
+ 404:
+ description: resource type not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorReport'
+ /api/users/v2/self/favoriteResources/{resourceTypeName}/{resourceId}:
+ put:
+ tags:
+ - Users
+ summary: Add a resource to the calling user's favorites
+ operationId: addFavoriteResource
+ parameters:
+ - name: resourceTypeName
+ in: path
+ description: Resource type of the resource to favorite
+ required: true
+ schema:
+ type: string
+ - name: resourceId
+ in: path
+ description: Resource ID of the resource to favorite
+ required: true
+ schema:
+ type: string
+ responses:
+ 202:
+ description: Favorite resource added
+ content: { }
+ 404:
+ description: resource not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorReport'
+ delete:
+ tags:
+ - Users
+ summary: Remove a resource from the calling user's favorites
+ operationId: removeFavoriteResource
+ parameters:
+ - name: resourceTypeName
+ in: path
+ description: Resource type of the resource to favorite
+ required: true
+ schema:
+ type: string
+ - name: resourceId
+ in: path
+ description: Resource ID of the resource to favorite
+ required: true
+ schema:
+ type: string
+ responses:
+ 202:
+ description: Favorite resource removed
+ content: { }
/api/users/v2/{sam_user_id}:
get:
tags:
diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/SecurityDirectives.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/SecurityDirectives.scala
index 3f5e337f2..662bf31c9 100644
--- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/SecurityDirectives.scala
+++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/SecurityDirectives.scala
@@ -145,6 +145,25 @@ trait SecurityDirectives {
}
}
+ def requireAnyAction(
+ resource: FullyQualifiedResourceId,
+ userId: WorkbenchUserId,
+ samRequestContext: SamRequestContext
+ ): Directive0 =
+ Directives.mapInnerRoute { innerRoute =>
+ onSuccess(policyEvaluatorService.listUserResourceActions(resource, userId, samRequestContext)) { actions =>
+ if (actions.nonEmpty) {
+ innerRoute
+ } else {
+ Directives.failWith(
+ new WorkbenchExceptionWithErrorReport(
+ ErrorReport(StatusCodes.Forbidden, s"You do not have access to ${resource.resourceTypeName.value}/${resource.resourceId.value}")
+ )
+ )
+ }
+ }
+ }
+
/** in the case where we don't have the required action, we need to figure out if we should return a Not Found (you have no access) vs a Forbidden (you have
* access, just not the right kind)
*/
diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV2.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV2.scala
index ba8fc057f..d631d3aba 100644
--- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV2.scala
+++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV2.scala
@@ -8,15 +8,17 @@ import akka.http.scaladsl.server.{Directive0, ExceptionHandler, Route}
import cats.effect.IO
import org.broadinstitute.dsde.workbench.model._
import org.broadinstitute.dsde.workbench.sam.model.api.SamUserResponse._
+import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._
import org.broadinstitute.dsde.workbench.sam.model.api._
-import org.broadinstitute.dsde.workbench.sam.model.{ResourceRoleName, ResourceTypeName, TermsOfServiceDetails}
+import org.broadinstitute.dsde.workbench.sam.model.{FullyQualifiedResourceId, ResourceId, ResourceRoleName, ResourceTypeName, TermsOfServiceDetails}
import org.broadinstitute.dsde.workbench.sam.service.{ResourceService, TosService, UserService}
import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext
import spray.json.enrichAny
+import spray.json.DefaultJsonProtocol._
/** Created by tlangs on 10/12/2023.
*/
-trait UserRoutesV2 extends SamUserDirectives with SamRequestContextDirectives {
+trait UserRoutesV2 extends SamUserDirectives with SamRequestContextDirectives with SamModelDirectives with SecurityDirectives {
val userService: UserService
val tosService: TosService
val resourceService: ResourceService
@@ -81,6 +83,27 @@ trait UserRoutesV2 extends SamUserDirectives with SamRequestContextDirectives {
getSamUserCombinedState(samUser, samRequestContext)
}
}
+ } ~
+ // api/user/v2/self/favoriteResources
+ pathPrefix("favoriteResources") {
+ withActiveUser(samRequestContextWithoutUser) { samUser: SamUser =>
+ val samRequestContext = samRequestContextWithoutUser.copy(samUser = Some(samUser))
+ pathEndOrSingleSlash {
+ getFavoriteResources(samUser, samRequestContext)
+ } ~
+ // api/user/v2/self/favoriteResources/{resourceTypeName}
+ pathPrefix(Segment) { resourceTypeName =>
+ withNonAdminResourceType(ResourceTypeName(resourceTypeName)) { resourceType =>
+ getFavoriteResourcesOfType(samUser, resourceType.name, samRequestContext) ~
+ // api/user/v2/self/favoriteResources/{resourceTypeName}/{resourceId}
+ pathPrefix(Segment) { resourceId =>
+ val resource = FullyQualifiedResourceId(resourceType.name, ResourceId(resourceId))
+ addFavoriteResource(samUser, resource, samRequestContext) ~
+ removeFavoriteResource(samUser, resource, samRequestContext)
+ }
+ }
+ }
+ }
}
} ~
pathPrefix(Segment) { samUserId =>
@@ -194,12 +217,14 @@ trait UserRoutesV2 extends SamUserDirectives with SamRequestContextDirectives {
includePublic = false,
samRequestContext
)
+ favoriteResources <- resourceService.getUserFavoriteResources(samUser.id, samRequestContext)
} yield SamUserCombinedStateResponse(
samUser,
allowances,
maybeAttributes,
termsOfServiceDetails.getOrElse(TermsOfServiceDetails(None, None, permitsSystemUsage = false, isCurrentVersion = false)),
- Map("enterpriseFeatures" -> enterpriseFeatures.toJson)
+ Map("enterpriseFeatures" -> enterpriseFeatures.toJson),
+ favoriteResources
)
}
}
@@ -217,4 +242,39 @@ trait UserRoutesV2 extends SamUserDirectives with SamRequestContextDirectives {
}
}
}
+
+ private def getFavoriteResources(samUser: SamUser, samRequestContext: SamRequestContext): Route =
+ getWithTelemetry(samRequestContext) {
+ complete {
+ resourceService.getUserFavoriteResources(samUser.id, samRequestContext).map(StatusCodes.OK -> _)
+ }
+ }
+
+ private def getFavoriteResourcesOfType(samUser: SamUser, resourceTypeName: ResourceTypeName, samRequestContext: SamRequestContext): Route =
+ getWithTelemetry(samRequestContext) {
+ complete {
+ resourceService.getUserFavoriteResourcesOfType(samUser.id, resourceTypeName, samRequestContext).map(StatusCodes.OK -> _)
+ }
+ }
+
+ private def addFavoriteResource(samUser: SamUser, resource: FullyQualifiedResourceId, samRequestContext: SamRequestContext): Route =
+ putWithTelemetry(samRequestContext) {
+ changeForbiddenToNotFound {
+ requireAnyAction(resource, samUser.id, samRequestContext) {
+ complete {
+ resourceService.addUserFavoriteResource(samUser.id, resource, samRequestContext).map {
+ case true => StatusCodes.Accepted
+ case false => StatusCodes.NotFound
+ }
+ }
+ }
+ }
+ }
+
+ private def removeFavoriteResource(samUser: SamUser, resource: FullyQualifiedResourceId, samRequestContext: SamRequestContext): Route =
+ deleteWithTelemetry(samRequestContext) {
+ complete {
+ resourceService.removeUserFavoriteResource(samUser.id, resource, samRequestContext).map(_ => StatusCodes.Accepted)
+ }
+ }
}
diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/DirectoryDAO.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/DirectoryDAO.scala
index 3d03f77da..eeca37f18 100644
--- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/DirectoryDAO.scala
+++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/DirectoryDAO.scala
@@ -12,7 +12,7 @@ import org.broadinstitute.dsde.workbench.sam.azure.{
PetManagedIdentityId
}
import org.broadinstitute.dsde.workbench.sam.model.api.{AdminUpdateUserRequest, SamUser, SamUserAttributes}
-import org.broadinstitute.dsde.workbench.sam.model.{BasicWorkbenchGroup, FullyQualifiedResourceId, ResourceAction, SamUserTos}
+import org.broadinstitute.dsde.workbench.sam.model.{BasicWorkbenchGroup, FullyQualifiedResourceId, ResourceAction, ResourceTypeName, SamUserTos}
import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext
import java.time.Instant
@@ -178,4 +178,16 @@ trait DirectoryDAO {
def setUserAttributes(samUserAttributes: SamUserAttributes, samRequestContext: SamRequestContext): IO[Unit]
def listParentGroups(groupName: WorkbenchGroupName, samRequestContext: SamRequestContext): IO[Set[WorkbenchGroupName]]
+
+ def addUserFavoriteResource(userId: WorkbenchUserId, resourceId: FullyQualifiedResourceId, samRequestContext: SamRequestContext): IO[Boolean]
+
+ def removeUserFavoriteResource(userId: WorkbenchUserId, resourceId: FullyQualifiedResourceId, samRequestContext: SamRequestContext): IO[Unit]
+
+ def getUserFavoriteResources(userId: WorkbenchUserId, samRequestContext: SamRequestContext): IO[Set[FullyQualifiedResourceId]]
+
+ def getUserFavoriteResourcesOfType(
+ userId: WorkbenchUserId,
+ resourceTypeName: ResourceTypeName,
+ samRequestContext: SamRequestContext
+ ): IO[Set[FullyQualifiedResourceId]]
}
diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresDirectoryDAO.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresDirectoryDAO.scala
index 140d2a501..8018692e9 100644
--- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresDirectoryDAO.scala
+++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresDirectoryDAO.scala
@@ -1378,4 +1378,91 @@ class PostgresDirectoryDAO(protected val writeDbRef: DbReference, protected val
loadParentGroupsQuery.map(rs => WorkbenchGroupName(rs.string(parent.resultName.name))).list().apply().toSet
}
+
+ override def addUserFavoriteResource(
+ userId: WorkbenchUserId,
+ fullyQualifiedResourceId: FullyQualifiedResourceId,
+ samRequestContext: SamRequestContext
+ ): IO[Boolean] =
+ serializableWriteTransaction("addUserFavoriteResource", samRequestContext) { implicit session =>
+ val resourceTable = ResourceTable.syntax
+ val resourceTypeTable = ResourceTypeTable.syntax
+ val userFavoriteResourcesTable = UserFavoriteResourcesTable.syntax
+ val userFavoriteResourcesColumns = UserFavoriteResourcesTable.column
+ Try {
+ samsql"""
+ insert into ${UserFavoriteResourcesTable as userFavoriteResourcesTable} (${userFavoriteResourcesColumns.samUserId}, ${userFavoriteResourcesColumns.resourceId})
+ values (
+ $userId,
+ (select ${resourceTable.result.id} from ${ResourceTable as resourceTable} left join ${ResourceTypeTable as resourceTypeTable} on ${resourceTable.resourceTypeId} = ${resourceTypeTable.id} where ${resourceTable.name} = ${fullyQualifiedResourceId.resourceId} and ${resourceTypeTable.name} = ${fullyQualifiedResourceId.resourceTypeName})
+ )
+ on conflict do nothing
+ """.update().apply() > 0
+ }.getOrElse(false)
+ }
+
+ override def removeUserFavoriteResource(
+ userId: WorkbenchUserId,
+ fullyQualifiedResourceId: FullyQualifiedResourceId,
+ samRequestContext: SamRequestContext
+ ): IO[Unit] =
+ serializableWriteTransaction("removeUserFavoriteResource", samRequestContext) { implicit session =>
+ val resourceTable = ResourceTable.syntax
+ val resourceTypeTable = ResourceTypeTable.syntax
+ val userFavoriteResourcesColumns = UserFavoriteResourcesTable.column
+ samsql"""
+ delete from ${UserFavoriteResourcesTable.table}
+ where ${userFavoriteResourcesColumns.samUserId} = $userId
+ and ${userFavoriteResourcesColumns.resourceId} = (select ${resourceTable.result.id} from ${ResourceTable as resourceTable} left join ${ResourceTypeTable as resourceTypeTable} on ${resourceTable.resourceTypeId} = ${resourceTypeTable.id} where ${resourceTable.name} = ${fullyQualifiedResourceId.resourceId} and ${resourceTypeTable.name} = ${fullyQualifiedResourceId.resourceTypeName})
+ """.update().apply()
+ }
+
+ override def getUserFavoriteResources(userId: WorkbenchUserId, samRequestContext: SamRequestContext): IO[Set[FullyQualifiedResourceId]] =
+ readOnlyTransaction("getUserFavoriteResources", samRequestContext) { implicit session =>
+ val resourceTable = ResourceTable.syntax
+ val resourceTypeTable = ResourceTypeTable.syntax
+ val userFavoriteResourcesTable = UserFavoriteResourcesTable.syntax
+
+ val loadUserFavoriteResourcesQuery =
+ samsql"""select ${resourceTable.result.name}, ${resourceTypeTable.result.name}
+ from ${ResourceTable as resourceTable}
+ join ${ResourceTypeTable as resourceTypeTable} on ${resourceTable.resourceTypeId} = ${resourceTypeTable.id}
+ join ${UserFavoriteResourcesTable as userFavoriteResourcesTable} on ${resourceTable.id} = ${userFavoriteResourcesTable.resourceId}
+ where ${userFavoriteResourcesTable.samUserId} = $userId"""
+
+ loadUserFavoriteResourcesQuery
+ .map(rs =>
+ FullyQualifiedResourceId(ResourceTypeName(rs.string(resourceTypeTable.resultName.name)), ResourceId(rs.string(resourceTable.resultName.name)))
+ )
+ .list()
+ .apply()
+ .toSet
+ }
+
+ def getUserFavoriteResourcesOfType(
+ userId: WorkbenchUserId,
+ resourceTypeName: ResourceTypeName,
+ samRequestContext: SamRequestContext
+ ): IO[Set[FullyQualifiedResourceId]] =
+ readOnlyTransaction("getUserFavoriteResourcesOfType", samRequestContext) { implicit session =>
+ val resourceTable = ResourceTable.syntax
+ val resourceTypeTable = ResourceTypeTable.syntax
+ val userFavoriteResourcesTable = UserFavoriteResourcesTable.syntax
+
+ val loadUserFavoriteResourcesQuery =
+ samsql"""select ${resourceTable.result.name}
+ from ${ResourceTable as resourceTable}
+ join ${ResourceTypeTable as resourceTypeTable} on ${resourceTable.resourceTypeId} = ${resourceTypeTable.id}
+ join ${UserFavoriteResourcesTable as userFavoriteResourcesTable} on ${resourceTable.id} = ${userFavoriteResourcesTable.resourceId}
+ where ${userFavoriteResourcesTable.samUserId} = $userId
+ and ${resourceTypeTable.name} = $resourceTypeName
+ """
+
+ loadUserFavoriteResourcesQuery
+ .map(rs => FullyQualifiedResourceId(resourceTypeName, ResourceId(rs.string(resourceTable.resultName.name))))
+ .list()
+ .apply()
+ .toSet
+ }
+
}
diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/db/tables/UserFavoriteResourcesTable.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/db/tables/UserFavoriteResourcesTable.scala
new file mode 100644
index 000000000..a13da4373
--- /dev/null
+++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/db/tables/UserFavoriteResourcesTable.scala
@@ -0,0 +1,26 @@
+package org.broadinstitute.dsde.workbench.sam.db.tables
+
+import org.broadinstitute.dsde.workbench.model._
+import org.broadinstitute.dsde.workbench.sam.db.SamTypeBinders
+import scalikejdbc._
+
+import java.time.Instant
+
+final case class UserFavoriteResourcesRecord(
+ samUserId: WorkbenchUserId,
+ resourceId: ResourcePK,
+ createdAt: Instant
+)
+
+object UserFavoriteResourcesTable extends SQLSyntaxSupportWithDefaultSamDB[UserFavoriteResourcesRecord] {
+ override def tableName: String = "SAM_USER_FAVORITE_RESOURCES"
+
+ import SamTypeBinders._
+ def apply(e: ResultName[UserFavoriteResourcesRecord])(rs: WrappedResultSet): UserFavoriteResourcesRecord = UserFavoriteResourcesRecord(
+ rs.get(e.samUserId),
+ rs.get(e.resourceId),
+ rs.get(e.createdAt)
+ )
+
+ def apply(o: SyntaxProvider[UserFavoriteResourcesRecord])(rs: WrappedResultSet): UserFavoriteResourcesRecord = apply(o.resultName)(rs)
+}
diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/SamUserCombinedStateResponse.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/SamUserCombinedStateResponse.scala
index c34d53c84..a0e1e309e 100644
--- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/SamUserCombinedStateResponse.scala
+++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/SamUserCombinedStateResponse.scala
@@ -1,6 +1,6 @@
package org.broadinstitute.dsde.workbench.sam.model.api
-import org.broadinstitute.dsde.workbench.sam.model.TermsOfServiceDetails
+import org.broadinstitute.dsde.workbench.sam.model.{FullyQualifiedResourceId, TermsOfServiceDetails}
import spray.json.DefaultJsonProtocol._
import spray.json._
import org.broadinstitute.dsde.workbench.sam.model.api.SamUserAllowances.SamUserAllowedResponseFormat
@@ -8,12 +8,13 @@ import org.broadinstitute.dsde.workbench.sam.model.api.SamUserAttributes.SamUser
import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._
object SamUserCombinedStateResponse {
- implicit val SamUserResponseFormat: RootJsonFormat[SamUserCombinedStateResponse] = jsonFormat5(SamUserCombinedStateResponse.apply)
+ implicit val SamUserResponseFormat: RootJsonFormat[SamUserCombinedStateResponse] = jsonFormat6(SamUserCombinedStateResponse.apply)
}
final case class SamUserCombinedStateResponse(
samUser: SamUser,
allowances: SamUserAllowances,
attributes: Option[SamUserAttributes],
termsOfServiceDetails: TermsOfServiceDetails,
- additionalDetails: Map[String, JsValue]
+ additionalDetails: Map[String, JsValue],
+ favoriteResources: Set[FullyQualifiedResourceId]
)
diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceService.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceService.scala
index 707d63cff..39ff9b3a5 100644
--- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceService.scala
+++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceService.scala
@@ -958,6 +958,22 @@ class ResourceService(
def listResourceChildren(resourceId: FullyQualifiedResourceId, samRequestContext: SamRequestContext): IO[Set[FullyQualifiedResourceId]] =
accessPolicyDAO.listResourceChildren(resourceId, samRequestContext)
+ def addUserFavoriteResource(userId: WorkbenchUserId, resourceId: FullyQualifiedResourceId, samRequestContext: SamRequestContext): IO[Boolean] =
+ directoryDAO.addUserFavoriteResource(userId, resourceId, samRequestContext)
+
+ def removeUserFavoriteResource(userId: WorkbenchUserId, resourceId: FullyQualifiedResourceId, samRequestContext: SamRequestContext): IO[Unit] =
+ directoryDAO.removeUserFavoriteResource(userId, resourceId, samRequestContext)
+
+ def getUserFavoriteResources(userId: WorkbenchUserId, samRequestContext: SamRequestContext): IO[Set[FullyQualifiedResourceId]] =
+ directoryDAO.getUserFavoriteResources(userId, samRequestContext)
+
+ def getUserFavoriteResourcesOfType(
+ userId: WorkbenchUserId,
+ resourceType: ResourceTypeName,
+ samRequestContext: SamRequestContext
+ ): IO[Set[FullyQualifiedResourceId]] =
+ directoryDAO.getUserFavoriteResourcesOfType(userId, resourceType, samRequestContext)
+
private[service] def createAccessChangeEvents(
resource: FullyQualifiedResourceId,
beforePolicies: Iterable[AccessPolicy],
diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/MockSamRoutesBuilder.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/MockSamRoutesBuilder.scala
index 74486f798..072fdb58d 100644
--- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/MockSamRoutesBuilder.scala
+++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/MockSamRoutesBuilder.scala
@@ -137,6 +137,7 @@ class MockSamRoutesBuilder(allUsersGroup: WorkbenchGroup)(implicit system: Actor
val mockUserService = userServiceBuilder.build
val mockCloudExtensions = cloudExtensionsBuilder.build
val mockTosService = mockTosServiceBuilder.build
+ val mockPolicyEvaluatorService = mock[PolicyEvaluatorService]
new SamRoutes(
mockResourceService,
@@ -144,7 +145,7 @@ class MockSamRoutesBuilder(allUsersGroup: WorkbenchGroup)(implicit system: Actor
null,
null,
null,
- null,
+ mockPolicyEvaluatorService,
mockTosService,
null,
null,
diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV2Spec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV2Spec.scala
index f166e3839..1a76d2521 100644
--- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV2Spec.scala
+++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV2Spec.scala
@@ -8,6 +8,7 @@ import org.broadinstitute.dsde.workbench.model.{ErrorReport, WorkbenchEmail, Wor
import org.broadinstitute.dsde.workbench.model.ErrorReportJsonSupport._
import org.broadinstitute.dsde.workbench.sam.matchers.BeForSamUserResponseMatcher.beForUser
import org.broadinstitute.dsde.workbench.sam.model._
+import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._
import org.broadinstitute.dsde.workbench.sam.model.api.{
FilteredResourceFlat,
FilteredResourcesFlat,
@@ -28,8 +29,10 @@ import org.scalatest.matchers.should.Matchers
import java.time.Instant
import org.broadinstitute.dsde.workbench.sam.matchers.TimeMatchers
import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext
+import org.mockito.ArgumentMatchers
import org.mockito.Mockito.lenient
import spray.json.enrichAny
+import spray.json.DefaultJsonProtocol._
class UserRoutesV2Spec extends AnyFlatSpec with Matchers with TimeMatchers with ScalatestRouteTest with MockitoSugar with TestSupport {
val defaultUser: SamUser = Generator.genWorkbenchUserGoogle.sample.get
@@ -260,6 +263,7 @@ class UserRoutesV2Spec extends AnyFlatSpec with Matchers with TimeMatchers with
"GET /api/users/v2/self/combinedState" should "get the user combined state of the calling user" in {
// Arrange
val userAttributes = SamUserAttributes(defaultUser.id, marketingConsent = true)
+ val favoriteResources = Set(FullyQualifiedResourceId(ResourceTypeName("workspaceType"), ResourceId("workspaceName")))
val enterpriseFeature = FilteredResourceFlat(
resourceType = ResourceTypeName("enterprise-feature"),
resourceId = ResourceId("enterprise-feature"),
@@ -275,7 +279,8 @@ class UserRoutesV2Spec extends AnyFlatSpec with Matchers with TimeMatchers with
SamUserAllowances(enabled = true, termsOfService = true),
Option(SamUserAttributes(defaultUser.id, marketingConsent = true)),
TermsOfServiceDetails(Option("v1"), Option(Instant.now()), permitsSystemUsage = true, isCurrentVersion = true),
- Map("enterpriseFeatures" -> FilteredResourcesFlat(Set(enterpriseFeature)).toJson)
+ Map("enterpriseFeatures" -> FilteredResourcesFlat(Set(enterpriseFeature)).toJson),
+ favoriteResources
)
val samRoutes = new MockSamRoutesBuilder(allUsersGroup)
@@ -298,6 +303,11 @@ class UserRoutesV2Spec extends AnyFlatSpec with Matchers with TimeMatchers with
any[SamRequestContext]
)
+ lenient()
+ .doReturn(IO.pure(favoriteResources))
+ .when(samRoutes.resourceService)
+ .getUserFavoriteResources(any[WorkbenchUserId], any[SamRequestContext])
+
// Act and Assert
Get(s"/api/users/v2/self/combinedState") ~> samRoutes.route ~> check {
status shouldEqual StatusCodes.OK
@@ -310,6 +320,7 @@ class UserRoutesV2Spec extends AnyFlatSpec with Matchers with TimeMatchers with
response.termsOfServiceDetails.permitsSystemUsage should be(userCombinedStateResponse.termsOfServiceDetails.permitsSystemUsage)
response.termsOfServiceDetails.latestAcceptedVersion should be(userCombinedStateResponse.termsOfServiceDetails.latestAcceptedVersion)
response.additionalDetails should be(Map("enterpriseFeatures" -> filteresResourcesFlat.toJson))
+ response.favoriteResources should be(favoriteResources)
}
}
@@ -330,7 +341,8 @@ class UserRoutesV2Spec extends AnyFlatSpec with Matchers with TimeMatchers with
SamUserAllowances(enabled = true, termsOfService = true),
Option(SamUserAttributes(defaultUser.id, marketingConsent = true)),
TermsOfServiceDetails(Option("v1"), Option(Instant.now()), permitsSystemUsage = true, isCurrentVersion = true),
- Map("enterpriseFeatures" -> FilteredResourcesFlat(Set(enterpriseFeature)).toJson)
+ Map("enterpriseFeatures" -> FilteredResourcesFlat(Set(enterpriseFeature)).toJson),
+ Set.empty
)
val samRoutes = new MockSamRoutesBuilder(allUsersGroup)
@@ -352,6 +364,11 @@ class UserRoutesV2Spec extends AnyFlatSpec with Matchers with TimeMatchers with
any[SamRequestContext]
)
+ lenient()
+ .doReturn(IO.pure(Set.empty))
+ .when(samRoutes.resourceService)
+ .getUserFavoriteResources(any[WorkbenchUserId], any[SamRequestContext])
+
// Act and Assert
Get(s"/api/users/v2/self/combinedState") ~> samRoutes.route ~> check {
status shouldEqual StatusCodes.OK
@@ -364,18 +381,20 @@ class UserRoutesV2Spec extends AnyFlatSpec with Matchers with TimeMatchers with
response.termsOfServiceDetails.permitsSystemUsage should be(userCombinedStateResponse.termsOfServiceDetails.permitsSystemUsage)
response.termsOfServiceDetails.latestAcceptedVersion should be(userCombinedStateResponse.termsOfServiceDetails.latestAcceptedVersion)
response.additionalDetails should be(Map("enterpriseFeatures" -> filteresResourcesFlat.toJson))
+
}
}
it should "return falsy terms of service if the user has no tos history" in {
// Arrange
- val filteresResourcesFlat = FilteredResourcesFlat(Set.empty)
+ val filteredResourcesFlat = FilteredResourcesFlat(Set.empty)
val userCombinedStateResponse = SamUserCombinedStateResponse(
defaultUser,
SamUserAllowances(enabled = false, termsOfService = false),
Option(SamUserAttributes(defaultUser.id, marketingConsent = true)),
TermsOfServiceDetails(None, None, permitsSystemUsage = false, isCurrentVersion = false),
- Map("enterpriseFeatures" -> filteresResourcesFlat.toJson)
+ Map("enterpriseFeatures" -> filteredResourcesFlat.toJson),
+ Set.empty
)
val samRoutes = new MockSamRoutesBuilder(allUsersGroup)
@@ -397,6 +416,11 @@ class UserRoutesV2Spec extends AnyFlatSpec with Matchers with TimeMatchers with
any[SamRequestContext]
)
+ lenient()
+ .doReturn(IO.pure(Set.empty))
+ .when(samRoutes.resourceService)
+ .getUserFavoriteResources(any[WorkbenchUserId], any[SamRequestContext])
+
// Act and Assert
Get(s"/api/users/v2/self/combinedState") ~> samRoutes.route ~> check {
status shouldEqual StatusCodes.OK
@@ -408,7 +432,184 @@ class UserRoutesV2Spec extends AnyFlatSpec with Matchers with TimeMatchers with
response.termsOfServiceDetails.isCurrentVersion should be(userCombinedStateResponse.termsOfServiceDetails.isCurrentVersion)
response.termsOfServiceDetails.permitsSystemUsage should be(userCombinedStateResponse.termsOfServiceDetails.permitsSystemUsage)
response.termsOfServiceDetails.latestAcceptedVersion shouldBe None
- response.additionalDetails should be(Map("enterpriseFeatures" -> filteresResourcesFlat.toJson))
+ response.additionalDetails should be(Map("enterpriseFeatures" -> filteredResourcesFlat.toJson))
+ response.favoriteResources should be(Set.empty)
+
+ }
+ }
+
+ "GET /api/user/v2/self/favoriteResources" should "return the user's favorite resources" in {
+ // Arrange
+ val user = Generator.genWorkbenchUserGoogle.sample.get
+ val resourceType = ResourceTypeName("workspace")
+ val resourceId = ResourceId("workspace")
+ val resource = FullyQualifiedResourceId(resourceType, resourceId)
+ val favoriteResources = Set(resource)
+ val samRoutes = new MockSamRoutesBuilder(allUsersGroup)
+ .withEnabledUser(user)
+ .withAllowedUser(user)
+ .callAsNonAdminUser(Some(user))
+ .build
+
+ when(samRoutes.resourceService.getUserFavoriteResources(ArgumentMatchers.eq(user.id), any[SamRequestContext]))
+ .thenReturn(IO.pure(favoriteResources))
+
+ // Act and Assert
+ Get(s"/api/users/v2/self/favoriteResources") ~> samRoutes.route ~> check {
+ status shouldEqual StatusCodes.OK
+ responseAs[Set[FullyQualifiedResourceId]] shouldEqual favoriteResources
+ }
+ }
+
+ "GET /api/user/v2/self/favoriteResources/{resourceTypeName}" should "return the user's favorite resources of a given type" in {
+ // Arrange
+ val user = Generator.genWorkbenchUserGoogle.sample.get
+ val resourceType = ResourceTypeName("workspace")
+ val resourceId = ResourceId("workspace")
+ val resource = FullyQualifiedResourceId(resourceType, resourceId)
+ val favoriteResources = Set(resource)
+ val samRoutes = new MockSamRoutesBuilder(allUsersGroup)
+ .withEnabledUser(user)
+ .withAllowedUser(user)
+ .callAsNonAdminUser(Some(user))
+ .build
+
+ when(samRoutes.resourceService.getResourceType(ArgumentMatchers.eq(resourceType)))
+ .thenReturn(IO.pure(Some(ResourceType(resourceType, Set.empty, Set.empty, ResourceRoleName("owner")))))
+
+ when(samRoutes.resourceService.getUserFavoriteResourcesOfType(ArgumentMatchers.eq(user.id), ArgumentMatchers.eq(resourceType), any[SamRequestContext]))
+ .thenReturn(IO.pure(favoriteResources))
+
+ // Act and Assert
+ Get(s"/api/users/v2/self/favoriteResources/$resourceType") ~> samRoutes.route ~> check {
+ status shouldEqual StatusCodes.OK
+ responseAs[Set[FullyQualifiedResourceId]] shouldEqual favoriteResources
+ }
+ }
+
+ "PUT /api/user/v2/self/favoriteResources/{resourceTypeName}/{resourceId}" should "add a resource to a users favorites" in {
+ // Arrange
+ val user = Generator.genWorkbenchUserGoogle.sample.get
+ val resourceType = ResourceTypeName("workspace")
+ val resourceId = ResourceId("workspace")
+ val resource = FullyQualifiedResourceId(resourceType, resourceId)
+ val samRoutes = new MockSamRoutesBuilder(allUsersGroup)
+ .withEnabledUser(user)
+ .withAllowedUser(user)
+ .callAsNonAdminUser(Some(user))
+ .build
+
+ when(samRoutes.resourceService.getResourceType(ArgumentMatchers.eq(resourceType)))
+ .thenReturn(IO.pure(Some(ResourceType(resourceType, Set.empty, Set.empty, ResourceRoleName("owner")))))
+
+ when(samRoutes.policyEvaluatorService.listUserResourceActions(ArgumentMatchers.eq(resource), ArgumentMatchers.eq(user.id), any[SamRequestContext]))
+ .thenReturn(IO.pure(Set(ResourceAction("read"))))
+
+ when(samRoutes.resourceService.addUserFavoriteResource(ArgumentMatchers.eq(user.id), ArgumentMatchers.eq(resource), any[SamRequestContext]))
+ .thenReturn(IO.pure(true))
+
+ // Act and Assert
+ Put(s"/api/users/v2/self/favoriteResources/$resourceType/$resourceId") ~> samRoutes.route ~> check {
+ status shouldEqual StatusCodes.Accepted
+ }
+ }
+
+ it should "forbid adding a resource to a users favorites if they have no access to it, but return Not Found" in {
+ // Arrange
+ val user = Generator.genWorkbenchUserGoogle.sample.get
+ val resourceType = ResourceTypeName("workspace")
+ val resourceId = ResourceId("workspace")
+ val resource = FullyQualifiedResourceId(resourceType, resourceId)
+ val samRoutes = new MockSamRoutesBuilder(allUsersGroup)
+ .withEnabledUser(user)
+ .withAllowedUser(user)
+ .callAsNonAdminUser(Some(user))
+ .build
+
+ when(samRoutes.resourceService.getResourceType(ArgumentMatchers.eq(resourceType)))
+ .thenReturn(IO.pure(Some(ResourceType(resourceType, Set.empty, Set.empty, ResourceRoleName("owner")))))
+
+ when(samRoutes.policyEvaluatorService.listUserResourceActions(ArgumentMatchers.eq(resource), ArgumentMatchers.eq(user.id), any[SamRequestContext]))
+ .thenReturn(IO.pure(Set(ResourceAction("read"))))
+
+ when(samRoutes.resourceService.addUserFavoriteResource(ArgumentMatchers.eq(user.id), ArgumentMatchers.eq(resource), any[SamRequestContext]))
+ .thenReturn(IO.pure(false))
+
+ // Act and Assert
+ Put(s"/api/users/v2/self/favoriteResources/$resourceType/$resourceId") ~> samRoutes.route ~> check {
+ status shouldEqual StatusCodes.NotFound
+ }
+ }
+
+ it should "return Not Found if adding a resource that doesn't exist" in {
+ // Arrange
+ val user = Generator.genWorkbenchUserGoogle.sample.get
+ val resourceType = ResourceTypeName("workspace")
+ val resourceId = ResourceId("workspace")
+ val resource = FullyQualifiedResourceId(resourceType, resourceId)
+ val samRoutes = new MockSamRoutesBuilder(allUsersGroup)
+ .withEnabledUser(user)
+ .withAllowedUser(user)
+ .callAsNonAdminUser(Some(user))
+ .build
+
+ when(samRoutes.resourceService.getResourceType(ArgumentMatchers.eq(resourceType)))
+ .thenReturn(IO.pure(Some(ResourceType(resourceType, Set.empty, Set.empty, ResourceRoleName("owner")))))
+
+ when(samRoutes.policyEvaluatorService.listUserResourceActions(ArgumentMatchers.eq(resource), ArgumentMatchers.eq(user.id), any[SamRequestContext]))
+ .thenReturn(IO.pure(Set.empty))
+
+ // Act and Assert
+ Put(s"/api/users/v2/self/favoriteResources/$resourceType/$resourceId") ~> samRoutes.route ~> check {
+ status shouldEqual StatusCodes.NotFound
+ }
+ }
+
+ "DELETE /api/user/v2/self/favoriteResources/{resourceTypeName}/{resourceId}" should "remove a resource to a users favorites" in {
+ // Arrange
+ val user = Generator.genWorkbenchUserGoogle.sample.get
+ val resourceType = ResourceTypeName("workspace")
+ val resourceId = ResourceId("workspace")
+ val resource = FullyQualifiedResourceId(resourceType, resourceId)
+ val samRoutes = new MockSamRoutesBuilder(allUsersGroup)
+ .withEnabledUser(user)
+ .withAllowedUser(user)
+ .callAsNonAdminUser(Some(user))
+ .build
+
+ when(samRoutes.resourceService.getResourceType(ArgumentMatchers.eq(resourceType)))
+ .thenReturn(IO.pure(Some(ResourceType(resourceType, Set.empty, Set.empty, ResourceRoleName("owner")))))
+
+ when(samRoutes.resourceService.removeUserFavoriteResource(ArgumentMatchers.eq(user.id), ArgumentMatchers.eq(resource), any[SamRequestContext]))
+ .thenReturn(IO.pure(()))
+
+ // Act and Assert
+ Delete(s"/api/users/v2/self/favoriteResources/$resourceType/$resourceId") ~> samRoutes.route ~> check {
+ status shouldEqual StatusCodes.Accepted
+ }
+ }
+
+ it should "allow removing a resource to a users favorites if they have no access to it" in {
+ // Arrange
+ val user = Generator.genWorkbenchUserGoogle.sample.get
+ val resourceType = ResourceTypeName("workspace")
+ val resourceId = ResourceId("workspace")
+ val resource = FullyQualifiedResourceId(resourceType, resourceId)
+ val samRoutes = new MockSamRoutesBuilder(allUsersGroup)
+ .withEnabledUser(user)
+ .withAllowedUser(user)
+ .callAsNonAdminUser(Some(user))
+ .build
+
+ when(samRoutes.resourceService.getResourceType(ArgumentMatchers.eq(resourceType)))
+ .thenReturn(IO.pure(Some(ResourceType(resourceType, Set.empty, Set.empty, ResourceRoleName("owner")))))
+
+ when(samRoutes.resourceService.removeUserFavoriteResource(ArgumentMatchers.eq(user.id), ArgumentMatchers.eq(resource), any[SamRequestContext]))
+ .thenReturn(IO.pure(()))
+
+ // Act and Assert
+ Delete(s"/api/users/v2/self/favoriteResources/$resourceType/$resourceId") ~> samRoutes.route ~> check {
+ status shouldEqual StatusCodes.Accepted
}
}
}
diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/MockDirectoryDAO.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/MockDirectoryDAO.scala
index 835bdc6fb..ed417cd39 100644
--- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/MockDirectoryDAO.scala
+++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/MockDirectoryDAO.scala
@@ -16,7 +16,7 @@ import org.broadinstitute.dsde.workbench.sam.azure.{
}
import org.broadinstitute.dsde.workbench.sam.db.tables.TosTable
import org.broadinstitute.dsde.workbench.sam.model.api.{AdminUpdateUserRequest, SamUser, SamUserAttributes}
-import org.broadinstitute.dsde.workbench.sam.model.{AccessPolicy, BasicWorkbenchGroup, FullyQualifiedResourceId, ResourceAction, SamUserTos}
+import org.broadinstitute.dsde.workbench.sam.model.{AccessPolicy, BasicWorkbenchGroup, FullyQualifiedResourceId, ResourceAction, ResourceTypeName, SamUserTos}
import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext
import java.time.Instant
@@ -43,6 +43,8 @@ class MockDirectoryDAO(val groups: mutable.Map[WorkbenchGroupIdentity, Workbench
private val petManagedIdentitiesByUser: mutable.Map[PetManagedIdentityId, PetManagedIdentity] = new TrieMap()
+ private val userFavoriteResources: mutable.Map[WorkbenchUserId, Set[FullyQualifiedResourceId]] = new TrieMap()
+
override def createGroup(
group: BasicWorkbenchGroup,
accessInstruction: Option[String] = None,
@@ -480,4 +482,33 @@ class MockDirectoryDAO(val groups: mutable.Map[WorkbenchGroupIdentity, Workbench
}
override def listParentGroups(groupName: WorkbenchGroupName, samRequestContext: SamRequestContext): IO[Set[WorkbenchGroupName]] = IO.pure(Set.empty)
+
+ override def addUserFavoriteResource(userId: WorkbenchUserId, resourceId: FullyQualifiedResourceId, samRequestContext: SamRequestContext): IO[Boolean] = {
+ if (userFavoriteResources.keySet.contains(userId)) {
+ val updatedResources = userFavoriteResources(userId) + resourceId
+ userFavoriteResources += userId -> updatedResources
+ } else {
+ userFavoriteResources += userId -> Set(resourceId)
+ }
+ IO.pure(true)
+ }
+
+ override def removeUserFavoriteResource(userId: WorkbenchUserId, resourceId: FullyQualifiedResourceId, samRequestContext: SamRequestContext): IO[Unit] = {
+ if (userFavoriteResources.keySet.contains(userId)) {
+ val updatedResources = userFavoriteResources(userId) - resourceId
+ userFavoriteResources += userId -> updatedResources
+ }
+ IO.unit
+ }
+
+ override def getUserFavoriteResources(userId: WorkbenchUserId, samRequestContext: SamRequestContext): IO[Set[FullyQualifiedResourceId]] = IO {
+ userFavoriteResources.getOrElse(userId, Set.empty)
+ }
+
+ override def getUserFavoriteResourcesOfType(
+ userId: WorkbenchUserId,
+ resourceTypeName: ResourceTypeName,
+ samRequestContext: SamRequestContext
+ ): IO[Set[FullyQualifiedResourceId]] =
+ IO.pure(userFavoriteResources.getOrElse(userId, Set.empty).filter(_.resourceTypeName == resourceTypeName))
}
diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresDirectoryDAOSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresDirectoryDAOSpec.scala
index 7c04091fa..fcb0d910a 100644
--- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresDirectoryDAOSpec.scala
+++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresDirectoryDAOSpec.scala
@@ -2043,5 +2043,83 @@ class PostgresDirectoryDAOSpec extends RetryableAnyFreeSpec with Matchers with B
dao.listParentGroups(WorkbenchGroupName("nonexistentGroup"), samRequestContext).unsafeRunSync() shouldBe empty
}
}
+
+ "UserFavoriteResources" - {
+ "add a favorite resource for a user" in {
+ assume(databaseEnabled, databaseEnabledClue)
+ val user = Generator.genWorkbenchUserGoogle.sample.get
+ dao.createUser(user, samRequestContext).unsafeRunSync()
+ policyDAO.createResourceType(resourceType, samRequestContext).unsafeRunSync()
+ policyDAO.createResource(defaultResource, samRequestContext).unsafeRunSync()
+ val result = dao.addUserFavoriteResource(user.id, defaultResource.fullyQualifiedId, samRequestContext).unsafeRunSync()
+ result should be(true)
+
+ val loadedFavoriteResources = dao.getUserFavoriteResources(user.id, samRequestContext).unsafeRunSync()
+ loadedFavoriteResources should contain theSameElementsAs Set(defaultResource.fullyQualifiedId)
+ }
+
+ "return false if adding a favorite resource for a user that doesn't exist" in {
+ assume(databaseEnabled, databaseEnabledClue)
+ val user = Generator.genWorkbenchUserGoogle.sample.get
+ dao.createUser(user, samRequestContext).unsafeRunSync()
+ policyDAO.createResourceType(resourceType, samRequestContext).unsafeRunSync()
+ policyDAO.createResource(defaultResource, samRequestContext).unsafeRunSync()
+
+ val otherResource = Resource(resourceTypeName, ResourceId("otherResource"), Set.empty)
+ val result = dao.addUserFavoriteResource(user.id, otherResource.fullyQualifiedId, samRequestContext).unsafeRunSync()
+ result should be(false)
+ }
+
+ "remove a favorite resource for a user" in {
+ assume(databaseEnabled, databaseEnabledClue)
+ val user = Generator.genWorkbenchUserGoogle.sample.get
+ dao.createUser(user, samRequestContext).unsafeRunSync()
+ val otherResource = Resource(resourceType.name, ResourceId("otherResource"), Set.empty)
+ policyDAO.createResourceType(resourceType, samRequestContext).unsafeRunSync()
+ policyDAO.createResource(defaultResource, samRequestContext).unsafeRunSync()
+ policyDAO.createResource(otherResource, samRequestContext).unsafeRunSync()
+
+ dao.addUserFavoriteResource(user.id, defaultResource.fullyQualifiedId, samRequestContext).unsafeRunSync()
+ dao.addUserFavoriteResource(user.id, otherResource.fullyQualifiedId, samRequestContext).unsafeRunSync()
+ dao.removeUserFavoriteResource(user.id, defaultResource.fullyQualifiedId, samRequestContext).unsafeRunSync()
+
+ val loadedFavoriteResources = dao.getUserFavoriteResources(user.id, samRequestContext).unsafeRunSync()
+ loadedFavoriteResources should contain theSameElementsAs Set(otherResource.fullyQualifiedId)
+ }
+
+ "remove a favorite resource for a user when the resource doesn't exist" in {
+ assume(databaseEnabled, databaseEnabledClue)
+ val user = Generator.genWorkbenchUserGoogle.sample.get
+ dao.createUser(user, samRequestContext).unsafeRunSync()
+ val otherResource = Resource(resourceType.name, ResourceId("otherResource"), Set.empty)
+ policyDAO.createResourceType(resourceType, samRequestContext).unsafeRunSync()
+ policyDAO.createResource(defaultResource, samRequestContext).unsafeRunSync()
+
+ dao.removeUserFavoriteResource(user.id, otherResource.fullyQualifiedId, samRequestContext).unsafeRunSync()
+
+ val loadedFavoriteResources = dao.getUserFavoriteResources(user.id, samRequestContext).unsafeRunSync()
+ loadedFavoriteResources should contain theSameElementsAs Set.empty
+ }
+
+ "get the favorite resources of a specific resource type for a user" in {
+ assume(databaseEnabled, databaseEnabledClue)
+ val user = Generator.genWorkbenchUserGoogle.sample.get
+ dao.createUser(user, samRequestContext).unsafeRunSync()
+ policyDAO.createResourceType(resourceType, samRequestContext).unsafeRunSync()
+ policyDAO.createResource(defaultResource, samRequestContext).unsafeRunSync()
+
+ val otherResourceTypeName: ResourceTypeName = ResourceTypeName("awesomeType2")
+ val otherResourceType: ResourceType = ResourceType(otherResourceTypeName, actionPatterns, roles, ownerRoleName)
+ val otherResource = Resource(otherResourceTypeName, ResourceId("otherResource"), Set.empty)
+ policyDAO.createResourceType(otherResourceType, samRequestContext).unsafeRunSync()
+ policyDAO.createResource(otherResource, samRequestContext).unsafeRunSync()
+
+ dao.addUserFavoriteResource(user.id, defaultResource.fullyQualifiedId, samRequestContext).unsafeRunSync()
+ dao.addUserFavoriteResource(user.id, otherResource.fullyQualifiedId, samRequestContext).unsafeRunSync()
+
+ val loadedFavoriteResources = dao.getUserFavoriteResourcesOfType(user.id, otherResourceTypeName, samRequestContext).unsafeRunSync()
+ loadedFavoriteResources should contain theSameElementsAs Set(otherResource.fullyQualifiedId)
+ }
+ }
}
}
diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceServiceSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceServiceSpec.scala
index f7536dd7b..a40b51f3d 100644
--- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceServiceSpec.scala
+++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceServiceSpec.scala
@@ -3311,6 +3311,55 @@ class ResourceServiceSpec
returnedPolicies.head._2.isLeft shouldBe true
}
+ "UserFavoriteResource" should "add, remove, and list favorite resources for a user" in {
+ assume(databaseEnabled, databaseEnabledClue)
+
+ val resourceName = ResourceId("resource")
+ val resource2Name = ResourceId("resource2")
+ val resource = FullyQualifiedResourceId(defaultResourceType.name, resourceName)
+ val resource2 = FullyQualifiedResourceId(otherResourceType.name, resource2Name)
+
+ service.createResourceType(defaultResourceType, samRequestContext).unsafeRunSync()
+ service.createResourceType(otherResourceType, samRequestContext).unsafeRunSync()
+
+ service.createResource(defaultResourceType, resourceName, dummyUser, samRequestContext).unsafeRunSync()
+ service.createResource(otherResourceType, resource2Name, dummyUser, samRequestContext).unsafeRunSync()
+
+ service.addUserFavoriteResource(dummyUser.id, resource, samRequestContext).unsafeRunSync()
+
+ service.getUserFavoriteResources(dummyUser.id, samRequestContext).unsafeRunSync() should contain theSameElementsAs Set(resource)
+
+ service.addUserFavoriteResource(dummyUser.id, resource2, samRequestContext).unsafeRunSync()
+
+ service.getUserFavoriteResources(dummyUser.id, samRequestContext).unsafeRunSync() should contain theSameElementsAs Set(resource, resource2)
+
+ service.removeUserFavoriteResource(dummyUser.id, resource, samRequestContext).unsafeRunSync()
+
+ service.getUserFavoriteResources(dummyUser.id, samRequestContext).unsafeRunSync() should contain theSameElementsAs Set(resource2)
+
+ service.removeUserFavoriteResource(dummyUser.id, resource2, samRequestContext).unsafeRunSync()
+
+ service.getUserFavoriteResources(dummyUser.id, samRequestContext).unsafeRunSync() shouldBe empty
+ }
+
+ it should "not return favorite resources for another user" in {
+ assume(databaseEnabled, databaseEnabledClue)
+
+ def otherUser = Generator.genWorkbenchUserBoth.sample.get
+ dirDAO.createUser(otherUser, samRequestContext).unsafeRunSync()
+
+ val resourceName = ResourceId("resource")
+ val resource = FullyQualifiedResourceId(defaultResourceType.name, resourceName)
+
+ service.createResourceType(defaultResourceType, samRequestContext).unsafeRunSync()
+
+ service.createResource(defaultResourceType, resourceName, dummyUser, samRequestContext).unsafeRunSync()
+
+ service.addUserFavoriteResource(dummyUser.id, resource, samRequestContext).unsafeRunSync()
+
+ service.getUserFavoriteResources(otherUser.id, samRequestContext).unsafeRunSync() shouldBe empty
+ }
+
/** Sets up a test log appender attached to the audit logger, runs the `test` IO, ensures that `events` were appended. If tryTwice` run `test` again to make
* sure subsequent calls to no log more messages. Ends by tearing down the log appender.
*/