diff --git a/.scalafmt.conf b/.scalafmt.conf index 6741dea..b53bd23 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = 3.7.2 +version = 3.8.2 runner.dialect = scala3 align.preset = more maxColumn = 120 diff --git a/db/src/main/scala/org/updraft0/controltower/db/query/map.scala b/db/src/main/scala/org/updraft0/controltower/db/query/map.scala index d054d4f..d25451e 100644 --- a/db/src/main/scala/org/updraft0/controltower/db/query/map.scala +++ b/db/src/main/scala/org/updraft0/controltower/db/query/map.scala @@ -93,6 +93,19 @@ object map: .run(quote { mapWormholeConnection.filter(whc => whc.mapId == lift(mapId) && whc.id == lift(connectionId)) }) .map(_.headOption) + def getWormholeConnections( + mapId: MapId, + connectionIds: Chunk[ConnectionId], + isDeleted: Boolean + ): DbOperation[List[MapWormholeConnection]] = + ctx.run( + quote( + mapWormholeConnection.filter(whc => + whc.mapId == lift(mapId) && whc.isDeleted == lift(isDeleted) && liftQuery(connectionIds).contains(whc.id) + ) + ) + ) + def getWormholeSystemNames: DbOperation[Map[String, Long]] = ctx .run(quote(sde.schema.solarSystem.map(ss => ss.name -> ss.id))) @@ -101,9 +114,9 @@ object map: def getWormholeTypeNames: DbOperation[List[(String, Long)]] = ctx.run(quote { sde.schema.itemType.filter(_.groupId == lift(WormholeGroupId)).map(it => it.name -> it.id) }) - def getMapSystem(mapId: MapId, systemId: Long) = + def getMapSystem(mapId: MapId, systemId: Long): DbOperation[Option[MapSystem]] = ctx - .run(quote { mapSystem.filter(ms => ms.mapId == lift(mapId) && ms.systemId == lift(systemId)) }) + .run(quote(mapSystem.filter(ms => ms.mapId == lift(mapId) && ms.systemId == lift(systemId)))) .map(_.headOption) private inline def findWormholeMapSignatures(mapId: MapId, systemId: SystemId) = diff --git a/db/src/main/scala/org/updraft0/controltower/db/query/sde.scala b/db/src/main/scala/org/updraft0/controltower/db/query/sde.scala index a3752fa..8331c50 100644 --- a/db/src/main/scala/org/updraft0/controltower/db/query/sde.scala +++ b/db/src/main/scala/org/updraft0/controltower/db/query/sde.scala @@ -63,6 +63,9 @@ object sde: def upsertRegion(region: Region): DbOperation[Long] = ctx.run(insert(schema.region, lift(region)).onConflictIgnore) + def upsertItemName(name: ItemName): DbOperation[Long] = + ctx.run(insert(schema.itemName, lift(name)).onConflictIgnore) + // inserts def insertDogmaAttributeCategories(categories: Vector[DogmaAttributeCategory]): DbOperation[Long] = ctx.run(insertAll(schema.dogmaAttributeCategory, categories), BatchRows).map(_.sum) @@ -130,7 +133,16 @@ object sde: // queries def getLatestVersion: DbOperation[Option[Version]] = - ctx.run(quote(quote(schema.version).sortBy(_.createdAt)(Ord.desc).take(1))).map(_.headOption) + ctx.run(quote(schema.version.sortBy(_.createdAt)(Ord.desc).take(1))).map(_.headOption) + + def getRegions: DbOperation[List[Region]] = + ctx.run(quote(schema.region)) + + def getConstellations: DbOperation[List[Constellation]] = + ctx.run(quote(schema.constellation)) + + def getSolarSystem: DbOperation[List[SolarSystem]] = + ctx.run(quote(schema.solarSystem)) // deletes def deleteConstellation: DbOperation[Long] = diff --git a/esi-client/src/main/scala/org/updraft0/esi/client/client.scala b/esi-client/src/main/scala/org/updraft0/esi/client/client.scala index 85ccac5..cba2738 100644 --- a/esi-client/src/main/scala/org/updraft0/esi/client/client.scala +++ b/esi-client/src/main/scala/org/updraft0/esi/client/client.scala @@ -68,8 +68,8 @@ object EsiClient: def apply(sttp: SttpClient): ZIO[Config, Throwable, EsiClient] = for config <- ZIO.service[Config] - // FIXME wait until improvements land in generic aliases in layers after ZIO 2.1.1 - // sttp <- ZIO.service[SttpClient] + // FIXME wait until improvements land in generic aliases in layers after ZIO 2.1.1 + // sttp <- ZIO.service[SttpClient] yield new EsiClient(config, zioLoggingBackend(sttp), SttpClientInterpreter()) inline def withZIO[R, E, A](inline f: EsiClient => ZIO[R, E, A]): ZIO[R & EsiClient, E, A] = diff --git a/esi-client/src/main/scala/org/updraft0/esi/client/sde.scala b/esi-client/src/main/scala/org/updraft0/esi/client/sde.scala index 6cc1f0f..5d94248 100644 --- a/esi-client/src/main/scala/org/updraft0/esi/client/sde.scala +++ b/esi-client/src/main/scala/org/updraft0/esi/client/sde.scala @@ -53,6 +53,6 @@ object SdeClient: def apply(sttp: SttpClient): ZIO[Config, Throwable, SdeClient] = for config <- ZIO.service[Config] - // FIXME + // FIXME // sttp <- ZIO.service[SttpClient] yield apply(config.base, zioLoggingBackend(sttp), SttpClientInterpreter()) diff --git a/mini-reactive/src/test/scala/org/updraft0/minireactive/ReactiveEntitySpec.scala b/mini-reactive/src/test/scala/org/updraft0/minireactive/ReactiveEntitySpec.scala index 811e949..f1a3fe0 100644 --- a/mini-reactive/src/test/scala/org/updraft0/minireactive/ReactiveEntitySpec.scala +++ b/mini-reactive/src/test/scala/org/updraft0/minireactive/ReactiveEntitySpec.scala @@ -26,11 +26,11 @@ object ReactiveEntitySpec extends ZIOSpecDefault: override def spec = suite("counter reactive entity")( test("can respond to a sequence of get/increment messages"): - for - entity <- MiniReactive(CounterEntity, MiniReactiveConfig(16, Duration.Infinity)) - inQ <- entity.enqueue("test") - outQ <- entity.subscribe("test") - _ <- ZStream(Get, Incr, Get, Incr, Incr, Get, Incr, Get).mapZIO(inQ.offer).runDrain.forkScoped - res <- ZStream.fromQueue(outQ).take(4).runCollect - yield assertTrue(res == Chunk(0, 1, 3, 4).map(CounterReply.Current.apply)) + for + entity <- MiniReactive(CounterEntity, MiniReactiveConfig(16, Duration.Infinity)) + inQ <- entity.enqueue("test") + outQ <- entity.subscribe("test") + _ <- ZStream(Get, Incr, Get, Incr, Incr, Get, Incr, Get).mapZIO(inQ.offer).runDrain.forkScoped + res <- ZStream.fromQueue(outQ).take(4).runCollect + yield assertTrue(res == Chunk(0, 1, 3, 4).map(CounterReply.Current.apply)) ) diff --git a/protocol/shared/src/test/scala/org/updraft0/controltower/protocol/ReferenceProtocolSpec.scala b/protocol/shared/src/test/scala/org/updraft0/controltower/protocol/ReferenceProtocolSpec.scala index 592fff6..3781431 100644 --- a/protocol/shared/src/test/scala/org/updraft0/controltower/protocol/ReferenceProtocolSpec.scala +++ b/protocol/shared/src/test/scala/org/updraft0/controltower/protocol/ReferenceProtocolSpec.scala @@ -14,31 +14,31 @@ object ReferenceProtocolSpec extends ZIOSpecDefault: def spec = suite("reference protocol")( test("can encode/decode StationOperation"): - val value = StationOperation( - operationId = 42, - operationName = "Law School", - services = Array( - StationService(id = 5, name = "Reprocessing Plant"), - StationService(id = 9, name = "Stock Exchange") - ) + val value = StationOperation( + operationId = 42, + operationName = "Law School", + services = Array( + StationService(id = 5, name = "Reprocessing Plant"), + StationService(id = 9, name = "Stock Exchange") ) - val json = Json.Obj( - "operationId" -> Json.Num(42), - "operationName" -> Json.Str("Law School"), - "services" -> Json.Arr( - Json.Obj("id" -> Json.Num(5), "name" -> Json.Str("Reprocessing Plant")), - Json.Obj("id" -> Json.Num(9), "name" -> Json.Str("Stock Exchange")) - ) + ) + val json = Json.Obj( + "operationId" -> Json.Num(42), + "operationName" -> Json.Str("Law School"), + "services" -> Json.Arr( + Json.Obj("id" -> Json.Num(5), "name" -> Json.Str("Reprocessing Plant")), + Json.Obj("id" -> Json.Num(9), "name" -> Json.Str("Stock Exchange")) ) + ) - val res = json.as[StationOperation] + val res = json.as[StationOperation] - assertTrue( - res.map(_.operationId) == Right(42), - res.map(_.operationName) == Right("Law School"), - res.map(_.services.toList) == Right( - List(StationService(5, "Reprocessing Plant"), StationService(9, "Stock Exchange")) - ), - value.toJsonAST == Right(json) - ) + assertTrue( + res.map(_.operationId) == Right(42), + res.map(_.operationName) == Right("Law School"), + res.map(_.services.toList) == Right( + List(StationService(5, "Reprocessing Plant"), StationService(9, "Stock Exchange")) + ), + value.toJsonAST == Right(json) + ) ) diff --git a/server/src/main/scala/org/updraft0/controltower/server/map/MapReactive.scala b/server/src/main/scala/org/updraft0/controltower/server/map/MapReactive.scala index f9d69ab..b74b41d 100644 --- a/server/src/main/scala/org/updraft0/controltower/server/map/MapReactive.scala +++ b/server/src/main/scala/org/updraft0/controltower/server/map/MapReactive.scala @@ -24,6 +24,8 @@ private[map] case class MapSolarSystem( systemId: SystemId, name: String, whClass: WormholeClass, + regionId: Long, + constellationId: Long, gates: Map[SystemId, Long] ) derives CanEqual @@ -432,12 +434,8 @@ object MapEntity extends ReactiveEntity[MapEnv, MapId, MapState, Identified[MapR _ <- query.map.upsertMapSystemDisplay( model.MapSystemDisplay(mapId, add.systemId, add.displayData.displayType, add.displayData) ) - sys <- loadSingleSystem(mapId, add.systemId) - conns <- MapQueries.getWormholeConnectionsWithSigsBySystemId(mapId, add.systemId) - ranks <- MapQueries.getWormholeConnectionRanksForSystem(mapId, add.systemId) - yield withState(state.updateOne(sys.sys.systemId, sys, conns, ranks))(s => - s -> broadcast(s.systemSnapshot(sys.sys.systemId)) - ) + resp <- reloadSystemSnapshot(mapId, add.systemId)(state) + yield resp private def insertSystemConnection( mapId: MapId, @@ -584,13 +582,8 @@ object MapEntity extends ReactiveEntity[MapEnv, MapId, MapState, Identified[MapR .map(lookupExisting(mapSystem, _)) .map((prevOpt, newSig) => toModelSignature(now, sessionId, mapSystemId, prevOpt, newSig)) )(query.map.upsertMapSystemSignature) - // reload the whole system - sys <- loadSingleSystem(mapId, uss.systemId) - conns <- MapQueries.getWormholeConnectionsWithSigsBySystemId(mapId, uss.systemId) - ranks <- MapQueries.getWormholeConnectionRanksForSystem(mapId, uss.systemId) - yield withState(state.updateOne(sys.sys.systemId, sys, conns, ranks))(s => - s -> broadcast(s.systemSnapshot(sys.sys.systemId)) - ) + resp <- reloadSystemSnapshot(mapId, uss.systemId)(state) + yield resp private def removeSystemAndConnection( mapId: MapId, @@ -653,18 +646,52 @@ object MapEntity extends ReactiveEntity[MapEnv, MapId, MapState, Identified[MapR now: Instant, rss: MapRequest.RemoveSystemSignatures ) = - // TODO need to recompute state for all the connection ids :) <-- aka if the connection is being deleted, - // remove the whole connection on both sides! for + // gather all connection ids present in the signatures about to be deleted connectionIds <- query.map.getSystemConnectionIdsInSignatures(mapId, rss.systemId, rss.signatures.map(_.toChunk)) - _ <- query.map.deleteMapWormholeConnections(Chunk.fromIterable(connectionIds), sessionId.characterId) + // gather all system ids those connections affect + systemIdsToRefresh = Chunk.fromIterable( + connectionIds + .map(state.connections) + .toSet + .flatMap(whc => Set(whc.connection.toSystemId, whc.connection.fromSystemId)) + ) + // delete the connections if any were found + _ <- query.map.deleteMapWormholeConnections(Chunk.fromIterable(connectionIds), sessionId.characterId) + // delete any signatures that map to those connection ids + _ <- query.map.deleteSignaturesWithConnectionIds(Chunk.fromIterable(connectionIds), sessionId.characterId) + deletedConnections <- query.map.getWormholeConnections(mapId, Chunk.fromIterable(connectionIds), isDeleted = true) + // delete the signatures _ <- rss.signatures match case None => query.map.deleteMapSystemSignaturesAll(mapId, rss.systemId, now, sessionId.characterId) case Some(ids) => query.map.deleteMapSystemSignatures(mapId, rss.systemId, ids, sessionId.characterId) - // reload the whole system - sys <- loadSingleSystem(mapId, rss.systemId) - conns <- MapQueries.getWormholeConnectionsWithSigsBySystemId(mapId, rss.systemId) - ranks <- MapQueries.getWormholeConnectionRanksForSystem(mapId, rss.systemId) + // if some connections were found, will reload multiple systems + resp <- + if (systemIdsToRefresh.nonEmpty) + combineMany( + state, + Chunk(removeConnections(deletedConnections)) ++ + systemIdsToRefresh.map(sId => reloadSystemSnapshot(mapId, sId)) + ) + else reloadSystemSnapshot(mapId, rss.systemId)(state) + yield resp + + // partially remove connections - state will be updated later with systems + private def removeConnections(connectionsRemoved: List[MapWormholeConnection])(state: MapState) = + val connectionIds = connectionsRemoved.map(_.id) + val nextState = state.copy( + connections = state.connections.removedAll(connectionIds), + connectionRanks = state.connectionRanks.removedAll(connectionIds) + ) + val resp = + if (connectionsRemoved.nonEmpty) broadcast(MapResponse.ConnectionsRemoved(connectionsRemoved)) else Chunk.empty + ZIO.succeed((nextState, resp)) + + private def reloadSystemSnapshot(mapId: MapId, systemId: SystemId)(state: MapState) = + for + sys <- loadSingleSystem(mapId, systemId) + conns <- MapQueries.getWormholeConnectionsWithSigsBySystemId(mapId, systemId) + ranks <- MapQueries.getWormholeConnectionRanksForSystem(mapId, systemId) yield withState(state.updateOne(sys.sys.systemId, sys, conns, ranks))(s => s -> broadcast(s.systemSnapshot(sys.sys.systemId)) ) @@ -730,6 +757,7 @@ object MapEntity extends ReactiveEntity[MapEnv, MapId, MapState, Identified[MapR model.SystemDisplayData.Manual(0, 0) // origin position case Some(model.SystemDisplayData.Manual(x, y)) => // this must necessarily replicate the frontend code + // TODO: need to take collisions etc. into account val newX = x + MagicConstant.SystemBoxSizeX + (MagicConstant.SystemBoxSizeX / 3) model.SystemDisplayData.Manual(newX - (newX % MagicConstant.GridSnapPx), y) @@ -850,7 +878,9 @@ object MapEntity extends ReactiveEntity[MapEnv, MapId, MapState, Identified[MapR private inline def loadSingleSystem(mapId: MapId, systemId: SystemId) = MapQueries .getMapSystemAll(mapId, Some(systemId)) - .filterOrDieMessage(_.size == 1)(s"BUG: expected exactly 1 system to be returned") + .filterOrDieMessage(_.size == 1)( + s"BUG: expected exactly 1 (map) system to be returned for (map=$mapId, system=$systemId)" + ) .map(_.head) private inline def loadSingleConnection(mapId: MapId, connectionId: ConnectionId) = @@ -1002,6 +1032,13 @@ private inline def removeSignatureById(arr: Array[MapSystemSignature], idOpt: Op ) .getOrElse(arr) +private def combineMany( + initial: MapState, + fs: Chunk[MapState => ZIO[MapEnv, Throwable, (MapState, Chunk[Identified[MapResponse]])]] +) = + ZIO.foldLeft(fs)((initial, Chunk.empty[Identified[MapResponse]])): + case ((prev, responses), f) => f(prev).map((s, r) => (s, responses ++ r)) + private def loadMapRef() = ReferenceQueries.getAllSolarSystemsWithGates.map(allSolar => MapRef( @@ -1012,6 +1049,8 @@ private def loadMapRef() = systemId = ss.sys.id, name = ss.sys.name, whClass = WormholeClasses.ById(ss.sys.whClassId.get), + regionId = ss.sys.regionId, + constellationId = ss.sys.constellationId, gates = ss.gates.map(sg => sg.outSystemId.value -> sg.inGateId).toMap ) ) diff --git a/server/src/test/scala/org/updraft0/controltower/server/auth/UsersSpec.scala b/server/src/test/scala/org/updraft0/controltower/server/auth/UsersSpec.scala index 74923d3..94386f8 100644 --- a/server/src/test/scala/org/updraft0/controltower/server/auth/UsersSpec.scala +++ b/server/src/test/scala/org/updraft0/controltower/server/auth/UsersSpec.scala @@ -21,24 +21,24 @@ object UsersSpec extends ZIOSpecDefault: def spec = suite("JWT auth token")( test("can be encrypted and decrypted"): - val meta = EsiTokenMeta(CharacterId(1234), "Name1", "abcdef", Instant.EPOCH) - val jwt = JwtAuthResponse(JwtString(SampleTokenValue), 1L, "?", SampleRefresh) - - for - enc <- Users.encryptJwtResponse(meta, jwt) - dec <- Users.decryptAuthToken(enc) - yield assertTrue( - enc.characterId == CharacterId(1234L), - enc.expiresAt == meta.expiry, - enc.refreshToken == encrypt( - Base64.raw(enc.nonce), - SampleKey, - Base64.raw(SampleRefresh).toBytes - ).stringValue, - enc.token == encrypt(Base64.raw(enc.nonce), SampleKey, SampleTokenValue.getBytes).stringValue, - dec._1 == SampleTokenValue, - dec._2.stringValue == SampleRefresh - ) + val meta = EsiTokenMeta(CharacterId(1234), "Name1", "abcdef", Instant.EPOCH) + val jwt = JwtAuthResponse(JwtString(SampleTokenValue), 1L, "?", SampleRefresh) + + for + enc <- Users.encryptJwtResponse(meta, jwt) + dec <- Users.decryptAuthToken(enc) + yield assertTrue( + enc.characterId == CharacterId(1234L), + enc.expiresAt == meta.expiry, + enc.refreshToken == encrypt( + Base64.raw(enc.nonce), + SampleKey, + Base64.raw(SampleRefresh).toBytes + ).stringValue, + enc.token == encrypt(Base64.raw(enc.nonce), SampleKey, SampleTokenValue.getBytes).stringValue, + dec._1 == SampleTokenValue, + dec._2.stringValue == SampleRefresh + ) ).provide( ZLayer(ZIO.attempt(new SecureRandom())), ZLayer(ZIO.attempt(TokenCrypto(new SecretKeySpec(SampleKey, "AES")))) diff --git a/server/src/test/scala/org/updraft0/controltower/server/db/MapQueriesSpec.scala b/server/src/test/scala/org/updraft0/controltower/server/db/MapQueriesSpec.scala index 3cedf27..87f01de 100644 --- a/server/src/test/scala/org/updraft0/controltower/server/db/MapQueriesSpec.scala +++ b/server/src/test/scala/org/updraft0/controltower/server/db/MapQueriesSpec.scala @@ -16,7 +16,7 @@ object MapQueriesSpec extends ZIOSpecDefault: override def spec = suite("MapQueries::map_wormhole_connection")( test("can compute ranks of incoming wormholes"): - /* + /* format: off ______________ @@ -28,68 +28,68 @@ object MapQueriesSpec extends ZIOSpecDefault: \------|--------> S5 format: on - */ - - val systems = List( - system(100L, Some("System 1")), - system(200L, Some("System 2")), - system(300L, Some("System 3")), - system(400L, Some("System 4")), - system(500L, Some("System 5")), - system(600L, Some("System 6")) - ) - - val connections = List( - connection(100L, 200L), - connection(200L, 400L), - connection(400L, 100L), - connection(300L, 200L), - connection(300L, 500L), - connection(100L, 500L), - connection(300L, 600L), - connection(400L, 300L) - ) - - val connectionRanks = Map( - (100L, 200L) -> MapWormholeConnectionRank(ConnectionId(0L), 1, 2, 1, 2), - (200L, 400L) -> MapWormholeConnectionRank(ConnectionId(0L), 1, 1, 1, 1), - (400L, 100L) -> MapWormholeConnectionRank(ConnectionId(0L), 1, 2, 1, 1), - (300L, 200L) -> MapWormholeConnectionRank(ConnectionId(0L), 1, 3, 2, 2), - (300L, 500L) -> MapWormholeConnectionRank(ConnectionId(0L), 2, 3, 1, 2), - (100L, 500L) -> MapWormholeConnectionRank(ConnectionId(0L), 2, 2, 2, 2), - (300L, 600L) -> MapWormholeConnectionRank(ConnectionId(0L), 3, 3, 1, 1), - (400L, 300L) -> MapWormholeConnectionRank(ConnectionId(0L), 2, 2, 1, 1) - ) - - query.transaction( - for - // 0. insert reference data, systems + connections - _ <- query.map.upsertMap(DefaultMap) - _ <- ZIO.foreachDiscard(systems)(query.map.upsertMapSystem) - connsInserted <- ZIO.foreach(connections)(query.map.insertMapWormholeConnection) - // 1. compute the ranks of all connections - allRanks <- MapQueries.getWormholeConnectionRanksAll(DefaultMap.id) - // 2. get the expected values (fixup database generated ids) - connsMap = connectionsById(connsInserted) - connRanksExpected = connectionRanks.map((k, v) => connsMap(k).id -> v.copy(connectionId = connsMap(k).id)) - allRanksMap = ranksById(allRanks) - // 3. test getting ranks for particular systems - ranks100 <- MapQueries.getWormholeConnectionRanksForSystem(DefaultMap.id, 100L).map(ranksById) - ranks200 <- MapQueries.getWormholeConnectionRanksForSystem(DefaultMap.id, 200L).map(ranksById) - ranks300 <- MapQueries.getWormholeConnectionRanksForSystem(DefaultMap.id, 300L).map(ranksById) - ranks400 <- MapQueries.getWormholeConnectionRanksForSystem(DefaultMap.id, 400L).map(ranksById) - ranks500 <- MapQueries.getWormholeConnectionRanksForSystem(DefaultMap.id, 500L).map(ranksById) - ranks600 <- MapQueries.getWormholeConnectionRanksForSystem(DefaultMap.id, 600L).map(ranksById) - yield assertTrue( - connsInserted.size == connections.size && allRanksMap == connRanksExpected && - containsAll(ranks100, connRanksExpected) && ranks100.size == 3 && - containsAll(ranks200, connRanksExpected) && ranks200.size == 3 && - containsAll(ranks300, connRanksExpected) && ranks300.size == 4 && - containsAll(ranks400, connRanksExpected) && ranks400.size == 3 && - containsAll(ranks500, connRanksExpected) && ranks500.size == 2 && - containsAll(ranks600, connRanksExpected) && ranks600.size == 1 - ) + */ + + val systems = List( + system(100L, Some("System 1")), + system(200L, Some("System 2")), + system(300L, Some("System 3")), + system(400L, Some("System 4")), + system(500L, Some("System 5")), + system(600L, Some("System 6")) + ) + + val connections = List( + connection(100L, 200L), + connection(200L, 400L), + connection(400L, 100L), + connection(300L, 200L), + connection(300L, 500L), + connection(100L, 500L), + connection(300L, 600L), + connection(400L, 300L) + ) + + val connectionRanks = Map( + (100L, 200L) -> MapWormholeConnectionRank(ConnectionId(0L), 1, 2, 1, 2), + (200L, 400L) -> MapWormholeConnectionRank(ConnectionId(0L), 1, 1, 1, 1), + (400L, 100L) -> MapWormholeConnectionRank(ConnectionId(0L), 1, 2, 1, 1), + (300L, 200L) -> MapWormholeConnectionRank(ConnectionId(0L), 1, 3, 2, 2), + (300L, 500L) -> MapWormholeConnectionRank(ConnectionId(0L), 2, 3, 1, 2), + (100L, 500L) -> MapWormholeConnectionRank(ConnectionId(0L), 2, 2, 2, 2), + (300L, 600L) -> MapWormholeConnectionRank(ConnectionId(0L), 3, 3, 1, 1), + (400L, 300L) -> MapWormholeConnectionRank(ConnectionId(0L), 2, 2, 1, 1) + ) + + query.transaction( + for + // 0. insert reference data, systems + connections + _ <- query.map.upsertMap(DefaultMap) + _ <- ZIO.foreachDiscard(systems)(query.map.upsertMapSystem) + connsInserted <- ZIO.foreach(connections)(query.map.insertMapWormholeConnection) + // 1. compute the ranks of all connections + allRanks <- MapQueries.getWormholeConnectionRanksAll(DefaultMap.id) + // 2. get the expected values (fixup database generated ids) + connsMap = connectionsById(connsInserted) + connRanksExpected = connectionRanks.map((k, v) => connsMap(k).id -> v.copy(connectionId = connsMap(k).id)) + allRanksMap = ranksById(allRanks) + // 3. test getting ranks for particular systems + ranks100 <- MapQueries.getWormholeConnectionRanksForSystem(DefaultMap.id, 100L).map(ranksById) + ranks200 <- MapQueries.getWormholeConnectionRanksForSystem(DefaultMap.id, 200L).map(ranksById) + ranks300 <- MapQueries.getWormholeConnectionRanksForSystem(DefaultMap.id, 300L).map(ranksById) + ranks400 <- MapQueries.getWormholeConnectionRanksForSystem(DefaultMap.id, 400L).map(ranksById) + ranks500 <- MapQueries.getWormholeConnectionRanksForSystem(DefaultMap.id, 500L).map(ranksById) + ranks600 <- MapQueries.getWormholeConnectionRanksForSystem(DefaultMap.id, 600L).map(ranksById) + yield assertTrue( + connsInserted.size == connections.size && allRanksMap == connRanksExpected && + containsAll(ranks100, connRanksExpected) && ranks100.size == 3 && + containsAll(ranks200, connRanksExpected) && ranks200.size == 3 && + containsAll(ranks300, connRanksExpected) && ranks300.size == 4 && + containsAll(ranks400, connRanksExpected) && ranks400.size == 3 && + containsAll(ranks500, connRanksExpected) && ranks500.size == 2 && + containsAll(ranks600, connRanksExpected) && ranks600.size == 1 ) + ) ).provideLayer(TempDb.empty) private def system(id: model.SystemId, name: Option[String]): model.MapSystem = diff --git a/server/src/test/scala/org/updraft0/controltower/server/map/MapReactiveSpec.scala b/server/src/test/scala/org/updraft0/controltower/server/map/MapReactiveSpec.scala index 515b508..0521236 100644 --- a/server/src/test/scala/org/updraft0/controltower/server/map/MapReactiveSpec.scala +++ b/server/src/test/scala/org/updraft0/controltower/server/map/MapReactiveSpec.scala @@ -1,8 +1,21 @@ package org.updraft0.controltower.server.map -import org.updraft0.controltower.constant.{CharacterId, ConnectionId, MapId, SigId} +import org.updraft0.controltower.constant.{SystemId as _, *} import org.updraft0.controltower.db.model.* +import org.updraft0.controltower.db.query +import org.updraft0.controltower.server.db.{ + MapQueries, + MapWormholeConnectionWithSigs, + MapWormholeConnectionRank, + MapSystemWithAll, + TempDb +} +import org.updraft0.controltower.server.tracking.{TestLocationTracker, TestPermissionTracker} +import zio.* +import zio.logging.slf4j.bridge.Slf4jBridge import zio.test.* +import zio.test.Assertion.* +import zio.logging.{ConsoleLoggerConfig, LogFilter, LogFormat, consoleLogger} import java.time.Instant import java.util.UUID @@ -18,357 +31,676 @@ object MapReactiveSpec extends ZIOSpecDefault: val MapId1 = MapId(1) val SystemId1 = 30000142L val SystemId2 = 31002604L + val SystemId3 = 30000140L + val SystemId4 = 30001447L + val SystemId5 = 30000848L + val SystemId6 = 30045328L + val SystemId7 = 31000003L + + val TestSolarSystems = Map( + SystemId1 -> MapSolarSystem(SystemId1, "Jita", WormholeClass.H, 10000002L, 20000020L, Map(SystemId3 -> 50001248)), + SystemId2 -> MapSolarSystem(SystemId2, "J000102", WormholeClass.ShatteredFrig, 11000032L, 21000333L, Map.empty), + SystemId3 -> MapSolarSystem( + SystemId3, + "Maurasi", + WormholeClass.H, + 10000002L, + 20000020L, + Map(SystemId1 -> 50000802) + ), + SystemId4 -> MapSolarSystem(SystemId4, "Taisy", WormholeClass.L, 10000016L, 20000212L, Map(SystemId5 -> 50014054)), + SystemId5 -> MapSolarSystem( + SystemId5, + "M-OEE8", + WormholeClass.NS, + 10000010L, + 20000124L, + Map(SystemId4 -> 50014053) + ), + SystemId6 -> MapSolarSystem(SystemId6, "Ahtila", WormholeClass.Pochven, 10000070L, 20000788L, Map.empty), + SystemId7 -> MapSolarSystem(SystemId7, "J164710", WormholeClass.VidetteDrifter, 11000033L, 21000334L, Map.empty) + ) + + val EmptyConnections = Map.empty[ConnectionId, MapWormholeConnectionWithSigs] + val EmptyConnectionRanks = Map.empty[ConnectionId, MapWormholeConnectionRank] override def spec = suite("toModelSignature")( test("converts an Unknown signature"): - val newSig = NewMapSystemSignature(SigId("UKN-000"), SignatureGroup.Unknown) - val expected = modelSignature( - MapId1, - SystemId1, - "UKN-000", - createdAt = Time1, - createdByCharacterId = DummySession.characterId, - updatedAt = Time1, - updatedByCharacterId = DummySession.characterId - ) - assertTrue( - toModelSignature(Time1, DummySession, (MapId1, SystemId1), None, newSig) == expected - ) + val newSig = NewMapSystemSignature(SigId("UKN-000"), SignatureGroup.Unknown) + val expected = modelSignature( + MapId1, + SystemId1, + "UKN-000", + createdAt = Time1, + createdByCharacterId = DummySession.characterId, + updatedAt = Time1, + updatedByCharacterId = DummySession.characterId + ) + assertTrue( + toModelSignature(Time1, DummySession, (MapId1, SystemId1), None, newSig) == expected + ) , test("converts a Data signature"): - val newSig = - NewMapSystemSignature(SigId("DAT-001"), SignatureGroup.Data, Some("Unsecured Frontier Enclave Relay")) - val expected = modelSignature( - MapId1, - SystemId1, - "DAT-001", - signatureGroup = SignatureGroup.Data, - signatureTypeName = Some("Unsecured Frontier Enclave Relay"), - createdAt = Time1, - createdByCharacterId = DummySession.characterId, - updatedAt = Time1, - updatedByCharacterId = DummySession.characterId - ) - assertTrue( - toModelSignature(Time1, DummySession, (MapId1, SystemId1), None, newSig) == expected - ) + val newSig = + NewMapSystemSignature(SigId("DAT-001"), SignatureGroup.Data, Some("Unsecured Frontier Enclave Relay")) + val expected = modelSignature( + MapId1, + SystemId1, + "DAT-001", + signatureGroup = SignatureGroup.Data, + signatureTypeName = Some("Unsecured Frontier Enclave Relay"), + createdAt = Time1, + createdByCharacterId = DummySession.characterId, + updatedAt = Time1, + updatedByCharacterId = DummySession.characterId + ) + assertTrue( + toModelSignature(Time1, DummySession, (MapId1, SystemId1), None, newSig) == expected + ) , test("converts a Wormhole signature of Unknown subtype"): - val newSig = NewMapSystemSignature(SigId("WHO-002"), SignatureGroup.Wormhole) - val expected = modelSignature( - MapId1, - SystemId1, - "WHO-002", - signatureGroup = SignatureGroup.Wormhole, - wormholeMassSize = Some(WormholeMassSize.Unknown), - wormholeMassStatus = Some(WormholeMassStatus.Unknown), - createdAt = Time1, - createdByCharacterId = DummySession.characterId, - updatedAt = Time1, - updatedByCharacterId = DummySession.characterId - ) - assertTrue( - toModelSignature(Time1, DummySession, (MapId1, SystemId1), None, newSig) == expected - ) + val newSig = NewMapSystemSignature(SigId("WHO-002"), SignatureGroup.Wormhole) + val expected = modelSignature( + MapId1, + SystemId1, + "WHO-002", + signatureGroup = SignatureGroup.Wormhole, + wormholeMassSize = Some(WormholeMassSize.Unknown), + wormholeMassStatus = Some(WormholeMassStatus.Unknown), + createdAt = Time1, + createdByCharacterId = DummySession.characterId, + updatedAt = Time1, + updatedByCharacterId = DummySession.characterId + ) + assertTrue( + toModelSignature(Time1, DummySession, (MapId1, SystemId1), None, newSig) == expected + ) , test("converts a Wormhole signature with a known type id and mass"): - val newSig = NewMapSystemSignature(SigId("WHO-003"), SignatureGroup.Wormhole) - val expected = modelSignature( - MapId1, - SystemId1, - "WHO-003", - signatureGroup = SignatureGroup.Wormhole, - wormholeMassSize = Some(WormholeMassSize.Unknown), - wormholeMassStatus = Some(WormholeMassStatus.Unknown), - createdAt = Time1, - createdByCharacterId = DummySession.characterId, - updatedAt = Time1, - updatedByCharacterId = DummySession.characterId - ) - assertTrue( - toModelSignature(Time1, DummySession, (MapId1, SystemId1), None, newSig) == expected - ) + val newSig = NewMapSystemSignature(SigId("WHO-003"), SignatureGroup.Wormhole) + val expected = modelSignature( + MapId1, + SystemId1, + "WHO-003", + signatureGroup = SignatureGroup.Wormhole, + wormholeMassSize = Some(WormholeMassSize.Unknown), + wormholeMassStatus = Some(WormholeMassStatus.Unknown), + createdAt = Time1, + createdByCharacterId = DummySession.characterId, + updatedAt = Time1, + updatedByCharacterId = DummySession.characterId + ) + assertTrue( + toModelSignature(Time1, DummySession, (MapId1, SystemId1), None, newSig) == expected + ) , test("updates a signature Unknown -> Relic"): - val prevSig = modelSignature( - MapId1, - SystemId1, - "REL-001", - signatureGroup = SignatureGroup.Unknown, - createdAt = Time1, - createdByCharacterId = CharacterId(42L), - updatedAt = Time1, - updatedByCharacterId = CharacterId(42L) - ) - val newSig = - NewMapSystemSignature(SigId("REL-001"), SignatureGroup.Relic, Some("Decayed Blood Raider Mass Grave")) - val expected = modelSignature( - MapId1, - SystemId1, - "REL-001", - signatureGroup = SignatureGroup.Relic, - signatureTypeName = Some("Decayed Blood Raider Mass Grave"), - createdAt = Time1, - createdByCharacterId = CharacterId(42L), - updatedAt = Time2, - updatedByCharacterId = DummySession.characterId - ) - assertTrue( - toModelSignature(Time2, DummySession, (MapId1, SystemId1), Some(prevSig), newSig) == expected - ) + val prevSig = modelSignature( + MapId1, + SystemId1, + "REL-001", + signatureGroup = SignatureGroup.Unknown, + createdAt = Time1, + createdByCharacterId = CharacterId(42L), + updatedAt = Time1, + updatedByCharacterId = CharacterId(42L) + ) + val newSig = + NewMapSystemSignature(SigId("REL-001"), SignatureGroup.Relic, Some("Decayed Blood Raider Mass Grave")) + val expected = modelSignature( + MapId1, + SystemId1, + "REL-001", + signatureGroup = SignatureGroup.Relic, + signatureTypeName = Some("Decayed Blood Raider Mass Grave"), + createdAt = Time1, + createdByCharacterId = CharacterId(42L), + updatedAt = Time2, + updatedByCharacterId = DummySession.characterId + ) + assertTrue( + toModelSignature(Time2, DummySession, (MapId1, SystemId1), Some(prevSig), newSig) == expected + ) , test("updates a signature Unknown -> Wormhole (Unknown)"): - val prevSig = modelSignature( - MapId1, - SystemId1, - "WHO-003", - signatureGroup = SignatureGroup.Unknown, - createdAt = Time1, - createdByCharacterId = CharacterId(42L), - updatedAt = Time1, - updatedByCharacterId = CharacterId(42L) - ) - val newSig = NewMapSystemSignature(SigId("WHO-003"), SignatureGroup.Wormhole) - val expected = modelSignature( - MapId1, - SystemId1, - "WHO-003", - signatureGroup = SignatureGroup.Wormhole, - wormholeMassSize = Some(WormholeMassSize.Unknown), - wormholeMassStatus = Some(WormholeMassStatus.Unknown), - createdAt = Time1, - createdByCharacterId = CharacterId(42L), - updatedAt = Time2, - updatedByCharacterId = DummySession.characterId - ) - assertTrue( - toModelSignature(Time2, DummySession, (MapId1, SystemId1), Some(prevSig), newSig) == expected - ) + val prevSig = modelSignature( + MapId1, + SystemId1, + "WHO-003", + signatureGroup = SignatureGroup.Unknown, + createdAt = Time1, + createdByCharacterId = CharacterId(42L), + updatedAt = Time1, + updatedByCharacterId = CharacterId(42L) + ) + val newSig = NewMapSystemSignature(SigId("WHO-003"), SignatureGroup.Wormhole) + val expected = modelSignature( + MapId1, + SystemId1, + "WHO-003", + signatureGroup = SignatureGroup.Wormhole, + wormholeMassSize = Some(WormholeMassSize.Unknown), + wormholeMassStatus = Some(WormholeMassStatus.Unknown), + createdAt = Time1, + createdByCharacterId = CharacterId(42L), + updatedAt = Time2, + updatedByCharacterId = DummySession.characterId + ) + assertTrue( + toModelSignature(Time2, DummySession, (MapId1, SystemId1), Some(prevSig), newSig) == expected + ) , test("updates a signature Wormhole (Unknown) -> Wormhole (K162)"): - val prevSig = modelSignature( - MapId1, - SystemId1, - "WHO-004", - signatureGroup = SignatureGroup.Wormhole, - wormholeMassSize = Some(WormholeMassSize.Unknown), - wormholeMassStatus = Some(WormholeMassStatus.Unknown), - createdAt = Time1, - createdByCharacterId = CharacterId(42L), - updatedAt = Time1, - updatedByCharacterId = CharacterId(42L) - ) - val newSig = NewMapSystemSignature( - SigId("WHO-004"), - SignatureGroup.Wormhole, - wormholeMassStatus = WormholeMassStatus.Fresh, - wormholeK162Type = Some(WormholeK162Type.Dangerous) - ) - val expected = modelSignature( - MapId1, - SystemId1, - SigId("WHO-004"), - signatureGroup = SignatureGroup.Wormhole, - wormholeMassSize = Some(WormholeMassSize.Unknown), - wormholeMassStatus = Some(WormholeMassStatus.Fresh), - wormholeK162Type = Some(WormholeK162Type.Dangerous), - createdAt = Time1, - createdByCharacterId = CharacterId(42L), - updatedAt = Time2, - updatedByCharacterId = DummySession.characterId - ) - assertTrue( - toModelSignature(Time2, DummySession, (MapId1, SystemId1), Some(prevSig), newSig) == expected - ) + val prevSig = modelSignature( + MapId1, + SystemId1, + "WHO-004", + signatureGroup = SignatureGroup.Wormhole, + wormholeMassSize = Some(WormholeMassSize.Unknown), + wormholeMassStatus = Some(WormholeMassStatus.Unknown), + createdAt = Time1, + createdByCharacterId = CharacterId(42L), + updatedAt = Time1, + updatedByCharacterId = CharacterId(42L) + ) + val newSig = NewMapSystemSignature( + SigId("WHO-004"), + SignatureGroup.Wormhole, + wormholeMassStatus = WormholeMassStatus.Fresh, + wormholeK162Type = Some(WormholeK162Type.Dangerous) + ) + val expected = modelSignature( + MapId1, + SystemId1, + SigId("WHO-004"), + signatureGroup = SignatureGroup.Wormhole, + wormholeMassSize = Some(WormholeMassSize.Unknown), + wormholeMassStatus = Some(WormholeMassStatus.Fresh), + wormholeK162Type = Some(WormholeK162Type.Dangerous), + createdAt = Time1, + createdByCharacterId = CharacterId(42L), + updatedAt = Time2, + updatedByCharacterId = DummySession.characterId + ) + assertTrue( + toModelSignature(Time2, DummySession, (MapId1, SystemId1), Some(prevSig), newSig) == expected + ) , test("updates a signature Wormhole (Unknown) -> Wormhole (A009)"): - val prevSig = modelSignature( - MapId1, - SystemId1, - "WHO-005", - signatureGroup = SignatureGroup.Wormhole, - wormholeMassSize = Some(WormholeMassSize.Unknown), - wormholeMassStatus = Some(WormholeMassStatus.Unknown), - createdAt = Time1, - createdByCharacterId = CharacterId(42L), - updatedAt = Time1, - updatedByCharacterId = CharacterId(42L) - ) - val newSig = NewMapSystemSignature( - SigId("WHO-005"), - SignatureGroup.Wormhole, - wormholeMassSize = WormholeMassSize.S, - wormholeMassStatus = WormholeMassStatus.Fresh, - wormholeTypeId = Some(34439) - ) - val expected = modelSignature( - MapId1, - SystemId1, - "WHO-005", - signatureGroup = SignatureGroup.Wormhole, - wormholeMassSize = Some(WormholeMassSize.S), - wormholeMassStatus = Some(WormholeMassStatus.Fresh), - wormholeTypeId = Some(34439), - createdAt = Time1, - createdByCharacterId = CharacterId(42L), - updatedAt = Time2, - updatedByCharacterId = DummySession.characterId - ) - assertTrue( - toModelSignature(Time2, DummySession, (MapId1, SystemId1), Some(prevSig), newSig) == expected - ) + val prevSig = modelSignature( + MapId1, + SystemId1, + "WHO-005", + signatureGroup = SignatureGroup.Wormhole, + wormholeMassSize = Some(WormholeMassSize.Unknown), + wormholeMassStatus = Some(WormholeMassStatus.Unknown), + createdAt = Time1, + createdByCharacterId = CharacterId(42L), + updatedAt = Time1, + updatedByCharacterId = CharacterId(42L) + ) + val newSig = NewMapSystemSignature( + SigId("WHO-005"), + SignatureGroup.Wormhole, + wormholeMassSize = WormholeMassSize.S, + wormholeMassStatus = WormholeMassStatus.Fresh, + wormholeTypeId = Some(34439) + ) + val expected = modelSignature( + MapId1, + SystemId1, + "WHO-005", + signatureGroup = SignatureGroup.Wormhole, + wormholeMassSize = Some(WormholeMassSize.S), + wormholeMassStatus = Some(WormholeMassStatus.Fresh), + wormholeTypeId = Some(34439), + createdAt = Time1, + createdByCharacterId = CharacterId(42L), + updatedAt = Time2, + updatedByCharacterId = DummySession.characterId + ) + assertTrue( + toModelSignature(Time2, DummySession, (MapId1, SystemId1), Some(prevSig), newSig) == expected + ) , test("updates a signature Wormhole with the scanning time only"): - val prevSig = modelSignature( - MapId1, - SystemId1, - "WHO-006", - signatureGroup = SignatureGroup.Wormhole, - wormholeMassSize = Some(WormholeMassSize.S), - wormholeMassStatus = Some(WormholeMassStatus.Fresh), - wormholeTypeId = Some(34439), - wormholeConnectionId = Some(ConnectionId(1234L)), - createdAt = Time1, - createdByCharacterId = CharacterId(42L), - updatedAt = Time1, - updatedByCharacterId = CharacterId(42L) - ) - val newSig = NewMapSystemSignature(SigId("WHO-006"), SignatureGroup.Wormhole) - val expected = prevSig.copy( - updatedAt = Time2, - updatedByCharacterId = DummySession.characterId - ) + val prevSig = modelSignature( + MapId1, + SystemId1, + "WHO-006", + signatureGroup = SignatureGroup.Wormhole, + wormholeMassSize = Some(WormholeMassSize.S), + wormholeMassStatus = Some(WormholeMassStatus.Fresh), + wormholeTypeId = Some(34439), + wormholeConnectionId = Some(ConnectionId(1234L)), + createdAt = Time1, + createdByCharacterId = CharacterId(42L), + updatedAt = Time1, + updatedByCharacterId = CharacterId(42L) + ) + val newSig = NewMapSystemSignature(SigId("WHO-006"), SignatureGroup.Wormhole) + val expected = prevSig.copy( + updatedAt = Time2, + updatedByCharacterId = DummySession.characterId + ) - assertTrue( - toModelSignature(Time2, DummySession, (MapId1, SystemId1), Some(prevSig), newSig) == expected - ) + assertTrue( + toModelSignature(Time2, DummySession, (MapId1, SystemId1), Some(prevSig), newSig) == expected + ) , test("updates a signature Wormhole with the scanning time, leaving the EOL status (and time) untouched"): - val prevSig = modelSignature( - MapId1, - SystemId1, - "WHO-007", - signatureGroup = SignatureGroup.Wormhole, - wormholeIsEol = Some(true), - wormholeEolAt = Some(Time1), - wormholeTypeId = Some(34439), - createdAt = Time1, - createdByCharacterId = CharacterId(42L), - updatedAt = Time1, - updatedByCharacterId = CharacterId(42L) - ) - val newSig = NewMapSystemSignature(SigId("WHO-007"), SignatureGroup.Unknown) - val expected = prevSig.copy( - updatedAt = Time2, - updatedByCharacterId = DummySession.characterId - ) + val prevSig = modelSignature( + MapId1, + SystemId1, + "WHO-007", + signatureGroup = SignatureGroup.Wormhole, + wormholeIsEol = Some(true), + wormholeEolAt = Some(Time1), + wormholeTypeId = Some(34439), + createdAt = Time1, + createdByCharacterId = CharacterId(42L), + updatedAt = Time1, + updatedByCharacterId = CharacterId(42L) + ) + val newSig = NewMapSystemSignature(SigId("WHO-007"), SignatureGroup.Unknown) + val expected = prevSig.copy( + updatedAt = Time2, + updatedByCharacterId = DummySession.characterId + ) - assertTrue( - toModelSignature(Time2, DummySession, (MapId1, SystemId1), Some(prevSig), newSig) == expected - ) + assertTrue( + toModelSignature(Time2, DummySession, (MapId1, SystemId1), Some(prevSig), newSig) == expected + ) , test("updates a signature Wormhole from K162 -> A009"): - val prevSig = modelSignature( - MapId1, - SystemId1, - "WHO-008", - signatureGroup = SignatureGroup.Wormhole, - wormholeK162Type = Some(WormholeK162Type.Unknown), - wormholeMassSize = Some(WormholeMassSize.S), - wormholeMassStatus = Some(WormholeMassStatus.Fresh), - wormholeConnectionId = Some(ConnectionId(1234)), - createdAt = Time1, - createdByCharacterId = CharacterId(42L), - updatedAt = Time1, - updatedByCharacterId = CharacterId(42L) - ) - val newSig = NewMapSystemSignature(SigId("WHO-008"), SignatureGroup.Wormhole, wormholeTypeId = Some(34439)) - val expected = modelSignature( - MapId1, - SystemId1, - "WHO-008", - signatureGroup = SignatureGroup.Wormhole, - wormholeMassSize = Some(WormholeMassSize.S), - wormholeMassStatus = Some(WormholeMassStatus.Fresh), - wormholeTypeId = Some(34439), - wormholeConnectionId = Some(ConnectionId(1234L)), - createdAt = Time1, - createdByCharacterId = CharacterId(42L), - updatedAt = Time2, - updatedByCharacterId = DummySession.characterId - ) + val prevSig = modelSignature( + MapId1, + SystemId1, + "WHO-008", + signatureGroup = SignatureGroup.Wormhole, + wormholeK162Type = Some(WormholeK162Type.Unknown), + wormholeMassSize = Some(WormholeMassSize.S), + wormholeMassStatus = Some(WormholeMassStatus.Fresh), + wormholeConnectionId = Some(ConnectionId(1234)), + createdAt = Time1, + createdByCharacterId = CharacterId(42L), + updatedAt = Time1, + updatedByCharacterId = CharacterId(42L) + ) + val newSig = NewMapSystemSignature(SigId("WHO-008"), SignatureGroup.Wormhole, wormholeTypeId = Some(34439)) + val expected = modelSignature( + MapId1, + SystemId1, + "WHO-008", + signatureGroup = SignatureGroup.Wormhole, + wormholeMassSize = Some(WormholeMassSize.S), + wormholeMassStatus = Some(WormholeMassStatus.Fresh), + wormholeTypeId = Some(34439), + wormholeConnectionId = Some(ConnectionId(1234L)), + createdAt = Time1, + createdByCharacterId = CharacterId(42L), + updatedAt = Time2, + updatedByCharacterId = DummySession.characterId + ) - assertTrue( - toModelSignature(Time2, DummySession, (MapId1, SystemId1), Some(prevSig), newSig) == expected - ) + assertTrue( + toModelSignature(Time2, DummySession, (MapId1, SystemId1), Some(prevSig), newSig) == expected + ) , test("updates a signature Wormhole by removing its EOL status"): - val prevSig = modelSignature( - MapId1, - SystemId1, - "WHO-009", - signatureGroup = SignatureGroup.Wormhole, - wormholeIsEol = Some(true), - wormholeEolAt = Some(Time1), - wormholeTypeId = Some(34439), - createdAt = Time1, - createdByCharacterId = CharacterId(42L), - updatedAt = Time1, - updatedByCharacterId = CharacterId(42L) - ) - val newSig = NewMapSystemSignature(SigId("WHO-009"), SignatureGroup.Wormhole, wormholeIsEol = Some(false)) - val expected = prevSig.copy( - wormholeIsEol = Some(false), - wormholeEolAt = None, - updatedAt = Time2, - updatedByCharacterId = DummySession.characterId - ) + val prevSig = modelSignature( + MapId1, + SystemId1, + "WHO-009", + signatureGroup = SignatureGroup.Wormhole, + wormholeIsEol = Some(true), + wormholeEolAt = Some(Time1), + wormholeTypeId = Some(34439), + createdAt = Time1, + createdByCharacterId = CharacterId(42L), + updatedAt = Time1, + updatedByCharacterId = CharacterId(42L) + ) + val newSig = NewMapSystemSignature(SigId("WHO-009"), SignatureGroup.Wormhole, wormholeIsEol = Some(false)) + val expected = prevSig.copy( + wormholeIsEol = Some(false), + wormholeEolAt = None, + updatedAt = Time2, + updatedByCharacterId = DummySession.characterId + ) - assertTrue( - toModelSignature(Time2, DummySession, (MapId1, SystemId1), Some(prevSig), newSig) == expected - ) - ) + assertTrue( + toModelSignature(Time2, DummySession, (MapId1, SystemId1), Some(prevSig), newSig) == expected + ) + ) + (suite("MapEntity (connection and signature handling)")( + test( + "Creating a connection, linking with signatures and removing one of the signatures removes both signatures and the connection" + )( + for + initial <- setupInitial(MapId1, TestSolarSystems) + // create two systems, a connection between them and a wormhole signature + respOne <- runMultiple( + MapId1, + initial.state, + Chunk( + addSystemRequest(SystemId2), // -> SystemSnapshot(Id2) + addSystemRequest(SystemId5), // -> SystemSnapshot(Id5) + addConnectionRequest(SystemId5, SystemId2), // -> ConnectionSnapshot + addSignaturesRequest( + SystemId2, + NewMapSystemSignature(SigId("ABC-222"), SignatureGroup.Wormhole) + ) // -> SystemSnapshot(Id2) + ) + ) + (oneState, oneResponses) = respOne + oneConnectionId = oneState.connections.keys.head + // connect the signature with the connection in Id2 + respTwo <- runMultiple( + MapId1, + oneState, + Chunk( + updateSignatureRequest( + SystemId2, + NewMapSystemSignature( + SigId("ABC-222"), + SignatureGroup.Wormhole, + wormholeTypeId = Some(34140), + wormholeMassSize = WormholeMassSize.S, + wormholeConnectionId = Some(oneConnectionId) + ) + ) // -> SystemSnapshot(Id2) + ) + ) + (twoState, twoResponses) = respTwo + // connect the signature with the connection in Id5 + respThree <- runMultiple( + MapId1, + twoState, + Chunk( + updateSignatureRequest( + SystemId5, + NewMapSystemSignature( + SigId("DEF-678"), + SignatureGroup.Wormhole, + wormholeTypeId = None, + wormholeMassSize = WormholeMassSize.S, + wormholeConnectionId = Some(oneConnectionId) + ) + ) // -> SystemSnapshot(Id5) + ) + ) + (threeState, threeResponses) = respThree + // delete the signature (and expect the connection and associated signatures to be gone) + respFour <- runMultiple(MapId1, threeState, Chunk(removeSignatureRequest(SystemId2, SigId("ABC-222")))) + (fourState, fourResponses) = respFour + yield assertTrue(oneResponses.size == 4) && + assert(oneResponses)(hasAt(0)(matches: + case Identified( + None, + MapResponse.SystemSnapshot(`SystemId2`, _, `EmptyConnections`, `EmptyConnectionRanks`) + ) => + true + )) && + assert(oneResponses)(hasAt(1)(matches: + case Identified( + None, + MapResponse.SystemSnapshot(`SystemId5`, _, `EmptyConnections`, `EmptyConnectionRanks`) + ) => + true + )) && + assert(oneResponses)(hasAt(2)(matches: + case Identified( + None, + MapResponse.ConnectionSnapshot( + MapWormholeConnectionWithSigs( + MapWormholeConnection(_, `MapId1`, `SystemId5`, `SystemId2`, false, _, _, _, _), + _, + None, + None + ), + rank + ) + ) => + true + )) && + assert(oneResponses)(hasAt(3)(matches: + case Identified( + None, + MapResponse.SystemSnapshot( + `SystemId2`, + MapSystemWithAll( + _, + _, + _, + _, + sigs, + connections + ), + connectionMap, + connectionRanks + ) + ) => + sigs.length == 1 && connections.length == 1 && connectionMap.size == 1 && connectionRanks.size == 1 && + connections(0) == connectionMap(oneConnectionId).connection && sigs(0).signatureId == SigId("ABC-222") + )) && assertTrue(twoResponses.size == 1) && + assert(twoResponses)(hasAt(0)(matches: + case Identified( + None, + MapResponse.SystemSnapshot( + `SystemId2`, + MapSystemWithAll( + _, + _, + _, + _, + sigs, + connections + ), + connectionMap, + connectionRanks + ) + ) => + sigs.length == 1 && connections.length == 1 && connectionMap.size == 1 && connectionRanks.size == 1 && + connections(0) == connectionMap(oneConnectionId).connection && sigs(0).signatureId == SigId("ABC-222") && + sigs(0).wormholeConnectionId.contains(oneConnectionId) && + connectionMap(oneConnectionId).toSignature.contains(sigs(0)) && + sigs(0).wormholeMassSize.contains(WormholeMassSize.S) + )) && assertTrue(threeResponses.size == 1) && + assert(threeResponses)(hasAt(0)(matches: + case Identified( + None, + MapResponse.SystemSnapshot( + `SystemId5`, + MapSystemWithAll( + _, + _, + _, + _, + sigs, + connections + ), + connectionMap, + connectionRanks + ) + ) => + sigs.length == 1 && connections.length == 1 && connectionMap.size == 1 && connectionRanks.size == 1 && + connections(0) == connectionMap(oneConnectionId).connection && sigs(0).signatureId == SigId("DEF-678") && + sigs(0).wormholeConnectionId.contains(oneConnectionId) && + connectionMap(oneConnectionId).fromSignature.contains(sigs(0)) && + sigs(0).wormholeMassSize.contains(WormholeMassSize.S) + )) && assertTrue(fourResponses.size == 3) && + assert(fourResponses)(hasAt(0)(matches: + case Identified( + None, + MapResponse.ConnectionsRemoved( + MapWormholeConnection(`oneConnectionId`, _, `SystemId5`, `SystemId2`, true, _, _, _, _) :: Nil + ) + ) => + true + )) && + assert(fourResponses)(hasAt(1)(matches: + case Identified( + None, + MapResponse.SystemSnapshot(`SystemId2`, _, `EmptyConnections`, `EmptyConnectionRanks`) + ) => + true + )) && + assert(fourResponses)(hasAt(2)(matches: + case Identified( + None, + MapResponse.SystemSnapshot(`SystemId5`, _, `EmptyConnections`, `EmptyConnectionRanks`) + ) => + true + )) + ) + ).provideSome[Scope](testMapLayer) @@ TestAspect.withLiveEnvironment) // TODO: specs for MapReactive // group: connection handling - // - adding a signature with a connection sends appropriate updates - // - rank of system signatures (e.g. when you have a ring) + // - [x] adding a signature with a connection sends appropriate updates + // - [ ] rank of system signatures (e.g. when you have a ring) + + private def modelSignature( + mapId: MapId, + systemId: SystemId, + signatureId: String, + isDeleted: Boolean = false, + signatureGroup: SignatureGroup = SignatureGroup.Unknown, + signatureTypeName: Option[String] = None, + wormholeIsEol: Option[Boolean] = None, + wormholeEolAt: Option[Instant] = None, + wormholeTypeId: Option[Long] = None, + wormholeMassSize: Option[WormholeMassSize] = None, + wormholeMassStatus: Option[WormholeMassStatus] = None, + wormholeK162Type: Option[WormholeK162Type] = None, + wormholeConnectionId: Option[ConnectionId] = None, + createdAt: Instant = Instant.EPOCH, + createdByCharacterId: CharacterId = CharacterId(0L), + updatedAt: Instant = Instant.EPOCH, + updatedByCharacterId: CharacterId = CharacterId(0L) + ): MapSystemSignature = + MapSystemSignature( + mapId = mapId, + systemId = systemId, + signatureId = SigId(signatureId), + isDeleted = isDeleted, + signatureGroup = signatureGroup, + signatureTypeName = signatureTypeName, + wormholeIsEol = wormholeIsEol, + wormholeEolAt = wormholeEolAt, + wormholeTypeId = wormholeTypeId, + wormholeMassSize = wormholeMassSize, + wormholeMassStatus = wormholeMassStatus, + wormholeK162Type = wormholeK162Type, + wormholeConnectionId = wormholeConnectionId, + createdAt = createdAt, + createdByCharacterId = createdByCharacterId, + updatedAt = updatedAt, + updatedByCharacterId = updatedByCharacterId + ) + + case class InitialState(inQ: Queue[Identified[MapRequest]], state: MapState) + + private def setupInitial(mapId: MapId, solarSystems: Map[SystemId, MapSolarSystem]) = + for + _ <- query.transaction(insertSolarSystems(solarSystems) *> insertMap(mapId)) + inQ <- Queue.unbounded[Identified[MapRequest]] + initialState <- MapEntity.hydrate(MapId1, inQ).map(updateRefData(_, TestSolarSystems)) + yield InitialState(inQ, initialState) + + private def addSystemRequest( + systemId: SystemId, + sessionId: MapSessionId = DummySession, + name: Option[String] = None, + isPinned: Boolean = false, + displayData: SystemDisplayData = SystemDisplayData.Manual(0, 0), + stance: Option[IntelStance] = None + ) = Identified(Some(sessionId), MapRequest.AddSystem(systemId, name, isPinned, displayData, stance)) + + private def addConnectionRequest( + fromSystemId: SystemId, + toSystemId: SystemId, + sessionId: MapSessionId = DummySession + ) = + Identified(Some(sessionId), MapRequest.AddSystemConnection(fromSystemId, toSystemId)) -private def modelSignature( - mapId: MapId, - systemId: SystemId, - signatureId: String, - isDeleted: Boolean = false, - signatureGroup: SignatureGroup = SignatureGroup.Unknown, - signatureTypeName: Option[String] = None, - wormholeIsEol: Option[Boolean] = None, - wormholeEolAt: Option[Instant] = None, - wormholeTypeId: Option[Long] = None, - wormholeMassSize: Option[WormholeMassSize] = None, - wormholeMassStatus: Option[WormholeMassStatus] = None, - wormholeK162Type: Option[WormholeK162Type] = None, - wormholeConnectionId: Option[ConnectionId] = None, - createdAt: Instant = Instant.EPOCH, - createdByCharacterId: CharacterId = CharacterId(0L), - updatedAt: Instant = Instant.EPOCH, - updatedByCharacterId: CharacterId = CharacterId(0L) -): MapSystemSignature = - MapSystemSignature( - mapId = mapId, - systemId = systemId, - signatureId = SigId(signatureId), - isDeleted = isDeleted, - signatureGroup = signatureGroup, - signatureTypeName = signatureTypeName, - wormholeIsEol = wormholeIsEol, - wormholeEolAt = wormholeEolAt, - wormholeTypeId = wormholeTypeId, - wormholeMassSize = wormholeMassSize, - wormholeMassStatus = wormholeMassStatus, - wormholeK162Type = wormholeK162Type, - wormholeConnectionId = wormholeConnectionId, - createdAt = createdAt, - createdByCharacterId = createdByCharacterId, - updatedAt = updatedAt, - updatedByCharacterId = updatedByCharacterId + private def addSignaturesRequest( + systemId: SystemId, + newSig: NewMapSystemSignature, + sessionId: MapSessionId = DummySession + ) = + Identified(Some(sessionId), MapRequest.AddSystemSignature(systemId, newSig)) + + private def updateSignatureRequest( + systemId: SystemId, + update: NewMapSystemSignature, + sessionId: MapSessionId = DummySession + ) = Identified(Some(sessionId), MapRequest.UpdateSystemSignatures(systemId, false, List(update))) + + private def removeSignatureRequest(systemId: SystemId, sigId: SigId, sessionId: MapSessionId = DummySession) = + Identified(Some(sessionId), MapRequest.RemoveSystemSignatures(systemId, Some(NonEmptyChunk(sigId)))) + + private def testMapLayer: ZLayer[Scope, Throwable, MapEnv] = ZLayer.make[MapEnv]( + TempDb.empty, + TestLocationTracker.empty, + TestPermissionTracker.empty, + // uncomment to enable debug logging in tests +// consoleLogger(ConsoleLoggerConfig.apply(LogFormat.colored, LogFilter.LogLevelByNameConfig(LogLevel.Debug))), +// Slf4jBridge.init(LogFilter.acceptAll) ) + + private def insertSolarSystems(solarSystems: Map[SystemId, MapSolarSystem]) = + for + countR <- ZIO.foreach(solarSystems.view.values.map(mss => (mss.regionId, mss.whClass.value)).toSet): + (rId, classId) => + query.sde.upsertItemName(ItemName(rId, 3, s"region-${rId}")) *> + query.sde.upsertRegion(Region(rId, s"region-${rId}", Some(classId), None)) + countC <- ZIO.foreach(solarSystems.view.values.map(mss => (mss.regionId, mss.constellationId)).toSet)( + (rId, cId) => + query.sde.upsertItemName(ItemName(rId, 4, s"constellation-${rId}")) *> + query.sde.insertConstellation(Constellation(cId, s"constellation-${cId}", rId, s"region-${rId}")) + ) + countS <- ZIO.foreach(solarSystems.values): mss => + query.sde.insertSolarSystem( + SolarSystem( + id = mss.systemId, + starId = None, + starTypeId = None, + name = mss.name, + regionName = "region", + regionId = mss.regionId, + constellationName = "constellation", + constellationId = mss.constellationId, + effectTypeId = None, + whClassId = None, + securityClass = None, + security = None, + border = false, + corridor = false, + fringe = false, + hub = false, + international = false, + regional = false + ) + ) + _ <- ZIO.logDebug(s"Inserted ${countC.sum} constellations, ${countR.sum} regions, ${countS.sum} solar systems") + yield () + + private def insertMap(mapId: MapId) = + query.map.upsertMap( + MapModel(mapId, s"test-${mapId}", MapDisplayType.Manual, Instant.EPOCH, UserId.Invalid, None, None) + ) + + private def runMultiple( + mapId: MapId, + initialState: MapState, + messages: Chunk[Identified[MapRequest]] + ): URIO[MapEnv, (MapState, Chunk[Identified[MapResponse]])] = + ZIO.foldLeft(messages)((initialState, Chunk.empty[Identified[MapResponse]])): + case ((state, responses), req) => MapEntity.handle(mapId, state, req).map((s, r) => (s, responses ++ r)) + + private def updateRefData(state: MapState, solarSystems: Map[SystemId, MapSolarSystem]) = + state.copy(ref = MapRef(solarSystems = solarSystems)) + + private inline def matches[A](pf: PartialFunction[A, Boolean]): Assertion[A] = + Assertion[A](TestArrow.fromFunction(a => pf.unapply(a).getOrElse(false))) diff --git a/server/src/test/scala/org/updraft0/controltower/server/tracking/TestTrackers.scala b/server/src/test/scala/org/updraft0/controltower/server/tracking/TestTrackers.scala new file mode 100644 index 0000000..d967df1 --- /dev/null +++ b/server/src/test/scala/org/updraft0/controltower/server/tracking/TestTrackers.scala @@ -0,0 +1,55 @@ +package org.updraft0.controltower.server.tracking + +import org.updraft0.controltower.constant.{CharacterId, MapId} +import org.updraft0.controltower.db.model +import org.updraft0.controltower.db.model.MapRole +import org.updraft0.controltower.server.map.MapSessionMessage.MapCharacters +import org.updraft0.controltower.server.map.{MapPermissionTracker, MapSessionMessage} +import zio.* + +/** An in-memory location tracker that can be manually updated during test code + */ +trait TestLocationTracker extends LocationTracker: + // test introspection + def testTrackingRequest: Queue[LocationTrackingRequest] + def testLocationUpdate: Hub[LocationUpdate] + +object TestLocationTracker: + + def empty: ZLayer[Any, Nothing, TestLocationTracker] = + ZLayer.scoped: + for + incoming <- Queue.unbounded[LocationTrackingRequest] + updateHub <- Hub.unbounded[LocationUpdate] + yield new TestLocationTracker: + override def testTrackingRequest: Queue[LocationTrackingRequest] = incoming + override def testLocationUpdate: Hub[LocationUpdate] = updateHub + override def inbound: Enqueue[LocationTrackingRequest] = incoming + override def updates: URIO[Scope, Dequeue[LocationUpdate]] = updateHub.subscribe + +/** An in-memory permission tracker that can be manually updated with a map of roles per character + */ +trait TestPermissionTracker extends MapPermissionTracker: + // test introspection + def testPushPermissions(mapId: MapId, all: Map[CharacterId, model.MapRole]): UIO[Unit] + def testHub: Hub[MapSessionMessage] + +object TestPermissionTracker: + + def empty: ZLayer[Any, Nothing, TestPermissionTracker] = + ZLayer.scoped: + for + stateRef <- Ref.make(Map.empty[MapId, Map[CharacterId, model.MapRole]]) + hub <- Hub.unbounded[MapSessionMessage] + yield new TestPermissionTracker: + override def testPushPermissions(mapId: MapId, all: Map[CharacterId, MapRole]): UIO[Unit] = + // note: currently no way to issue individual character role changes for testing + stateRef.update(s => s.updated(mapId, all)) *> hub.publish(MapCharacters(mapId, all)).unit + override def testHub: Hub[MapSessionMessage] = hub + override def reloadPermissions(mapId: MapId): UIO[Unit] = ZIO.unit + override def subscribe(mapId: MapId): ZIO[Scope, Nothing, Dequeue[MapSessionMessage]] = hub.subscribe + override def subscribeSession( + mapId: MapId, + characterId: CharacterId + ): ZIO[Scope, Nothing, Dequeue[MapSessionMessage]] = + ZIO.dieMessage("subscribing by session not supported in test permission tracker") diff --git a/ui/src/main/scala/controltower/page/map/view/SystemSignatureView.scala b/ui/src/main/scala/controltower/page/map/view/SystemSignatureView.scala index d7c180b..7920e02 100644 --- a/ui/src/main/scala/controltower/page/map/view/SystemSignatureView.scala +++ b/ui/src/main/scala/controltower/page/map/view/SystemSignatureView.scala @@ -594,7 +594,7 @@ private[view] def timeDiff(time: Observable[Instant], start: Instant) = private inline def displayDuration(d: Duration) = if (d.getSeconds < 60) s"${d.getSeconds.max(0)}s" else if (d.getSeconds < 60 * 60) s"${d.getSeconds / 60}m ${d.getSeconds % 60}s" - else s"${d.getSeconds / 3_600}h ${d.getSeconds % 3_600 / 60}m" + else s"${d.getSeconds / 3_600}h ${d.getSeconds % 3_600 / 60}m" private def scanClass(sigs: Array[MapSystemSignature]) = if (sigs.forall(!sigIsScanned(_, fakeScan = true))) "unscanned" diff --git a/ui/src/main/scala/controltower/ui/ArrayRenderedVar.scala b/ui/src/main/scala/controltower/ui/ArrayRenderedVar.scala index 866f8a3..82cc884 100644 --- a/ui/src/main/scala/controltower/ui/ArrayRenderedVar.scala +++ b/ui/src/main/scala/controltower/ui/ArrayRenderedVar.scala @@ -25,8 +25,8 @@ final class ArrayRenderedVar[A: ClassTag] private (render: RenderT[A])(using Can private inline def removeOrUpdateFor(k: Long, v: Var[A]) = Observer[Option[A]]: - case None => removeAt(k) - case Some(next) => updateAt(k, next) + case None => removeAt(k) + case Some(next) => updateAt(k, next) // TODO: not very nice def itemsNow: Seq[A] = internalVar.now().toSeq.map(_._2).map(_.now())