diff --git a/src/main/resources/db/migration/postgresql/V6_0__Multipieces_bis.sql b/src/main/resources/db/migration/postgresql/V6_0__Multipieces_bis.sql new file mode 100644 index 0000000..6252c42 --- /dev/null +++ b/src/main/resources/db/migration/postgresql/V6_0__Multipieces_bis.sql @@ -0,0 +1,2 @@ +drop index bis_piece_player_id_idx; +create index bis_piece_type_player_id_idx on bis(player_id, piece, piece_type); \ No newline at end of file diff --git a/src/main/resources/db/migration/sqlite/V6_0__Multipieces_bis.sql b/src/main/resources/db/migration/sqlite/V6_0__Multipieces_bis.sql new file mode 100644 index 0000000..6252c42 --- /dev/null +++ b/src/main/resources/db/migration/sqlite/V6_0__Multipieces_bis.sql @@ -0,0 +1,2 @@ +drop index bis_piece_player_id_idx; +create index bis_piece_type_player_id_idx on bis(player_id, piece, piece_type); \ No newline at end of file diff --git a/src/main/scala/me/arcanis/ffxivbis/http/BiSHelper.scala b/src/main/scala/me/arcanis/ffxivbis/http/BiSHelper.scala index 6dd2e4e..25b71b1 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/BiSHelper.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/BiSHelper.scala @@ -37,10 +37,13 @@ trait BiSHelper extends BisProviderHelper { } def putBiS(playerId: PlayerId, link: String) - (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = - downloadBiS(link, playerId.job).flatMap { bis => - Future.traverse(bis.pieces)(addPieceBiS(playerId, _)) - }.map(_ => ()) + (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = { + (storage ? DatabaseBiSHandler.RemovePiecesFromBiS(playerId)).flatMap { _ => + downloadBiS(link, playerId.job).flatMap { bis => + Future.traverse(bis.pieces)(addPieceBiS(playerId, _)) + }.map(_ => ()) + } + } def removePieceBiS(playerId: PlayerId, piece: Piece) (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] = diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerResponse.scala index 277baf7..2225f09 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerResponse.scala @@ -21,13 +21,15 @@ case class PlayerResponse( @Schema(description = "player loot priority", `type` = "number") priority: Option[Int]) { def toPlayer: Player = Player(-1, partyId, Job.withName(job), nick, - BiS(bis.getOrElse(Seq.empty).map(_.toPiece)), loot.getOrElse(Seq.empty).map(_.toLoot), + BiS(bis.getOrElse(Seq.empty).map(_.toPiece)), + loot.getOrElse(Seq.empty).map(_.toLoot), link, priority.getOrElse(0)) } object PlayerResponse { def fromPlayer(player: Player): PlayerResponse = PlayerResponse(player.partyId, player.job.toString, player.nick, - Some(player.bis.pieces.map(PieceResponse.fromPiece)), Some(player.loot.map(LootResponse.fromLoot)), + Some(player.bis.pieces.map(PieceResponse.fromPiece)), + Some(player.loot.map(LootResponse.fromLoot)), player.link, Some(player.priority)) } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/BiSView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/BiSView.scala index 501580b..87f740f 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/BiSView.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/view/BiSView.scala @@ -113,7 +113,7 @@ object BiSView { input(name:="add", id:="add", `type`:="submit", value:="add") ), - form(action:="/bis", method:="post")( + form(action:=s"/party/$partyId/bis", method:="post")( select(name:="player", id:="player", title:="player") (for (player <- party) yield option(player.playerId.toString)), input(name:="link", id:="link", placeholder:="player bis link", title:="link", `type`:="text"), diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/PlayerView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/PlayerView.scala index ebc1bb0..932b582 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/PlayerView.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/view/PlayerView.scala @@ -47,8 +47,8 @@ class PlayerView(override val storage: ActorRef, override val ariyala: ActorRef) post { formFields("nick".as[String], "job".as[String], "priority".as[Int].?, "link".as[String].?, "action".as[String]) { (nick, job, maybePriority, maybeLink, action) => - onComplete(modifyPartyCall(partyId, nick, job, maybePriority, maybeLink, action)) { - case _ => redirect(s"/party/$partyId/players", StatusCodes.Found) + onComplete(modifyPartyCall(partyId, nick, job, maybePriority, maybeLink, action)) { _ => + redirect(s"/party/$partyId/players", StatusCodes.Found) } } } @@ -62,7 +62,7 @@ class PlayerView(override val storage: ActorRef, override val ariyala: ActorRef) (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = { def maybePlayerId = PlayerId(partyId, Some(nick), Some(job)) def player(playerId: PlayerId) = - Player(-1, partyId, playerId.job, playerId.nick, BiS(), Seq.empty, maybeLink, maybePriority.getOrElse(0)) + Player(-1, partyId, playerId.job, playerId.nick, BiS.empty, Seq.empty, maybeLink, maybePriority.getOrElse(0)) (action, maybePlayerId) match { case ("add", Some(playerId)) => addPlayer(player(playerId)).map(_ => ()) diff --git a/src/main/scala/me/arcanis/ffxivbis/models/BiS.scala b/src/main/scala/me/arcanis/ffxivbis/models/BiS.scala index fca30ca..bfe2762 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/BiS.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/BiS.scala @@ -8,21 +8,7 @@ */ package me.arcanis.ffxivbis.models -case class BiS(weapon: Option[Piece], - head: Option[Piece], - body: Option[Piece], - hands: Option[Piece], - waist: Option[Piece], - legs: Option[Piece], - feet: Option[Piece], - ears: Option[Piece], - neck: Option[Piece], - wrist: Option[Piece], - leftRing: Option[Piece], - rightRing: Option[Piece]) { - - val pieces: Seq[Piece] = - Seq(weapon, head, body, hands, waist, legs, feet, ears, neck, wrist, leftRing, rightRing).flatten +case class BiS(pieces: Seq[Piece]) { def hasPiece(piece: Piece): Boolean = piece match { case upgrade: PieceUpgrade => upgrades.contains(upgrade) @@ -31,50 +17,27 @@ case class BiS(weapon: Option[Piece], def upgrades: Map[PieceUpgrade, Int] = pieces.groupBy(_.upgrade).foldLeft(Map.empty[PieceUpgrade, Int]) { - case (acc, (Some(k), v)) => acc + (k -> v.length) + case (acc, (Some(k), v)) => acc + (k -> v.size) case (acc, _) => acc } withDefaultValue 0 - def withPiece(piece: Piece): BiS = copyWithPiece(piece.piece, Some(piece)) - def withoutPiece(piece: Piece): BiS = copyWithPiece(piece.piece, None) + def withPiece(piece: Piece): BiS = copy(pieces :+ piece) + def withoutPiece(piece: Piece): BiS = copy(pieces.filterNot(_.strictEqual(piece))) - private def copyWithPiece(name: String, piece: Option[Piece]): BiS = { - val params = Map( - "weapon" -> weapon, - "head" -> head, - "body" -> body, - "hands" -> hands, - "waist" -> waist, - "legs" -> legs, - "feet" -> feet, - "ears" -> ears, - "neck" -> neck, - "wrist" -> wrist, - "left ring" -> leftRing, - "right ring" -> rightRing - ) + (name -> piece) - BiS(params) + override def equals(obj: Any): Boolean = { + def comparePieces(left: Seq[Piece], right: Seq[Piece]): Boolean = + left.groupBy(identity).view.mapValues(_.size).forall { + case (key, count) => right.count(_.strictEqual(key)) == count + } + + obj match { + case left: BiS => comparePieces(left.pieces, pieces) + case _ => false + } } } object BiS { - def apply(data: Map[String, Option[Piece]]): BiS = - BiS( - data.get("weapon").flatten, - data.get("head").flatten, - data.get("body").flatten, - data.get("hands").flatten, - data.get("waist").flatten, - data.get("legs").flatten, - data.get("feet").flatten, - data.get("ears").flatten, - data.get("neck").flatten, - data.get("wrist").flatten, - data.get("left ring").flatten, - data.get("right ring").flatten) - def apply(): BiS = BiS(Seq.empty) - - def apply(pieces: Seq[Piece]): BiS = - BiS(pieces.map(piece => piece.piece -> Some(piece)).toMap) + def empty: BiS = BiS(Seq.empty) } diff --git a/src/main/scala/me/arcanis/ffxivbis/models/Job.scala b/src/main/scala/me/arcanis/ffxivbis/models/Job.scala index 6135f1c..415eb6f 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/Job.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/Job.scala @@ -9,6 +9,7 @@ package me.arcanis.ffxivbis.models object Job { + sealed trait RightSide object AccessoriesDex extends RightSide object AccessoriesInt extends RightSide @@ -26,6 +27,7 @@ object Job { object BodyRanges extends LeftSide sealed trait Job { + def leftSide: LeftSide def rightSide: RightSide diff --git a/src/main/scala/me/arcanis/ffxivbis/models/Loot.scala b/src/main/scala/me/arcanis/ffxivbis/models/Loot.scala index 056a196..1f1af48 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/Loot.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/Loot.scala @@ -11,5 +11,6 @@ package me.arcanis.ffxivbis.models import java.time.Instant case class Loot(playerId: Long, piece: Piece, timestamp: Instant, isFreeLoot: Boolean) { + def isFreeLootToString: String = if (isFreeLoot) "yes" else "no" } diff --git a/src/main/scala/me/arcanis/ffxivbis/models/Party.scala b/src/main/scala/me/arcanis/ffxivbis/models/Party.scala index 81894f4..1976fc1 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/Party.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/Party.scala @@ -36,6 +36,7 @@ case class Party(partyDescription: PartyDescription, rules: Seq[String], players } object Party { + def apply(party: PartyDescription, config: Config, players: Map[Long, Player], bis: Seq[Loot], loot: Seq[Loot]): Party = { val bisByPlayer = bis.groupBy(_.playerId).view.mapValues(piece => BiS(piece.map(_.piece))) diff --git a/src/main/scala/me/arcanis/ffxivbis/models/PartyDescription.scala b/src/main/scala/me/arcanis/ffxivbis/models/PartyDescription.scala index 8279560..ea87e53 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/PartyDescription.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/PartyDescription.scala @@ -9,9 +9,11 @@ package me.arcanis.ffxivbis.models case class PartyDescription(partyId: String, partyAlias: Option[String]) { + def alias: String = partyAlias.getOrElse(partyId) } object PartyDescription { + def empty(partyId: String): PartyDescription = PartyDescription(partyId, None) } \ No newline at end of file diff --git a/src/main/scala/me/arcanis/ffxivbis/models/Piece.scala b/src/main/scala/me/arcanis/ffxivbis/models/Piece.scala index 29da709..c50edb8 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/Piece.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/Piece.scala @@ -8,7 +8,8 @@ */ package me.arcanis.ffxivbis.models -sealed trait Piece { +sealed trait Piece extends Equals { + def pieceType: PieceType.PieceType def job: Job.Job def piece: String @@ -22,6 +23,9 @@ sealed trait Piece { case _: PieceBody => Some(BodyUpgrade) case _: PieceWeapon => Some(WeaponUpgrade) } + + // used for ring comparison + def strictEqual(obj: Any): Boolean = equals(obj) } trait PieceAccessory extends Piece @@ -78,10 +82,16 @@ case class Wrist(override val pieceType: PieceType.PieceType, override val job: case class Ring(override val pieceType: PieceType.PieceType, override val job: Job.Job, override val piece: String = "ring") extends PieceAccessory { def withJob(other: Job.Job): Piece = copy(job = other) + override def equals(obj: Any): Boolean = obj match { case Ring(thatPieceType, thatJob, _) => (thatPieceType == pieceType) && (thatJob == job) case _ => false } + + override def strictEqual(obj: Any): Boolean = obj match { + case ring: Ring => equals(obj) && (ring.piece == this.piece) + case _ => false + } } case object AccessoryUpgrade extends PieceUpgrade { diff --git a/src/main/scala/me/arcanis/ffxivbis/models/PieceType.scala b/src/main/scala/me/arcanis/ffxivbis/models/PieceType.scala index cc57618..43c0b61 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/PieceType.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/PieceType.scala @@ -1,6 +1,7 @@ package me.arcanis.ffxivbis.models object PieceType { + sealed trait PieceType case object Crafted extends PieceType diff --git a/src/main/scala/me/arcanis/ffxivbis/models/PlayerId.scala b/src/main/scala/me/arcanis/ffxivbis/models/PlayerId.scala index 6e2ad7b..6fbdf68 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/PlayerId.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/PlayerId.scala @@ -12,6 +12,7 @@ import scala.util.Try import scala.util.matching.Regex trait PlayerIdBase { + def job: Job.Job def nick: String @@ -21,6 +22,7 @@ trait PlayerIdBase { case class PlayerId(partyId: String, job: Job.Job, nick: String) extends PlayerIdBase object PlayerId { + def apply(partyId: String, maybeNick: Option[String], maybeJob: Option[String]): Option[PlayerId] = (maybeNick, maybeJob) match { case (Some(nick), Some(job)) => Try(PlayerId(partyId, Job.withName(job), nick)).toOption diff --git a/src/main/scala/me/arcanis/ffxivbis/models/PlayerIdWithCounters.scala b/src/main/scala/me/arcanis/ffxivbis/models/PlayerIdWithCounters.scala index f28c6bc..3e3627d 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/PlayerIdWithCounters.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/PlayerIdWithCounters.scala @@ -38,6 +38,7 @@ case class PlayerIdWithCounters(partyId: String, } object PlayerIdWithCounters { + private case class PlayerCountersComparator(values: Int*) { def >(that: PlayerCountersComparator): Boolean = { @scala.annotation.tailrec diff --git a/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabaseBiSHandler.scala b/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabaseBiSHandler.scala index b9d2eb6..50a6e36 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabaseBiSHandler.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabaseBiSHandler.scala @@ -29,6 +29,10 @@ trait DatabaseBiSHandler { this: Database => case RemovePieceFromBiS(playerId, piece) => val client = sender() profile.deletePieceBiS(playerId, piece).pipeTo(client) + + case RemovePiecesFromBiS(playerId) => + val client = sender() + profile.deletePiecesBiS(playerId).pipeTo(client) } } @@ -40,4 +44,7 @@ object DatabaseBiSHandler { case class RemovePieceFromBiS(playerId: PlayerId, piece: Piece) extends Database.DatabaseRequest { override def partyId: String = playerId.partyId } + case class RemovePiecesFromBiS(playerId: PlayerId) extends Database.DatabaseRequest { + override def partyId: String = playerId.partyId + } } diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala index 91c369b..cf3015b 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala @@ -46,14 +46,21 @@ trait BiSProfile { this: DatabaseProfile => def deletePieceBiSById(piece: Piece)(playerId: Long): Future[Int] = db.run(pieceBiS(BiSRep.fromPiece(playerId, piece)).delete) + def deletePiecesBiSById(playerId: Long): Future[Int] = + db.run(piecesBiS(Seq(playerId)).delete) def getPiecesBiSById(playerId: Long): Future[Seq[Loot]] = getPiecesBiSById(Seq(playerId)) def getPiecesBiSById(playerIds: Seq[Long]): Future[Seq[Loot]] = db.run(piecesBiS(playerIds).result).map(_.map(_.toLoot)) def insertPieceBiSById(piece: Piece)(playerId: Long): Future[Int] = - db.run(bisTable.insertOrUpdate(BiSRep.fromPiece(playerId, piece))) + getPiecesBiSById(playerId).flatMap { + case pieces if pieces.exists(loot => loot.piece.strictEqual(piece)) => Future.successful(0) + case _ => db.run(bisTable.insertOrUpdate(BiSRep.fromPiece(playerId, piece))) + } private def pieceBiS(piece: BiSRep) = - piecesBiS(Seq(piece.playerId)).filter(_.piece === piece.piece) + piecesBiS(Seq(piece.playerId)).filter { stored => + (stored.piece === piece.piece) && (stored.pieceType === piece.pieceType) + } private def piecesBiS(playerIds: Seq[Long]) = bisTable.filter(_.playerId.inSet(playerIds.toSet)) } diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/DatabaseProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/DatabaseProfile.scala index 0c2a2f8..45658ef 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/DatabaseProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/DatabaseProfile.scala @@ -36,6 +36,8 @@ class DatabaseProfile(context: ExecutionContext, config: Config) // generic bis api def deletePieceBiS(playerId: PlayerId, piece: Piece): Future[Int] = byPlayerId(playerId, deletePieceBiSById(piece)) + def deletePiecesBiS(playerId: PlayerId): Future[Int] = + byPlayerId(playerId, deletePiecesBiSById) def getPiecesBiS(playerId: PlayerId): Future[Seq[Loot]] = byPlayerId(playerId, getPiecesBiSById) def getPiecesBiS(partyId: String): Future[Seq[Loot]] = @@ -57,7 +59,7 @@ class DatabaseProfile(context: ExecutionContext, config: Config) byPlayerId(playerId, insertPieceById(loot)) private def byPartyId[T](partyId: String, callback: Seq[Long] => Future[T]): Future[T] = - getPlayers(partyId).map(callback).flatten + getPlayers(partyId).flatMap(callback) private def byPlayerId[T](playerId: PlayerId, callback: Long => Future[T]): Future[T] = getPlayer(playerId).flatMap { case Some(id) => callback(id) diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala index e04664b..568d305 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala @@ -19,7 +19,7 @@ trait PlayersProfile { this: DatabaseProfile => nick: String, job: String, link: Option[String], priority: Int) { def toPlayer: Player = Player(playerId.getOrElse(-1), partyId, Job.withName(job), nick, - BiS(Seq.empty), List.empty, link, priority) + BiS.empty, Seq.empty, link, priority) } object PlayerRep { def fromPlayer(player: Player, id: Option[Long]): PlayerRep = diff --git a/src/test/scala/me/arcanis/ffxivbis/Fixtures.scala b/src/test/scala/me/arcanis/ffxivbis/Fixtures.scala index 1b98ed5..a672992 100644 --- a/src/test/scala/me/arcanis/ffxivbis/Fixtures.scala +++ b/src/test/scala/me/arcanis/ffxivbis/Fixtures.scala @@ -26,6 +26,7 @@ object Fixtures { lazy val lootWeapon: Piece = Weapon(pieceType = PieceType.Tome, Job.AnyJob) lazy val lootBody: Piece = Body(pieceType = PieceType.Savage, Job.AnyJob) + lazy val lootBodyCrafted: Piece = Body(pieceType = PieceType.Crafted, Job.AnyJob) lazy val lootHands: Piece = Hands(pieceType = PieceType.Tome, Job.AnyJob) lazy val lootWaist: Piece = Waist(pieceType = PieceType.Tome, Job.AnyJob) lazy val lootLegs: Piece = Legs(pieceType = PieceType.Savage, Job.AnyJob) @@ -40,7 +41,7 @@ object Fixtures { lazy val partyId2: String = Party.randomPartyId lazy val playerEmpty: Player = - Player(1, partyId, Job.DNC, "Siuan Sanche", BiS(), Seq.empty, Some(link)) + Player(1, partyId, Job.DNC, "Siuan Sanche", BiS.empty, Seq.empty, Some(link)) lazy val playerWithBiS: Player = playerEmpty.copy(bis = bis) lazy val userPassword: String = "password" diff --git a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpointTest.scala b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpointTest.scala index 27c7fe7..e63a678 100644 --- a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpointTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpointTest.scala @@ -10,10 +10,11 @@ import akka.testkit.TestKit import com.typesafe.config.Config import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.http.api.v1.json._ -import me.arcanis.ffxivbis.models.BiS +import me.arcanis.ffxivbis.models.{BiS, Body, Job, PieceType} import me.arcanis.ffxivbis.service.bis.BisProvider import me.arcanis.ffxivbis.service.{PartyService, impl} import me.arcanis.ffxivbis.storage.Migration +import me.arcanis.ffxivbis.utils.Compare import org.scalatest.{Matchers, WordSpec} import scala.concurrent.Await @@ -48,6 +49,15 @@ class BiSEndpointTest extends WordSpec Settings.clearDatabase(system.settings.config) } + private def compareBiSResponse(actual: PlayerResponse, expected: PlayerResponse): Unit = { + actual.partyId shouldEqual expected.partyId + actual.nick shouldEqual expected.nick + actual.job shouldEqual expected.job + Compare.seqEquals(actual.bis.get, expected.bis.get) shouldEqual true + actual.link shouldEqual expected.link + actual.priority shouldEqual expected.priority + } + "api v1 bis endpoint" must { "create best in slot set from ariyala" in { @@ -61,11 +71,13 @@ class BiSEndpointTest extends WordSpec "return best in slot set" in { val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) - val response = Seq(PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis)))) + val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) Get(uri).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - responseAs[Seq[PlayerResponse]] shouldEqual response + val actual = responseAs[Seq[PlayerResponse]] + actual.length shouldEqual 1 + actual.foreach(compareBiSResponse(_, response)) } } @@ -80,11 +92,13 @@ class BiSEndpointTest extends WordSpec val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) val bis = BiS(Fixtures.bis.pieces.filterNot(_ == Fixtures.lootBody)) - val response = Seq(PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(bis)))) + val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(bis))) Get(uri).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - responseAs[Seq[PlayerResponse]] shouldEqual response + val actual = responseAs[Seq[PlayerResponse]] + actual.length shouldEqual 1 + actual.foreach(compareBiSResponse(_, response)) } } @@ -98,11 +112,102 @@ class BiSEndpointTest extends WordSpec } val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) - val response = Seq(PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis)))) + val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) Get(uri).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - responseAs[Seq[PlayerResponse]] shouldEqual response + val actual = responseAs[Seq[PlayerResponse]] + actual.length shouldEqual 1 + actual.foreach(compareBiSResponse(_, response)) + } + } + + "do not allow to add same item to best in slot set" in { + val piece = PieceResponse.fromPiece(Fixtures.lootBody.withJob(Job.DNC)) + val entity = PieceActionResponse(ApiAction.add, piece, playerId, None) + + Post(endpoint, entity).withHeaders(auth) ~> route ~> check { + status shouldEqual StatusCodes.Accepted + responseAs[String] shouldEqual "" + } + + val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) + val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) + + Get(uri).withHeaders(auth) ~> route ~> check { + status shouldEqual StatusCodes.OK + val actual = responseAs[Seq[PlayerResponse]] + actual.length shouldEqual 1 + actual.foreach(compareBiSResponse(_, response)) + } + } + + "allow to add item with another type to best in slot set" in { + val piece = PieceResponse.fromPiece(Fixtures.lootBodyCrafted.withJob(Job.DNC)) + val entity = PieceActionResponse(ApiAction.add, piece, playerId, None) + + Post(endpoint, entity).withHeaders(auth) ~> route ~> check { + status shouldEqual StatusCodes.Accepted + responseAs[String] shouldEqual "" + } + + val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) + val bis = Fixtures.bis.withPiece(piece.toPiece) + val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(bis))) + + Get(uri).withHeaders(auth) ~> route ~> check { + status shouldEqual StatusCodes.OK + val actual = responseAs[Seq[PlayerResponse]] + actual.length shouldEqual 1 + actual.foreach(compareBiSResponse(_, response)) + } + } + + "remove only specific item from best in slot set" in { + val piece = PieceResponse.fromPiece(Fixtures.lootBodyCrafted.withJob(Job.DNC)) + val entity = PieceActionResponse(ApiAction.remove, piece, playerId, None) + + Post(endpoint, entity).withHeaders(auth) ~> route ~> check { + status shouldEqual StatusCodes.Accepted + responseAs[String] shouldEqual "" + } + + val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) + val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) + + Get(uri).withHeaders(auth) ~> route ~> check { + status shouldEqual StatusCodes.OK + val actual = responseAs[Seq[PlayerResponse]] + actual.length shouldEqual 1 + actual.foreach(compareBiSResponse(_, response)) + } + } + + "totaly replace player bis" in { + // add random item first + val piece = PieceResponse.fromPiece(Fixtures.lootBodyCrafted.withJob(Job.DNC)) + val entity = PieceActionResponse(ApiAction.add, piece, playerId, None) + + Post(endpoint, entity).withHeaders(auth) ~> route ~> check { + status shouldEqual StatusCodes.Accepted + responseAs[String] shouldEqual "" + } + + val bisEntity = PlayerBiSLinkResponse(Fixtures.link, playerId) + + Put(endpoint, bisEntity).withHeaders(auth) ~> route ~> check { + status shouldEqual StatusCodes.Created + responseAs[String] shouldEqual "" + } + + val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) + val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) + + Get(uri).withHeaders(auth) ~> route ~> check { + status shouldEqual StatusCodes.OK + val actual = responseAs[Seq[PlayerResponse]] + actual.length shouldEqual 1 + actual.foreach(compareBiSResponse(_, response)) } } diff --git a/src/test/scala/me/arcanis/ffxivbis/models/BiSTest.scala b/src/test/scala/me/arcanis/ffxivbis/models/BiSTest.scala index 647f7c4..ef03846 100644 --- a/src/test/scala/me/arcanis/ffxivbis/models/BiSTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/models/BiSTest.scala @@ -29,18 +29,14 @@ class BiSTest extends WordSpecLike with Matchers with BeforeAndAfterAll { val bis = BiS(Seq(Fixtures.lootLegs)) val newBis = bis.withPiece(Fixtures.lootHands) - newBis.legs shouldEqual Some(Fixtures.lootLegs) - newBis.hands shouldEqual Some(Fixtures.lootHands) - newBis.pieces.length shouldEqual 2 + newBis shouldEqual BiS(Seq(Fixtures.lootLegs, Fixtures.lootHands)) } "create copy without piece" in { val bis = BiS(Seq(Fixtures.lootHands, Fixtures.lootLegs)) val newBis = bis.withoutPiece(Fixtures.lootHands) - newBis.legs shouldEqual Some(Fixtures.lootLegs) - newBis.hands shouldEqual None - newBis.pieces.length shouldEqual 1 + newBis shouldEqual BiS(Seq(Fixtures.lootLegs)) } "ignore upgrade on modification" in { diff --git a/src/test/scala/me/arcanis/ffxivbis/service/DatabaseBiSHandlerTest.scala b/src/test/scala/me/arcanis/ffxivbis/service/DatabaseBiSHandlerTest.scala index 0c29e5e..c849169 100644 --- a/src/test/scala/me/arcanis/ffxivbis/service/DatabaseBiSHandlerTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/service/DatabaseBiSHandlerTest.scala @@ -72,7 +72,7 @@ class DatabaseBiSHandlerTest database ! impl.DatabaseBiSHandler.GetBiS(Fixtures.playerEmpty.partyId, None) expectMsgPF(timeout) { - case party: Seq[_] if partyBiSCompare(party, Seq(newPiece)) => () + case party: Seq[_] if partyBiSCompare(party, Seq(Fixtures.lootHands, newPiece)) => () } } diff --git a/src/test/scala/me/arcanis/ffxivbis/service/LootSelectorTest.scala b/src/test/scala/me/arcanis/ffxivbis/service/LootSelectorTest.scala index 01cc5e2..a914076 100644 --- a/src/test/scala/me/arcanis/ffxivbis/service/LootSelectorTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/service/LootSelectorTest.scala @@ -18,8 +18,8 @@ class LootSelectorTest extends TestKit(ActorSystem("lootselector")) import me.arcanis.ffxivbis.utils.Converters._ private var default: Party = Party(PartyDescription.empty(Fixtures.partyId), Settings.config(Map.empty), Map.empty, Seq.empty, Seq.empty) - private var dnc: Player = Player(-1, Fixtures.partyId, Job.DNC, "a nick", BiS(), Seq.empty, Some(Fixtures.link)) - private var drg: Player = Player(-1, Fixtures.partyId, Job.DRG, "another nick", BiS(), Seq.empty, Some(Fixtures.link2)) + private var dnc: Player = Player(-1, Fixtures.partyId, Job.DNC, "a nick", BiS.empty, Seq.empty, Some(Fixtures.link)) + private var drg: Player = Player(-1, Fixtures.partyId, Job.DRG, "another nick", BiS.empty, Seq.empty, Some(Fixtures.link2)) private val timeout: FiniteDuration = 60 seconds override def beforeAll(): Unit = { @@ -51,7 +51,7 @@ class LootSelectorTest extends TestKit(ActorSystem("lootselector")) "suggest upgrade" in { val party = default.withPlayer( dnc.withBiS( - Some(dnc.bis.copy(weapon = Some(Weapon(pieceType = PieceType.Tome, Job.DNC)))) + Some(dnc.bis.withPiece(Weapon(pieceType = PieceType.Tome, Job.DNC))) ) )