~jan0sch/smederee
Showing details for patch 837d6a6728a25f7d0ccfa7368b4cd99c670d5f6c.
diff -rN -u old-smederee/modules/hub/src/main/resources/db/migration/hub/V11__repository_health_tables_stdout_and_stderr_to_array.sql new-smederee/modules/hub/src/main/resources/db/migration/hub/V11__repository_health_tables_stdout_and_stderr_to_array.sql --- old-smederee/modules/hub/src/main/resources/db/migration/hub/V11__repository_health_tables_stdout_and_stderr_to_array.sql 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/resources/db/migration/hub/V11__repository_health_tables_stdout_and_stderr_to_array.sql 2025-06-20 16:42:14.175271697 +0000 @@ -0,0 +1,2 @@ +ALTER TABLE hub.repository_health ALTER COLUMN stderr SET DATA TYPE TEXT[] USING string_to_array(stderr, '\n'); +ALTER TABLE hub.repository_health ALTER COLUMN stdout SET DATA TYPE TEXT[] USING string_to_array(stdout, '\n'); diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieVcsMetadataRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieVcsMetadataRepository.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieVcsMetadataRepository.scala 2025-06-20 16:42:14.171271703 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieVcsMetadataRepository.scala 2025-06-20 16:42:14.175271697 +0000 @@ -8,6 +8,7 @@ import java.util.UUID +import cats.data.* import cats.effect.* import cats.syntax.all.* import de.smederee.email.EmailAddress @@ -23,6 +24,7 @@ import org.http4s.Uri final class DoobieVcsMetadataRepository[F[_]: Async](tx: Transactor[F]) extends VcsMetadataRepository[F] { + given Get[Chain[String]] = Get[Array[String]].tmap(Chain.fromIterableOnce) given Meta[EmailAddress] = Meta[String].timap(EmailAddress.apply)(_.toString) given Meta[OrganisationId] = Meta[UUID].timap(OrganisationId.apply)(_.toUUID) given Meta[Set[Permission]] = Meta[Int].timap(Permission.decode)(_.encode) @@ -86,6 +88,19 @@ WHERE owner = ${repository.owner.uid} AND name = ${repository.name}""".update.run.transact(tx) + override def findHealthCheck(repositoryId: VcsRepositoryId): F[Option[VcsRepositoryHealth]] = + sql"""SELECT + command, + exit_code, + stderr, + stdout, + created_at + FROM hub.repository_health + WHERE repository = $repositoryId""" + .query[VcsRepositoryHealth] + .option + .transact(tx) + override def findVcsRepository( owner: VcsRepositoryOwner, name: VcsRepositoryName @@ -172,6 +187,28 @@ query.query[VcsRepository].stream.transact(tx) } + override def saveHealthCheck(repositoryId: VcsRepositoryId)(health: VcsRepositoryHealth): F[Int] = + sql"""INSERT INTO hub.repository_health ( + repository, + command, + exit_code, + stderr, + stdout, + created_at + ) VALUES ( + $repositoryId, + ${health.command}, + ${health.exitCode}, + ${health.stderr.toList}, + ${health.stdout.toList}, + ${health.createdAt} + ) ON CONFLICT (repository) DO UPDATE SET + command = EXCLUDED.command, + exit_code = EXCLUDED.exit_code, + stderr = EXCLUDED.stderr, + stdout = EXCLUDED.stdout, + created_at = EXCLUDED.created_at""".update.run.transact(tx) + override def setPermissions(repositoryId: VcsRepositoryId)(organisation: Organisation)( permissions: Set[Permission] ): F[Int] = diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsMetadataRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsMetadataRepository.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsMetadataRepository.scala 2025-06-20 16:42:14.171271703 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsMetadataRepository.scala 2025-06-20 16:42:14.175271697 +0000 @@ -58,6 +58,15 @@ */ def getPermissions(repositoryId: VcsRepositoryId)(organisation: Organisation): F[Set[Permission]] + /** Search for a health check result for the given vcs repository id. + * + * @param repositoryId + * The id of the repository. + * @return + * An option to the result of a repository health check. + */ + def findHealthCheck(repositoryId: VcsRepositoryId): F[Option[VcsRepositoryHealth]] + /** Search for the vcs repository entry with the given owner and name. * * @param owner @@ -137,6 +146,17 @@ */ def listRepositories(requester: Option[Account])(owner: VcsRepositoryOwner): Stream[F, VcsRepository] + /** Save the given result of a repository health check to the database overriding an existing one. + * + * @param repositoryId + * The id of the repository. + * @param health + * The result of a repository health check. + * @return + * The number of affected database rows. + */ + def saveHealthCheck(repositoryId: VcsRepositoryId)(health: VcsRepositoryHealth): F[Int] + /** Set the permissions on the given repository for the given organisation. * * @param repositoryId diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala 2025-06-20 16:42:14.171271703 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala 2025-06-20 16:42:14.175271697 +0000 @@ -561,6 +561,10 @@ createdAt: OffsetDateTime ) +object VcsRepositoryHealth { + given Eq[VcsRepositoryHealth] = Eq.fromUniversalEquals +} + /** Data about a VCS respository. * * @param name diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieVcsMetadataRepositoryTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieVcsMetadataRepositoryTest.scala --- old-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieVcsMetadataRepositoryTest.scala 2025-06-20 16:42:14.175271697 +0000 +++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieVcsMetadataRepositoryTest.scala 2025-06-20 16:42:14.175271697 +0000 @@ -6,6 +6,8 @@ package de.smederee.hub +import java.time.ZoneOffset + import cats.data.NonEmptyList import cats.effect.* import cats.syntax.all.* @@ -168,6 +170,36 @@ } } + test("findHealthCheck must return an existing health check".tag(NeedsDatabase)) { + PropF.forAllF { (account: Account, repository: VcsRepository, health: VcsRepositoryHealth) => + val vcsRepository = repository.copy(owner = account.toVcsRepositoryOwner) + val dbConfig = configuration.database + val tx = Transactor.fromDriverManager[IO]( + driver = dbConfig.driver, + url = dbConfig.url, + user = dbConfig.user, + password = dbConfig.pass, + logHandler = None + ) + val repo = new DoobieVcsMetadataRepository[IO](tx) + val test = for { + _ <- createAccount(account, PasswordHash("I am not a password hash!"), None, None) + _ <- repo.createVcsRepository(vcsRepository) + repoId <- repo.findVcsRepositoryId(vcsRepository.owner, vcsRepository.name) + saved <- repoId.traverse(id => repo.saveHealthCheck(id)(health)) + found <- repoId.traverse(id => repo.findHealthCheck(id)) + } yield (repoId, saved, found.getOrElse(None)) + test.start.flatMap(_.joinWithNever).map { result => + val (repoId, saved, found) = result + // Adjust timestamp because the one from the database will be in UTC. + val expectedHealth = health.copy(createdAt = health.createdAt.withOffsetSameInstant(ZoneOffset.UTC)) + assert(repoId.nonEmpty, "No test repository created in database!") + assert(saved.exists(_ === 1), "No rows written to database!") + assert(found.exists(_ === expectedHealth), "Returned health status does not match!") + } + } + } + test("findVcsRepository must return an existing repository".tag(NeedsDatabase)) { PropF.forAllF { (account: Account, repository: VcsRepository) => val vcsRepository = repository.copy(owner = account.toVcsRepositoryOwner) @@ -638,6 +670,34 @@ } } } + + test("saveHealthCheck must save the data".tag(NeedsDatabase)) { + PropF.forAllF { (account: Account, repository: VcsRepository, health: VcsRepositoryHealth) => + val vcsRepository = repository.copy(owner = account.toVcsRepositoryOwner) + val dbConfig = configuration.database + val tx = Transactor.fromDriverManager[IO]( + driver = dbConfig.driver, + url = dbConfig.url, + user = dbConfig.user, + password = dbConfig.pass, + logHandler = None + ) + val repo = new DoobieVcsMetadataRepository[IO](tx) + val test = for { + _ <- createAccount(account, PasswordHash("I am not a password hash!"), None, None) + _ <- repo.createVcsRepository(vcsRepository) + repoId <- repo.findVcsRepositoryId(vcsRepository.owner, vcsRepository.name) + saved <- repoId.traverse(id => repo.saveHealthCheck(id)(health)) + updated <- repoId.traverse(id => repo.saveHealthCheck(id)(health)) + } yield (repoId, saved, updated) + test.start.flatMap(_.joinWithNever).map { result => + val (repoId, saved, updated) = result + assert(repoId.nonEmpty, "No test repository created in database!") + assert(saved.exists(_ === 1), "No rows written to database!") + assert(updated.exists(_ === 1), "No rows updated in to database!") + } + } + } test("setPermissions must create permissions correctly".tag(NeedsDatabase)) { PropF.forAllF { diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/Generators.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/Generators.scala --- old-smederee/modules/hub/src/test/scala/de/smederee/hub/Generators.scala 2025-06-20 16:42:14.175271697 +0000 +++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/Generators.scala 2025-06-20 16:42:14.175271697 +0000 @@ -11,6 +11,7 @@ import java.util.Locale import java.util.UUID +import cats.data.Chain import de.smederee.email.EmailAddress import de.smederee.i18n.LanguageCode import de.smederee.security.* @@ -221,6 +222,22 @@ given Arbitrary[VcsRepository] = Arbitrary(genValidVcsRepository) + val genVcsRepositoryHealth: Gen[VcsRepositoryHealth] = for { + command <- Gen.nonEmptyStringOf(Gen.alphaChar) + exitCode <- Gen.choose(Int.MinValue, Int.MaxValue) + stderr <- Gen.listOf(Gen.alphaNumStr).map(Chain.fromSeq) + stdout <- Gen.listOf(Gen.alphaNumStr).map(Chain.fromSeq) + createdAt <- genOffsetDateTime + } yield VcsRepositoryHealth( + command = command, + exitCode = exitCode, + stderr = stderr, + stdout = stdout, + createdAt = createdAt + ) + + given Arbitrary[VcsRepositoryHealth] = Arbitrary(genVcsRepositoryHealth) + val genPermissions: Gen[Set[Permission]] = Gen.choose(0, 7).map(Permission.decode) given Arbitrary[Set[Permission]] = Arbitrary(genPermissions)