From 5531675e9118bf6cd21961f527dd2ca2f722c4b6 Mon Sep 17 00:00:00 2001 From: eikek <701128+eikek@users.noreply.github.com> Date: Wed, 19 Jun 2024 09:46:12 +0200 Subject: [PATCH] Return details for a project namespace The solr query is amended with another subquery to resolve existing `namespace` fields in the search results. This is only interesting for projects, as these entities use that field denoting "being in that namespace". `Group` and `User` entities on the other hand use the `namespace` field to *define* this namespace. So the sub-query is constraint to only `User` or `Group` entities. Solr applies this to all result documents, but it is only decoded for projects. `User` and `Group` entities resolve to themselves. So we potentially transfer lots of data from solr only to discard it, sadly. I couldn't find a way around it with the current model. It would be nice to have a clear distinction between defining the namespace and being a member of it (using different fields), but unfortunately we decided against that in the past. The search http api *replaces* the value of the `namespace` field with the full object of either `User` or `Group` in its results, making this a breaking change wrt the search api result structure. Since SOLR cannot guarantee to have a `User` or `Group` being resolved given a namespace, the result is defined as optional. When this happens an error is logged and `None` is returned as a result. --- .../search/api/data/EntityConverter.scala | 10 ++- .../io/renku/search/api/data/FacetData.scala | 15 ---- .../io/renku/search/api/data/PageDef.scala | 2 - .../search/api/data/PageWithTotals.scala | 2 - .../renku/search/api/data/SearchEntity.scala | 13 ++- .../io/renku/search/api/tapir/ApiSchema.scala | 90 ++++++++++++++----- .../renku/search/api/tapir/TapirCodecs.scala | 7 -- .../io/renku/search/api/SearchApiSpec.scala | 5 +- .../solr/client/SearchSolrClientImpl.scala | 9 ++ .../solr/documents/EntityDocument.scala | 31 ++++++- .../solr/client/SearchSolrClientSpec.scala | 36 ++++++++ .../solr/client/SolrDocumentGenerators.scala | 37 +++++--- .../search/solr/documents/EntityOps.scala | 5 ++ .../search/solr/query/AuthTestData.scala | 1 + 14 files changed, 194 insertions(+), 69 deletions(-) diff --git a/modules/search-api/src/main/scala/io/renku/search/api/data/EntityConverter.scala b/modules/search-api/src/main/scala/io/renku/search/api/data/EntityConverter.scala index f2fb0db8..755c03d8 100644 --- a/modules/search-api/src/main/scala/io/renku/search/api/data/EntityConverter.scala +++ b/modules/search-api/src/main/scala/io/renku/search/api/data/EntityConverter.scala @@ -31,7 +31,15 @@ trait EntityConverter: id = p.id, name = p.name, slug = p.slug, - namespace = p.namespace, + namespace = p.namespaceDetails.flatMap(_.docs.headOption).flatMap { + case u: UserDocument => Some(user(u)) + case g: GroupDocument => Some(group(g)) + case v => + scribe.error( + s"Error converting project namespace due to incorrect data. A user or group was expected as the project namespace of ${p.id}/${p.slug}, but found: $v" + ) + None + }, repositories = p.repositories, visibility = p.visibility, description = p.description, diff --git a/modules/search-api/src/main/scala/io/renku/search/api/data/FacetData.scala b/modules/search-api/src/main/scala/io/renku/search/api/data/FacetData.scala index 483f9c07..c5a0dfdc 100644 --- a/modules/search-api/src/main/scala/io/renku/search/api/data/FacetData.scala +++ b/modules/search-api/src/main/scala/io/renku/search/api/data/FacetData.scala @@ -21,9 +21,7 @@ package io.renku.search.api.data import io.bullet.borer.Decoder import io.bullet.borer.Encoder import io.bullet.borer.derivation.MapBasedCodecs -import io.renku.search.api.tapir.SchemaSyntax.* import io.renku.search.model.EntityType -import sttp.tapir.Schema final case class FacetData( entityType: Map[EntityType, Int] @@ -34,16 +32,3 @@ object FacetData: given Decoder[FacetData] = MapBasedCodecs.deriveDecoder given Encoder[FacetData] = MapBasedCodecs.deriveEncoder - given Schema[FacetData] = { - given Schema[Map[EntityType, Int]] = Schema.schemaForMap(_.name) - Schema - .derived[FacetData] - .jsonExample( - FacetData( - Map( - EntityType.Project -> 15, - EntityType.User -> 3 - ) - ) - ) - } diff --git a/modules/search-api/src/main/scala/io/renku/search/api/data/PageDef.scala b/modules/search-api/src/main/scala/io/renku/search/api/data/PageDef.scala index d470edff..6e010b21 100644 --- a/modules/search-api/src/main/scala/io/renku/search/api/data/PageDef.scala +++ b/modules/search-api/src/main/scala/io/renku/search/api/data/PageDef.scala @@ -21,7 +21,6 @@ package io.renku.search.api.data import io.bullet.borer.Decoder import io.bullet.borer.Encoder import io.bullet.borer.derivation.MapBasedCodecs -import sttp.tapir.Schema final case class PageDef( limit: Int, @@ -41,4 +40,3 @@ object PageDef: given Encoder[PageDef] = MapBasedCodecs.deriveEncoder given Decoder[PageDef] = MapBasedCodecs.deriveDecoder - given Schema[PageDef] = Schema.derived diff --git a/modules/search-api/src/main/scala/io/renku/search/api/data/PageWithTotals.scala b/modules/search-api/src/main/scala/io/renku/search/api/data/PageWithTotals.scala index 2e4444f1..759592cf 100644 --- a/modules/search-api/src/main/scala/io/renku/search/api/data/PageWithTotals.scala +++ b/modules/search-api/src/main/scala/io/renku/search/api/data/PageWithTotals.scala @@ -21,7 +21,6 @@ package io.renku.search.api.data import io.bullet.borer.NullOptions.given import io.bullet.borer.derivation.MapBasedCodecs import io.bullet.borer.{Decoder, Encoder} -import sttp.tapir.Schema final case class PageWithTotals( page: PageDef, @@ -34,7 +33,6 @@ final case class PageWithTotals( object PageWithTotals: given Encoder[PageWithTotals] = MapBasedCodecs.deriveEncoder given Decoder[PageWithTotals] = MapBasedCodecs.deriveDecoder - given Schema[PageWithTotals] = Schema.derived def apply(page: PageDef, totalResults: Long, hasMore: Boolean): PageWithTotals = PageWithTotals( diff --git a/modules/search-api/src/main/scala/io/renku/search/api/data/SearchEntity.scala b/modules/search-api/src/main/scala/io/renku/search/api/data/SearchEntity.scala index dd95754a..4565e05d 100644 --- a/modules/search-api/src/main/scala/io/renku/search/api/data/SearchEntity.scala +++ b/modules/search-api/src/main/scala/io/renku/search/api/data/SearchEntity.scala @@ -26,6 +26,15 @@ import io.renku.search.model.* sealed trait SearchEntity: def id: Id + def widen: SearchEntity = this + +sealed trait UserOrGroup: + def id: Id + +object UserOrGroup: + given AdtEncodingStrategy = AdtEncodingStrategy.flat(SearchEntity.discriminatorField) + given Decoder[UserOrGroup] = MapBasedCodecs.deriveDecoder[UserOrGroup] + given Encoder[UserOrGroup] = EncoderSupport.derive[UserOrGroup] object SearchEntity: private[api] val discriminatorField = "type" @@ -37,7 +46,7 @@ object SearchEntity: id: Id, name: Name, slug: Slug, - namespace: Option[Namespace], + namespace: Option[UserOrGroup], repositories: Seq[Repository], visibility: Visibility, description: Option[Description] = None, @@ -60,6 +69,7 @@ object SearchEntity: lastName: Option[LastName] = None, score: Option[Double] = None ) extends SearchEntity + with UserOrGroup object User: given Encoder[User] = EncoderSupport.deriveWithDiscriminator(discriminatorField) @@ -73,6 +83,7 @@ object SearchEntity: description: Option[Description] = None, score: Option[Double] = None ) extends SearchEntity + with UserOrGroup object Group: given Encoder[Group] = EncoderSupport.deriveWithDiscriminator(discriminatorField) given Decoder[Group] = MapBasedCodecs.deriveDecoder diff --git a/modules/search-api/src/main/scala/io/renku/search/api/tapir/ApiSchema.scala b/modules/search-api/src/main/scala/io/renku/search/api/tapir/ApiSchema.scala index 41c5e813..f15e6a16 100644 --- a/modules/search-api/src/main/scala/io/renku/search/api/tapir/ApiSchema.scala +++ b/modules/search-api/src/main/scala/io/renku/search/api/tapir/ApiSchema.scala @@ -24,6 +24,7 @@ import io.renku.search.api.data.* import io.renku.search.api.data.SearchEntity.* import io.renku.search.api.tapir.SchemaSyntax.* import io.renku.search.model.* +import io.renku.search.query.Query import sttp.tapir.Schema.SName import sttp.tapir.SchemaType.* import sttp.tapir.generic.Configuration @@ -31,13 +32,28 @@ import sttp.tapir.{FieldName, Schema, SchemaType} /** tapir schema definitions for the search api data structures */ trait ApiSchema extends ApiSchema.Primitives: + given Schema[Query] = Schema.anyObject[Query] + given Schema[QueryInput] = Schema.derived + given Schema[User] = Schema .derived[User] - .jsonExample(ApiSchema.exampleUser) + .jsonExample(ApiSchema.exampleUser.widen) given Schema[Group] = Schema .derived[Group] - .jsonExample(ApiSchema.exampleGroup) + .jsonExample(ApiSchema.exampleGroup.widen) + + given (using + userSchema: Schema[User], + groupSchema: Schema[Group] + ): Schema[UserOrGroup] = + Schema + .derived[UserOrGroup] + .withDiscriminator( + SearchEntity.discriminatorField, + Map("User" -> userSchema, "Group" -> groupSchema) + ) + .jsonExample(ApiSchema.exampleGroup: UserOrGroup) given (using userSchema: Schema[User]): Schema[Project] = Schema .derived[Project] @@ -57,31 +73,37 @@ trait ApiSchema extends ApiSchema.Primitives: userSchema.copy(schemaType = userType.copy(fields = df :: userType.fields)) schemaOptUser.copy(schemaType = SOption(nextUserSchema)(identity)) } - .jsonExample(ApiSchema.exampleProject) + .jsonExample(ApiSchema.exampleProject.widen) given (using projectSchema: Schema[Project], userSchema: Schema[User], - groupSchema: Schema[Group] - ): Schema[SearchEntity] = { - val derived = Schema.derived[SearchEntity] - derived.schemaType match { - case s: SCoproduct[?] => - derived.copy(schemaType = - s.addDiscriminatorField( - FieldName(SearchEntity.discriminatorField), - Schema.string, - List( - projectSchema.name.map(SRef(_)).map("Project" -> _), - userSchema.name.map(SRef(_)).map("User" -> _), - groupSchema.name.map(SRef(_)).map("Group" -> _) - ).flatten.toMap + groupSchema: Schema[Group], + ug: Schema[UserOrGroup] + ): Schema[SearchEntity] = + Schema + .derived[SearchEntity] + .withDiscriminator( + SearchEntity.discriminatorField, + Map("Project" -> projectSchema, "User" -> userSchema, "Group" -> groupSchema) + ) + + given Schema[FacetData] = { + given Schema[Map[EntityType, Int]] = Schema.schemaForMap(_.name) + Schema + .derived[FacetData] + .jsonExample( + FacetData( + Map( + EntityType.Project -> 15, + EntityType.User -> 3 ) ) - case s => derived - } + ) } + given Schema[PageDef] = Schema.derived + given Schema[PageWithTotals] = Schema.derived given Schema[SearchResult] = Schema.derived end ApiSchema @@ -100,9 +122,29 @@ object ApiSchema: given Schema[FirstName] = Schema.string[FirstName] given Schema[LastName] = Schema.string[LastName] given Schema[Email] = Schema.string[Email] + given Schema[EntityType] = Schema.derivedEnumeration[EntityType].defaultStringBased + + extension [A](self: Schema[A]) + def withDiscriminator(property: String, subs: Map[String, Schema[?]]): Schema[A] = + self.schemaType match { + case s: SCoproduct[?] => + self.copy(schemaType = + s.addDiscriminatorField( + FieldName(property), + Schema.string, + subs.toList + .map { case (value, schema) => + schema.name.map(SRef(_)).map(value -> _) + } + .flatten + .toMap + ) + ) + case _ => self + } end Primitives - val exampleUser: SearchEntity = User( + val exampleUser: SearchEntity.User = User( Id("1CAF4C73F50D4514A041C9EDDB025A36"), Some(Namespace("renku/renku")), Some(FirstName("Albert")), @@ -110,7 +152,7 @@ object ApiSchema: Some(2.1) ) - val exampleGroup: SearchEntity = Group( + val exampleGroup: SearchEntity.Group = Group( Id("2CAF4C73F50D4514A041C9EDDB025A36"), Name("SDSC"), Namespace("SDSC"), @@ -118,15 +160,15 @@ object ApiSchema: Some(1.1) ) - val exampleProject: SearchEntity = Project( + val exampleProject: SearchEntity.Project = Project( id = Id("01HRA7AZ2Q234CDQWGA052F8MK"), name = Name("renku"), slug = Slug("renku"), - namespace = Some(Namespace("renku/renku")), + namespace = Some(exampleGroup), repositories = Seq(Repository("https://github.com/renku")), visibility = Visibility.Public, description = Some(Description("Renku project")), - createdBy = Some(exampleUser.asInstanceOf[User]), + createdBy = Some(exampleUser), creationDate = CreationDate(Instant.now), keywords = List(Keyword("data"), Keyword("science")), score = Some(1.0) diff --git a/modules/search-api/src/main/scala/io/renku/search/api/tapir/TapirCodecs.scala b/modules/search-api/src/main/scala/io/renku/search/api/tapir/TapirCodecs.scala index a1e13c16..f3ca0788 100644 --- a/modules/search-api/src/main/scala/io/renku/search/api/tapir/TapirCodecs.scala +++ b/modules/search-api/src/main/scala/io/renku/search/api/tapir/TapirCodecs.scala @@ -18,8 +18,6 @@ package io.renku.search.api.tapir -import cats.syntax.all.* - import io.renku.search.api.data.* import io.renku.search.model.{EntityType, Id} import io.renku.search.query.Query @@ -29,14 +27,9 @@ trait TapirCodecs: given Codec[String, Query, CodecFormat.TextPlain] = Codec.string.mapEither(Query.parse(_))(_.render) - given Schema[Query] = Schema.anyObject[Query] - given Schema[QueryInput] = Schema.derived - given Codec[String, EntityType, CodecFormat.TextPlain] = Codec.string.mapEither(EntityType.fromString(_))(_.name) - given Schema[EntityType] = Schema.derivedEnumeration.defaultStringBased - given Codec[String, AuthToken.JwtToken, CodecFormat.TextPlain] = Codec.string.map(AuthToken.JwtToken(_))(_.render) diff --git a/modules/search-api/src/test/scala/io/renku/search/api/SearchApiSpec.scala b/modules/search-api/src/test/scala/io/renku/search/api/SearchApiSpec.scala index 061cd7ad..84a401f9 100644 --- a/modules/search-api/src/test/scala/io/renku/search/api/SearchApiSpec.scala +++ b/modules/search-api/src/test/scala/io/renku/search/api/SearchApiSpec.scala @@ -45,12 +45,14 @@ class SearchApiSpec extends CatsEffectSuite with SearchSolrSuite: "matching", "matching description", Gen.const(None), + Gen.const(None), Gen.const(Visibility.Public) ).generateOne val project2 = projectDocumentGen( "disparate", "disparate description", Gen.const(None), + Gen.const(None), Gen.const(Visibility.Public) ).generateOne for { @@ -75,12 +77,13 @@ class SearchApiSpec extends CatsEffectSuite with SearchSolrSuite: "exclusive", "exclusive description", Gen.const(None), + Gen.const(None), Gen.const(Visibility.Public) ).generateOne.copy(createdBy = userId) for { client <- IO(searchSolrClient()) searchApi = new SearchApiImpl[IO](client) - _ <- client.upsert(project :: user :: Nil) + _ <- client.upsert[EntityDocument](project :: user :: Nil) results <- searchApi .query(AuthContext.anonymous)(mkQuery("exclusive")) .map(_.fold(err => fail(s"Calling Search API failed with $err"), identity)) diff --git a/modules/search-solr-client/src/main/scala/io/renku/search/solr/client/SearchSolrClientImpl.scala b/modules/search-solr-client/src/main/scala/io/renku/search/solr/client/SearchSolrClientImpl.scala index dcdb098d..ba7f7b25 100644 --- a/modules/search-solr-client/src/main/scala/io/renku/search/solr/client/SearchSolrClientImpl.scala +++ b/modules/search-solr-client/src/main/scala/io/renku/search/solr/client/SearchSolrClientImpl.scala @@ -41,6 +41,7 @@ private class SearchSolrClientImpl[F[_]: Async](solrClient: SolrClient[F]) private val interpreter = LuceneQueryInterpreter.forSync[F] private val creatorDetails: FieldName = FieldName("creatorDetails") + private val namespaceDetails: FieldName = FieldName("namespaceDetails") private val typeTerms = Facet.Terms( EntityDocumentSchema.Fields.entityType, @@ -79,6 +80,14 @@ private class SearchSolrClientImpl[F[_]: Async](solrClient: SolrClient[F]) 1 ).withFields(FieldName.all) ) + .addSubQuery( + namespaceDetails, + SubQuery( + "{!terms f=namespace v=$row.namespace}", + "(_type:User OR _type:Group) AND _kind:fullentity", + 1 + ).withFields(FieldName.all) + ) ) } yield res diff --git a/modules/search-solr-client/src/main/scala/io/renku/search/solr/documents/EntityDocument.scala b/modules/search-solr-client/src/main/scala/io/renku/search/solr/documents/EntityDocument.scala index 730593ee..0ed41e2e 100644 --- a/modules/search-solr-client/src/main/scala/io/renku/search/solr/documents/EntityDocument.scala +++ b/modules/search-solr-client/src/main/scala/io/renku/search/solr/documents/EntityDocument.scala @@ -33,13 +33,35 @@ sealed trait EntityDocument extends SolrDocument: def widen: EntityDocument = this def setVersion(v: DocVersion): EntityDocument +// This type is used to avoid a self-recursive type, because it +// doesn't work with deriving encoders. Recursive structures don't +// work with lazy-vals/given etc, it either deadlocks or +// stack-overflows in scala3 (it is officially undefined behaviour) +// +// Since solr cannot guarantee that there is a user or group, a +// fallback `Unknown` is used to catch other or missing data instead +// of failing decoding by a runtime exception +sealed trait NestedUserOrGroup: + def setVersion(v: DocVersion): NestedUserOrGroup +object NestedUserOrGroup: + final case class Unknown(id: Id, @key("type") entityType: Option[EntityType] = None) + extends NestedUserOrGroup: + def setVersion(v: DocVersion): NestedUserOrGroup = this + + object Unknown: + given Encoder[Unknown] = MapBasedCodecs.deriveEncoder + + given AdtEncodingStrategy = + AdtEncodingStrategy.flat(typeMemberName = Fields.entityType.name) + given Encoder[NestedUserOrGroup] = EncoderSupport.derive + given Decoder[NestedUserOrGroup] = MapBasedCodecs.deriveAllDecoders + object EntityDocument: given AdtEncodingStrategy = AdtEncodingStrategy.flat(typeMemberName = Fields.entityType.name) given Encoder[EntityDocument] = EncoderSupport.derive[EntityDocument] given Decoder[EntityDocument] = MapBasedCodecs.deriveAllDecoders[EntityDocument] - given Codec[EntityDocument] = Codec.of[EntityDocument] final case class Project( id: Id, @@ -61,6 +83,7 @@ final case class Project( groupViewers: Set[Id] = Set.empty, keywords: List[Keyword] = List.empty, namespace: Option[Namespace] = None, + namespaceDetails: Option[ResponseBody[NestedUserOrGroup]] = None, score: Option[Double] = None ) extends EntityDocument: def setVersion(v: DocVersion): Project = copy(version = v) @@ -103,7 +126,8 @@ final case class User( name: Option[Name] = None, namespace: Option[Namespace] = None, score: Option[Double] = None -) extends EntityDocument: +) extends EntityDocument + with NestedUserOrGroup: def setVersion(v: DocVersion): User = copy(version = v) object User: @@ -148,7 +172,8 @@ final case class Group( editors: Set[Id] = Set.empty, viewers: Set[Id] = Set.empty, score: Option[Double] = None -) extends EntityDocument: +) extends EntityDocument + with NestedUserOrGroup: def setVersion(v: DocVersion): Group = copy(version = v) def toEntityMembers: EntityMembers = diff --git a/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SearchSolrClientSpec.scala b/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SearchSolrClientSpec.scala index 49a774a2..bb137d6b 100644 --- a/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SearchSolrClientSpec.scala +++ b/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SearchSolrClientSpec.scala @@ -33,6 +33,7 @@ import io.renku.search.solr.documents.EntityOps.* import io.renku.search.solr.schema.EntityDocumentSchema.Fields import io.renku.solr.client.DocVersion import io.renku.solr.client.QueryData +import io.renku.solr.client.ResponseBody import munit.CatsEffectSuite import org.scalacheck.Gen @@ -40,11 +41,45 @@ class SearchSolrClientSpec extends CatsEffectSuite with SearchSolrSuite: override def munitFixtures: Seq[munit.AnyFixture[?]] = List(solrServer, searchSolrClient) + test("load project with resolved namespace and creator"): + val user = userDocumentGen.generateOne + val group = groupDocumentGen.generateOne + val project = projectDocumentGen( + "project-test0", + "project-test0 description", + Gen.const(None), + Gen.const(None) + ).generateOne.copy(createdBy = user.id, namespace = group.namespace.some) + + for + client <- IO(searchSolrClient()) + _ <- client.upsert(Seq(project.widen, user.widen, group.widen)) + + qr <- client.queryEntity( + SearchRole.admin(Id("admin")), + Query.parse("test0").toOption.get, + 10, + 0 + ) + _ = assertEquals(qr.responseBody.docs.size, 1) + _ = assert(qr.responseBody.docs.head.isInstanceOf[Project]) + p = qr.responseBody.docs.head.asInstanceOf[Project] + _ = assertEquals( + p.creatorDetails.map(_.map(_.setVersion(user.version))), + ResponseBody.single(user).some + ) + _ = assertEquals( + p.namespaceDetails.map(_.map(_.setVersion(group.version))), + ResponseBody.single[NestedUserOrGroup](group).some + ) + yield () + test("be able to insert and fetch a Project document"): val project = projectDocumentGen( "solr-project", "solr project description", + Gen.const(None), Gen.const(None) ).generateOne for { @@ -60,6 +95,7 @@ class SearchSolrClientSpec extends CatsEffectSuite with SearchSolrSuite: qr.responseBody.docs.map( _.noneScore .setCreatedBy(None) + .setNamespaceDetails(None) .assertVersionNot(DocVersion.NotExists) .setVersion(DocVersion.NotExists) ) contains project diff --git a/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SolrDocumentGenerators.scala b/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SolrDocumentGenerators.scala index b81bdfd5..2f3eca4e 100644 --- a/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SolrDocumentGenerators.scala +++ b/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SolrDocumentGenerators.scala @@ -49,7 +49,8 @@ trait SolrDocumentGenerators: projectDocumentGen( s"proj-$differentiator", s"proj desc $differentiator", - userDocumentGen.asOption + userDocumentGen.asOption, + userOrGroupDocumentGen.asOption ) val projectDocumentGenForInsert: Gen[Project] = @@ -57,6 +58,7 @@ trait SolrDocumentGenerators: projectDocumentGen( s"proj-$differentiator", s"proj desc $differentiator", + Gen.const(None), Gen.const(None) ) @@ -64,21 +66,27 @@ trait SolrDocumentGenerators: name: String, desc: String, creatorGen: Gen[Option[User]], + namespaceGen: Gen[Option[User | Group]], visibilityGen: Gen[Visibility] = visibilityGen ): Gen[Project] = - (idGen, idGen, visibilityGen, creationDateGen, creatorGen) - .mapN((projectId, creatorId, visibility, creationDate, creator) => + (idGen, idGen, namespaceGen, visibilityGen, creationDateGen, creatorGen) + .mapN((projectId, creatorId, namespace, visibility, creationDate, creator) => Project( - projectId, - DocVersion.NotExists, - Name(name), - Slug(name), - Seq(Repository(s"http://github.com/$name")), - visibility, - Option(Description(desc)), - creatorId, - creator.map(_.copy(id = creatorId)).map(ResponseBody.single), - creationDate + id = projectId, + version = DocVersion.NotExists, + name = Name(name), + slug = Slug(name), + namespace = namespace.flatMap { + case u: User => u.namespace + case g: Group => g.namespace.some + }, + namespaceDetails = namespace.map(ResponseBody.single), + repositories = Seq(Repository(s"http://github.com/$name")), + visibility = visibility, + description = Option(Description(desc)), + createdBy = creatorId, + creatorDetails = creator.map(_.copy(id = creatorId)).map(ResponseBody.single), + creationDate = creationDate ) ) @@ -115,3 +123,6 @@ trait SolrDocumentGenerators: desc ) ) + + def userOrGroupDocumentGen: Gen[User | Group] = + Gen.oneOf(userDocumentGen, groupDocumentGen) diff --git a/modules/search-solr-client/src/test/scala/io/renku/search/solr/documents/EntityOps.scala b/modules/search-solr-client/src/test/scala/io/renku/search/solr/documents/EntityOps.scala index 2dbf2010..bd50e236 100644 --- a/modules/search-solr-client/src/test/scala/io/renku/search/solr/documents/EntityOps.scala +++ b/modules/search-solr-client/src/test/scala/io/renku/search/solr/documents/EntityOps.scala @@ -38,6 +38,11 @@ trait EntityOps: case e: Project => e.copy(creatorDetails = user) case e => e + def setNamespaceDetails(n: Option[ResponseBody[NestedUserOrGroup]]): EntityDocument = + entity match + case e: Project => e.copy(namespaceDetails = n) + case e => e + def assertVersionNot(v: DocVersion): EntityDocument = assert(entity.version != v) entity diff --git a/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/AuthTestData.scala b/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/AuthTestData.scala index 351400de..6b156d37 100644 --- a/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/AuthTestData.scala +++ b/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/AuthTestData.scala @@ -109,6 +109,7 @@ object AuthTestData: s"user${user.id}-${vis.name}-proj", "description", SolrDocumentGenerators.userDocumentGen.asOption, + SolrDocumentGenerators.userOrGroupDocumentGen.asOption, Gen.const(vis) ) .map(p => (user.id, vis) -> p.copy(owners = Set(user.id)))