From 74a62b872db3a5a223989b57dea07f66cb87fb4d Mon Sep 17 00:00:00 2001 From: "Francois @fanf42 Armand" Date: Thu, 4 Aug 2022 08:50:48 +0200 Subject: [PATCH] Fixes #21539: Make campaign test work --- .../campaigns/CampaignEventRepository.scala | 6 ++ .../rudder/campaigns/DataTypes.scala | 20 ++++--- .../campaigns/JsonCampaignSerializer.scala | 31 +++++----- .../campaigns/MainCampaignService.scala | 12 ++-- .../rudder/rest/lift/CampaignApi.scala | 11 ++-- .../com/normation/rudder/MockServices.scala | 17 +++--- ...gnApiTests.scala => CampaignApiTest.scala} | 59 +++++++++++++++---- .../normation/rudder/rest/SystemApiTest.scala | 10 ++-- 8 files changed, 109 insertions(+), 57 deletions(-) rename webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/rest/{CampaignApiTests.scala => CampaignApiTest.scala} (62%) diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/campaigns/CampaignEventRepository.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/campaigns/CampaignEventRepository.scala index 8b4b0029383..650eeddad5b 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/campaigns/CampaignEventRepository.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/campaigns/CampaignEventRepository.scala @@ -56,6 +56,12 @@ import java.sql.Timestamp trait CampaignEventRepository { def get(campaignEventId: CampaignEventId) : IOResult[CampaignEvent] def saveCampaignEvent(c : CampaignEvent) : IOResult[CampaignEvent] + + /* + * Semantic is: + * - if Nil or None, clause is ignored + * - if a value is provided, then it is use to filter things accordingly + */ def getWithCriteria(states : List[CampaignEventState], campaignType: Option[CampaignType], campaignId : Option[CampaignId]) : IOResult[List[CampaignEvent]] } diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/campaigns/DataTypes.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/campaigns/DataTypes.scala index 65b5b6b3d6b..661acd15afd 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/campaigns/DataTypes.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/campaigns/DataTypes.scala @@ -49,9 +49,9 @@ import scala.concurrent.duration.Duration trait Campaign { - def info : CampaignInfo - def details : CampaignDetails - def campaignType : CampaignType + def info : CampaignInfo + def details : CampaignDetails + def campaignType: CampaignType def copyWithId(newId : CampaignId) : Campaign } @@ -144,11 +144,17 @@ case class WeeklySchedule( case class OneShot(start : DateTime) extends CampaignSchedule trait CampaignDetails -trait CampaignType { - def value : String -} -case class CampaignEvent(id : CampaignEventId, campaignId : CampaignId, state : CampaignEventState, start : DateTime, end : DateTime, campaignType : CampaignType ) +case class CampaignType(value : String) + +case class CampaignEvent( + id : CampaignEventId + , campaignId : CampaignId + , state : CampaignEventState + , start : DateTime + , end : DateTime + , campaignType: CampaignType +) case class CampaignEventId(value : String) sealed trait CampaignEventState{ diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/campaigns/JsonCampaignSerializer.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/campaigns/JsonCampaignSerializer.scala index 3dc89ffc507..db430f3f1fe 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/campaigns/JsonCampaignSerializer.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/campaigns/JsonCampaignSerializer.scala @@ -82,10 +82,7 @@ class CampaignSerializer { } val campaignTypeBase : PartialFunction[String, CampaignType]= { - case c: String => - new CampaignType { - def value: String = c - } + case c: String => CampaignType(c) } @@ -95,7 +92,9 @@ class CampaignSerializer { def parse(string : String) : IOResult[Campaign] = tranlaters.map(_.read()).fold(readBase) { case (a, b) => b orElse a }(string) def campaignType (string : String) : CampaignType = tranlaters.map(_.campaignType()).fold(campaignTypeBase) { case (a, b) => b orElse a }(string) +} +object CampaignSerializer { implicit val idEncoder : JsonEncoder[CampaignId]= JsonEncoder[String].contramap(_.value) implicit val dateTime : JsonEncoder[DateTime] = JsonEncoder[String].contramap(DateFormaterService.serialize) implicit val dayOfWeek : JsonEncoder[DayOfWeek] = JsonEncoder[Int].contramap(_.value) @@ -114,7 +113,6 @@ class CampaignSerializer { implicit val statusInfoEncoder : JsonEncoder[CampaignStatus] = DeriveJsonEncoder.gen implicit val scheduleEncoder : JsonEncoder[CampaignSchedule]= DeriveJsonEncoder.gen implicit val campaignInfoEncoder : JsonEncoder[CampaignInfo]= DeriveJsonEncoder.gen - implicit val typeEncoder : JsonEncoder[CampaignType] = JsonEncoder[String].contramap(_.value) implicit val idDecoder : JsonDecoder[CampaignId] = JsonDecoder[String].map(s => CampaignId(s)) @@ -148,19 +146,20 @@ class CampaignSerializer { } ) - implicit val durationDecoder : JsonDecoder[Duration] = JsonDecoder[Long].map(_.millis) - implicit val statusInfoDecoder : JsonDecoder[CampaignStatus] = DeriveJsonDecoder.gen - implicit val scheduleDecoder : JsonDecoder[CampaignSchedule]= DeriveJsonDecoder.gen - implicit val typeDecoder : JsonDecoder[CampaignType] = JsonDecoder[String].map(campaignType) - implicit val campaignInfoDecoder : JsonDecoder[CampaignInfo]= DeriveJsonDecoder.gen + implicit val durationDecoder : JsonDecoder[Duration] = JsonDecoder[Long].map(_.millis) + implicit val statusInfoDecoder : JsonDecoder[CampaignStatus] = DeriveJsonDecoder.gen + implicit val scheduleDecoder : JsonDecoder[CampaignSchedule]= DeriveJsonDecoder.gen + implicit val campaignTypeDecoder: JsonDecoder[CampaignType] = JsonDecoder[String].map(CampaignType) + implicit val campaignInfoDecoder: JsonDecoder[CampaignInfo]= DeriveJsonDecoder.gen - implicit val campaignEventIdDecoder : JsonDecoder[CampaignEventId] = JsonDecoder[String].map(CampaignEventId) - implicit val campaignEventStateDecoder : JsonDecoder[CampaignEventState] = JsonDecoder[String].mapOrFail(CampaignEventState.parse) - implicit val campaignEventDecoder : JsonDecoder[CampaignEvent] = DeriveJsonDecoder.gen + implicit val campaignEventIdDecoder : JsonDecoder[CampaignEventId] = JsonDecoder[String].map(CampaignEventId) + implicit val campaignEventStateDecoder: JsonDecoder[CampaignEventState] = JsonDecoder[String].mapOrFail(CampaignEventState.parse) + implicit val campaignEventDecoder : JsonDecoder[CampaignEvent] = DeriveJsonDecoder.gen - implicit val campaignEventIdEncoder : JsonEncoder[CampaignEventId] = JsonEncoder[String].contramap(_.value) - implicit val campaignEventStateEncoder : JsonEncoder[CampaignEventState] = JsonEncoder[String].contramap( _.value) - implicit val campaignEventEncoder : JsonEncoder[CampaignEvent] = DeriveJsonEncoder.gen + implicit val campaignTypeEncoder : JsonEncoder[CampaignType] = JsonEncoder[String].contramap(_.value) + implicit val campaignEventIdEncoder : JsonEncoder[CampaignEventId] = JsonEncoder[String].contramap(_.value) + implicit val campaignEventStateEncoder: JsonEncoder[CampaignEventState] = JsonEncoder[String].contramap( _.value) + implicit val campaignEventEncoder : JsonEncoder[CampaignEvent] = DeriveJsonEncoder.gen } diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/campaigns/MainCampaignService.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/campaigns/MainCampaignService.scala index 65023dbff51..16b62b18896 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/campaigns/MainCampaignService.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/campaigns/MainCampaignService.scala @@ -38,12 +38,15 @@ package com.normation.rudder.campaigns import cats.implicits._ + import com.normation.errors.IOResult import com.normation.errors.Inconsistency import com.normation.rudder.campaigns.CampaignEventState._ import com.normation.utils.StringUuidGenerator + import com.normation.zio.ZioRuntime import org.joda.time.DateTime + import zio.Queue import zio.ZIO import zio.clock.Clock @@ -286,10 +289,10 @@ class MainCampaignService(repo: CampaignEventRepository, campaignRepo: CampaignR alreadyScheduled <- repo.getWithCriteria(Running :: Scheduled :: Nil, None, None) campaigns <- campaignRepo.getAll() _ <- CampaignLogger.debug(s"Got ${campaigns.size} campaigns, check all started") - newEvents <- ZIO.foreach(campaigns.filterNot(c => alreadyScheduled.exists(_.campaignId == c.info.id))) { - c => - scheduleCampaignEvent(c) - } + toStart = campaigns.filterNot(c => alreadyScheduled.exists(_.campaignId == c.info.id)) + newEvents <- ZIO.foreach(toStart) { c => + scheduleCampaignEvent(c) + } _ <- CampaignLogger.debug(s"Scheduled ${newEvents.size} new events, queue them") _ <- ZIO.foreach(newEvents) { ev => s.queueCampaign(ev) } } yield { @@ -304,7 +307,6 @@ class MainCampaignService(repo: CampaignEventRepository, campaignRepo: CampaignR for { _ <- CampaignLogger.debug("Starting campaign scheduler") _ <- s.start().forkDaemon - _ <- CampaignLogger.debug("Starting campaign forked, now getting already created events") alreadyScheduled <- repo.getWithCriteria(Running :: Scheduled :: Nil, None, None) _ <- CampaignLogger.debug("Got events, queue them") diff --git a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/lift/CampaignApi.scala b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/lift/CampaignApi.scala index f4843b627ac..37970b183df 100644 --- a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/lift/CampaignApi.scala +++ b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/lift/CampaignApi.scala @@ -1,5 +1,4 @@ package com.normation.rudder.rest.lift -import com.normation.errors.Unexpected import com.normation.rudder.api.ApiVersion import com.normation.rudder.apidata.ZioJsonExtractor import com.normation.rudder.campaigns.CampaignEvent @@ -10,12 +9,13 @@ import com.normation.rudder.campaigns.CampaignId import com.normation.rudder.campaigns.CampaignLogger import com.normation.rudder.campaigns.CampaignRepository import com.normation.rudder.campaigns.CampaignSerializer +import com.normation.rudder.campaigns.CampaignSerializer._ import com.normation.rudder.campaigns.MainCampaignService import com.normation.rudder.rest.ApiPath import com.normation.rudder.rest.AuthzToken import com.normation.rudder.rest.RestExtractorService -import com.normation.rudder.rest.implicits.* -import com.normation.rudder.rest.CampaignApi as API +import com.normation.rudder.rest.implicits._ +import com.normation.rudder.rest.{CampaignApi => API} import com.normation.utils.StringUuidGenerator import net.liftweb.common.EmptyBox @@ -24,7 +24,8 @@ import net.liftweb.http.LiftResponse import net.liftweb.http.Req import zio.ZIO -import zio.syntax.* +import zio.syntax._ +import com.normation.errors.Unexpected class CampaignApi ( campaignRepository: CampaignRepository @@ -34,7 +35,6 @@ class CampaignApi ( , restExtractorService: RestExtractorService , stringUuidGenerator: StringUuidGenerator ) extends LiftApiModuleProvider[API] { - import campaignSerializer._ def schemas = API @@ -149,7 +149,6 @@ class CampaignApi ( val campaignType = req.params.get("campaignType").flatMap(_.headOption).map(campaignSerializer.campaignType) val campaignId = req.params.get("campaignId").flatMap(_.headOption).map(i => CampaignId(i)) campaignEventRepository.getWithCriteria(states,campaignType,campaignId).toLiftResponseList(params,schema ) - } } diff --git a/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/MockServices.scala b/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/MockServices.scala index f2a64ef575a..a02708694e0 100644 --- a/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/MockServices.scala +++ b/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/MockServices.scala @@ -149,7 +149,7 @@ import scala.concurrent.duration.Duration import scala.util.control.NonFatal import scala.xml.Elem import zio.syntax._ -import zio.{Tag as _, *} +import zio.{Tag => _, _} import com.normation.box._ import com.normation.errors.IOResult import com.normation.errors._ @@ -193,7 +193,7 @@ object TestActor { } object revisionRepo { - import GitVersion._ + import com.normation.GitVersion._ val revisionsMap = RefM.make(Map[Revision, RevisionInfo]()).runNow @@ -2346,12 +2346,13 @@ class MockSettings(wfservice: WorkflowLevelService, asyncWF: AsyncWorkflowInfo) // It would be much simpler if the root classes were concrete, parameterized with a A type: // case class Campaign[A](info: CampaignInfo, details: A) // or even info inlined -case object DumbCampaignType extends CampaignType { - val value = "dumb-campaign" -} +final object DumbCampaignType extends CampaignType("dumb-campaign") + final case class DumbCampaignDetails(name: String) extends CampaignDetails + @jsonDiscriminator("campaignType") sealed trait DumbCampaignTrait extends Campaign + @jsonHint(DumbCampaignType.value) final case class DumbCampaign(info: CampaignInfo, details: DumbCampaignDetails) extends DumbCampaignTrait { val campaignType = DumbCampaignType @@ -2390,7 +2391,7 @@ class MockCampaign() { object dumbCampaignTranslator extends JSONTranslateCampaign { import zio.json._ - import campaignSerializer._ + import com.normation.rudder.campaigns.CampaignSerializer._ implicit val dumbCampaignDetailsDecoder : JsonDecoder[DumbCampaignDetails] = DeriveJsonDecoder.gen implicit val dumbCampaignDecoder : JsonDecoder[DumbCampaignTrait] = DeriveJsonDecoder.gen implicit val dumbCampaignDetailsEncoder : JsonEncoder[DumbCampaignDetails] = DeriveJsonEncoder.gen @@ -2426,7 +2427,9 @@ class MockCampaign() { items.get.map(_.get(id)).notOptional(s"Campaign event not found: ${id.value}") } def saveCampaignEvent(c : CampaignEvent) : IOResult[CampaignEvent] = { - items.update(_ + (c.id -> c)) *> c.succeed + for { + _ <- items.update(map => map + ((c.id, c))) + } yield c } def getWithCriteria(states: List[CampaignEventState], campaignType: Option[CampaignType], campaignId: Option[CampaignId]): IOResult[List[CampaignEvent]] = { diff --git a/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/rest/CampaignApiTests.scala b/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/rest/CampaignApiTest.scala similarity index 62% rename from webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/rest/CampaignApiTests.scala rename to webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/rest/CampaignApiTest.scala index eac1094d841..4efdddeec75 100644 --- a/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/rest/CampaignApiTests.scala +++ b/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/rest/CampaignApiTest.scala @@ -37,6 +37,11 @@ package com.normation.rudder.rest +import com.normation.rudder.DumbCampaignType +import com.normation.rudder.campaigns.CampaignEvent +import com.normation.rudder.campaigns.CampaignEventId +import com.normation.rudder.campaigns.CampaignEventState +import com.normation.rudder.campaigns.CampaignId import com.normation.rudder.campaigns.MainCampaignService import com.normation.rudder.rest.RudderJsonResponse.JsonRudderApiResponse import com.normation.rudder.rest.RudderJsonResponse.LiftJsonResponse @@ -52,8 +57,11 @@ import org.specs2.mutable.Specification import org.specs2.runner.JUnitRunner import org.specs2.specification.AfterAll +import zio.json._ +import com.normation.zio._ + @RunWith(classOf[JUnitRunner]) -class CampaignApiTests extends Specification with AfterAll with Loggable { +class CampaignApiTest extends Specification with AfterAll with Loggable { val restTestSetUp = RestTestSetUp.newEnv val restTest = new RestTest(restTestSetUp.liftRules) @@ -72,7 +80,7 @@ class CampaignApiTests extends Specification with AfterAll with Loggable { def children(f: File) = f.children.toList.map(_.name) - // org.slf4j.LoggerFactory.getLogger("api-processing").asInstanceOf[ch.qos.logback.classic.Logger].setLevel(ch.qos.logback.classic.Level.TRACE) + org.slf4j.LoggerFactory.getLogger("campaign").asInstanceOf[ch.qos.logback.classic.Logger].setLevel(ch.qos.logback.classic.Level.TRACE) sequential @@ -83,32 +91,59 @@ class CampaignApiTests extends Specification with AfterAll with Loggable { |"name":"first campaign", |"description":"a test campaign present when rudder boot", |"status":{"value":"enabled"}, - |"schedule":{"day":1,"startHour":3,"type":"weekly"}, + |"schedule":{"day":1,"startHour":3,"startMinute":42,"type":"weekly"}, |"duration":3600000}, - |"details":{"name":"campaign #0"} + |"details":{"name":"campaign #0"}, + |"campaignType":"dumb-campaign" |}""".stripMargin.replaceAll("""\n""","") + // init in mock + val ce0 = CampaignEvent( + CampaignEventId("e0") + , CampaignId("c0") + , CampaignEventState.Finished + , new DateTime(0) + , new DateTime(1) + , DumbCampaignType + ) + "have one campaign" in { val resp = s"""[$c0json]""" - restTest.testGETResponse("/secure/api/campaigns/models") { + restTest.testGETResponse("/secure/api/campaigns") { case Full(LiftJsonResponse(JsonRudderApiResponse(_, _, _, Some(map), _), _, _)) => map.asInstanceOf[Map[String, List[zio.json.ast.Json]]]("campaigns").toJson must beEqualTo(resp) case err => ko(s"I got an error in test: ${err}") } } - // THIS TEST IS BROKEN FOR NOW + "and its json format is stable" in { + restTest.testGETResponse("/secure/api/campaigns/events") { + case Full(LiftJsonResponse(JsonRudderApiResponse(_, _, _, Some(map), _), _, _)) => + map.asInstanceOf[Map[String, List[CampaignEvent]]]("campaignEvents") must beEqualTo(List(ce0)) + case err => ko(s"I got an error in test: ${err}") + } + } - "have a second one when we trigger a scheduling check" in { - MainCampaignService.start(restTestSetUp.mockCampaign.mainCampaignService) - // the second one won't be C0, but for now, it is not even generated - val resp = s"""[$c0json,$c0json]""" + "have one more campaign event when we trigger a scheduling check" in { + ZioRuntime.unsafeRun(MainCampaignService.start(restTestSetUp.mockCampaign.mainCampaignService)) - restTest.testGETResponse("/secure/api/campaigns/models") { + Thread.sleep(50) // this is needed because the start is async and takes a bit of time + + restTest.testGETResponse("/secure/api/campaigns/events") { case Full(LiftJsonResponse(JsonRudderApiResponse(_, _, _, Some(map), _), _, _)) => - map.asInstanceOf[Map[String, List[zio.json.ast.Json]]]("campaigns").toJson must beEqualTo(resp) + val events = map.asInstanceOf[Map[String, List[CampaignEvent]]]("campaignEvents") + + (events.size must beEqualTo(2)) and + { + val next = events.collectFirst { case x if x.id != ce0.id => x }.getOrElse(throw new IllegalArgumentException(s"Missing test value")) + // it's in the future + (next.start.getMillis must be_>(System.currentTimeMillis())) and + (next.state must beEqualTo(CampaignEventState.Scheduled)) and + (next.campaignId must beEqualTo(ce0.campaignId)) + } + case err => ko(s"I got an error in test: ${err}") } diff --git a/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/rest/SystemApiTest.scala b/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/rest/SystemApiTest.scala index c3d11046656..b1059b3a03f 100644 --- a/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/rest/SystemApiTest.scala +++ b/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/rest/SystemApiTest.scala @@ -42,9 +42,10 @@ import java.nio.file.Files import java.util.zip.ZipFile import com.normation.rudder.rest.RestUtils.toJsonResponse import com.normation.rudder.rest.v1.RestStatus -import net.liftweb.common.{Full, Loggable} -import net.liftweb.http.{InMemoryResponse, Req} -import net.liftweb.json.JsonAST.{JArray, JField, JObject} +import net.liftweb.common._ +import net.liftweb.http.InMemoryResponse +import net.liftweb.http.Req +import net.liftweb.json.JsonAST._ import net.liftweb.json.JsonDSL._ import org.apache.commons.io.FileUtils import org.junit.runner.RunWith @@ -524,7 +525,8 @@ class SystemApiTest extends Specification with AfterAll with Loggable { import net.liftweb.http.js.JsExp._ (code must beEqualTo(500)) and (json.toJsCmd must beMatching(".*Error when trying to get archive as a Zip: SystemError: Error when retrieving commit revision.*")) - case _ => ko + case x => + ko } }