~jan0sch/smederee

Showing details for patch ccc9caffdf75cf57716fe6595cce4b0c0fe3cd42.
2024-07-21 (Sun), 7:42 PM - Jens Grassel - ccc9caffdf75cf57716fe6595cce4b0c0fe3cd42

chore: migrate test to ScalaCheckEffect

- rewrite generators to automatically provide a `NonEmptyList[A]` if needed
- adjust `DoobieOrganisationRepositoryTest`
Summary of changes
2 files modified with 431 lines added and 420 lines removed
  • modules/hub/src/test/scala/de/smederee/hub/DoobieOrganisationRepositoryTest.scala with 403 added and 420 removed lines
  • modules/hub/src/test/scala/de/smederee/hub/Generators.scala with 28 added and 0 removed lines
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieOrganisationRepositoryTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieOrganisationRepositoryTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieOrganisationRepositoryTest.scala	2025-01-11 02:59:38.403425737 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieOrganisationRepositoryTest.scala	2025-01-11 02:59:38.403425737 +0000
@@ -6,500 +6,483 @@
 
 package de.smederee.hub
 
+import cats.data.NonEmptyList
 import cats.effect.*
 import cats.syntax.all.*
 import de.smederee.TestTags.*
-import de.smederee.hub.Generators.*
+import de.smederee.hub.Generators.given
 import de.smederee.security.*
 import doobie.*
 
+import org.scalacheck.effect.PropF
+
 final class DoobieOrganisationRepositoryTest extends BaseSpec {
+    override def scalaCheckTestParameters = super.scalaCheckTestParameters.withMinSuccessfulTests(1)
+
     test("addAdministrator must add an administrator to the organisation".tag(NeedsDatabase)) {
-        (genValidAccounts.sample, genOrganisation.sample) match {
-            case (Some(owner :: accounts), Some(org)) =>
-                val organisation = org.copy(owner = owner.uid)
-                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 DoobieOrganisationRepository[IO](tx)
-                val test = for {
-                    _ <- createAccount(owner, PasswordHash("I am not a password hash!"), None, None)
-                    _ <- accounts.traverse(a => createAccount(a, PasswordHash("I am not a password hash!"), None, None))
-                    written <- repo.create(organisation)
-                    added   <- accounts.headOption.traverse(admin => repo.addAdministrator(organisation.oid)(admin.uid))
-                } yield (written, added.getOrElse(0))
-                test.map { result =>
-                    val (written, added) = result
-                    assert(written === 1, "Organisation not written to database!")
-                    accounts.headOption.foreach(_ => assert(added === 1, "Administrator not written to database!"))
-                }
-            case _ => fail("Could not generate data samples!")
+        PropF.forAllF { (genAccounts: NonEmptyList[Account], org: Organisation) =>
+            val owner        = genAccounts.head
+            val accounts     = genAccounts.toList.drop(1)
+            val organisation = org.copy(owner = owner.uid)
+            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 DoobieOrganisationRepository[IO](tx)
+            val test = for {
+                _ <- createAccount(owner, PasswordHash("I am not a password hash!"), None, None)
+                _ <- accounts.traverse(a => createAccount(a, PasswordHash("I am not a password hash!"), None, None))
+                written <- repo.create(organisation)
+                added   <- accounts.headOption.traverse(admin => repo.addAdministrator(organisation.oid)(admin.uid))
+            } yield (written, added.getOrElse(0))
+            test.start.flatMap(_.joinWithNever).map { result =>
+                val (written, added) = result
+                assert(written === 1, "Organisation not written to database!")
+                accounts.headOption.foreach(_ => assert(added === 1, "Administrator not written to database!"))
+            }
         }
     }
 
     test("addMember must add a member to the organisation".tag(NeedsDatabase)) {
-        (genValidAccounts.sample, genOrganisation.sample) match {
-            case (Some(owner :: accounts), Some(org)) =>
-                val organisation = org.copy(owner = owner.uid)
-                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 DoobieOrganisationRepository[IO](tx)
-                val test = for {
-                    _ <- createAccount(owner, PasswordHash("I am not a password hash!"), None, None)
-                    _ <- accounts.traverse(a => createAccount(a, PasswordHash("I am not a password hash!"), None, None))
-                    written <- repo.create(organisation)
-                    added   <- accounts.headOption.traverse(member => repo.addMember(organisation.oid)(member.uid))
-                } yield (written, added.getOrElse(0))
-                test.map { result =>
-                    val (written, added) = result
-                    assert(written === 1, "Organisation not written to database!")
-                    accounts.headOption.foreach(_ => assert(added === 1, "Member not written to database!"))
-                }
-            case _ => fail("Could not generate data samples!")
+        PropF.forAllF { (genAccounts: NonEmptyList[Account], org: Organisation) =>
+            val owner        = genAccounts.head
+            val accounts     = genAccounts.toList.drop(1)
+            val organisation = org.copy(owner = owner.uid)
+            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 DoobieOrganisationRepository[IO](tx)
+            val test = for {
+                _ <- createAccount(owner, PasswordHash("I am not a password hash!"), None, None)
+                _ <- accounts.traverse(a => createAccount(a, PasswordHash("I am not a password hash!"), None, None))
+                written <- repo.create(organisation)
+                added   <- accounts.headOption.traverse(member => repo.addMember(organisation.oid)(member.uid))
+            } yield (written, added.getOrElse(0))
+            test.start.flatMap(_.joinWithNever).map { result =>
+                val (written, added) = result
+                assert(written === 1, "Organisation not written to database!")
+                accounts.headOption.foreach(_ => assert(added === 1, "Member not written to database!"))
+            }
         }
     }
 
     test("create must create an organisation".tag(NeedsDatabase)) {
-        (genValidAccount.sample, genOrganisation.sample) match {
-            case (Some(account), Some(org)) =>
-                val organisation = org.copy(owner = account.uid)
-                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 DoobieOrganisationRepository[IO](tx)
-                val test = for {
-                    _       <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-                    written <- repo.create(organisation)
-                } yield written
-                test.map { written =>
-                    assert(written === 1, "Creating an organisation must modify one database row!")
-                }
-            case _ => fail("Could not generate data samples!")
+        PropF.forAllF { (account: Account, org: Organisation) =>
+            val organisation = org.copy(owner = account.uid)
+            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 DoobieOrganisationRepository[IO](tx)
+            val test = for {
+                _       <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                written <- repo.create(organisation)
+            } yield written
+            test.start.flatMap(_.joinWithNever).map { written =>
+                assert(written === 1, "Creating an organisation must modify one database row!")
+            }
         }
     }
 
     test("allByMember must return all organisations which the user is a member of".tag(NeedsDatabase)) {
-        (genValidAccount.sample, genValidAccount.sample, genOrganisations.sample) match {
-            case (Some(owner), Some(member), Some(orgs)) =>
-                val organisations = orgs.map(_.copy(owner = owner.uid))
-                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 DoobieOrganisationRepository[IO](tx)
-                val test = for {
-                    _                  <- createAccount(owner, PasswordHash("I am not a password hash!"), None, None)
-                    _                  <- createAccount(member, PasswordHash("I am not a password hash!"), None, None)
-                    writtenOrgs        <- organisations.traverse(repo.create)
-                    writtenMemberships <- organisations.traverse(org => repo.addMember(org.oid)(member.uid))
-                    found              <- repo.allByMember(member.uid).compile.toList
-                } yield (writtenOrgs, writtenMemberships, found)
-                test.map { result =>
-                    val (writtenOrgs, writtenMemberships, found) = result
-                    assert(writtenOrgs.sum === organisations.size, "Not all organisations written to database!")
-                    assert(writtenMemberships.sum === organisations.size, "Not all memberships written to database!")
-                    assertEquals(found.size, organisations.size, "Not all organisations found!")
-                    assertEquals(found.sortBy(_.oid), organisations.sortBy(_.oid))
-                }
-            case _ => fail("Could not generate data samples!")
+        PropF.forAllF { (owner: Account, member: Account, orgs: NonEmptyList[Organisation]) =>
+            val organisations = orgs.toList.map(_.copy(owner = owner.uid))
+            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 DoobieOrganisationRepository[IO](tx)
+            val test = for {
+                _                  <- createAccount(owner, PasswordHash("I am not a password hash!"), None, None)
+                _                  <- createAccount(member, PasswordHash("I am not a password hash!"), None, None)
+                writtenOrgs        <- organisations.traverse(repo.create)
+                writtenMemberships <- organisations.traverse(org => repo.addMember(org.oid)(member.uid))
+                found              <- repo.allByMember(member.uid).compile.toList
+            } yield (writtenOrgs, writtenMemberships, found)
+            test.start.flatMap(_.joinWithNever).map { result =>
+                val (writtenOrgs, writtenMemberships, found) = result
+                assert(writtenOrgs.sum === organisations.size, "Not all organisations written to database!")
+                assert(writtenMemberships.sum === organisations.size, "Not all memberships written to database!")
+                assertEquals(found.size, organisations.size, "Not all organisations found!")
+                assertEquals(found.sortBy(_.oid), organisations.sortBy(_.oid))
+            }
         }
     }
 
     test("allByOwner must return all organisations owned by the user".tag(NeedsDatabase)) {
-        (genValidAccount.sample, genOrganisations.sample) match {
-            case (Some(account), Some(orgs)) =>
-                val organisations = orgs.map(_.copy(owner = account.uid))
-                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 DoobieOrganisationRepository[IO](tx)
-                val test = for {
-                    _       <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-                    written <- organisations.traverse(repo.create)
-                    found   <- repo.allByOwner(account.uid).compile.toList
-                } yield (written, found)
-                test.map { result =>
-                    val (written, found) = result
-                    assert(written.sum === organisations.size, "Not all organisations written to database!")
-                    assertEquals(found.size, organisations.size, "Not all organisations found!")
-                    assertEquals(found.sortBy(_.oid), organisations.sortBy(_.oid))
-                }
-            case _ => fail("Could not generate data samples!")
+        PropF.forAllF { (account: Account, orgs: NonEmptyList[Organisation]) =>
+            val organisations = orgs.toList.map(_.copy(owner = account.uid))
+            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 DoobieOrganisationRepository[IO](tx)
+            val test = for {
+                _       <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                written <- organisations.traverse(repo.create)
+                found   <- repo.allByOwner(account.uid).compile.toList
+            } yield (written, found)
+            test.start.flatMap(_.joinWithNever).map { result =>
+                val (written, found) = result
+                assert(written.sum === organisations.size, "Not all organisations written to database!")
+                assertEquals(found.size, organisations.size, "Not all organisations found!")
+                assertEquals(found.sortBy(_.oid), organisations.sortBy(_.oid))
+            }
         }
     }
 
     test("delete must delete the organisation".tag(NeedsDatabase)) {
-        (genValidAccount.sample, genOrganisation.sample) match {
-            case (Some(account), Some(org)) =>
-                val organisation = org.copy(owner = account.uid)
-                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 DoobieOrganisationRepository[IO](tx)
-                val test = for {
-                    _       <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-                    written <- repo.create(organisation)
-                    deleted <- repo.delete(organisation.oid)
-                    found   <- repo.findByName(organisation.name)
-                } yield (written, deleted, found)
-                test.map { result =>
-                    val (written, deleted, found) = result
-                    assert(written === 1, "Creating an organisation must modify one database row!")
-                    assert(deleted === 1, "Deleting an organisation must modify one database row!")
-                    assertEquals(found, None, "Organisation was not deleted from database!")
-                }
-            case _ => fail("Could not generate data samples!")
+        PropF.forAllF { (account: Account, org: Organisation) =>
+            val organisation = org.copy(owner = account.uid)
+            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 DoobieOrganisationRepository[IO](tx)
+            val test = for {
+                _       <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                written <- repo.create(organisation)
+                deleted <- repo.delete(organisation.oid)
+                found   <- repo.findByName(organisation.name)
+            } yield (written, deleted, found)
+            test.start.flatMap(_.joinWithNever).map { result =>
+                val (written, deleted, found) = result
+                assert(written === 1, "Creating an organisation must modify one database row!")
+                assert(deleted === 1, "Deleting an organisation must modify one database row!")
+                assertEquals(found, None, "Organisation was not deleted from database!")
+            }
         }
     }
 
     test("find must return the organisation with the given id".tag(NeedsDatabase)) {
-        (genValidAccount.sample, genOrganisation.sample) match {
-            case (Some(account), Some(org)) =>
-                val organisation = org.copy(owner = account.uid)
-                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 DoobieOrganisationRepository[IO](tx)
-                val test = for {
-                    _       <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-                    written <- repo.create(organisation)
-                    found   <- repo.find(organisation.oid)
-                } yield (written, found)
-                test.map { result =>
-                    val (written, found) = result
-                    assert(written === 1, "Creating an organisation must modify one database row!")
-                    assertEquals(found, organisation.some)
-                }
-            case _ => fail("Could not generate data samples!")
+        PropF.forAllF { (account: Account, org: Organisation) =>
+            val organisation = org.copy(owner = account.uid)
+            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 DoobieOrganisationRepository[IO](tx)
+            val test = for {
+                _       <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                written <- repo.create(organisation)
+                found   <- repo.find(organisation.oid)
+            } yield (written, found)
+            test.start.flatMap(_.joinWithNever).map { result =>
+                val (written, found) = result
+                assert(written === 1, "Creating an organisation must modify one database row!")
+                assertEquals(found, organisation.some)
+            }
         }
     }
 
     test("findByName must return a matching organisation".tag(NeedsDatabase)) {
-        (genValidAccount.sample, genOrganisation.sample) match {
-            case (Some(account), Some(org)) =>
-                val organisation = org.copy(owner = account.uid)
-                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 DoobieOrganisationRepository[IO](tx)
-                val test = for {
-                    _       <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-                    written <- repo.create(organisation)
-                    found   <- repo.findByName(organisation.name)
-                } yield (written, found)
-                test.map { result =>
-                    val (written, found) = result
-                    assert(written === 1, "Creating an organisation must modify one database row!")
-                    assertEquals(found, organisation.some)
-                }
-            case _ => fail("Could not generate data samples!")
+        PropF.forAllF { (account: Account, org: Organisation) =>
+            val organisation = org.copy(owner = account.uid)
+            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 DoobieOrganisationRepository[IO](tx)
+            val test = for {
+                _       <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                written <- repo.create(organisation)
+                found   <- repo.findByName(organisation.name)
+            } yield (written, found)
+            test.start.flatMap(_.joinWithNever).map { result =>
+                val (written, found) = result
+                assert(written === 1, "Creating an organisation must modify one database row!")
+                assertEquals(found, organisation.some)
+            }
         }
     }
 
     test("findAccountByName must return an unlocked and validated account".tag(NeedsDatabase)) {
-        genValidAccount.sample match {
-            case Some(genAccount) =>
-                val account  = genAccount.copy(language = None, validatedEmail = true)
-                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 DoobieOrganisationRepository[IO](tx)
-                val test = for {
-                    _     <- createAccount(account, PasswordHash("I am not a password hash!"))
-                    found <- repo.findAccountByName(account.name)
-                } yield found
-                test.map { found =>
-                    assertEquals(found, account.some)
-                }
-            case _ => fail("Could not generate data samples!")
+        PropF.forAllF { (genAccount: Account) =>
+            val account  = genAccount.copy(language = None, validatedEmail = true)
+            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 DoobieOrganisationRepository[IO](tx)
+            val test = for {
+                _     <- createAccount(account, PasswordHash("I am not a password hash!"))
+                found <- repo.findAccountByName(account.name)
+            } yield found
+            test.start.flatMap(_.joinWithNever).map { found =>
+                assertEquals(found, account.some)
+            }
         }
     }
 
     test("findAccountByName must not return a locked account".tag(NeedsDatabase)) {
-        genValidAccount.sample match {
-            case Some(genAccount) =>
-                val account  = genAccount.copy(validatedEmail = true)
-                val token    = UnlockToken.generate
-                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 DoobieOrganisationRepository[IO](tx)
-                val test = for {
-                    _     <- createAccount(account, PasswordHash("I am not a password hash!"), unlockToken = token.some)
-                    found <- repo.findAccountByName(account.name)
-                } yield found
-                test.map { found =>
-                    assertEquals(found, None)
-                }
-            case _ => fail("Could not generate data samples!")
+        PropF.forAllF { (genAccount: Account) =>
+            val account  = genAccount.copy(validatedEmail = true)
+            val token    = UnlockToken.generate
+            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 DoobieOrganisationRepository[IO](tx)
+            val test = for {
+                _     <- createAccount(account, PasswordHash("I am not a password hash!"), unlockToken = token.some)
+                found <- repo.findAccountByName(account.name)
+            } yield found
+            test.start.flatMap(_.joinWithNever).map { found =>
+                assertEquals(found, None)
+            }
         }
     }
 
     test("findAccountByName must not return an unvalidated account".tag(NeedsDatabase)) {
-        genValidAccount.sample match {
-            case Some(genAccount) =>
-                val account  = genAccount.copy(validatedEmail = false)
-                val token    = ValidationToken.generate
-                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 DoobieOrganisationRepository[IO](tx)
-                val test = for {
-                    _ <- createAccount(account, PasswordHash("I am not a password hash!"), validationToken = token.some)
-                    found <- repo.findAccountByName(account.name)
-                } yield found
-                test.map { found =>
-                    assertEquals(found, None)
-                }
-            case _ => fail("Could not generate data samples!")
+        PropF.forAllF { (genAccount: Account) =>
+            val account  = genAccount.copy(validatedEmail = false)
+            val token    = ValidationToken.generate
+            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 DoobieOrganisationRepository[IO](tx)
+            val test = for {
+                _     <- createAccount(account, PasswordHash("I am not a password hash!"), validationToken = token.some)
+                found <- repo.findAccountByName(account.name)
+            } yield found
+            test.start.flatMap(_.joinWithNever).map { found =>
+                assertEquals(found, None)
+            }
         }
     }
 
     test("findOwner must return the user account of the owner".tag(NeedsDatabase)) {
-        (genValidAccount.sample, genOrganisation.sample) match {
-            case (Some(account), Some(org)) =>
-                val organisation = org.copy(owner = account.uid)
-                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 DoobieOrganisationRepository[IO](tx)
-                val test = for {
-                    _       <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-                    written <- repo.create(organisation)
-                    found   <- repo.findOwner(organisation.oid)
-                } yield (written, found)
-                test.map { result =>
-                    val (written, found) = result
-                    assert(written === 1, "Creating an organisation must modify one database row!")
-                    assertEquals(found, account.copy(language = None).some)
-                }
-            case _ => fail("Could not generate data samples!")
+        PropF.forAllF { (account: Account, org: Organisation) =>
+            val organisation = org.copy(owner = account.uid)
+            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 DoobieOrganisationRepository[IO](tx)
+            val test = for {
+                _       <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                written <- repo.create(organisation)
+                found   <- repo.findOwner(organisation.oid)
+            } yield (written, found)
+            test.start.flatMap(_.joinWithNever).map { result =>
+                val (written, found) = result
+                assert(written === 1, "Creating an organisation must modify one database row!")
+                assertEquals(found, account.copy(language = None).some)
+            }
         }
     }
 
     test("getAdministrators must return all administrators".tag(NeedsDatabase)) {
-        (genValidAccounts.sample, genOrganisation.sample) match {
-            case (Some(owner :: accounts), Some(org)) =>
-                val admins = accounts.zipWithLongIndex.foldLeft(List.empty[Account])((acc, tuple) =>
-                    if (tuple._2 % 2 === 0) {
-                        tuple._1 :: acc
-                    } else {
-                        acc
-                    }
-                )
-                val organisation = org.copy(owner = owner.uid)
-                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 DoobieOrganisationRepository[IO](tx)
-                val addAdmin = repo.addAdministrator(organisation.oid)
-                val test = for {
-                    _ <- createAccount(owner, PasswordHash("I am not a password hash!"), None, None)
-                    _ <- accounts.traverse(a => createAccount(a, PasswordHash("I am not a password hash!"), None, None))
-                    written     <- repo.create(organisation)
-                    added       <- admins.map(_.uid).traverse(addAdmin)
-                    foundAdmins <- repo.getAdministrators(organisation.oid).compile.toList
-                } yield (written, added.sum, foundAdmins)
-                test.map { result =>
-                    val (written, added, foundAdmins) = result
-                    assert(written === 1, "Organisation not written to database!")
-                    assert(added === admins.size, "Wrong number of administrators created!")
-                    assertEquals(foundAdmins.map(_.uid).sorted, admins.map(_.uid).sorted)
-                }
-            case _ => fail("Could not generate data samples!")
+        PropF.forAllF { (as: NonEmptyList[Account], org: Organisation) =>
+            val owner    = as.head
+            val accounts = as.tail
+            val admins = accounts.zipWithLongIndex.foldLeft(List.empty[Account])((acc, tuple) =>
+                if (tuple._2 % 2 === 0) {
+                    tuple._1 :: acc
+                } else {
+                    acc
+                }
+            )
+            val organisation = org.copy(owner = owner.uid)
+            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 DoobieOrganisationRepository[IO](tx)
+            val addAdmin = repo.addAdministrator(organisation.oid)
+            val test = for {
+                _ <- createAccount(owner, PasswordHash("I am not a password hash!"), None, None)
+                _ <- accounts.traverse(a => createAccount(a, PasswordHash("I am not a password hash!"), None, None))
+                written     <- repo.create(organisation)
+                added       <- admins.map(_.uid).traverse(addAdmin)
+                foundAdmins <- repo.getAdministrators(organisation.oid).compile.toList
+            } yield (written, added.sum, foundAdmins)
+            test.start.flatMap(_.joinWithNever).map { result =>
+                val (written, added, foundAdmins) = result
+                assert(written === 1, "Organisation not written to database!")
+                assert(added === admins.size, "Wrong number of administrators created!")
+                assertEquals(foundAdmins.map(_.uid).sorted, admins.map(_.uid).sorted)
+            }
         }
     }
 
     test("getMembers must return all members".tag(NeedsDatabase)) {
-        (genValidAccounts.sample, genOrganisation.sample) match {
-            case (Some(owner :: accounts), Some(org)) =>
-                val members = accounts.zipWithLongIndex.foldLeft(List.empty[Account])((acc, tuple) =>
-                    if (tuple._2 % 2 === 0) {
-                        tuple._1 :: acc
-                    } else {
-                        acc
-                    }
-                )
-                val organisation = org.copy(owner = owner.uid)
-                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 DoobieOrganisationRepository[IO](tx)
-                val addMember = repo.addMember(organisation.oid)
-                val test = for {
-                    _ <- createAccount(owner, PasswordHash("I am not a password hash!"), None, None)
-                    _ <- accounts.traverse(a => createAccount(a, PasswordHash("I am not a password hash!"), None, None))
-                    written      <- repo.create(organisation)
-                    added        <- members.map(_.uid).traverse(addMember)
-                    foundMembers <- repo.getMembers(organisation.oid).compile.toList
-                } yield (written, added.sum, foundMembers)
-                test.map { result =>
-                    val (written, added, foundMembers) = result
-                    assert(written === 1, "Organisation not written to database!")
-                    assert(added === members.size, "Wrong number of members created!")
-                    assertEquals(foundMembers.map(_.uid).sorted, members.map(_.uid).sorted)
-                }
-            case _ => fail("Could not generate data samples!")
+        PropF.forAllF { (as: NonEmptyList[Account], org: Organisation) =>
+            val owner    = as.head
+            val accounts = as.tail
+            val members = accounts.zipWithLongIndex.foldLeft(List.empty[Account])((acc, tuple) =>
+                if (tuple._2 % 2 === 0) {
+                    tuple._1 :: acc
+                } else {
+                    acc
+                }
+            )
+            val organisation = org.copy(owner = owner.uid)
+            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 DoobieOrganisationRepository[IO](tx)
+            val addMember = repo.addMember(organisation.oid)
+            val test = for {
+                _ <- createAccount(owner, PasswordHash("I am not a password hash!"), None, None)
+                _ <- accounts.traverse(a => createAccount(a, PasswordHash("I am not a password hash!"), None, None))
+                written      <- repo.create(organisation)
+                added        <- members.map(_.uid).traverse(addMember)
+                foundMembers <- repo.getMembers(organisation.oid).compile.toList
+            } yield (written, added.sum, foundMembers)
+            test.start.flatMap(_.joinWithNever).map { result =>
+                val (written, added, foundMembers) = result
+                assert(written === 1, "Organisation not written to database!")
+                assert(added === members.size, "Wrong number of members created!")
+                assertEquals(foundMembers.map(_.uid).sorted, members.map(_.uid).sorted)
+            }
         }
     }
 
     test("removeAdministrator must remove an administrator from an organisation".tag(NeedsDatabase)) {
-        (genValidAccounts.sample, genOrganisation.sample) match {
-            case (Some(owner :: accounts), Some(org)) =>
-                val organisation = org.copy(owner = owner.uid)
-                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 DoobieOrganisationRepository[IO](tx)
-                val test = for {
-                    _ <- createAccount(owner, PasswordHash("I am not a password hash!"), None, None)
-                    _ <- accounts.traverse(a => createAccount(a, PasswordHash("I am not a password hash!"), None, None))
-                    written <- repo.create(organisation)
-                    added   <- accounts.traverse(admin => repo.addAdministrator(organisation.oid)(admin.uid))
-                    removed <- accounts.traverse(admin => repo.removeAdministrator(organisation.oid)(admin.uid))
-                } yield (written, added.sum, removed.sum)
-                test.map { result =>
-                    val (written, added, removed) = result
-                    assert(written === 1, "Organisation not written to database!")
-                    assertEquals(added, removed)
-                }
-            case _ => fail("Could not generate data samples!")
+        PropF.forAllF { (as: NonEmptyList[Account], org: Organisation) =>
+            val owner        = as.head
+            val accounts     = as.tail
+            val organisation = org.copy(owner = owner.uid)
+            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 DoobieOrganisationRepository[IO](tx)
+            val test = for {
+                _ <- createAccount(owner, PasswordHash("I am not a password hash!"), None, None)
+                _ <- accounts.traverse(a => createAccount(a, PasswordHash("I am not a password hash!"), None, None))
+                written <- repo.create(organisation)
+                added   <- accounts.traverse(admin => repo.addAdministrator(organisation.oid)(admin.uid))
+                removed <- accounts.traverse(admin => repo.removeAdministrator(organisation.oid)(admin.uid))
+            } yield (written, added.sum, removed.sum)
+            test.start.flatMap(_.joinWithNever).map { result =>
+                val (written, added, removed) = result
+                assert(written === 1, "Organisation not written to database!")
+                assertEquals(added, removed)
+            }
         }
     }
 
     test("removeMember must remove a member from an organisation".tag(NeedsDatabase)) {
-        (genValidAccounts.sample, genOrganisation.sample) match {
-            case (Some(owner :: accounts), Some(org)) =>
-                val organisation = org.copy(owner = owner.uid)
-                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 DoobieOrganisationRepository[IO](tx)
-                val test = for {
-                    _ <- createAccount(owner, PasswordHash("I am not a password hash!"), None, None)
-                    _ <- accounts.traverse(a => createAccount(a, PasswordHash("I am not a password hash!"), None, None))
-                    written   <- repo.create(organisation)
-                    added     <- accounts.traverse(member => repo.addMember(organisation.oid)(member.uid))
-                    removed   <- accounts.traverse(member => repo.removeMember(organisation.oid)(member.uid))
-                    foundOrgs <- accounts.traverse(member => repo.allByMember(member.uid).compile.toList)
-                } yield (written, added.sum, removed.sum, foundOrgs)
-                test.map { result =>
-                    val (written, added, removed, foundOrgs) = result
-                    assert(written === 1, "Organisation not written to database!")
-                    assertEquals(added, removed)
-                    foundOrgs.foreach(orgs => assert(orgs.isEmpty))
-                }
-            case _ => fail("Could not generate data samples!")
+        PropF.forAllF { (as: NonEmptyList[Account], org: Organisation) =>
+            val owner        = as.head
+            val accounts     = as.tail
+            val organisation = org.copy(owner = owner.uid)
+            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 DoobieOrganisationRepository[IO](tx)
+            val test = for {
+                _ <- createAccount(owner, PasswordHash("I am not a password hash!"), None, None)
+                _ <- accounts.traverse(a => createAccount(a, PasswordHash("I am not a password hash!"), None, None))
+                written   <- repo.create(organisation)
+                added     <- accounts.traverse(member => repo.addMember(organisation.oid)(member.uid))
+                removed   <- accounts.traverse(member => repo.removeMember(organisation.oid)(member.uid))
+                foundOrgs <- accounts.traverse(member => repo.allByMember(member.uid).compile.toList)
+            } yield (written, added.sum, removed.sum, foundOrgs)
+            test.start.flatMap(_.joinWithNever).map { result =>
+                val (written, added, removed, foundOrgs) = result
+                assert(written === 1, "Organisation not written to database!")
+                assertEquals(added, removed)
+                foundOrgs.foreach(orgs => assert(orgs.isEmpty))
+            }
         }
     }
 
     test("update must update the organisation data".tag(NeedsDatabase)) {
-        (genValidAccount.sample, genOrganisation.sample, genOrganisation.sample) match {
-            case (Some(account), Some(org), Some(anotherOrg)) =>
-                val organisation        = org.copy(owner = account.uid)
-                val updatedOrganisation = anotherOrg.copy(oid = organisation.oid, owner = organisation.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 repo = new DoobieOrganisationRepository[IO](tx)
-                val test = for {
-                    _       <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-                    written <- repo.create(organisation)
-                    updated <- repo.update(updatedOrganisation)
-                    found   <- repo.find(organisation.oid)
-                } yield (written, updated, found)
-                test.map { result =>
-                    val (written, updated, found) = result
-                    assert(written === 1, "Creating an organisation must modify one database row!")
-                    assert(updated === 1, "Updating an organisation must modify one database row!")
-                    assertEquals(found, updatedOrganisation.some)
-                }
-            case _ => fail("Could not generate data samples!")
+        PropF.forAllF { (account: Account, org: Organisation, anotherOrg: Organisation) =>
+            val organisation        = org.copy(owner = account.uid)
+            val updatedOrganisation = anotherOrg.copy(oid = organisation.oid, owner = organisation.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 repo = new DoobieOrganisationRepository[IO](tx)
+            val test = for {
+                _       <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                written <- repo.create(organisation)
+                updated <- repo.update(updatedOrganisation)
+                found   <- repo.find(organisation.oid)
+            } yield (written, updated, found)
+            test.start.flatMap(_.joinWithNever).map { result =>
+                val (written, updated, found) = result
+                assert(written === 1, "Creating an organisation must modify one database row!")
+                assert(updated === 1, "Updating an organisation must modify one database row!")
+                assertEquals(found, updatedOrganisation.some)
+            }
         }
     }
 }
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-01-11 02:59:38.403425737 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/Generators.scala	2025-01-11 02:59:38.403425737 +0000
@@ -11,6 +11,7 @@
 import java.util.Locale
 import java.util.UUID
 
+import cats.data.NonEmptyList
 import cats.syntax.all.*
 import de.smederee.email.EmailAddress
 import de.smederee.i18n.LanguageCode
@@ -129,6 +130,8 @@
                 a :: acc
         }) // Ensure distinct user names.
 
+    given Arbitrary[List[Account]] = Arbitrary(genValidAccounts)
+
     val genOrganisationId: Gen[OrganisationId] = Gen.delay(OrganisationId.randomOrganisationId)
 
     val genOrganisation: Gen[Organisation] =
@@ -236,4 +239,29 @@
     val genPermissions: Gen[Set[Permission]] = Gen.choose(0, 7).map(Permission.decode)
 
     given Arbitrary[Set[Permission]] = Arbitrary(genPermissions)
+
+    /** Provide a generator that generates a [[NonEmptyList]] with head and a non empty tail given that an [[Arbitrary]]
+      * for the desired type `A` is in scope. This is defined as a `Given` to be used later on to create an arbitrary of
+      * this generator.
+      *
+      * @param singleValue
+      *   An arbitrary instance for the desired type `A`.
+      * @return
+      *   A generator for a `NonEmptyList[A]`.
+      */
+    given genNel[A](using singleValue: Arbitrary[A]): Gen[NonEmptyList[A]] =
+        for {
+            head <- singleValue.arbitrary
+            tail <- Gen.nonEmptyListOf(singleValue.arbitrary)
+        } yield NonEmptyList.of(head, tail: _*)
+
+    /** Provide an arbitrary for a [[NonEmptyList]] of the desired type `A` if a generator for `NonEmptyList[A]` is in
+      * scope.
+      *
+      * @param genNel
+      *   A generator for a `NonEmptyList[A]`.
+      * @return
+      *   An arbitrary instance of `NonEmptyList[A]`.
+      */
+    given [A](using genNel: Gen[NonEmptyList[A]]): Arbitrary[NonEmptyList[A]] = Arbitrary(genNel)
 }