~jan0sch/smederee
Showing details for patch ccc9caffdf75cf57716fe6595cce4b0c0fe3cd42.
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) }