~jan0sch/smederee

Showing details for patch f2403e44057ea4a9534fc86a6d2ac8530baeec00.
2024-07-28 (Sun), 3:04 PM - Jens Grassel - f2403e44057ea4a9534fc86a6d2ac8530baeec00

chore: migrate test to ScalaCheckEffect

Summary of changes
2 files modified with 283 lines added and 293 lines removed
  • modules/hub/src/test/scala/de/smederee/tickets/DoobieMilestoneRepositoryTest.scala with 279 added and 293 removed lines
  • modules/hub/src/test/scala/de/smederee/tickets/Generators.scala with 4 added and 0 removed lines
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/tickets/DoobieMilestoneRepositoryTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/tickets/DoobieMilestoneRepositoryTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/tickets/DoobieMilestoneRepositoryTest.scala	2025-01-11 00:01:25.713397455 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/tickets/DoobieMilestoneRepositoryTest.scala	2025-01-11 00:01:25.713397455 +0000
@@ -8,13 +8,17 @@
 
 import java.time.*
 
+import cats.data.NonEmptyList
 import cats.effect.*
 import cats.syntax.all.*
 import de.smederee.TestTags.*
-import de.smederee.tickets.Generators.*
+import de.smederee.tickets.Generators.given
 import doobie.*
 
+import org.scalacheck.effect.PropF
+
 final class DoobieMilestoneRepositoryTest extends BaseSpec {
+    override def scalaCheckTestParameters = super.scalaCheckTestParameters.withMinSuccessfulTests(1)
 
     /** Find the milestone ID for the given repository and milestone title.
       *
@@ -56,47 +60,47 @@
         }
 
     test("allMilestones must return all milestones".tag(NeedsDatabase)) {
-        (genProjectOwner.sample, genProject.sample, genMilestones.sample) match {
-            case (Some(owner), Some(generatedProject), Some(milestones)) =>
-                val project  = generatedProject.copy(owner = owner)
-                val dbConfig = configuration.database
-                val tx = Transactor.fromDriverManager[IO](
-                    driver = dbConfig.driver,
-                    url = dbConfig.url,
-                    user = dbConfig.user,
-                    password = dbConfig.pass,
-                    logHandler = None
-                )
-                val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
-                val test = for {
-                    _            <- createProjectOwner(owner)
-                    createdRepos <- createTicketsProject(project)
-                    repoId       <- loadProjectId(owner.uid, project.name)
-                    createdMilestones <- repoId match {
-                        case None => IO.pure(List.empty)
-                        case Some(repoId) =>
-                            milestones.traverse(milestone => milestoneRepo.createMilestone(repoId)(milestone))
-                    }
-                    foundMilestones <- repoId match {
-                        case None         => IO.pure(List.empty)
-                        case Some(repoId) => milestoneRepo.allMilestones(repoId).compile.toList
-                    }
-                } yield foundMilestones
-                test.map { foundMilestones =>
-                    assert(foundMilestones.size === milestones.size, "Different number of milestones!")
-                    foundMilestones.sortBy(_.title).zip(milestones.sortBy(_.title)).map { (found, expected) =>
-                        assertEquals(found.copy(id = expected.id), expected)
-                    }
+        PropF.forAllF { (owner: ProjectOwner, generatedProject: Project, milestones: NonEmptyList[Milestone]) =>
+            val project  = generatedProject.copy(owner = owner)
+            val dbConfig = configuration.database
+            val tx = Transactor.fromDriverManager[IO](
+                driver = dbConfig.driver,
+                url = dbConfig.url,
+                user = dbConfig.user,
+                password = dbConfig.pass,
+                logHandler = None
+            )
+            val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
+            val test = for {
+                _            <- createProjectOwner(owner)
+                createdRepos <- createTicketsProject(project)
+                repoId       <- loadProjectId(owner.uid, project.name)
+                createdMilestones <- repoId match {
+                    case None => IO.pure(List.empty)
+                    case Some(repoId) =>
+                        milestones.toList.traverse(milestone => milestoneRepo.createMilestone(repoId)(milestone))
+                }
+                foundMilestones <- repoId match {
+                    case None         => IO.pure(List.empty)
+                    case Some(repoId) => milestoneRepo.allMilestones(repoId).compile.toList
                 }
-            case _ => fail("Could not generate data samples!")
+            } yield foundMilestones
+            test.start.flatMap(_.joinWithNever).map { foundMilestones =>
+                assert(foundMilestones.size === milestones.size, "Different number of milestones!")
+                val cleanedFound =
+                    foundMilestones.sortBy(_.title).zip(milestones.toList.sortBy(_.title)).map { (found, expected) =>
+                        found.copy(id = expected.id)
+                    }
+                assertEquals(cleanedFound.sortBy(_.title), milestones.toList.sortBy(_.title))
+            }
         }
     }
 
     test("allTickets must return all tickets associated with the milestone".tag(NeedsDatabase)) {
-        (genProjectOwner.sample, genProject.sample, genMilestone.sample, genTickets.sample) match {
-            case (Some(owner), Some(generatedProject), Some(milestone), Some(rawTickets)) =>
+        PropF.forAllF {
+            (owner: ProjectOwner, generatedProject: Project, milestone: Milestone, rawTickets: NonEmptyList[Ticket]) =>
                 val project  = generatedProject.copy(owner = owner)
-                val tickets  = rawTickets.map(_.copy(submitter = None))
+                val tickets  = rawTickets.toList.map(_.copy(submitter = None))
                 val dbConfig = configuration.database
                 val tx = Transactor.fromDriverManager[IO](
                     driver = dbConfig.driver,
@@ -132,304 +136,286 @@
                             } yield foundTickets
                     }
                 } yield foundTickets
-                test.map { foundTickets =>
+                test.start.flatMap(_.joinWithNever).map { foundTickets =>
                     assertEquals(foundTickets.size, tickets.size, "Different number of tickets!")
-                    foundTickets.sortBy(_.number).zip(tickets.sortBy(_.number)).map { (found, expected) =>
-                        assertEquals(
-                            found.copy(createdAt = expected.createdAt, updatedAt = expected.updatedAt),
-                            expected
-                        )
-                    }
+                    val cleanedFound =
+                        foundTickets.sortBy(_.number).zip(tickets.sortBy(_.number)).map { (found, expected) =>
+                            found.copy(createdAt = expected.createdAt, updatedAt = expected.updatedAt)
+                        }
+                    assertEquals(cleanedFound.sortBy(_.number), tickets.sortBy(_.number))
                 }
-            case _ => fail("Could not generate data samples!")
         }
     }
 
     test("closeMilestone must set the closed flag correctly".tag(NeedsDatabase)) {
-        (genProjectOwner.sample, genProject.sample, genMilestone.sample) match {
-            case (Some(owner), Some(generatedProject), Some(generatedMilestone)) =>
-                val milestone = generatedMilestone.copy(closed = false)
-                val project   = generatedProject.copy(owner = owner)
-                val dbConfig  = configuration.database
-                val tx = Transactor.fromDriverManager[IO](
-                    driver = dbConfig.driver,
-                    url = dbConfig.url,
-                    user = dbConfig.user,
-                    password = dbConfig.pass,
-                    logHandler = None
-                )
-                val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
-                val test = for {
-                    _            <- createProjectOwner(owner)
-                    createdRepos <- createTicketsProject(project)
-                    repoId       <- loadProjectId(owner.uid, project.name)
-                    milestones <- repoId match {
-                        case None => IO.pure((None, None))
-                        case Some(projectId) =>
-                            for {
-                                _      <- milestoneRepo.createMilestone(projectId)(milestone)
-                                before <- milestoneRepo.findMilestone(projectId)(milestone.title)
-                                _      <- before.flatMap(_.id).traverse(milestoneRepo.closeMilestone)
-                                after  <- milestoneRepo.findMilestone(projectId)(milestone.title)
-                            } yield (before, after)
-                    }
-                } yield milestones
-                test.map { result =>
-                    val (before, after) = result
-                    val expected        = before.map(m => milestone.copy(id = m.id))
-                    assertEquals(before, expected, "Test milestone not properly initialised!")
-                    assertEquals(after, before.map(_.copy(closed = true)), "Test milestone not properly closed!")
+        PropF.forAllF { (owner: ProjectOwner, generatedProject: Project, generatedMilestone: Milestone) =>
+            val milestone = generatedMilestone.copy(closed = false)
+            val project   = generatedProject.copy(owner = owner)
+            val dbConfig  = configuration.database
+            val tx = Transactor.fromDriverManager[IO](
+                driver = dbConfig.driver,
+                url = dbConfig.url,
+                user = dbConfig.user,
+                password = dbConfig.pass,
+                logHandler = None
+            )
+            val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
+            val test = for {
+                _            <- createProjectOwner(owner)
+                createdRepos <- createTicketsProject(project)
+                repoId       <- loadProjectId(owner.uid, project.name)
+                milestones <- repoId match {
+                    case None => IO.pure((None, None))
+                    case Some(projectId) =>
+                        for {
+                            _      <- milestoneRepo.createMilestone(projectId)(milestone)
+                            before <- milestoneRepo.findMilestone(projectId)(milestone.title)
+                            _      <- before.flatMap(_.id).traverse(milestoneRepo.closeMilestone)
+                            after  <- milestoneRepo.findMilestone(projectId)(milestone.title)
+                        } yield (before, after)
                 }
-            case _ => fail("Could not generate data samples!")
+            } yield milestones
+            test.start.flatMap(_.joinWithNever).map { result =>
+                val (before, after) = result
+                val expected        = before.map(m => milestone.copy(id = m.id))
+                assertEquals(before, expected, "Test milestone not properly initialised!")
+                assertEquals(after, before.map(_.copy(closed = true)), "Test milestone not properly closed!")
+            }
         }
     }
 
     test("createMilestone must create the milestone".tag(NeedsDatabase)) {
-        (genProjectOwner.sample, genProject.sample, genMilestone.sample) match {
-            case (Some(owner), Some(generatedProject), Some(milestone)) =>
-                val project  = generatedProject.copy(owner = owner)
-                val dbConfig = configuration.database
-                val tx = Transactor.fromDriverManager[IO](
-                    driver = dbConfig.driver,
-                    url = dbConfig.url,
-                    user = dbConfig.user,
-                    password = dbConfig.pass,
-                    logHandler = None
-                )
-                val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
-                val test = for {
-                    _                 <- createProjectOwner(owner)
-                    createdRepos      <- createTicketsProject(project)
-                    repoId            <- loadProjectId(owner.uid, project.name)
-                    createdMilestones <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
-                    foundMilestone    <- repoId.traverse(id => milestoneRepo.findMilestone(id)(milestone.title))
-                } yield (createdRepos, repoId, createdMilestones, foundMilestone)
-                test.map { tuple =>
-                    val (createdRepos, repoId, createdMilestones, foundMilestone) = tuple
-                    assert(createdRepos === 1, "Test vcs generatedProject was not created!")
-                    assert(repoId.nonEmpty, "No vcs generatedProject id found!")
-                    assert(createdMilestones.exists(_ === 1), "Test milestone was not created!")
-                    foundMilestone.getOrElse(None) match {
-                        case None => fail("Created milestone not found!")
-                        case Some(foundMilestone) =>
-                            assertEquals(foundMilestone, milestone.copy(id = foundMilestone.id))
-                    }
+        PropF.forAllF { (owner: ProjectOwner, generatedProject: Project, milestone: Milestone) =>
+            val project  = generatedProject.copy(owner = owner)
+            val dbConfig = configuration.database
+            val tx = Transactor.fromDriverManager[IO](
+                driver = dbConfig.driver,
+                url = dbConfig.url,
+                user = dbConfig.user,
+                password = dbConfig.pass,
+                logHandler = None
+            )
+            val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
+            val test = for {
+                _                 <- createProjectOwner(owner)
+                createdRepos      <- createTicketsProject(project)
+                repoId            <- loadProjectId(owner.uid, project.name)
+                createdMilestones <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
+                foundMilestone    <- repoId.traverse(id => milestoneRepo.findMilestone(id)(milestone.title))
+            } yield (createdRepos, repoId, createdMilestones, foundMilestone)
+            test.start.flatMap(_.joinWithNever).map { tuple =>
+                val (createdRepos, repoId, createdMilestones, foundMilestone) = tuple
+                assert(createdRepos === 1, "Test vcs generatedProject was not created!")
+                assert(repoId.nonEmpty, "No vcs generatedProject id found!")
+                assert(createdMilestones.exists(_ === 1), "Test milestone was not created!")
+                foundMilestone.getOrElse(None) match {
+                    case None => fail("Created milestone not found!")
+                    case Some(foundMilestone) =>
+                        assertEquals(foundMilestone, milestone.copy(id = foundMilestone.id))
                 }
-            case _ => fail("Could not generate data samples!")
+            }
         }
     }
 
     test("createMilestone must fail if the milestone name already exists".tag(NeedsDatabase)) {
-        (genProjectOwner.sample, genProject.sample, genMilestone.sample) match {
-            case (Some(owner), Some(generatedProject), Some(milestone)) =>
-                val project  = generatedProject.copy(owner = owner)
-                val dbConfig = configuration.database
-                val tx = Transactor.fromDriverManager[IO](
-                    driver = dbConfig.driver,
-                    url = dbConfig.url,
-                    user = dbConfig.user,
-                    password = dbConfig.pass,
-                    logHandler = None
-                )
-                val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
-                val test = for {
-                    _                 <- createProjectOwner(owner)
-                    createdRepos      <- createTicketsProject(project)
-                    repoId            <- loadProjectId(owner.uid, project.name)
-                    createdMilestones <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
-                    _                 <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
-                } yield (createdRepos, repoId, createdMilestones)
-                test.attempt.map(r => assert(r.isLeft, "Creating a milestone with a duplicate name must fail!"))
-            case _ => fail("Could not generate data samples!")
+        PropF.forAllF { (owner: ProjectOwner, generatedProject: Project, milestone: Milestone) =>
+            val project  = generatedProject.copy(owner = owner)
+            val dbConfig = configuration.database
+            val tx = Transactor.fromDriverManager[IO](
+                driver = dbConfig.driver,
+                url = dbConfig.url,
+                user = dbConfig.user,
+                password = dbConfig.pass,
+                logHandler = None
+            )
+            val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
+            val test = for {
+                _                 <- createProjectOwner(owner)
+                createdRepos      <- createTicketsProject(project)
+                repoId            <- loadProjectId(owner.uid, project.name)
+                createdMilestones <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
+                _                 <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
+            } yield (createdRepos, repoId, createdMilestones)
+            test.attempt.map(r => assert(r.isLeft, "Creating a milestone with a duplicate name must fail!"))
         }
     }
 
     test("deleteMilestone must delete an existing milestone".tag(NeedsDatabase)) {
-        (genProjectOwner.sample, genProject.sample, genMilestone.sample) match {
-            case (Some(owner), Some(generatedProject), Some(milestone)) =>
-                val project  = generatedProject.copy(owner = owner)
-                val dbConfig = configuration.database
-                val tx = Transactor.fromDriverManager[IO](
-                    driver = dbConfig.driver,
-                    url = dbConfig.url,
-                    user = dbConfig.user,
-                    password = dbConfig.pass,
-                    logHandler = None
-                )
-                val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
-                val test = for {
-                    _                 <- createProjectOwner(owner)
-                    createdRepos      <- createTicketsProject(project)
-                    repoId            <- loadProjectId(owner.uid, project.name)
-                    createdMilestones <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
-                    milestoneId       <- findMilestoneId(owner.uid, project.name, milestone.title)
-                    deletedMilestones <- milestoneRepo.deleteMilestone(
-                        milestone.copy(id = milestoneId.flatMap(MilestoneId.from))
-                    )
-                    foundMilestone <- repoId.traverse(id => milestoneRepo.findMilestone(id)(milestone.title))
-                } yield (createdRepos, repoId, createdMilestones, deletedMilestones, foundMilestone)
-                test.map { tuple =>
-                    val (createdRepos, repoId, createdMilestones, deletedMilestones, foundMilestone) = tuple
-                    assert(createdRepos === 1, "Test vcs generatedProject was not created!")
-                    assert(repoId.nonEmpty, "No vcs generatedProject id found!")
-                    assert(createdMilestones.exists(_ === 1), "Test milestone was not created!")
-                    assert(deletedMilestones === 1, "Test milestone was not deleted!")
-                    assert(foundMilestone.getOrElse(None).isEmpty, "Test milestone was not deleted!")
-                }
-            case _ => fail("Could not generate data samples!")
+        PropF.forAllF { (owner: ProjectOwner, generatedProject: Project, milestone: Milestone) =>
+            val project  = generatedProject.copy(owner = owner)
+            val dbConfig = configuration.database
+            val tx = Transactor.fromDriverManager[IO](
+                driver = dbConfig.driver,
+                url = dbConfig.url,
+                user = dbConfig.user,
+                password = dbConfig.pass,
+                logHandler = None
+            )
+            val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
+            val test = for {
+                _                 <- createProjectOwner(owner)
+                createdRepos      <- createTicketsProject(project)
+                repoId            <- loadProjectId(owner.uid, project.name)
+                createdMilestones <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
+                milestoneId       <- findMilestoneId(owner.uid, project.name, milestone.title)
+                deletedMilestones <- milestoneRepo.deleteMilestone(
+                    milestone.copy(id = milestoneId.flatMap(MilestoneId.from))
+                )
+                foundMilestone <- repoId.traverse(id => milestoneRepo.findMilestone(id)(milestone.title))
+            } yield (createdRepos, repoId, createdMilestones, deletedMilestones, foundMilestone)
+            test.start.flatMap(_.joinWithNever).map { tuple =>
+                val (createdRepos, repoId, createdMilestones, deletedMilestones, foundMilestone) = tuple
+                assert(createdRepos === 1, "Test vcs generatedProject was not created!")
+                assert(repoId.nonEmpty, "No vcs generatedProject id found!")
+                assert(createdMilestones.exists(_ === 1), "Test milestone was not created!")
+                assert(deletedMilestones === 1, "Test milestone was not deleted!")
+                assert(foundMilestone.getOrElse(None).isEmpty, "Test milestone was not deleted!")
+            }
         }
     }
 
     test("findMilestone must find existing milestones".tag(NeedsDatabase)) {
-        (genProjectOwner.sample, genProject.sample, genMilestones.sample) match {
-            case (Some(owner), Some(generatedProject), Some(milestones)) =>
-                val project           = generatedProject.copy(owner = owner)
-                val dbConfig          = configuration.database
-                val expectedMilestone = milestones(scala.util.Random.nextInt(milestones.size))
-                val tx = Transactor.fromDriverManager[IO](
-                    driver = dbConfig.driver,
-                    url = dbConfig.url,
-                    user = dbConfig.user,
-                    password = dbConfig.pass,
-                    logHandler = None
-                )
-                val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
-                val test = for {
-                    _            <- createProjectOwner(owner)
-                    createdRepos <- createTicketsProject(project)
-                    repoId       <- loadProjectId(owner.uid, project.name)
-                    createdMilestones <- repoId match {
-                        case None => IO.pure(List.empty)
-                        case Some(repoId) =>
-                            milestones.traverse(milestone => milestoneRepo.createMilestone(repoId)(milestone))
-                    }
-                    foundMilestone <- repoId.traverse(id => milestoneRepo.findMilestone(id)(expectedMilestone.title))
-                } yield foundMilestone.flatten
-                test.map { foundMilestone =>
-                    assertEquals(foundMilestone.map(_.copy(id = expectedMilestone.id)), Option(expectedMilestone))
+        PropF.forAllF { (owner: ProjectOwner, generatedProject: Project, milestones: NonEmptyList[Milestone]) =>
+            val project           = generatedProject.copy(owner = owner)
+            val dbConfig          = configuration.database
+            val expectedMilestone = milestones.toList(scala.util.Random.nextInt(milestones.size))
+            val tx = Transactor.fromDriverManager[IO](
+                driver = dbConfig.driver,
+                url = dbConfig.url,
+                user = dbConfig.user,
+                password = dbConfig.pass,
+                logHandler = None
+            )
+            val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
+            val test = for {
+                _            <- createProjectOwner(owner)
+                createdRepos <- createTicketsProject(project)
+                repoId       <- loadProjectId(owner.uid, project.name)
+                createdMilestones <- repoId match {
+                    case None => IO.pure(List.empty)
+                    case Some(repoId) =>
+                        milestones.toList.traverse(milestone => milestoneRepo.createMilestone(repoId)(milestone))
                 }
-            case _ => fail("Could not generate data samples!")
+                foundMilestone <- repoId.traverse(id => milestoneRepo.findMilestone(id)(expectedMilestone.title))
+            } yield foundMilestone.flatten
+            test.start.flatMap(_.joinWithNever).map { foundMilestone =>
+                assertEquals(foundMilestone.map(_.copy(id = expectedMilestone.id)), Option(expectedMilestone))
+            }
         }
     }
 
     test("openMilestone must reset the closed flag correctly".tag(NeedsDatabase)) {
-        (genProjectOwner.sample, genProject.sample, genMilestone.sample) match {
-            case (Some(owner), Some(generatedProject), Some(generatedMilestone)) =>
-                val milestone = generatedMilestone.copy(closed = true)
-                val project   = generatedProject.copy(owner = owner)
-                val dbConfig  = configuration.database
-                val tx = Transactor.fromDriverManager[IO](
-                    driver = dbConfig.driver,
-                    url = dbConfig.url,
-                    user = dbConfig.user,
-                    password = dbConfig.pass,
-                    logHandler = None
-                )
-                val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
-                val test = for {
-                    _            <- createProjectOwner(owner)
-                    createdRepos <- createTicketsProject(project)
-                    repoId       <- loadProjectId(owner.uid, project.name)
-                    milestones <- repoId match {
-                        case None => IO.pure((None, None))
-                        case Some(projectId) =>
-                            for {
-                                _      <- milestoneRepo.createMilestone(projectId)(milestone)
-                                before <- milestoneRepo.findMilestone(projectId)(milestone.title)
-                                _      <- before.flatMap(_.id).traverse(milestoneRepo.openMilestone)
-                                after  <- milestoneRepo.findMilestone(projectId)(milestone.title)
-                            } yield (before, after)
-                    }
-                } yield milestones
-                test.map { result =>
-                    val (before, after) = result
-                    val expected        = before.map(m => milestone.copy(id = m.id))
-                    assertEquals(before, expected, "Test milestone not properly initialised!")
-                    assertEquals(after, before.map(_.copy(closed = false)), "Test milestone not properly opened!")
+        PropF.forAllF { (owner: ProjectOwner, generatedProject: Project, generatedMilestone: Milestone) =>
+            val milestone = generatedMilestone.copy(closed = true)
+            val project   = generatedProject.copy(owner = owner)
+            val dbConfig  = configuration.database
+            val tx = Transactor.fromDriverManager[IO](
+                driver = dbConfig.driver,
+                url = dbConfig.url,
+                user = dbConfig.user,
+                password = dbConfig.pass,
+                logHandler = None
+            )
+            val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
+            val test = for {
+                _            <- createProjectOwner(owner)
+                createdRepos <- createTicketsProject(project)
+                repoId       <- loadProjectId(owner.uid, project.name)
+                milestones <- repoId match {
+                    case None => IO.pure((None, None))
+                    case Some(projectId) =>
+                        for {
+                            _      <- milestoneRepo.createMilestone(projectId)(milestone)
+                            before <- milestoneRepo.findMilestone(projectId)(milestone.title)
+                            _      <- before.flatMap(_.id).traverse(milestoneRepo.openMilestone)
+                            after  <- milestoneRepo.findMilestone(projectId)(milestone.title)
+                        } yield (before, after)
                 }
-            case _ => fail("Could not generate data samples!")
+            } yield milestones
+            test.start.flatMap(_.joinWithNever).map { result =>
+                val (before, after) = result
+                val expected        = before.map(m => milestone.copy(id = m.id))
+                assertEquals(before, expected, "Test milestone not properly initialised!")
+                assertEquals(after, before.map(_.copy(closed = false)), "Test milestone not properly opened!")
+            }
         }
     }
 
     test("updateMilestone must update an existing milestone".tag(NeedsDatabase)) {
-        (genProjectOwner.sample, genProject.sample, genMilestone.sample) match {
-            case (Some(owner), Some(generatedProject), Some(milestone)) =>
-                val updatedMilestone = milestone.copy(
-                    title = MilestoneTitle("updated milestone"),
-                    description = Option(MilestoneDescription("I am an updated milestone description...")),
-                    dueDate = Option(LocalDate.of(1879, 3, 14))
-                )
-                val project  = generatedProject.copy(owner = owner)
-                val dbConfig = configuration.database
-                val tx = Transactor.fromDriverManager[IO](
-                    driver = dbConfig.driver,
-                    url = dbConfig.url,
-                    user = dbConfig.user,
-                    password = dbConfig.pass,
-                    logHandler = None
-                )
-                val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
-                val test = for {
-                    _                 <- createProjectOwner(owner)
-                    createdRepos      <- createTicketsProject(project)
-                    repoId            <- loadProjectId(owner.uid, project.name)
-                    createdMilestones <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
-                    milestoneId       <- findMilestoneId(owner.uid, project.name, milestone.title)
-                    updatedMilestones <- milestoneRepo.updateMilestone(
-                        updatedMilestone.copy(id = milestoneId.map(MilestoneId.apply))
-                    )
-                    foundMilestone <- repoId.traverse(id => milestoneRepo.findMilestone(id)(updatedMilestone.title))
-                } yield (createdRepos, repoId, createdMilestones, updatedMilestones, foundMilestone.flatten)
-                test.map { tuple =>
-                    val (createdRepos, repoId, createdMilestones, updatedMilestones, foundMilestone) = tuple
-                    assert(createdRepos === 1, "Test vcs generatedProject was not created!")
-                    assert(repoId.nonEmpty, "No vcs generatedProject id found!")
-                    assert(createdMilestones.exists(_ === 1), "Test milestone was not created!")
-                    assert(updatedMilestones === 1, "Test milestone was not updated!")
-                    assert(foundMilestone.nonEmpty, "Updated milestone not found!")
-                    foundMilestone.map { milestone =>
-                        assertEquals(milestone, updatedMilestone.copy(id = milestone.id))
-                    }
+        PropF.forAllF { (owner: ProjectOwner, generatedProject: Project, milestone: Milestone) =>
+            val updatedMilestone = milestone.copy(
+                title = MilestoneTitle("updated milestone"),
+                description = Option(MilestoneDescription("I am an updated milestone description...")),
+                dueDate = Option(LocalDate.of(1879, 3, 14))
+            )
+            val project  = generatedProject.copy(owner = owner)
+            val dbConfig = configuration.database
+            val tx = Transactor.fromDriverManager[IO](
+                driver = dbConfig.driver,
+                url = dbConfig.url,
+                user = dbConfig.user,
+                password = dbConfig.pass,
+                logHandler = None
+            )
+            val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
+            val test = for {
+                _                 <- createProjectOwner(owner)
+                createdRepos      <- createTicketsProject(project)
+                repoId            <- loadProjectId(owner.uid, project.name)
+                createdMilestones <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
+                milestoneId       <- findMilestoneId(owner.uid, project.name, milestone.title)
+                updatedMilestones <- milestoneRepo.updateMilestone(
+                    updatedMilestone.copy(id = milestoneId.map(MilestoneId.apply))
+                )
+                foundMilestone <- repoId.traverse(id => milestoneRepo.findMilestone(id)(updatedMilestone.title))
+            } yield (createdRepos, repoId, createdMilestones, updatedMilestones, foundMilestone.flatten)
+            test.start.flatMap(_.joinWithNever).map { tuple =>
+                val (createdRepos, repoId, createdMilestones, updatedMilestones, foundMilestone) = tuple
+                assert(createdRepos === 1, "Test vcs generatedProject was not created!")
+                assert(repoId.nonEmpty, "No vcs generatedProject id found!")
+                assert(createdMilestones.exists(_ === 1), "Test milestone was not created!")
+                assert(updatedMilestones === 1, "Test milestone was not updated!")
+                assert(foundMilestone.nonEmpty, "Updated milestone not found!")
+                foundMilestone.foreach { milestone =>
+                    assertEquals(milestone, updatedMilestone.copy(id = milestone.id))
                 }
-            case _ => fail("Could not generate data samples!")
+            }
         }
     }
 
     test("updateMilestone must do nothing if id attribute is empty".tag(NeedsDatabase)) {
-        (genProjectOwner.sample, genProject.sample, genMilestone.sample) match {
-            case (Some(owner), Some(generatedProject), Some(milestone)) =>
-                val updatedMilestone = milestone.copy(
-                    id = None,
-                    title = MilestoneTitle("updated milestone"),
-                    description = Option(MilestoneDescription("I am an updated milestone description...")),
-                    dueDate = Option(LocalDate.of(1879, 3, 14))
-                )
-                val project  = generatedProject.copy(owner = owner)
-                val dbConfig = configuration.database
-                val tx = Transactor.fromDriverManager[IO](
-                    driver = dbConfig.driver,
-                    url = dbConfig.url,
-                    user = dbConfig.user,
-                    password = dbConfig.pass,
-                    logHandler = None
-                )
-                val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
-                val test = for {
-                    _                 <- createProjectOwner(owner)
-                    createdRepos      <- createTicketsProject(project)
-                    repoId            <- loadProjectId(owner.uid, project.name)
-                    createdMilestones <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
-                    milestoneId       <- findMilestoneId(owner.uid, project.name, milestone.title)
-                    updatedMilestones <- milestoneRepo.updateMilestone(updatedMilestone)
-                } yield (createdRepos, repoId, createdMilestones, updatedMilestones)
-                test.map { tuple =>
-                    val (createdRepos, repoId, createdMilestones, updatedMilestones) = tuple
-                    assert(createdRepos === 1, "Test vcs generatedProject was not created!")
-                    assert(repoId.nonEmpty, "No vcs generatedProject id found!")
-                    assert(createdMilestones.exists(_ === 1), "Test milestone was not created!")
-                    assert(updatedMilestones === 0, "Milestone with empty id must not be updated!")
-                }
-            case _ => fail("Could not generate data samples!")
+        PropF.forAllF { (owner: ProjectOwner, generatedProject: Project, milestone: Milestone) =>
+            val updatedMilestone = milestone.copy(
+                id = None,
+                title = MilestoneTitle("updated milestone"),
+                description = Option(MilestoneDescription("I am an updated milestone description...")),
+                dueDate = Option(LocalDate.of(1879, 3, 14))
+            )
+            val project  = generatedProject.copy(owner = owner)
+            val dbConfig = configuration.database
+            val tx = Transactor.fromDriverManager[IO](
+                driver = dbConfig.driver,
+                url = dbConfig.url,
+                user = dbConfig.user,
+                password = dbConfig.pass,
+                logHandler = None
+            )
+            val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
+            val test = for {
+                _                 <- createProjectOwner(owner)
+                createdRepos      <- createTicketsProject(project)
+                repoId            <- loadProjectId(owner.uid, project.name)
+                createdMilestones <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
+                milestoneId       <- findMilestoneId(owner.uid, project.name, milestone.title)
+                updatedMilestones <- milestoneRepo.updateMilestone(updatedMilestone)
+            } yield (createdRepos, repoId, createdMilestones, updatedMilestones)
+            test.start.flatMap(_.joinWithNever).map { tuple =>
+                val (createdRepos, repoId, createdMilestones, updatedMilestones) = tuple
+                assert(createdRepos === 1, "Test vcs generatedProject was not created!")
+                assert(repoId.nonEmpty, "No vcs generatedProject id found!")
+                assert(createdMilestones.exists(_ === 1), "Test milestone was not created!")
+                assert(updatedMilestones === 0, "Milestone with empty id must not be updated!")
+            }
         }
     }
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/tickets/Generators.scala new-smederee/modules/hub/src/test/scala/de/smederee/tickets/Generators.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/tickets/Generators.scala	2025-01-11 00:01:25.713397455 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/tickets/Generators.scala	2025-01-11 00:01:25.717397462 +0000
@@ -141,6 +141,8 @@
         updatedAt
     )
 
+    given Arbitrary[Ticket] = Arbitrary(genTicket)
+
     val genTickets: Gen[List[Ticket]] =
         Gen.nonEmptyListOf(genTicket)
             .map(_.zipWithIndex.map(tuple => tuple._1.copy(number = TicketNumber(tuple._2 + 1))))
@@ -229,6 +231,8 @@
             closed <- Gen.oneOf(List(false, true))
         } yield Milestone(id, title, descr, due, closed)
 
+    given Arbitrary[Milestone] = Arbitrary(genMilestone)
+
     val genMilestones: Gen[List[Milestone]] = Gen.nonEmptyListOf(genMilestone).map(_.distinct)
 
     val genProjectName: Gen[ProjectName] = Gen