~jan0sch/smederee
Showing details for patch 2fdbbeb715dd112a1d1a1c24dc030a2be3bc0dce.
diff -rN -u old-smederee/build.sbt new-smederee/build.sbt --- old-smederee/build.sbt 2025-02-01 01:55:04.502884805 +0000 +++ new-smederee/build.sbt 2025-02-01 01:55:04.506884811 +0000 @@ -323,7 +323,8 @@ val circe = "0.14.3" val commonMark = "0.21.0" val doobie = "1.0.0-RC2" - val flyway = "9.11.0" + val flyway = "9.12.0" + val fs2 = "3.5.0" val http4s = "1.0.0-M38" val ip4s = "3.2.0" val jansi = "2.4.2" @@ -354,6 +355,8 @@ val doobiePostgres = "org.tpolecat" %% "doobie-postgres" % Version.doobie val doobieScalaTest = "org.tpolecat" %% "doobie-scalatest" % Version.doobie val flywayCore = "org.flywaydb" % "flyway-core" % Version.flyway + val fs2Core = "co.fs2" %% "fs2-core" % Version.fs2 + val fs2IO = "co.fs2" %% "fs2-io" % Version.fs2 val http4sCirce = "org.http4s" %% "http4s-circe" % Version.http4s val http4sCore = "org.http4s" %% "http4s-core" % Version.http4s val http4sDsl = "org.http4s" %% "http4s-dsl" % Version.http4s diff -rN -u old-smederee/modules/hub/src/main/resources/db/migration/V4__ticket_tables.sql new-smederee/modules/hub/src/main/resources/db/migration/V4__ticket_tables.sql --- old-smederee/modules/hub/src/main/resources/db/migration/V4__ticket_tables.sql 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/resources/db/migration/V4__ticket_tables.sql 2025-02-01 01:55:04.506884811 +0000 @@ -0,0 +1,131 @@ +CREATE TABLE "labels" +( + "id" BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "repository" BIGINT NOT NULL, + "name" CHARACTER VARYING(40) NOT NULL, + "description" CHARACTER VARYING(254) DEFAULT NULL, + "colour" CHARACTER VARYING(7) NOT NULL, + CONSTRAINT "labels_unique_repo_label" UNIQUE ("repository", "name"), + CONSTRAINT "labels_fk_repo" FOREIGN KEY ("repository") + REFERENCES "repositories" ("id") ON UPDATE CASCADE ON DELETE CASCADE +) +WITH ( + OIDS=FALSE +); + +COMMENT ON TABLE "labels" IS 'Labels used to add information to tickets.'; +COMMENT ON COLUMN "labels"."id" IS 'An auto generated primary key.'; +COMMENT ON COLUMN "labels"."repository" IS 'The repository to which this label belongs.'; +COMMENT ON COLUMN "labels"."name" IS 'A short descriptive name for the label which is supposed to be unique in a project context.'; +COMMENT ON COLUMN "labels"."description" IS 'An optional description if needed.'; +COMMENT ON COLUMN "labels"."colour" IS 'A hexadecimal HTML colour code which can be used to mark the label on a rendered website.'; + +CREATE TABLE "milestones" +( + "id" BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "repository" BIGINT NOT NULL, + "title" CHARACTER VARYING(64) NOT NULL, + "due_date" DATE DEFAULT NULL, + "description" TEXT DEFAULT NULL, + CONSTRAINT "milestones_unique_repo_title" UNIQUE ("repository", "title"), + CONSTRAINT "milestones_fk_repo" FOREIGN KEY ("repository") + REFERENCES "repositories" ("id") ON UPDATE CASCADE ON DELETE CASCADE +) +WITH ( + OIDS=FALSE +); + +COMMENT ON TABLE "milestones" IS 'Milestones used to organise tickets'; +COMMENT ON COLUMN "milestones"."repository" IS 'The repository to which this milestone belongs.'; +COMMENT ON COLUMN "milestones"."title" IS 'A title for the milestone, usually a version number, a word or a short phrase that is supposed to be unique within a project context.'; +COMMENT ON COLUMN "milestones"."due_date" IS 'An optional date on which the milestone is supposed to be reached.'; +COMMENT ON COLUMN "milestones"."description" IS 'An optional longer description of the milestone.'; + +CREATE TABLE "tickets" +( + "id" BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "repository" BIGINT NOT NULL, + "number" INT NOT NULL, + "title" CHARACTER VARYING(72) NOT NULL, + "content" TEXT DEFAULT NULL, + "status" CHARACTER VARYING(16) NOT NULL, + "submitter" UUID DEFAULT NULL, + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, + "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL, + CONSTRAINT "tickets_unique_repo_ticket" UNIQUE ("repository", "number"), + CONSTRAINT "tickets_fk_repo" FOREIGN KEY ("repository") + REFERENCES "repositories" ("id") ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT "tickets_fk_submitter" FOREIGN KEY ("submitter") + REFERENCES "accounts" ("uid") ON UPDATE CASCADE ON DELETE SET NULL +) +WITH ( + OIDS=FALSE +); + +CREATE INDEX "tickets_status" ON "tickets" ("status"); + +COMMENT ON TABLE "tickets" IS 'Information about tickets for projects.'; +COMMENT ON COLUMN "tickets"."id" IS 'An auto generated primary key.'; +COMMENT ON COLUMN "tickets"."repository" IS 'The unique ID of the repository which is associated with the ticket.'; +COMMENT ON COLUMN "tickets"."number" IS 'The number of the ticket which must be unique within the scope of the project.'; +COMMENT ON COLUMN "tickets"."title" IS 'A concise and short description of the ticket which should not exceed 72 characters.'; +COMMENT ON COLUMN "tickets"."content" IS 'An optional field to describe the ticket in great detail if needed.'; +COMMENT ON COLUMN "tickets"."status" IS 'The current status of the ticket describing its life cycle.'; +COMMENT ON COLUMN "tickets"."submitter" IS 'The person who submitted (created) this ticket which is optional because of possible account deletion or other reasons.'; +COMMENT ON COLUMN "tickets"."created_at" IS 'The timestamp when the ticket was created / submitted.'; +COMMENT ON COLUMN "tickets"."updated_at" IS 'The timestamp when the ticket was last updated. Upon creation the update time equals the creation time.'; + +CREATE TABLE "milestone_tickets" +( + "milestone" BIGINT NOT NULL, + "ticket" BIGINT NOT NULL, + CONSTRAINT "milestone_tickets_pk" PRIMARY KEY ("milestone", "ticket"), + CONSTRAINT "milestone_tickets_fk_milestone" FOREIGN KEY ("milestone") + REFERENCES "milestones" ("id") ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT "milestone_tickets_fk_ticket" FOREIGN KEY ("ticket") + REFERENCES "tickets" ("id") ON UPDATE CASCADE ON DELETE CASCADE +) +WITH ( + OIDS=FALSE +); + +COMMENT ON TABLE "milestone_tickets" IS 'This table stores the relation between milestones and their tickets.'; +COMMENT ON COLUMN "milestone_tickets"."milestone" IS 'The unique ID of the milestone.'; +COMMENT ON COLUMN "milestone_tickets"."ticket" IS 'The unique ID of the ticket that is attached to the milestone.'; + +CREATE TABLE "ticket_assignees" +( + "ticket" BIGINT NOT NULL, + "assignee" UUID NOT NULL, + CONSTRAINT "ticket_assignees_pk" PRIMARY KEY ("ticket", "assignee"), + CONSTRAINT "ticket_assignees_fk_ticket" FOREIGN KEY ("ticket") + REFERENCES "tickets" ("id") ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT "ticket_assignees_fk_assignee" FOREIGN KEY ("assignee") + REFERENCES "accounts" ("uid") ON UPDATE CASCADE ON DELETE CASCADE +) +WITH ( + OIDS=FALSE +); + +COMMENT ON TABLE "ticket_assignees" IS 'This table stores the relation between tickets and their assignees.'; +COMMENT ON COLUMN "ticket_assignees"."ticket" IS 'The unqiue ID of the ticket.'; +COMMENT ON COLUMN "ticket_assignees"."assignee" IS 'The unique ID of the user account that is assigned to the ticket.'; + +CREATE TABLE "ticket_lables" +( + "ticket" BIGINT NOT NULL, + "label" BIGINT NOT NULL, + CONSTRAINT "ticket_lables_pk" PRIMARY KEY ("ticket", "label"), + CONSTRAINT "ticket_labels_fk_ticket" FOREIGN KEY ("ticket") + REFERENCES "tickets" ("id") ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT "ticket_labels_fk_label" FOREIGN KEY ("label") + REFERENCES "labels" ("id") ON UPDATE CASCADE ON DELETE CASCADE +) +WITH ( + OIDS=FALSE +); + +COMMENT ON TABLE "ticket_lables" IS 'This table stores the relation between tickets and their lables.'; +COMMENT ON COLUMN "ticket_lables"."ticket" IS 'The unqiue ID of the ticket.'; +COMMENT ON COLUMN "ticket_lables"."label" IS 'The unique ID of the label that is attached to the ticket.'; + diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/Account.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/Account.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/hub/Account.scala 2025-02-01 01:55:04.502884805 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/Account.scala 2025-02-01 01:55:04.506884811 +0000 @@ -24,10 +24,13 @@ import cats.data._ import cats.syntax.all._ import de.smederee.email.{ FromAddress, ToAddress } +import de.smederee.tickets._ import org.springframework.security.crypto.argon2.Argon2PasswordEncoder import scala.util.matching.Regex +/** An email address must fulfil several format requirements which in detail should be looked up in the implementation. + */ opaque type Email = String object Email { given Eq[Email] = Eq.fromUniversalEquals @@ -96,6 +99,9 @@ def toToAddress: ToAddress = ToAddress(email.toString) } +/** A password is stored as an `Array[Byte]` internally and its `validate(source: String)` function will check that the + * input has a minimum length. + */ opaque type Password = Array[Byte] object Password { @@ -275,6 +281,10 @@ } +/** A username for an account has to obey several restrictions which are similiar to the ones found for Unix usernames. + * It must start with a letter, contain only alphanumeric characters, be at least 2 and at most 31 characters long and + * be all lowercase. + */ opaque type Username = String object Username { given Eq[Username] = Eq.fromUniversalEquals @@ -407,6 +417,20 @@ extension (account: Account) { + /** Create an assignee entity from this account. + * + * @return + * An [[Assignee]] which is used to related to tickets being worked on. + */ + def toAssignee: Assignee = Assignee(account.uid, account.name) + + /** Create a submitter entity from this account. + * + * @return + * A [[Submitter]] who created a ticket. + */ + def toSubmitter: Submitter = Submitter(account.uid, account.name) + /** Create vcs repository owner metadata from the account. * * @return diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/types.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/types.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/hub/types.scala 2025-02-01 01:55:04.502884805 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/types.scala 2025-02-01 01:55:04.506884811 +0000 @@ -102,6 +102,8 @@ } } +/** A user id is supposed to be globally unique and maps to the [[java.util.UUID]] type beneath. + */ opaque type UserId = UUID object UserId { val Format = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$".r diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/tickets/Assignee.scala new-smederee/modules/hub/src/main/scala/de/smederee/tickets/Assignee.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/tickets/Assignee.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/tickets/Assignee.scala 2025-02-01 01:55:04.506884811 +0000 @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.tickets + +import java.util.UUID + +import cats._ +import cats.data._ +import cats.syntax.all._ +import de.smederee.hub.{ UserId, Username } + +/** The assignee for a ticket i.e. the person supposed to be working on it. + * + * @param id + * A globally unique ID identifying the assignee. + * @param name + * The name associated with the assignee which is supposed to be unique. + */ +final case class Assignee(id: UserId, name: Username) + +object Assignee { + given Eq[Assignee] = Eq.fromUniversalEquals +} diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/tickets/Label.scala new-smederee/modules/hub/src/main/scala/de/smederee/tickets/Label.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/tickets/Label.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/tickets/Label.scala 2025-02-01 01:55:04.506884811 +0000 @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.tickets + +import cats._ +import cats.syntax.all._ + +import scala.util.matching.Regex + +/** A short descriptive name for the label which is supposed to be unique in a project context. It must not be empty and + * not exceed 40 characters in length. + */ +opaque type LabelName = String +object LabelName { + given Eq[LabelName] = Eq.fromUniversalEquals + + val MaxLength: Int = 40 + + /** Create an instance of LabelName from the given String type. + * + * @param source + * An instance of type String which will be returned as a LabelName. + * @return + * The appropriate instance of LabelName. + */ + def apply(source: String): LabelName = source + + /** Try to create an instance of LabelName from the given String. + * + * @param source + * A String that should fulfil the requirements to be converted into a LabelName. + * @return + * An option to the successfully converted LabelName. + */ + def from(source: String): Option[LabelName] = + Option(source).filter(string => string.nonEmpty && string.length <= MaxLength) + +} + +/** A maybe needed description of a label which must not be empty and not exceed 254 characters in length. + */ +opaque type LabelDescription = String +object LabelDescription { + given Eq[LabelDescription] = Eq.fromUniversalEquals + + val MaxLength: Int = 254 + + /** Create an instance of LabelDescription from the given String type. + * + * @param source + * An instance of type String which will be returned as a LabelDescription. + * @return + * The appropriate instance of LabelDescription. + */ + def apply(source: String): LabelDescription = source + + /** Try to create an instance of LabelDescription from the given String. + * + * @param source + * A String that should fulfil the requirements to be converted into a LabelDescription. + * @return + * An option to the successfully converted LabelDescription. + */ + def from(source: String): Option[LabelDescription] = + Option(source).filter(string => string.nonEmpty && string.length <= MaxLength) +} + +/** An HTML colour code in hexadecimal notation e.g. `#12ab3f`. It must match the format of starting with a hashtag + * followed by three 2-digit hexadecimal codes (`00-ff`). + */ +opaque type ColourCode = String +object ColourCode { + given Eq[ColourCode] = Eq.fromUniversalEquals + + val Format: Regex = "^#[0-9a-fA-F]{6}$".r + + /** Create an instance of ColourCode from the given String type. + * + * @param source + * An instance of type String which will be returned as a ColourCode. + * @return + * The appropriate instance of ColourCode. + */ + def apply(source: String): ColourCode = source + + /** Try to create an instance of ColourCode from the given String. + * + * @param source + * A String that should fulfil the requirements to be converted into a ColourCode. + * @return + * An option to the successfully converted ColourCode. + */ + def from(source: String): Option[ColourCode] = Option(source).filter(string => Format.matches(string)) + +} + +/** A label is intended to mark tickets with keywords and colours to allow filtering on them. + * + * @param name + * A short descriptive name for the label which is supposed to be unique in a project context. + * @param description + * An optional description if needed. + * @param colour + * A hexadecimal HTML colour code which can be used to mark the label on a rendered website. + */ +final case class Label(name: LabelName, description: Option[LabelDescription], colour: ColourCode) + +object Label { + given Eq[Label] = + Eq.instance((thisLabel, thatLabel) => + thisLabel.name === thatLabel.name && thisLabel.description === thatLabel.description && thisLabel.colour === thatLabel.colour + ) +} diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/tickets/Milestone.scala new-smederee/modules/hub/src/main/scala/de/smederee/tickets/Milestone.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/tickets/Milestone.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/tickets/Milestone.scala 2025-02-01 01:55:04.506884811 +0000 @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.tickets + +import java.time.LocalDate + +import cats._ +import cats.syntax.all._ + +/** A title for a milestone, usually a version number, a word or a short phrase that is supposed to be unique within a + * project context. It must not be empty and not exceed 64 characters in length. + */ +opaque type MilestoneTitle = String +object MilestoneTitle { + given Eq[MilestoneTitle] = Eq.fromUniversalEquals + + val MaxLength: Int = 64 + + /** Create an instance of MilestoneTitle from the given String type. + * + * @param source + * An instance of type String which will be returned as a MilestoneTitle. + * @return + * The appropriate instance of MilestoneTitle. + */ + def apply(source: String): MilestoneTitle = source + + /** Try to create an instance of MilestoneTitle from the given String. + * + * @param source + * A String that should fulfil the requirements to be converted into a MilestoneTitle. + * @return + * An option to the successfully converted MilestoneTitle. + */ + def from(source: String): Option[MilestoneTitle] = + Option(source).filter(string => string.nonEmpty && string.length <= MaxLength) + +} + +/** A longer detailed description of a project milestone which must not be empty. + */ +opaque type MilestoneDescription = String +object MilestoneDescription { + given Eq[MilestoneDescription] = Eq.fromUniversalEquals + + /** Create an instance of MilestoneDescription from the given String type. + * + * @param source + * An instance of type String which will be returned as a MilestoneDescription. + * @return + * The appropriate instance of MilestoneDescription. + */ + def apply(source: String): MilestoneDescription = source + + /** Try to create an instance of MilestoneDescription from the given String. + * + * @param source + * A String that should fulfil the requirements to be converted into a MilestoneDescription. + * @return + * An option to the successfully converted MilestoneDescription. + */ + def from(source: String): Option[MilestoneDescription] = Option(source).map(_.trim).filter(_.nonEmpty) + +} + +/** A milestone can be used to organise tickets and progress inside a project. + * + * @param title + * A title for the milestone, usually a version number, a word or a short phrase that is supposed to be unique within + * a project context. + * @param dueDate + * An optional date on which the milestone is supposed to be reached. + * @param description + * An optional longer description of the milestone. + */ +final case class Milestone(title: MilestoneTitle, dueDate: Option[LocalDate], description: Option[MilestoneDescription]) + +object Milestone { + + given Eq[LocalDate] = Eq.instance((a, b) => a.compareTo(b) === 0) + + given Eq[Milestone] = + Eq.instance((a, b) => a.title === b.title && a.dueDate === b.dueDate && a.description === b.description) + +} diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/tickets/Submitter.scala new-smederee/modules/hub/src/main/scala/de/smederee/tickets/Submitter.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/tickets/Submitter.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/tickets/Submitter.scala 2025-02-01 01:55:04.506884811 +0000 @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.tickets + +import java.util.UUID + +import cats._ +import cats.data._ +import cats.syntax.all._ +import de.smederee.hub.{ UserId, Username } + +/** The submitter for a ticket i.e. the person supposed to be working on it. + * + * @param id + * A globally unique ID identifying the submitter. + * @param name + * The name associated with the submitter which is supposed to be unique. + */ +final case class Submitter(id: UserId, name: Username) + +object Submitter { + given Eq[Submitter] = Eq.fromUniversalEquals +} diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/tickets/TicketRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/tickets/TicketRepository.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/tickets/TicketRepository.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/tickets/TicketRepository.scala 2025-02-01 01:55:04.506884811 +0000 @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.tickets + +import fs2.Stream + +/** The base class that defines the needed functionality to handle tickets and related data types within a database. + * + * @tparam F + * A higher kinded type which wraps the actual return values. + */ +abstract class TicketRepository[F[_]] { + + /** Return all labels associated with the given repository. + * + * @param vcsRepositoryId + * The unique internal ID of a vcs repository metadata entry for which all labels shall be returned. + * @return + * A stream of labels associated with the vcs repository which may be empty. + */ + def allLables(vcsRepositoryId: Long): Stream[F, Label] + + /** Return all milestones associated with the given repository. + * + * @param vcsRepositoryId + * The unique internal ID of a vcs repository metadata entry for which all milestones shall be returned. + * @return + * A stream of milestones associated with the vcs repository which may be empty. + */ + def allMilestones(vcsRepositoryId: Long): Stream[F, Milestone] + + /** Return all tickets associated with the given repository. + * + * @param vcsRepositoryId + * The unique internal ID of a vcs repository metadata entry for which all tickets shall be returned. + * @return + * A stream of tickets associated with the vcs repository which may be empty. + */ + def allTickets(vcsRepositoryId: Long): Stream[F, Ticket] + + /** Create a database entry for the given label definition. + * + * @param vcsRepositoryId + * The unique internal ID of a vcs repository metadata entry to which the label belongs. + * @param label + * The label definition that shall be written to the database. + * @return + * The number of affected database rows. + */ + def createLabel(vcsRepositoryId: Long)(label: Label): F[Int] + + /** Create a database entry for the given milestone definition. + * + * @param vcsRepositoryId + * The unique internal ID of a vcs repository metadata entry to which the milestone belongs. + * @param milestone + * The milestone definition that shall be written to the database. + * @return + * The number of affected database rows. + */ + def createMilestone(vcsRepositoryId: Long)(milestone: Milestone): F[Int] + + /** Create a database entry for the given ticket definition. + * + * @param vcsRepositoryId + * The unique internal ID of a vcs repository metadata entry to which the ticket belongs. + * @param ticket + * The ticket definition that shall be written to the database. + * @return + * The number of affected database rows. + */ + def createTicket(vcsRepositoryId: Long)(ticket: Ticket): F[Int] + + /** Delete the label from the database. + * + * @param vcsRepositoryId + * The unique internal ID of a vcs repository metadata entry to which the label belongs. + * @param label + * The label definition that shall be deleted. + * @return + * The number of affected database rows. + */ + def deleteLabel(vcsRepositoryId: Long)(label: Label): F[Int] + + /** Delete the milestone from the database. + * + * @param vcsRepositoryId + * The unique internal ID of a vcs repository metadata entry to which the milestone belongs. + * @param milestone + * The milestone definition that shall be deleted. + * @return + * The number of affected database rows. + */ + def deleteMilestone(vcsRepositoryId: Long)(milestone: Milestone): F[Int] + + /** Update the database entry for the given label. + * + * @param vcsRepositoryId + * The unique internal ID of a vcs repository metadata entry to which the label belongs. + * @param label + * The label definition that shall be updated within the database. + * @return + * The number of affected database rows. + */ + def updateLabel(vcsRepositoryId: Long)(label: Label): F[Int] + + /** Update the database entry for the given milestone. + * + * @param vcsRepositoryId + * The unique internal ID of a vcs repository metadata entry to which the milestone belongs. + * @param milestone + * The milestone definition that shall be updated within the database. + * @return + * The number of affected database rows. + */ + def updateMilestone(vcsRepositoryId: Long)(milestone: Milestone): F[Int] + + /** Update the database entry for the given ticket. + * + * @param vcsRepositoryId + * The unique internal ID of a vcs repository metadata entry to which the ticket belongs. + * @param ticket + * The ticket definition that shall be updated within the database. + * @return + * The number of affected database rows. + */ + def updateTicket(vcsRepositoryId: Long)(ticket: Ticket): F[Int] + +} diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/tickets/Ticket.scala new-smederee/modules/hub/src/main/scala/de/smederee/tickets/Ticket.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/tickets/Ticket.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/tickets/Ticket.scala 2025-02-01 01:55:04.506884811 +0000 @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.tickets + +import java.time.OffsetDateTime + +import cats._ + +/** An unlimited text field which must be not empty to describe the ticket in great detail if needed. + */ +opaque type TicketContent = String +object TicketContent { + + /** Create an instance of TicketContent from the given String type. + * + * @param source + * An instance of type String which will be returned as a TicketContent. + * @return + * The appropriate instance of TicketContent. + */ + def apply(source: String): TicketContent = source + + /** Try to create an instance of TicketContent from the given String. + * + * @param source + * A String that should fulfil the requirements to be converted into a TicketContent. + * @return + * An option to the successfully converted TicketContent. + */ + def from(source: String): Option[TicketContent] = Option(source).filter(_.nonEmpty) + +} + +/** A ticket number maps to an integer beneath and has the requirement to be greater than zero. + */ +opaque type TicketNumber = Int +object TicketNumber { + + /** Create an instance of TicketNumber from the given Int type. + * + * @param source + * An instance of type Int which will be returned as a TicketNumber. + * @return + * The appropriate instance of TicketNumber. + */ + def apply(source: Int): TicketNumber = source + + /** Try to create an instance of TicketNumber from the given Int. + * + * @param source + * A Int that should fulfil the requirements to be converted into a TicketNumber. + * @return + * An option to the successfully converted TicketNumber. + */ + def from(source: Int): Option[TicketNumber] = Option(source).filter(_ > 0) +} + +/** Possible states of a ticket which shall model the life cycle from ticket creation until it is closed. To keep things + * simple we do not model the kind of a resolution (e.g. fixed, won't fix or so) for a ticket. + */ +enum TicketStatus { + + /** The ticket is considered to be a valid ticket / ready to be worked on e.g. a bug has been confirmed to be present. + */ + case Confirmed + + /** The ticket is resolved (i.e. closed) and considered done. + */ + case Done + + /** The ticket is being worked on i.e. it is in progress. + */ + case InProgress + + /** The ticket was reported and is open for further investigation. This might map to what is often called "Backlog" + * nowadays. + */ + case Reported + + /** The ticket is being reviewed and might be moved to another status after the review process is being done. + */ + case Review + +} + +/** A concise and short description of the ticket which should not exceed 80 characters. + */ +opaque type TicketTitle = String +object TicketTitle { + + val MaxLength: Int = 72 + + /** Create an instance of TicketTitle from the given String type. + * + * @param source + * An instance of type String which will be returned as a TicketTitle. + * @return + * The appropriate instance of TicketTitle. + */ + def apply(source: String): TicketTitle = source + + /** Try to create an instance of TicketTitle from the given String. + * + * @param source + * A String that should fulfil the requirements to be converted into a TicketTitle. + * @return + * An option to the successfully converted TicketTitle. + */ + def from(source: String): Option[TicketTitle] = + Option(source).filter(string => string.nonEmpty && string.length <= MaxLength) +} + +/** An ticket used to describe a problem or a task (e.g. implement a concrete feature) within the scope of a project. + * + * @param number + * The unique identifier of a ticket within the project scope is its number. + * @param title + * A concise and short description of the ticket which should not exceed 72 characters. + * @param content + * An optional field to describe the ticket in great detail if needed. + * @param status + * The current status of the ticket describing its life cycle. + * @param labels + * A list of labels assigned to this ticket. + * @param milestones + * A list of milestones to which this ticket is assigned. + * @param submitter + * The person who submitted (created) this ticket which is optional because of possible account deletion or other + * reasons. + * @param assignees + * A list of assignees working on this ticket which might be no-one. + * @param createdAt + * The timestamp when the ticket was created / submitted. + * @param updatedAt + * The timestamp when the ticket was last updated. Upon creation the update time equals the creation time. + */ +final case class Ticket( + number: TicketNumber, + title: TicketTitle, + content: Option[TicketContent], + status: TicketStatus, + labels: List[Label], + milestones: List[Milestone], + submitter: Option[Submitter], + assignees: List[Assignee], + createdAt: OffsetDateTime, + updatedAt: OffsetDateTime +) diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/tickets/ColourCodeTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/tickets/ColourCodeTest.scala --- old-smederee/modules/hub/src/test/scala/de/smederee/tickets/ColourCodeTest.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/test/scala/de/smederee/tickets/ColourCodeTest.scala 2025-02-01 01:55:04.506884811 +0000 @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.tickets + +import de.smederee.tickets.Generators._ + +import munit._ +import org.scalacheck._ +import org.scalacheck.Prop._ + +final class ColourCodeTest extends ScalaCheckSuite { + given Arbitrary[ColourCode] = Arbitrary(genColourCode) + + property("ColourCode.from must fail on invalid input") { + forAll { (input: String) => + assertEquals(ColourCode.from(input), None) + } + } + + property("ColourCode.from must succeed on valid input") { + forAll { (colourCode: ColourCode) => + val input = colourCode.toString + assertEquals(ColourCode.from(input), Option(colourCode)) + } + } +} 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 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/test/scala/de/smederee/tickets/Generators.scala 2025-02-01 01:55:04.506884811 +0000 @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.tickets + +import org.scalacheck.{ Arbitrary, Gen } +import java.time.LocalDate + +object Generators { + + /** Prepend a zero to a single character hexadecimal code. + * + * @param hexCode + * A string supposed to contain a hexadecimal code between 0 and ff. + * @return + * Either the given code prepended with a leading zero if it had only a single character or the originally given + * code otherwise. + */ + private def hexPadding(hexCode: String): String = + if (hexCode.length < 2) + "0" + hexCode + else + hexCode + + val genLabelName: Gen[LabelName] = + Gen.nonEmptyListOf(Gen.alphaNumChar).map(_.take(LabelName.MaxLength).mkString).map(LabelName.apply) + + val genLabelDescription: Gen[LabelDescription] = + Gen.nonEmptyListOf(Gen.alphaNumChar).map(_.take(LabelDescription.MaxLength).mkString).map(LabelDescription.apply) + + val genColourCode: Gen[ColourCode] = for { + red <- Gen.choose(0, 255).map(_.toHexString).map(hexPadding) + green <- Gen.choose(0, 255).map(_.toHexString).map(hexPadding) + blue <- Gen.choose(0, 255).map(_.toHexString).map(hexPadding) + hexString = s"#$red$green$blue" + } yield ColourCode(hexString) + + val genLabel: Gen[Label] = for { + name <- genLabelName + description <- Gen.option(genLabelDescription) + colour <- genColourCode + } yield Label(name, description, colour) + + val genLocalDate: Gen[LocalDate] = + for { + year <- Gen.choose(LocalDate.MIN.getYear(), LocalDate.MAX.getYear()) + month <- Gen.choose(1, 12) + maxDays = LocalDate.of(year, month, 1).lengthOfMonth() + day <- Gen.choose(1, maxDays) + } yield LocalDate.of(year, month, day) + + val genMilestoneTitle: Gen[MilestoneTitle] = + Gen.nonEmptyListOf(Gen.alphaNumChar).map(_.take(MilestoneTitle.MaxLength).mkString).map(MilestoneTitle.apply) + + val genMilestoneDescription: Gen[MilestoneDescription] = + Gen.nonEmptyListOf(Gen.alphaNumChar).map(_.mkString).map(MilestoneDescription.apply) + + val genMilestone: Gen[Milestone] = + for { + title <- genMilestoneTitle + due <- Gen.option(genLocalDate) + descr <- Gen.option(genMilestoneDescription) + } yield Milestone(title, due, descr) + +} diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/tickets/LabelDescriptionTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/tickets/LabelDescriptionTest.scala --- old-smederee/modules/hub/src/test/scala/de/smederee/tickets/LabelDescriptionTest.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/test/scala/de/smederee/tickets/LabelDescriptionTest.scala 2025-02-01 01:55:04.506884811 +0000 @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.tickets + +import de.smederee.tickets.Generators._ + +import munit._ +import org.scalacheck._ +import org.scalacheck.Prop._ + +final class LabelDescriptionTest extends ScalaCheckSuite { + given Arbitrary[LabelDescription] = Arbitrary(genLabelDescription) + + test("LabelDescription.from must fail on empty input") { + assertEquals(LabelDescription.from(""), None) + } + + property("LabelDescription.from must fail on too long input") { + forAll { (input: String) => + if (input.length > LabelDescription.MaxLength) + assertEquals(LabelDescription.from(input), None) + } + } + + property("LabelDescription.from must succeed on valid input") { + forAll { (label: LabelDescription) => + val input = label.toString + assertEquals(LabelDescription.from(input), Option(label)) + } + } +} diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/tickets/LabelNameTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/tickets/LabelNameTest.scala --- old-smederee/modules/hub/src/test/scala/de/smederee/tickets/LabelNameTest.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/test/scala/de/smederee/tickets/LabelNameTest.scala 2025-02-01 01:55:04.506884811 +0000 @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.tickets + +import de.smederee.tickets.Generators._ + +import munit._ +import org.scalacheck._ +import org.scalacheck.Prop._ + +final class LabelNameTest extends ScalaCheckSuite { + given Arbitrary[LabelName] = Arbitrary(genLabelName) + + test("LabelName.from must fail on empty input") { + assertEquals(LabelName.from(""), None) + } + + property("LabelName.from must fail on too long input") { + forAll { (input: String) => + if (input.length > LabelName.MaxLength) + assertEquals(LabelName.from(input), None) + } + } + + property("LabelName.from must succeed on valid input") { + forAll { (label: LabelName) => + val input = label.toString + assertEquals(LabelName.from(input), Option(label)) + } + } +} diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/tickets/LabelTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/tickets/LabelTest.scala --- old-smederee/modules/hub/src/test/scala/de/smederee/tickets/LabelTest.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/test/scala/de/smederee/tickets/LabelTest.scala 2025-02-01 01:55:04.506884811 +0000 @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.tickets + +import cats.syntax.all._ +import de.smederee.tickets.Generators._ + +import munit._ +import org.scalacheck._ +import org.scalacheck.Prop._ + +final class LabelTest extends ScalaCheckSuite { + given Arbitrary[Label] = Arbitrary(genLabel) + + property("Eq must hold") { + forAll { (label: Label) => + assert(label === label, "Identical labels must be considered equal!") + } + } +} diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/tickets/MilestoneDescriptionTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/tickets/MilestoneDescriptionTest.scala --- old-smederee/modules/hub/src/test/scala/de/smederee/tickets/MilestoneDescriptionTest.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/test/scala/de/smederee/tickets/MilestoneDescriptionTest.scala 2025-02-01 01:55:04.506884811 +0000 @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.tickets + +import de.smederee.tickets.Generators._ + +import munit._ +import org.scalacheck._ +import org.scalacheck.Prop._ + +final class MilestoneDescriptionTest extends ScalaCheckSuite { + given Arbitrary[MilestoneDescription] = Arbitrary(genMilestoneDescription) + + test("MilestoneDescription.from must fail on empty input") { + assertEquals(MilestoneDescription.from(""), None) + } + + property("MilestoneDescription.from must succeed on valid input") { + forAll { (label: MilestoneDescription) => + val input = label.toString + assertEquals(MilestoneDescription.from(input), Option(label)) + } + } +} diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/tickets/MilestoneTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/tickets/MilestoneTest.scala --- old-smederee/modules/hub/src/test/scala/de/smederee/tickets/MilestoneTest.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/test/scala/de/smederee/tickets/MilestoneTest.scala 2025-02-01 01:55:04.506884811 +0000 @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.tickets + +import cats.syntax.all._ +import de.smederee.tickets.Generators._ + +import munit._ +import org.scalacheck._ +import org.scalacheck.Prop._ + +final class MilestoneTest extends ScalaCheckSuite { + given Arbitrary[Milestone] = Arbitrary(genMilestone) + + property("Eq must hold") { + forAll { (label: Milestone) => + assert(label === label, "Identical milestones must be considered equal!") + } + } +} diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/tickets/MilestoneTitleTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/tickets/MilestoneTitleTest.scala --- old-smederee/modules/hub/src/test/scala/de/smederee/tickets/MilestoneTitleTest.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/test/scala/de/smederee/tickets/MilestoneTitleTest.scala 2025-02-01 01:55:04.506884811 +0000 @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.tickets + +import de.smederee.tickets.Generators._ + +import munit._ +import org.scalacheck._ +import org.scalacheck.Prop._ + +final class MilestoneTitleTest extends ScalaCheckSuite { + given Arbitrary[MilestoneTitle] = Arbitrary(genMilestoneTitle) + + test("MilestoneTitle.from must fail on empty input") { + assertEquals(MilestoneTitle.from(""), None) + } + + property("MilestoneTitle.from must fail on too long input") { + forAll { (input: String) => + if (input.length > MilestoneTitle.MaxLength) + assertEquals(MilestoneTitle.from(input), None) + } + } + + property("MilestoneTitle.from must succeed on valid input") { + forAll { (label: MilestoneTitle) => + val input = label.toString + assertEquals(MilestoneTitle.from(input), Option(label)) + } + } +} diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/tickets/TicketContentTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/tickets/TicketContentTest.scala --- old-smederee/modules/hub/src/test/scala/de/smederee/tickets/TicketContentTest.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/test/scala/de/smederee/tickets/TicketContentTest.scala 2025-02-01 01:55:04.506884811 +0000 @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.tickets + +import munit._ +import org.scalacheck._ +import org.scalacheck.Prop._ + +final class TicketContentTest extends ScalaCheckSuite { + + property("TicketContent.from must only accept valid input") { + forAll { (input: String) => + if (input.nonEmpty) + assertEquals(TicketContent.from(input), Some(TicketContent(input))) + else + assertEquals(TicketContent.from(input), None) + } + } + +} diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/tickets/TicketNumberTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/tickets/TicketNumberTest.scala --- old-smederee/modules/hub/src/test/scala/de/smederee/tickets/TicketNumberTest.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/test/scala/de/smederee/tickets/TicketNumberTest.scala 2025-02-01 01:55:04.506884811 +0000 @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.tickets + +import munit._ +import org.scalacheck._ +import org.scalacheck.Prop._ + +final class TicketNumberTest extends ScalaCheckSuite { + + property("TicketNumber.from must only accept valid input") { + forAll { (integer: Int) => + if (integer > 0) + assertEquals(TicketNumber.from(integer), Some(TicketNumber(integer))) + else + assertEquals(TicketNumber.from(integer), None) + } + } + +} diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/tickets/TicketTitleTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/tickets/TicketTitleTest.scala --- old-smederee/modules/hub/src/test/scala/de/smederee/tickets/TicketTitleTest.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/test/scala/de/smederee/tickets/TicketTitleTest.scala 2025-02-01 01:55:04.506884811 +0000 @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.tickets + +import munit._ +import org.scalacheck._ +import org.scalacheck.Prop._ + +final class TicketTitleTest extends ScalaCheckSuite { + + property("TicketTitle.from must only accept valid input") { + forAll { (input: String) => + if (input.nonEmpty && input.length <= TicketTitle.MaxLength) + assertEquals(TicketTitle.from(input), Some(TicketTitle(input))) + else + assertEquals(TicketTitle.from(input), None) + } + } + +}