diff --git a/build.sbt b/build.sbt index 65bbc5c3aa..164be6eb5d 100644 --- a/build.sbt +++ b/build.sbt @@ -14,7 +14,13 @@ val scala3LTSVersion = "3.3.6" ThisBuild / scalaVersion := scala213Version ThisBuild / crossScalaVersions := Seq(scala213Version, scala3LTSVersion) -ThisBuild / libraryDependencies ++= Seq(specs2, specs2Matchers, scalacheck, scalactic, scalatest) +ThisBuild / libraryDependencies ++= Seq( + specs2(scalaVersion.value), + specs2Matchers(scalaVersion.value), + scalacheck(scalaVersion.value), + scalactic, + scalatest +) ThisBuild / scalacOptions ++= Seq("-deprecation") @@ -124,16 +130,16 @@ lazy val webkit = commons_fileupload, rhino, servlet_api, - specs2Prov, - specs2MatchersProv, + specs2Prov(scalaVersion.value), + specs2MatchersProv(scalaVersion.value), jetty11, jettywebapp, jwebunit, - mockito_scalatest, + mockito_scalatest(scalaVersion.value), jquery, jasmineCore, jasmineAjax, - specs2Mock + specs2Mock(scalaVersion.value) ), libraryDependencies ++= { CrossVersion.partialVersion(scalaVersion.value) match { diff --git a/project/Dependencies.scala b/project/Dependencies.scala index e207ed8e13..7e7745e5f2 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -82,17 +82,38 @@ object Dependencies { lazy val derby = "org.apache.derby" % "derby" % "10.7.1.1" % Test lazy val h2database = "com.h2database" % "h2" % "1.2.147" % Test - lazy val specs2 = "org.specs2" %% "specs2-core" % "4.21.0" % Test - lazy val scalacheck = "org.specs2" %% "specs2-scalacheck" % specs2.revision % Test - lazy val specs2Prov = "org.specs2" %% "specs2-core" % specs2.revision % Provided - lazy val specs2Matchers = "org.specs2" %% "specs2-matcher-extra" % specs2.revision % Test - lazy val specs2MatchersProv = "org.specs2" %% "specs2-matcher-extra" % specs2.revision % Provided - lazy val specs2Mock = "org.specs2" %% "specs2-mock" % specs2.revision % Test + // Specs2 versions differ between Scala 2 and Scala 3 + def specs2Version(scalaVersion: String): String = { + CrossVersion.partialVersion(scalaVersion) match { + case Some((2, 13)) => "4.21.0" + case Some((3, _)) => "5.6.4" + case _ => "4.21.0" + } + } + + lazy val specs2: ModuleMap = (version: String) => "org.specs2" %% "specs2-core" % specs2Version(version) % Test + lazy val scalacheck: ModuleMap = (version: String) => "org.specs2" %% "specs2-scalacheck" % specs2Version(version) % Test + lazy val specs2Prov: ModuleMap = (version: String) => "org.specs2" %% "specs2-core" % specs2Version(version) % Provided + lazy val specs2Matchers: ModuleMap = (version: String) => "org.specs2" %% "specs2-matcher-extra" % specs2Version(version) % Test + lazy val specs2MatchersProv: ModuleMap = (version: String) => "org.specs2" %% "specs2-matcher-extra" % specs2Version(version) % Provided + lazy val specs2Mock: ModuleMap = (version: String) => { + CrossVersion.partialVersion(version) match { + case Some((2, 13)) => "org.specs2" %% "specs2-mock" % specs2Version(version) % Test + case Some((3, _)) => "org.scalatestplus" %% "mockito-5-18" % "3.2.19.0" % Test + case _ => "org.specs2" %% "specs2-mock" % specs2Version(version) % Test + } + } lazy val scalactic = "org.scalactic" %% "scalactic" % "3.2.19" % Test lazy val scalatest = "org.scalatest" %% "scalatest" % "3.2.19" % Test lazy val scalatest_junit = "org.scalatestplus" %% "junit-4-12" % "3.1.2.0" % Test - lazy val mockito_scalatest = "org.mockito" %% "mockito-scala-scalatest" % "1.14.3" % Test + lazy val mockito_scalatest: ModuleMap = (version: String) => { + CrossVersion.partialVersion(version) match { + case Some((2, 13)) => "org.mockito" %% "mockito-scala-scalatest" % "1.14.3" % Test + case Some((3, _)) => "org.scalatestplus" %% "mockito-5-18" % "3.2.19.0" % Test + case _ => "org.mockito" %% "mockito-scala-scalatest" % "1.14.3" % Test + } + } lazy val scalamock = "org.scalamock" %% "scalamock" % "7.4.1" % Test diff --git a/web/webkit/src/main/scala/net/liftweb/builtin/snippet/Menu.scala b/web/webkit/src/main/scala/net/liftweb/builtin/snippet/Menu.scala index 72f8775684..00604d952f 100644 --- a/web/webkit/src/main/scala/net/liftweb/builtin/snippet/Menu.scala +++ b/web/webkit/src/main/scala/net/liftweb/builtin/snippet/Menu.scala @@ -428,8 +428,6 @@ object Menu extends DispatchSnippet { for { name <- S.attr("name").toList } yield { - type T = Q forSome { type Q } - // Builds a link for the given loc def buildLink[T](loc : Loc[T]) = { Group(SiteMap.buildLink(name, text) match { @@ -441,8 +439,8 @@ object Menu extends DispatchSnippet { } (S.originalRequest.flatMap(_.location), S.attr("param"), SiteMap.findAndTestLoc(name)) match { - case (_, Full(param), Full(loc: Loc[_] with ConvertableLoc[_])) => { - val typedLoc = loc.asInstanceOf[Loc[T] with ConvertableLoc[T]] + case (_, Full(param), Full(loc: Loc[t] with ConvertableLoc[_])) => { + val typedLoc = loc.asInstanceOf[Loc[t] with ConvertableLoc[t]] (for { pv <- typedLoc.convert(param) diff --git a/web/webkit/src/main/scala/net/liftweb/builtin/snippet/Tail.scala b/web/webkit/src/main/scala/net/liftweb/builtin/snippet/Tail.scala index 40b70885e9..1cc2c11869 100644 --- a/web/webkit/src/main/scala/net/liftweb/builtin/snippet/Tail.scala +++ b/web/webkit/src/main/scala/net/liftweb/builtin/snippet/Tail.scala @@ -60,10 +60,10 @@ object Head extends DispatchSnippet { case e: Elem if (null eq e.prefix) => NodeSeq.Empty case x => x } - - val xhtml = validHeadTagsOnly(_xhtml) - { + val xhtml = validHeadTagsOnly(_xhtml) + + { if ((S.attr("withResourceId") or S.attr("withresourceid")).filter(Helpers.toBoolean).isDefined) { WithResourceId.render(xhtml) } else { diff --git a/web/webkit/src/main/scala/net/liftweb/http/CometActor.scala b/web/webkit/src/main/scala/net/liftweb/http/CometActor.scala index d435400cac..660613dc21 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/CometActor.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/CometActor.scala @@ -1240,7 +1240,7 @@ trait BaseCometActor extends LiftActor with LiftCometActor with CssBindImplicits Empty } - new RenderOut(Full(in: NodeSeq), internalFixedRender, additionalJs, Empty, false) + new RenderOut(Full(in: NodeSeq), internalFixedRender, additionalJs, Empty, false) } protected implicit def jsToXmlOrJsCmd(in: JsCmd): RenderOut = { diff --git a/web/webkit/src/main/scala/net/liftweb/http/ContentParser.scala b/web/webkit/src/main/scala/net/liftweb/http/ContentParser.scala index bb6bfae5dc..e0a4ff65ea 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/ContentParser.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/ContentParser.scala @@ -54,7 +54,7 @@ object ContentParser { * @return your parser wrapped up to handle an `InputStream` */ def toInputStreamParser(simpleParser: String=>Box[NodeSeq]): InputStream=>Box[NodeSeq] = { - is:InputStream => + (is: InputStream) => for { bytes <- Helpers.tryo(Helpers.readWholeStream(is)) elems <- simpleParser(new String(bytes, "UTF-8")) diff --git a/web/webkit/src/main/scala/net/liftweb/http/LiftMerge.scala b/web/webkit/src/main/scala/net/liftweb/http/LiftMerge.scala index cfbf17c0b3..192cbe588b 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/LiftMerge.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/LiftMerge.scala @@ -160,59 +160,59 @@ private[http] trait LiftMerge { startingState.copy(headChild = false, headInBodyChild = false, tailInBodyChild = false, bodyChild = false) } - val bodyHead = childInfo.headInBodyChild && ! headInBodyChild - val bodyTail = childInfo.tailInBodyChild && ! tailInBodyChild - - HtmlNormalizer - .normalizeNode(node, contextPath, stripComments, LiftRules.extractInlineJavaScript) - .map { - case normalized @ NodeAndEventJs(normalizedElement: Elem, _) => - val normalizedChildren = - normalizeMergeAndExtractEvents(normalizedElement.child, childInfo) - - normalized.copy( - normalizedElement.copy(child = normalizedChildren.nodes), - js = normalized.js & normalizedChildren.js - ) - - case other => - other - } - .map { normalizedResults: NodeAndEventJs => - node match { - case e: Elem if e.label == "node" && - e.prefix == "lift_deferred" => - val deferredNodes: Seq[NodesAndEventJs] = { - for { - idAttribute <- e.attributes("id").take(1) - id = idAttribute.text - nodes <- processedSnippets.get(id) - } yield { - normalizeMergeAndExtractEvents(nodes, startingState) - }}.toSeq - - deferredNodes.foldLeft(soFar.append(normalizedResults))(_ append _) - - case _ => - if (headChild) { - headChildren ++= normalizedResults.node - } else if (headInBodyChild) { - addlHead ++= normalizedResults.node - } else if (tailInBodyChild) { - addlTail ++= normalizedResults.node - } else if (_bodyChild && ! bodyHead && ! bodyTail) { - bodyChildren ++= normalizedResults.node - } - - if (bodyHead || bodyTail) { - soFar.append(normalizedResults.js) - } else { - soFar.append(normalizedResults) - } - } - } getOrElse { - soFar + val bodyHead = childInfo.headInBodyChild && ! headInBodyChild + val bodyTail = childInfo.tailInBodyChild && ! tailInBodyChild + + HtmlNormalizer + .normalizeNode(node, contextPath, stripComments, LiftRules.extractInlineJavaScript) + .map { + case normalized @ NodeAndEventJs(normalizedElement: Elem, _) => + val normalizedChildren = + normalizeMergeAndExtractEvents(normalizedElement.child, childInfo) + + normalized.copy( + normalizedElement.copy(child = normalizedChildren.nodes), + js = normalized.js & normalizedChildren.js + ) + + case other => + other + } + .map { (normalizedResults: NodeAndEventJs) => + node match { + case e: Elem if e.label == "node" && + e.prefix == "lift_deferred" => + val deferredNodes: Seq[NodesAndEventJs] = { + for { + idAttribute <- e.attributes("id").take(1) + id = idAttribute.text + nodes <- processedSnippets.get(id) + } yield { + normalizeMergeAndExtractEvents(nodes, startingState) + }}.toSeq + + deferredNodes.foldLeft(soFar.append(normalizedResults))(_ append _) + + case _ => + if (headChild) { + headChildren ++= normalizedResults.node + } else if (headInBodyChild) { + addlHead ++= normalizedResults.node + } else if (tailInBodyChild) { + addlTail ++= normalizedResults.node + } else if (_bodyChild && ! bodyHead && ! bodyTail) { + bodyChildren ++= normalizedResults.node + } + + if (bodyHead || bodyTail) { + soFar.append(normalizedResults.js) + } else { + soFar.append(normalizedResults) + } } + } getOrElse { + soFar + } } } @@ -248,7 +248,7 @@ private[http] trait LiftMerge { } // Appends ajax script to body - if (LiftRules.autoIncludeAjaxCalc.vend().apply(this)) { + if (LiftRules.autoIncludeAjaxCalc.vend()(this)) { bodyChildren += , , , @@ -70,7 +66,7 @@ class LiftMergeSpec extends Specification with XmlMatchers with Mockito { ): NodeSeq) } - "merge tail segments in the page body in order at the end of the body" in new WithRules(testRules) { + "merge tail segments in the page body in order at the end of the body" in withLiftRules(testRules) { val result = testSession.merge( @@ -97,14 +93,14 @@ class LiftMergeSpec extends Specification with XmlMatchers with Mockito { mockReq ) - (result \ "body" \ "_").takeRight(3) must_== (Seq( + (result \ "body" \ "_").takeRight(3) === (Seq( , , ): NodeSeq) } - "not merge tail segments in the head" in new WithRules(testRules) { + "not merge tail segments in the head" in withLiftRules(testRules) { val result = testSession.merge( @@ -133,14 +129,14 @@ class LiftMergeSpec extends Specification with XmlMatchers with Mockito { mockReq ) - (result \ "body" \ "_").takeRight(3) must_== (Seq( + (result \ "body" \ "_").takeRight(3) === (Seq( , , ): NodeSeq) } - "normalize absolute link hrefs everywhere" in new WithLiftContext(testRules, testSession) { + "normalize absolute link hrefs everywhere" in withLiftContext(testRules, testSession) { val result = testSession.merge( @@ -168,13 +164,13 @@ class LiftMergeSpec extends Specification with XmlMatchers with Mockito { mockReq ) - (result \\ "link").map(_ \@ "href") must_== + (result \\ "link").map(_ \@ "href") === "/context-path/testlink" :: "/context-path/testlink2" :: "/context-path/testlink3" :: Nil } - "normalize absolute script srcs everywhere" in new WithLiftContext(testRules, testSession) { + "normalize absolute script srcs everywhere" in withLiftContext(testRules, testSession) { val result = testSession.merge( @@ -202,12 +198,12 @@ class LiftMergeSpec extends Specification with XmlMatchers with Mockito { mockReq ) - (result \\ "script").map(_ \@ "src") must_== + (result \\ "script").map(_ \@ "src") === "/context-path/testscript" :: "/context-path/testscript2" :: Nil } - "normalize absolute a hrefs everywhere" in new WithLiftContext(testRules, testSession) { + "normalize absolute a hrefs everywhere" in withLiftContext(testRules, testSession) { val result = testSession.merge( @@ -235,7 +231,7 @@ class LiftMergeSpec extends Specification with XmlMatchers with Mockito { mockReq ) - (result \\ "a").map(_ \@ "href") must_== + (result \\ "a").map(_ \@ "href") === "/context-path/testa1" :: "testa3" :: "/context-path/testa2" :: @@ -244,7 +240,7 @@ class LiftMergeSpec extends Specification with XmlMatchers with Mockito { "/context-path/testa5" :: Nil } - "normalize absolute form actions everywhere" in new WithLiftContext(testRules, testSession) { + "normalize absolute form actions everywhere" in withLiftContext(testRules, testSession) { val result = testSession.merge( @@ -272,7 +268,7 @@ class LiftMergeSpec extends Specification with XmlMatchers with Mockito { mockReq ) - (result \\ "form").map(_ \@ "action") must_== + (result \\ "form").map(_ \@ "action") === "/context-path/testform1" :: "testform3" :: "/context-path/testform2" :: @@ -281,7 +277,7 @@ class LiftMergeSpec extends Specification with XmlMatchers with Mockito { "/context-path/testform5" :: Nil } - "not rewrite script srcs anywhere" in new WithLiftContext(testRules, testSession) { + "not rewrite script srcs anywhere" in withLiftContext(testRules, testSession) { val result = URLRewriter.doWith((_: String) => "rewritten") { testSession.merge( @@ -309,13 +305,13 @@ class LiftMergeSpec extends Specification with XmlMatchers with Mockito { ) } - (result \\ "script").map(_ \@ "src") must_== + (result \\ "script").map(_ \@ "src") === "testscript" :: "testscript2" :: "testscript3" :: Nil } - "not rewrite link hrefs anywhere" in new WithLiftContext(testRules, testSession) { + "not rewrite link hrefs anywhere" in withLiftContext(testRules, testSession) { val result = URLRewriter.doWith((_: String) => "rewritten") { testSession.merge( @@ -343,13 +339,13 @@ class LiftMergeSpec extends Specification with XmlMatchers with Mockito { ) } - (result \\ "link").map(_ \@ "href") must_== + (result \\ "link").map(_ \@ "href") === "testlink" :: "testlink2" :: "testlink3" :: Nil } - "rewrite a hrefs everywhere" in new WithLiftContext(testRules, testSession) { + "rewrite a hrefs everywhere" in withLiftContext(testRules, testSession) { val result = URLRewriter.doWith((_: String) => "rewritten") { testSession.merge( @@ -377,13 +373,13 @@ class LiftMergeSpec extends Specification with XmlMatchers with Mockito { ) } - (result \\ "a").map(_ \@ "href") must_== + (result \\ "a").map(_ \@ "href") === "rewritten" :: "rewritten" :: "rewritten" :: Nil } - "rewrite form actions everywhere" in new WithLiftContext(testRules, testSession) { + "rewrite form actions everywhere" in withLiftContext(testRules, testSession) { val result = URLRewriter.doWith((_: String) => "rewritten") { testSession.merge( @@ -411,13 +407,13 @@ class LiftMergeSpec extends Specification with XmlMatchers with Mockito { ) } - (result \\ "form").map(_ \@ "action") must_== + (result \\ "form").map(_ \@ "action") === "rewritten" :: "rewritten" :: "rewritten" :: Nil } - "include a page script in the page tail if events are extracted" in new WithLiftContext(eventExtractingTestRules, testSession) { + "include a page script in the page tail if events are extracted" in withLiftContext(eventExtractingTestRules, testSession) { val result = testSession.merge( @@ -437,7 +433,7 @@ class LiftMergeSpec extends Specification with XmlMatchers with Mockito { val scripts = (result \\ "script") - scripts must have length(1) + scripts must haveLength(1) scripts.map(_ \@ "src") must beLike { case scriptSrc :: Nil => scriptSrc must beMatching("/context-path/lift/page/F[^.]+.js") @@ -449,7 +445,7 @@ class LiftMergeSpec extends Specification with XmlMatchers with Mockito { } } - "include a page script in the page tail even if the page doesn't have a head and body" in new WithLiftContext(eventExtractingTestRules, testSession) { + "include a page script in the page tail even if the page doesn't have a head and body" in withLiftContext(eventExtractingTestRules, testSession) { val result = testSession.merge(
@@ -462,7 +458,7 @@ class LiftMergeSpec extends Specification with XmlMatchers with Mockito { val scripts = (result \\ "script") - scripts must have length(1) + scripts must haveLength(1) scripts.map(_ \@ "src") must beLike { case scriptSrc :: Nil => scriptSrc must beMatching("/context-path/lift/page/F[^.]+.js") diff --git a/web/webkit/src/test/scala-2.13/net/liftweb/http/LiftSessionSpec.scala b/web/webkit/src/test/scala-2.13/net/liftweb/http/LiftSessionSpec.scala new file mode 100644 index 0000000000..50701622c6 --- /dev/null +++ b/web/webkit/src/test/scala-2.13/net/liftweb/http/LiftSessionSpec.scala @@ -0,0 +1,141 @@ +/* + * Copyright 2010-2015 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package http + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.xml.NodeSeq +import net.liftweb.common.{Full, Empty, Failure} +import net.liftweb.util.Helpers.tryo +import org.specs2.specification.BeforeEach +import org.specs2.mutable.Specification + +object LiftSessionSpec { + private var receivedMessages = Vector[Int]() + private object NoOp + + private[LiftSessionSpec] class TestCometActor extends CometActor { + def render = NodeSeq.Empty + + override def lowPriority = { + case n: Int => + receivedMessages :+= n + case NoOp => + reply(NoOp) + case _ => + } + } + + private[LiftSessionSpec] class ExplodesInConstructorCometActor extends CometActor { + def render = NodeSeq.Empty + + throw new RuntimeException("boom, this explodes in the constructor!") + override def lowPriority = { + case _ => + } + } +} + +class LiftSessionSpec extends Specification with BeforeEach { + import LiftSessionSpec._ + + sequential + + // specs2 4.x: before method executes directly without step wrapper + override def before = { receivedMessages = Vector[Int]() } + + "A LiftSession" should { + + "Send accumulated messages to a newly-created comet actor in the order in which they arrived" in { + val session = new LiftSession("Test Session", "", Empty) + + S.init(Empty, session) { + val cometName = "TestCometActor" + val sendingMessages = 1 to 20 + + sendingMessages.foreach { message => + session.sendCometMessage(cometName, Full(cometName), message) + } + + session.findOrCreateComet[TestCometActor](Full(cometName), NodeSeq.Empty, Map.empty).map { comet => + comet !? NoOp /* Block to allow time for all messages to be collected */ + } + + receivedMessages === sendingMessages.toVector + } + } + + "Send messages to all comets of a particular type, regardless of name" in { + val session = new LiftSession("Test Session", "", Empty) + + S.init(Empty, session) { + val cometType = "TestCometActor" + val cometName = "Comet1" + + // Spin up two comets: one with a name and one without + session.sendCometMessage(cometType, Full(cometName), NoOp) + session.sendCometMessage(cometType, Empty, NoOp) + + // Send a message to both + session.sendCometMessage(cometType, 1) + + // Ensure both process the message + session.findOrCreateComet[TestCometActor](Full(cometName), NodeSeq.Empty, Map.empty).map { comet => + comet !? NoOp + } + session.findOrCreateComet[TestCometActor](Empty, NodeSeq.Empty, Map.empty).map { comet => + comet !? NoOp + } + + // Assert that the message was seen twice + receivedMessages === Vector(1, 1) + } + } + + "Surface exceptions from the no-arg comet constructor" in { + val session = new LiftSession("Test Session", "", Empty) + + S.init(Empty, session) { + val result = session.findOrCreateComet[ExplodesInConstructorCometActor](Empty, NodeSeq.Empty, Map.empty) + + result match { + case Failure(_, Full(ex: java.lang.reflect.InvocationTargetException), _) => + success + + case other => + failure("Comet did not fail with an InvocationTargetException. Please check to ensure error handling in no-arg comet constructors wasn't broken.") + } + } + } + } + + "LiftSession when building deferred functions" should { + + "not fail when the underlying container request is null" in { + val session = new LiftSession("Test Session", "", Empty) + + def stubFunction: () => Int = () => 3 + + S.init(Full(Req.nil), session) { + + val attempt = tryo(session.buildDeferredFunction(stubFunction)) + + attempt.toOption must beSome + } + } + } +} diff --git a/web/webkit/src/test/scala/net/liftweb/http/ReqSpec.scala b/web/webkit/src/test/scala-2.13/net/liftweb/http/ReqSpec.scala similarity index 88% rename from web/webkit/src/test/scala/net/liftweb/http/ReqSpec.scala rename to web/webkit/src/test/scala-2.13/net/liftweb/http/ReqSpec.scala index ec3392a54f..1c4a85ed93 100644 --- a/web/webkit/src/test/scala/net/liftweb/http/ReqSpec.scala +++ b/web/webkit/src/test/scala-2.13/net/liftweb/http/ReqSpec.scala @@ -22,12 +22,9 @@ import java.io.ByteArrayInputStream import scala.xml.XML import org.specs2.matcher.XmlMatchers - -import org.mockito.Mockito._ - import org.specs2.mutable.Specification -import org.specs2.mock.Mockito import org.specs2.specification.Scope +import org.specs2.mock.Mockito import common._ import org.json4s._ @@ -65,7 +62,7 @@ class ReqSpec extends Specification with XmlMatchers with Mockito { val uac = new UserAgentCalculator { def userAgent = Full("Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-HK) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5") } - uac.safariVersion.openOrThrowException("legacy code") must_== 5 + uac.safariVersion.openOrThrowException("legacy code") === 5 } "Do the right thing with iPhone" in { @@ -74,8 +71,8 @@ class ReqSpec extends Specification with XmlMatchers with Mockito { val uac = new UserAgentCalculator { def userAgent = Full(agent) } - uac.isIPhone must_== true - uac.isIPad must_== false + uac.isIPhone === true + uac.isIPad === false } } @@ -88,8 +85,8 @@ class ReqSpec extends Specification with XmlMatchers with Mockito { val uac = new UserAgentCalculator { def userAgent = Full(agent) } - uac.isIPhone must_== false - uac.isIPad must_== true + uac.isIPhone === false + uac.isIPad === true } } @@ -105,7 +102,7 @@ class ReqSpec extends Specification with XmlMatchers with Mockito { userAgentCalculator.ieVersion } - ieVersions must_== List(6, 7, 8, 9, 10, 11) + ieVersions === List(6, 7, 8, 9, 10, 11) } trait mockReq extends Scope { @@ -150,11 +147,11 @@ class ReqSpec extends Specification with XmlMatchers with Mockito { } "with an application/json Content-Type should return the result of parsing the JSON" in new mockJsonReq { - req("application/json").json should_== Full(parsedJson) + req("application/json").json === Full(parsedJson) } "with a text/json Content-Type should return the result of parsing the JSON" in new mockJsonReq { - req("text/json").json should_== Full(parsedJson) + req("text/json").json === Full(parsedJson) } "with invalid JSON and a text/json Content-Type should return a Failure" in new mockJsonReq("epic fail") { @@ -164,15 +161,15 @@ class ReqSpec extends Specification with XmlMatchers with Mockito { "when forcing a request body JSON parse with forcedBodyAsJson" in { "with an invalid Content-Type should return the result of parsing the JSON" in new mockJsonReq { - req("text/plain").forcedBodyAsJson should_== Full(parsedJson) + req("text/plain").forcedBodyAsJson === Full(parsedJson) } "with an application/json Content-Type should return the result of parsing the JSON" in new mockJsonReq { - req("application/json").forcedBodyAsJson should_== Full(parsedJson) + req("application/json").forcedBodyAsJson === Full(parsedJson) } "with a text/json Content-Type should return the result of parsing the JSON" in new mockJsonReq { - req("text/json").forcedBodyAsJson should_== Full(parsedJson) + req("text/json").forcedBodyAsJson === Full(parsedJson) } "with invalid JSON should return a Failure" in new mockJsonReq("epic fail") { @@ -186,11 +183,11 @@ class ReqSpec extends Specification with XmlMatchers with Mockito { } "with an application/xml Content-Type should return the result of parsing the JSON" in new mockXmlReq { - req("application/xml").xml should_== Full(parsedXml) + req("application/xml").xml === Full(parsedXml) } "with a text/xml Content-Type should return the result of parsing the JSON" in new mockXmlReq { - req("text/xml").xml should_== Full(parsedXml) + req("text/xml").xml === Full(parsedXml) } "with invalid XML and a text/xml Content-Type should return a Failure" in new mockXmlReq("epic fail") { @@ -200,15 +197,15 @@ class ReqSpec extends Specification with XmlMatchers with Mockito { "when forcing a request body XML parse with forcedBodyAsXml" in { "with an invalid Content-Type should return the result of parsing the JSON" in new mockXmlReq { - req("text/plain").forcedBodyAsXml should_== Full(parsedXml) + req("text/plain").forcedBodyAsXml === Full(parsedXml) } "with an application/json Content-Type should return the result of parsing the JSON" in new mockXmlReq { - req("application/xml").forcedBodyAsXml should_== Full(parsedXml) + req("application/xml").forcedBodyAsXml === Full(parsedXml) } "with a text/json Content-Type should return the result of parsing the JSON" in new mockXmlReq { - req("text/xml").forcedBodyAsXml should_== Full(parsedXml) + req("text/xml").forcedBodyAsXml === Full(parsedXml) } "with invalid XML should return a Failure" in new mockXmlReq("epic fail") { diff --git a/web/webkit/src/test/scala-2.13/net/liftweb/http/SpecContextHelpers.scala b/web/webkit/src/test/scala-2.13/net/liftweb/http/SpecContextHelpers.scala new file mode 100644 index 0000000000..19881f88b1 --- /dev/null +++ b/web/webkit/src/test/scala-2.13/net/liftweb/http/SpecContextHelpers.scala @@ -0,0 +1,59 @@ +package net.liftweb +package http + +import org.specs2.execute.{Result, AsResult} +import org.specs2.mutable.Specification + +import common.{Box, Empty} + +/** + * Helper functions for wrapping test execution with Lift context. + * + * These functions properly wrap test code with ThreadLocal state management, + * ensuring that LiftRules and S (session) scope remain active during test execution. + */ +object SpecContextHelpers { + /** + * Wraps test execution with LiftRules context. + * The rules are active for the duration of the test execution. + * + * Example usage: + * {{{ + * import SpecContextHelpers._ + * + * "my test" in withLiftRules(testRules) { + * // test code here - LiftRules are available + * } + * }}} + */ + def withLiftRules[T: AsResult](rules: LiftRules)(test: =>T): Result = { + LiftRulesMocker.devTestLiftRulesInstance.doWith(rules) { + AsResult(test) + } + } + + /** + * Wraps test execution with both LiftRules and S (session) context. + * Both the rules and S scope are active for the duration of the test execution. + * + * Example usage: + * {{{ + * import SpecContextHelpers._ + * + * "my test" in withLiftContext(testRules, testSession) { + * // test code here - LiftRules and S scope are available + * } + * }}} + */ + def withLiftContext[T: AsResult]( + rules: LiftRules, + session: LiftSession, + req: Box[Req] = Empty + )(test: =>T): Result = { + LiftRulesMocker.devTestLiftRulesInstance.doWith(rules) { + S.init(req, session) { + AsResult(test) + } + } + } +} diff --git a/web/webkit/src/test/scala-2.13/net/liftweb/http/js/LiftJavaScriptSpec.scala b/web/webkit/src/test/scala-2.13/net/liftweb/http/js/LiftJavaScriptSpec.scala new file mode 100644 index 0000000000..6393305620 --- /dev/null +++ b/web/webkit/src/test/scala-2.13/net/liftweb/http/js/LiftJavaScriptSpec.scala @@ -0,0 +1,240 @@ +/* + * Copyright 2013 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package http +package js + +import java.util.Locale + +import net.liftweb.http.js.extcore.ExtCoreArtifacts +import net.liftweb.http.js.jquery.JQueryArtifacts +import org.specs2.execute.{Result, AsResult} +import org.specs2.specification.Scope +import org.specs2.mutable.Specification + +import common._ +import http.js._ +import http.js.JsCmds._ +import http.js.JE._ +import util.Props +import util.Helpers._ + +/** + * System under specification for LiftJavaScript. + */ +class LiftJavaScriptSpec extends Specification { + sequential + "LiftJavaScript Specification".title + + private def session = new LiftSession("", randomString(20), Empty) + + "LiftJavaScript" should { + "create default settings" in new WithLocale(Locale.ENGLISH) { + S.initIfUninitted(session) { + val settings = LiftJavaScript.settings + settings.toJsCmd === formatjs( + """{"liftPath": "/lift", + |"ajaxRetryCount": 3, + |"ajaxPostTimeout": 5000, + |"gcPollingInterval": 75000, + |"gcFailureRetryTimeout": 15000, + |"cometGetTimeout": 140000, + |"cometFailureRetryTimeout": 10000, + |"cometServer": null, + |"logError": function(msg) {}, + |"ajaxOnFailure": function() {alert("The server cannot be contacted at this time");}, + |"ajaxOnStart": function() {}, + |"ajaxOnEnd": function() {}}""" + ) + } + } + "create internationalized default settings" in new WithLocale(Locale.forLanguageTag("pl-PL")) { + S.initIfUninitted(session) { + val settings = LiftJavaScript.settings + val internationalizedMessage = "Nie mo\\u017cna skontaktowa\\u0107 si\\u0119 z serwerem" + settings.toJsCmd === formatjs( + s"""{"liftPath": "/lift", + |"ajaxRetryCount": 3, + |"ajaxPostTimeout": 5000, + |"gcPollingInterval": 75000, + |"gcFailureRetryTimeout": 15000, + |"cometGetTimeout": 140000, + |"cometFailureRetryTimeout": 10000, + |"cometServer": null, + |"logError": function(msg) {}, + |"ajaxOnFailure": function() {alert("$internationalizedMessage");}, + |"ajaxOnStart": function() {}, + |"ajaxOnEnd": function() {}}""" + ) + } + } + "create custom static settings" in new WithLocale(Locale.ENGLISH) { + S.initIfUninitted(session) { + LiftRules.ajaxRetryCount = Full(4) + val settings = LiftJavaScript.settings + settings.toJsCmd === formatjs( + """{"liftPath": "/lift", + |"ajaxRetryCount": 4, + |"ajaxPostTimeout": 5000, + |"gcPollingInterval": 75000, + |"gcFailureRetryTimeout": 15000, + |"cometGetTimeout": 140000, + |"cometFailureRetryTimeout": 10000, + |"cometServer": null, + |"logError": function(msg) {}, + |"ajaxOnFailure": function() {alert("The server cannot be contacted at this time");}, + |"ajaxOnStart": function() {}, + |"ajaxOnEnd": function() {}}""" + ) + } + } + "create custom dynamic settings" in new WithLocale(Locale.ENGLISH) { + S.initIfUninitted(session) { + LiftRules.cometServer = () => Some("srvr1") + val settings = LiftJavaScript.settings + settings.toJsCmd === formatjs( + """{"liftPath": "/lift", + |"ajaxRetryCount": 4, + |"ajaxPostTimeout": 5000, + |"gcPollingInterval": 75000, + |"gcFailureRetryTimeout": 15000, + |"cometGetTimeout": 140000, + |"cometFailureRetryTimeout": 10000, + |"cometServer": "srvr1", + |"logError": function(msg) {}, + |"ajaxOnFailure": function() {alert("The server cannot be contacted at this time");}, + |"ajaxOnStart": function() {}, + |"ajaxOnEnd": function() {}}""" + ) + } + } + "create custom function settings" in new WithLocale(Locale.ENGLISH) { + S.initIfUninitted(session) { + LiftRules.jsLogFunc = Full(v => JE.Call("lift.logError", v)) + val settings = LiftJavaScript.settings + settings.toJsCmd === formatjs( + """{"liftPath": "/lift", + |"ajaxRetryCount": 4, + |"ajaxPostTimeout": 5000, + |"gcPollingInterval": 75000, + |"gcFailureRetryTimeout": 15000, + |"cometGetTimeout": 140000, + |"cometFailureRetryTimeout": 10000, + |"cometServer": "srvr1", + |"logError": function(msg) {lift.logError(msg);}, + |"ajaxOnFailure": function() {alert("The server cannot be contacted at this time");}, + |"ajaxOnStart": function() {}, + |"ajaxOnEnd": function() {}}""" + ) + } + } + "create init command" in new WithLocale(Locale.ENGLISH) { + S.initIfUninitted(session) { + val init = LiftRules.javaScriptSettings.vend().map(_.apply(session)).map(LiftJavaScript.initCmd(_).toJsCmd) + init === Full(formatjs(List( + "var lift_settings = {};", + "window.lift.extend(lift_settings,window.liftJQuery);", + """window.lift.extend(lift_settings,{"liftPath": "/lift", + |"ajaxRetryCount": 4, + |"ajaxPostTimeout": 5000, + |"gcPollingInterval": 75000, + |"gcFailureRetryTimeout": 15000, + |"cometGetTimeout": 140000, + |"cometFailureRetryTimeout": 10000, + |"cometServer": "srvr1", + |"logError": function(msg) {lift.logError(msg);}, + |"ajaxOnFailure": function() {alert("The server cannot be contacted at this time");}, + |"ajaxOnStart": function() {}, + |"ajaxOnEnd": function() {}});""", + "window.lift.init(lift_settings);" + ))) + } + } + "create init command with VanillaJS" in new WithLocale(Locale.ENGLISH) { + S.initIfUninitted(session) { + LiftRules.jsArtifacts = ExtCoreArtifacts + val init = LiftRules.javaScriptSettings.vend().map(_.apply(session)).map(LiftJavaScript.initCmd(_).toJsCmd) + init === Full(formatjs(List( + "var lift_settings = {};", + "window.lift.extend(lift_settings,window.liftVanilla);", + """window.lift.extend(lift_settings,{"liftPath": "/lift", + |"ajaxRetryCount": 4, + |"ajaxPostTimeout": 5000, + |"gcPollingInterval": 75000, + |"gcFailureRetryTimeout": 15000, + |"cometGetTimeout": 140000, + |"cometFailureRetryTimeout": 10000, + |"cometServer": "srvr1", + |"logError": function(msg) {lift.logError(msg);}, + |"ajaxOnFailure": function() {alert("The server cannot be contacted at this time");}, + |"ajaxOnStart": function() {}, + |"ajaxOnEnd": function() {}});""", + "window.lift.init(lift_settings);" + ))) + } + } + "create init command with custom setting" in new WithLocale(Locale.ENGLISH) { + S.initIfUninitted(session) { + LiftRules.jsArtifacts = JQueryArtifacts + val settings = LiftJavaScript.settings.extend(JsObj("liftPath" -> "liftyStuff", "mysetting" -> 99)) + val init = LiftJavaScript.initCmd(settings) + init.toJsCmd === formatjs(List( + "var lift_settings = {};", + "window.lift.extend(lift_settings,window.liftJQuery);", + """window.lift.extend(lift_settings,{"liftPath": "liftyStuff", + |"ajaxRetryCount": 4, + |"ajaxPostTimeout": 5000, + |"gcPollingInterval": 75000, + |"gcFailureRetryTimeout": 15000, + |"cometGetTimeout": 140000, + |"cometFailureRetryTimeout": 10000, + |"cometServer": "srvr1", + |"logError": function(msg) {lift.logError(msg);}, + |"ajaxOnFailure": function() {alert("The server cannot be contacted at this time");}, + |"ajaxOnStart": function() {}, + |"ajaxOnEnd": function() {}, + |"mysetting": 99});""", + "window.lift.init(lift_settings);" + )) + } + } + } + + def formatjs(line:String):String = formatjs(line :: Nil) + def formatjs(lines:List[String]):String = lines.map { _.stripMargin.linesIterator.toList match { + case init :+ last => (init.map(_ + " ") :+ last).mkString + case Nil => "" + }}.mkString("\n") + + object withEnglishLocale extends WithLocale(Locale.ENGLISH) + + object withPolishLocale extends WithLocale(Locale.forLanguageTag("pl-PL")) + + class WithLocale(locale: Locale) extends Scope { + val savedDefaultLocale = Locale.getDefault + Locale.setDefault(locale) + + // Cleanup happens automatically when scope exits via try/finally in specs2 + override def toString = { + try { + super.toString + } finally { + Locale.setDefault(savedDefaultLocale) + } + } + } +} diff --git a/web/webkit/src/test/scala-2.13/net/liftweb/http/provider/servlet/OfflineRequestSnapshotSpec.scala b/web/webkit/src/test/scala-2.13/net/liftweb/http/provider/servlet/OfflineRequestSnapshotSpec.scala new file mode 100644 index 0000000000..9db267cf6d --- /dev/null +++ b/web/webkit/src/test/scala-2.13/net/liftweb/http/provider/servlet/OfflineRequestSnapshotSpec.scala @@ -0,0 +1,86 @@ +/* + * Copyright 2010-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb.http.provider.servlet + +import net.liftweb.http.provider._ +import net.liftweb.mockweb.WebSpec +import org.specs2.mock.Mockito + + +object OfflineRequestSnapshotSpec extends WebSpec with Mockito { + + private[this] val X_SSL = "X-SSL" + private[this] val xSSLHeader = HTTPParam(X_SSL, List("true")) :: Nil + + "OfflineRequestSnapshot" should { + "have a 'headers' method that returns the list of headers with a given name" in { + val req = getRequestSnapshot(originalPort = 80, headers = xSSLHeader) + req.headers("X-SSL") === List("true") + req.headers("Unknown") must beEmpty + } + + "have the serverPort value" in { + "443 when the 'X-SSL' header is set to the string 'true' (case-insensitive) and original port is 80" in { + val port80Req = getRequestSnapshot(originalPort = 80, headers = xSSLHeader) + port80Req.serverPort === 443 + } + + s"equal to the original request-port when" in { + s"the '$X_SSL' header is absent" in { + val nonSSLReq = getRequestSnapshot(originalPort = 80) + nonSSLReq.serverPort === 80 + } + + s"the '$X_SSL' header is not set to the string 'true' (case-insensitive)" in { + val falseSSLHeaderReq = getRequestSnapshot(originalPort = 90, headers = HTTPParam(X_SSL, List("anything")) :: Nil) + falseSSLHeaderReq.serverPort === 90 + } + + "the original request-port is not 80" in { + val req = getRequestSnapshot(originalPort = 90, headers = xSSLHeader) + req.serverPort === 90 + } + } + } + + "have a 'param' method that returns the list of parameters with a given name (case-sensitive)" in { + val tennisParams = List("Roger Federer", "Raphael Nadal") + val swimmingParams = List("Michael Phelps", "Ian Thorpe") + val params = HTTPParam("tennis", tennisParams) :: HTTPParam("swimming", swimmingParams) :: Nil + val snapshot = getRequestSnapshot(80, params = params) + + snapshot.param("tennis") === tennisParams + snapshot.param("Tennis") should beEmpty + snapshot.param("swimming") === swimmingParams + } + } + + + private[this] def getRequestSnapshot(originalPort: Int, headers: List[HTTPParam] = Nil, params: List[HTTPParam] = Nil) = { + val mockHttpRequest = mock[HTTPRequest] + val httpProvider = new HTTPProvider { + override protected def context: HTTPContext = null + } + + mockHttpRequest.headers returns headers + mockHttpRequest.cookies returns Nil + mockHttpRequest.params returns params + mockHttpRequest.serverPort returns originalPort + new OfflineRequestSnapshot(mockHttpRequest, httpProvider) + } + +} diff --git a/web/webkit/src/test/scala/net/liftweb/http/rest/XMLApiSpec.scala b/web/webkit/src/test/scala-2.13/net/liftweb/http/rest/XMLApiSpec.scala similarity index 93% rename from web/webkit/src/test/scala/net/liftweb/http/rest/XMLApiSpec.scala rename to web/webkit/src/test/scala-2.13/net/liftweb/http/rest/XMLApiSpec.scala index b671597a38..8a89f0573d 100644 --- a/web/webkit/src/test/scala/net/liftweb/http/rest/XMLApiSpec.scala +++ b/web/webkit/src/test/scala-2.13/net/liftweb/http/rest/XMLApiSpec.scala @@ -91,12 +91,12 @@ class XmlApiSpec extends Specification { * new attributes makes comparison fail. Instead, we simply stringify and * reparse the response contents and that seems to fix the issue. */ val converted = secureXML.loadString(x.xml.toString) - result(converted == expected, - "%s matches %s".format(converted,expected), - "%s does not match %s".format(converted, expected), - response) + result(converted == expected, + "%s matches %s".format(converted,expected), + "%s does not match %s".format(converted, expected), + response) } - case other => result(false,"matches","not an XmlResponse", response) + case other => result(false, "XmlResponse", "not an XmlResponse", response) } } @@ -121,8 +121,8 @@ class XmlApiSpec extends Specification { failure must haveClass[XmlResponse] failure match { case x : XmlResponse => { - x.xml.attribute("success").map(_.text) must_== Some("false") - x.xml.attribute("msg").isDefined must_== true + x.xml.attribute("success").map(_.text) === Some("false") + x.xml.attribute("msg").isDefined === true } } } diff --git a/web/webkit/src/test/scala/net/liftweb/mockweb/MockWebSpec.scala b/web/webkit/src/test/scala-2.13/net/liftweb/mockweb/MockWebSpec.scala similarity index 85% rename from web/webkit/src/test/scala/net/liftweb/mockweb/MockWebSpec.scala rename to web/webkit/src/test/scala-2.13/net/liftweb/mockweb/MockWebSpec.scala index 3b79b62ce3..1e58dcee27 100644 --- a/web/webkit/src/test/scala/net/liftweb/mockweb/MockWebSpec.scala +++ b/web/webkit/src/test/scala-2.13/net/liftweb/mockweb/MockWebSpec.scala @@ -78,8 +78,8 @@ class MockWebSpec extends Specification { "provide a Req corresponding to a string url" in { testReq("http://foo.com/test/this?a=b&a=c", "/test") { req => - req.uri must_== "/this" - req.params("a") must_== List("b","c") + req.uri === "/this" + req.params("a") === List("b","c") } } @@ -90,12 +90,14 @@ class MockWebSpec extends Specification { mockReq.method = "POST" import org.json4s.JsonDSL._ + import org.json4s.native.JsonMethods._ - mockReq.body = ("name" -> "joe") ~ ("age" -> 35) + mockReq.body = compact(render(("name" -> "joe") ~ ("age" -> 35))).getBytes("UTF-8") + mockReq.contentType = "application/json" testReq(mockReq) { req => - req.json_? must_== true + req.json_? must_=== true } } @@ -103,7 +105,7 @@ class MockWebSpec extends Specification { LiftRulesMocker.devTestLiftRulesInstance.doWith(mockLiftRules) { useLiftRules.doWith(true) { testReq("http://foo.com/test/this") { - req => req.remoteAddr must_== "1.2.3.4" + req => req.remoteAddr === "1.2.3.4" } } } @@ -113,7 +115,7 @@ class MockWebSpec extends Specification { LiftRulesMocker.devTestLiftRulesInstance.doWith(mockLiftRules) { useLiftRules.doWith(true) { testReq("http://foo.com/test/stateless") { - req => req.path.partPath must_== List("stateless", "works") + req => req.path.partPath === List("stateless", "works") } } } @@ -121,7 +123,7 @@ class MockWebSpec extends Specification { "initialize S based on a string url" in { testS("http://foo.com/test/that?a=b&b=c") { - S.param("b") must_== Full("c") + S.param("b") mustEqual Full("c") } } @@ -130,9 +132,9 @@ class MockWebSpec extends Specification { new MockHttpServletRequest("http://foo.com/test/this?foo=bar", "/test") testS(mockReq) { - S.param("foo") must_== Full("bar") + S.param("foo") === Full("bar") - S.uri must_== "/this" + S.uri === "/this" } } @@ -140,7 +142,7 @@ class MockWebSpec extends Specification { LiftRulesMocker.devTestLiftRulesInstance.doWith(mockLiftRules) { useLiftRules.doWith(true) { testS("http://foo.com/test/stateless") { - S.request.foreach(_.path.partPath must_== List("stateless", "works")) + S.request.foreach(_.path.partPath === List("stateless", "works")) } } } @@ -151,7 +153,7 @@ class MockWebSpec extends Specification { LiftRulesMocker.devTestLiftRulesInstance.doWith(mockLiftRules) { useLiftRules.doWith(true) { testS("http://foo.com/test/stateful") { - S.request.foreach(_.path.partPath must_== List("stateful", "works")) + S.request.foreach(_.path.partPath === List("stateful", "works")) } } } @@ -161,8 +163,8 @@ class MockWebSpec extends Specification { "emulate a snippet invocation" in { testS("http://foo.com/test/stateful") { withSnippet("MyWidget.foo", new UnprefixedAttribute("bar", Text("bat"), Null)) { - S.currentSnippet must_== Full("MyWidget.foo") - S.attr("bar") must_== Full("bat") + S.currentSnippet mustEqual Full("MyWidget.foo") + S.attr("bar") mustEqual Full("bat") } } } @@ -178,7 +180,7 @@ class MockWebSpec extends Specification { // A second test testS("http://foo.com/test2", session) { - testVar.is must_== "Foo!" + testVar.is === "Foo!" } } diff --git a/web/webkit/src/test/scala/net/liftweb/mockweb/WebSpecSpec.scala b/web/webkit/src/test/scala-2.13/net/liftweb/mockweb/WebSpecSpec.scala similarity index 83% rename from web/webkit/src/test/scala/net/liftweb/mockweb/WebSpecSpec.scala rename to web/webkit/src/test/scala-2.13/net/liftweb/mockweb/WebSpecSpec.scala index 6ad8061f60..378efda9e8 100644 --- a/web/webkit/src/test/scala/net/liftweb/mockweb/WebSpecSpec.scala +++ b/web/webkit/src/test/scala-2.13/net/liftweb/mockweb/WebSpecSpec.scala @@ -81,53 +81,53 @@ class WebSpecSpec extends WebSpec(WebSpecSpecBoot.boot _) { "properly set up S with a String url" withSFor(testUrl) in { S.request match { - case Full(req) => req.path.partPath must_== List("stateless", "works") + case Full(req) => req.path.partPath === List("stateless", "works") case _ => failure("No request in S") } } "properly set up S with a String url and session" withSFor(testUrl, testSession) in { TestVar("foo!") - TestVar.is must_== "foo!" + TestVar.is === "foo!" } "properly re-use a provided session" withSFor(testUrl, testSession) in { - TestVar.is must_== "foo!" + TestVar.is === "foo!" } "properly set up S with a HttpServletRequest" withSFor(testReq) in { - S.uri must_== "/this" - S.param("foo") must_== Full("bar") + S.uri must_=== "/this" + S.param("foo") must_=== Full("bar") } "properly set up a Req with a String url" withReqFor(testUrl) in { - _.path.partPath must_== List("stateless", "works") + _.path.partPath === List("stateless", "works") } "properly set up a Req with a String url and context path" withReqFor(testUrl, "/test") in { - _.path.partPath must_== List("stateless") + _.path.partPath === List("stateless") } "properly set up a Req with a HttpServletRequest" withReqFor(testReq) in { - _.uri must_== "/this" + _.uri === "/this" } "properly set a plain text body" withReqFor(testUrl) withPost("This is a test") in { req => - req.contentType must_== Full("text/plain") - req.post_? must_== true + req.contentType === Full("text/plain") + req.post_? === true req.body match { - case Full(body) => (new String(body)) must_== "This is a test" + case Full(body) => (new String(body)) === "This is a test" case _ => failure("No body set") } } "properly set a JSON body" withReqFor(testUrl) withPut(("name" -> "Joe")) in { req => - req.json_? must_== true - req.put_? must_== true + req.json_? === true + req.put_? === true req.json match { - case Full(jval) => jval must_== JObject(List(JField("name", JString("Joe")))) + case Full(jval) => jval === JObject(List(JField("name", JString("Joe")))) case _ => failure("No body set") } } @@ -135,15 +135,15 @@ class WebSpecSpec extends WebSpec(WebSpecSpecBoot.boot _) { "properly set an XML body" withSFor(testUrl) withPost() in { S.request match { case Full(req) => - req.xml_? must_== true - req.post_? must_== true - req.xml must_== Full() + req.xml_? must_=== true + req.post_? must_=== true + req.xml must_=== Full() case _ => failure("No request found in S") } } "properly mutate the request" withSFor(testUrl) withMods(_.contentType = "application/xml") in { - (S.request.map(_.xml_?) openOr false) must_== true + (S.request.map(_.xml_?) openOr false) === true } "process a JSON RestHelper Request" withReqFor("http://foo.com/api/info.json") in { req => @@ -154,7 +154,7 @@ class WebSpecSpec extends WebSpec(WebSpecSpecBoot.boot _) { } "properly process a template" withTemplateFor("http://foo.com/net/liftweb/mockweb/webspecspectemplate") in { - case Full(template) => template.toString.contains("Hello, WebSpec!") must_== true + case Full(template) => template.toString.contains("Hello, WebSpec!") === true case other => failure("Error on template : " + other) } } diff --git a/web/webkit/src/test/scala/net/liftweb/sitemap/LocSpec.scala b/web/webkit/src/test/scala-2.13/net/liftweb/sitemap/LocSpec.scala similarity index 94% rename from web/webkit/src/test/scala/net/liftweb/sitemap/LocSpec.scala rename to web/webkit/src/test/scala-2.13/net/liftweb/sitemap/LocSpec.scala index 7fbb50caa2..bdf58d7dd2 100644 --- a/web/webkit/src/test/scala/net/liftweb/sitemap/LocSpec.scala +++ b/web/webkit/src/test/scala-2.13/net/liftweb/sitemap/LocSpec.scala @@ -38,12 +38,12 @@ class LocSpec extends Specification { "calculate default href for basic menu definition" in { val loc = (Menu("Test") / "foo" / "bar").toMenu.loc - loc.calcDefaultHref mustEqual "/foo/bar" + loc.calcDefaultHref === "/foo/bar" } "calculate href for menu with parameters" in { val loc = (Menu.param[Param]("Test", "Test", s => Full(Param(s)), p => p.s) / "foo" / "bar" / *).toLoc - loc.calcHref(Param("myparam")) mustEqual "/foo/bar/myparam" + loc.calcHref(Param("myparam")) === "/foo/bar/myparam" } "should not match a Req matching its Link when currentValue is Empty" in { @@ -55,7 +55,7 @@ class LocSpec extends Specification { testS(mockReq) { testReq(mockReq) { req => - testLoc.doesMatch_?(req) mustEqual false + testLoc.doesMatch_?(req) === false } } } @@ -87,7 +87,7 @@ class LocSpec extends Specification { val rewriteFn = testLoc.rewrite.openOrThrowException("No rewrite function") rewriteFn(rrq) must not(throwA[Exception]) - rewriteFn(rrq)._2 must_== Empty + rewriteFn(rrq)._2 mustEqual Empty } } } diff --git a/web/webkit/src/test/scala/net/liftweb/webapptest/MemoizeSpec.scala b/web/webkit/src/test/scala-2.13/net/liftweb/webapptest/MemoizeSpec.scala similarity index 58% rename from web/webkit/src/test/scala/net/liftweb/webapptest/MemoizeSpec.scala rename to web/webkit/src/test/scala-2.13/net/liftweb/webapptest/MemoizeSpec.scala index 9834ad2983..fa68cf8b2b 100644 --- a/web/webkit/src/test/scala/net/liftweb/webapptest/MemoizeSpec.scala +++ b/web/webkit/src/test/scala-2.13/net/liftweb/webapptest/MemoizeSpec.scala @@ -42,43 +42,43 @@ class MemoizeSpec extends Specification { import SessionInfo._ "Memoize" should { - "Session memo should default to empty" >> { - S.initIfUninitted(session1) { - sessionMemo.get(3) must_== Empty + "Session memo should default to empty" in { + S.init(Full(Req.nil), session1) { + sessionMemo.get(3) mustEqual Empty } } - "Session memo should be settable" >> { - S.initIfUninitted(session1) { - sessionMemo.get(3, 8) must_== 8 + "Session memo should be settable" in { + S.init(Full(Req.nil), session1) { + sessionMemo.get(3, 8) mustEqual 8 - sessionMemo.get(3) must_== Full(8) + sessionMemo.get(3) mustEqual Full(8) } } - "Session memo should survive across calls" >> { - S.initIfUninitted(session1) { - sessionMemo.get(3) must_== Full(8) + "Session memo should survive across calls" in { + S.init(Full(Req.nil), session1) { + sessionMemo.get(3) mustEqual Full(8) } } - "Session memo should not float across sessions" >> { - S.initIfUninitted(session2) { - sessionMemo.get(3) must_== Empty + "Session memo should not float across sessions" in { + S.init(Full(Req.nil), session2) { + sessionMemo.get(3) mustEqual Empty } } - "Request memo should work in the same request" >> { - S.initIfUninitted(session1) { - requestMemo(3) must_== Empty - requestMemo(3, 44) must_== 44 - requestMemo(3) must_== Full(44) + "Request memo should work in the same request" in { + S.init(Full(Req.nil), session1) { + requestMemo(3) mustEqual Empty + requestMemo(3, 44) mustEqual 44 + requestMemo(3) mustEqual Full(44) } } - "Request memo should not span requests" >> { - S.initIfUninitted(session1) { - requestMemo(3) must_== Empty + "Request memo should not span requests" in { + S.init(Full(Req.nil), session1) { + requestMemo(3) mustEqual Empty } } diff --git a/web/webkit/src/test/scala-3/net/liftweb/http/LAFutureWithSessionSpec.scala b/web/webkit/src/test/scala-3/net/liftweb/http/LAFutureWithSessionSpec.scala new file mode 100644 index 0000000000..c4a283c402 --- /dev/null +++ b/web/webkit/src/test/scala-3/net/liftweb/http/LAFutureWithSessionSpec.scala @@ -0,0 +1,314 @@ +package net.liftweb.http + +import net.liftweb.actor.LAFuture +import net.liftweb.common.{Box, Empty, Failure, Full} +import net.liftweb.mockweb.WebSpec +import org.specs2.matcher.ThrownMessages + +class LAFutureWithSessionSpec extends WebSpec with ThrownMessages { + + sequential + + object SessionVar1 extends SessionVar[String]("Uninitialized1") + object SessionVar2 extends SessionVar[String]("Uninitialized2") + + object ReqVar1 extends RequestVar[String]("Uninitialized1") + object ReqVar2 extends RequestVar[String]("Uninitialized2") + + val timeout = 10000L + + "LAFutureWithSession" should { + + "fail if session is not available" in { + val future = LAFutureWithSession.withCurrentSession("kaboom") + + future.get(timeout) must be_== (Failure("LiftSession not available in this thread context", Empty, Empty)) + } + + "succeed with original value if session is available" withSFor "/" in { + val future = LAFutureWithSession.withCurrentSession("works!") + + future.get(timeout) must be_== (Full("works!")) + } + + "have access to session variables in LAFuture task" withSFor "/" in { + SessionVar1("dzien dobry") + + val future = LAFutureWithSession.withCurrentSession(SessionVar1.is) + + future.get(timeout) must be_== (Full("dzien dobry")) + } + + "have access to request variables in LAFuture task" withSFor "/" in { + ReqVar1("guten tag") + + val future = LAFutureWithSession.withCurrentSession(ReqVar1.is) + + future.get(timeout) must be_== (Full("guten tag")) + } + + "have access to session variables in onComplete()" withSFor "/" in { + // workaround for a possible race condition in AnyVarTrait + // https://groups.google.com/forum/#!topic/liftweb/V1pWy14Wl3A + SessionVar1.is + + val future = LAFutureWithSession.withCurrentSession { + Thread.sleep(Long.MaxValue) + "292 billion years" + } + + future.onComplete { + case Full(v) => SessionVar1(v) + case problem => ko("Future computation failed: " + problem) + } + + future.satisfy("thorgal") + + SessionVar1.is must eventually(beEqualTo("thorgal")) + } + + "have access to request variables in onComplete()" withSFor "/" in { + // workaround for a possible race condition in AnyVarTrait + // https://groups.google.com/forum/#!topic/liftweb/V1pWy14Wl3A + ReqVar1.is + + val future = LAFutureWithSession.withCurrentSession { + Thread.sleep(Long.MaxValue) + "292 billion years" + } + + future.onComplete { + case Full(v) => ReqVar1(v) + case problem => ko("Future computation failed: " + problem) + } + + future.satisfy("thor") + + ReqVar1.is must eventually(beEqualTo("thor")) + } + + "have access to session variables in onFail()" withSFor "/" in { + // workaround for a possible race condition in SessionVar + // https://groups.google.com/forum/#!topic/liftweb/V1pWy14Wl3A + SessionVar1.is + + val future = LAFutureWithSession.withCurrentSession { + Thread.sleep(Long.MaxValue) + "292 billion years" + } + + future.onFail { + case f: Failure => SessionVar1(f.msg) + case _ => fail("The Future should have failed") + } + + future.fail(new Exception("kaboom!")) + + SessionVar1.is must eventually(beEqualTo("kaboom!")) + } + + "have access to request variables in onFail()" withSFor "/" in { + // workaround for a possible race condition in AnyVarTrait + // https://groups.google.com/forum/#!topic/liftweb/V1pWy14Wl3A + ReqVar1.is + + val future = LAFutureWithSession.withCurrentSession { + Thread.sleep(Long.MaxValue) + "292 billion years" + } + + future.onFail { + case f: Failure => ReqVar1(f.msg) + case _ => fail("The Future should have failed") + } + + future.fail(new Exception("nope!")) + + ReqVar1.is must eventually(beEqualTo("nope!")) + } + + "have access to session variables in onSuccess()" withSFor "/" in { + // workaround for a possible race condition in AnyVarTrait + // https://groups.google.com/forum/#!topic/liftweb/V1pWy14Wl3A + SessionVar1.is + + val future = LAFutureWithSession.withCurrentSession { + Thread.sleep(Long.MaxValue) + "292 billion years" + } + + future.onSuccess(SessionVar1(_)) + + future.satisfy("done") + + SessionVar1.is must eventually(beEqualTo("done")) + } + + "have access to request variables in onSuccess()" withSFor "/" in { + // workaround for a possible race condition in AnyVarTrait + // https://groups.google.com/forum/#!topic/liftweb/V1pWy14Wl3A + ReqVar1.is + + val future = LAFutureWithSession.withCurrentSession { + Thread.sleep(Long.MaxValue) + "292 billion years" + } + + future.onSuccess(ReqVar1(_)) + + future.satisfy("my preciousss") + + ReqVar1.is must eventually(beEqualTo("my preciousss")) + } + + "have access to session variables in chains of filter()" withSFor "/" in { + SessionVar1("see") + SessionVar2("me") + + val future = LAFutureWithSession.withCurrentSession("they see me rollin") + val filtered = future + .filter(_.contains(SessionVar1.is)) + .filter(_.contains(SessionVar2.is)) + + filtered.get(timeout) must eventually(===(Full("they see me rollin"): Box[String])) + } + + "have access to request variables in chains of filter()" withSFor "/" in { + ReqVar1("see") + ReqVar2("me") + + val future = LAFutureWithSession.withCurrentSession("they see me rollin") + val filtered = future + .filter(_.contains(ReqVar1.is)) + .filter(_.contains(ReqVar2.is)) + + filtered.get(timeout) must eventually(===(Full("they see me rollin"): Box[String])) + } + + "have access to session variables in chains of withFilter()" withSFor "/" in { + SessionVar1("come") + SessionVar2("prey") + + val future = LAFutureWithSession.withCurrentSession("do not come between the nazgul and his prey") + val filtered = future + .withFilter(_.contains(SessionVar1.is)) + .withFilter(_.contains(SessionVar2.is)) + + filtered.get(timeout) must eventually(===(Full("do not come between the nazgul and his prey"): Box[String])) + } + + "have access to request variables in chains of withFilter()" withSFor "/" in { + ReqVar1("hurt") + ReqVar2("precious") + + val future = LAFutureWithSession.withCurrentSession("mustn't go that way, mustn't hurt the precious!") + val filtered = future + .withFilter(_.contains(ReqVar1.is)) + .withFilter(_.contains(ReqVar2.is)) + + filtered.get(timeout) must eventually(===(Full("mustn't go that way, mustn't hurt the precious!"): Box[String])) + } + + "have access to session variables in chains of map()" withSFor "/" in { + SessionVar1("b") + SessionVar2("c") + + val future = LAFutureWithSession.withCurrentSession("a") + val mapped = future.map(_ + SessionVar1.is).map(_ + SessionVar2.is) + + mapped.get(timeout) must be_== (Full("abc")) + } + + "have access to request variables in chains of map()" withSFor "/" in { + ReqVar1("b") + ReqVar2("c") + + val future = LAFutureWithSession.withCurrentSession("a") + val mapped = future.map(_ + ReqVar1.is).map(_ + ReqVar2.is) + + mapped.get(timeout) must be_== (Full("abc")) + } + + "have access to session variables in chains of flatMap()" withSFor "/" in { + SessionVar1("e") + SessionVar2("f") + + val future = LAFutureWithSession.withCurrentSession("d") + val mapped = future + .flatMap { s => + val out = s + SessionVar1.is + LAFuture.build(out) + } + .flatMap { s => + val out = s + SessionVar2.is + LAFuture.build(out) + } + + mapped.get(timeout) must be_== (Full("def")) + } + + "have access to request variables in chains of flatMap()" withSFor "/" in { + ReqVar1("e") + ReqVar2("f") + + val future = LAFutureWithSession.withCurrentSession("d") + val mapped = future + .flatMap { s => + val out = s + ReqVar1.is + LAFuture.build(out) + } + .flatMap { s => + val out = s + ReqVar2.is + LAFuture.build(out) + } + + mapped.get(timeout) must be_== (Full("def")) + } + + "have access to session variables in foreach()" withSFor "/" in { + // workaround for a possible race condition in AnyVarTrait + // https://groups.google.com/forum/#!topic/liftweb/V1pWy14Wl3A + SessionVar1.is + + val future = LAFutureWithSession.withCurrentSession("cookie") + future.foreach(SessionVar1(_)) + + SessionVar1.is must eventually(beEqualTo("cookie")) + } + + "have access to request variables in foreach()" withSFor "/" in { + // workaround for a possible race condition in AnyVarTrait + // https://groups.google.com/forum/#!topic/liftweb/V1pWy14Wl3A + ReqVar1.is + + val future = LAFutureWithSession.withCurrentSession("monster") + future.foreach(ReqVar1(_)) + + ReqVar1.is must eventually(beEqualTo("monster")) + } + + "not leak out initial session between threads with their own sessions" in { + val session1 = new LiftSession("Test session 1", "", Empty) + val session2 = new LiftSession("Test session 2", "", Empty) + val session3 = new LiftSession("Test session 3", "", Empty) + + S.initIfUninitted(session1)(SessionVar1("one")) + S.initIfUninitted(session2)(SessionVar1("two")) + S.initIfUninitted(session3)(SessionVar1("three")) + + val future = S.initIfUninitted(session1)(LAFutureWithSession.withCurrentSession("zero")) + + S.initIfUninitted(session2) { + future.map(v => SessionVar1.is).get(timeout) must eventually(===(Full("two"): Box[String])) + } + + S.initIfUninitted(session3) { + future.map(v => SessionVar1.is).get(timeout) must eventually(===(Full("three"): Box[String])) + } + + S.initIfUninitted(session1) { + future.map(v => SessionVar1.is).get(timeout) must eventually(===(Full("one"): Box[String])) + } + } + } +} diff --git a/web/webkit/src/test/scala-3/net/liftweb/http/LiftMergeSpec.scala b/web/webkit/src/test/scala-3/net/liftweb/http/LiftMergeSpec.scala new file mode 100644 index 0000000000..7e79d4d34b --- /dev/null +++ b/web/webkit/src/test/scala-3/net/liftweb/http/LiftMergeSpec.scala @@ -0,0 +1,473 @@ +package net.liftweb +package http + +import scala.xml._ + +import org.specs2.mutable.Specification +import org.specs2.matcher.XmlMatchers +import org.mockito.Mockito.{mock, when} + +import common._ + +import js.JE.JsObj +import js.pageScript +import SpecContextHelpers._ + +class LiftMergeSpec extends Specification with XmlMatchers { + val mockReq = mock(classOf[Req]) + when(mockReq.contextPath).thenReturn("/context-path") + + val testSession = new LiftSession("/context-path", "underlying id", Empty) + + val testRules = new LiftRules() + // Avoid extra appended elements by default. + testRules.javaScriptSettings.default.set(() => () => Empty) + testRules.autoIncludeAjaxCalc.default.set(() => () => (_: LiftSession) => false) + testRules.excludePathFromContextPathRewriting.default + .set( + () => (in: String) => in.startsWith("exclude-me") + ) + + val eventExtractingTestRules = new LiftRules() + eventExtractingTestRules.javaScriptSettings.default.set(() => () => Empty) + eventExtractingTestRules.autoIncludeAjaxCalc.default.set(() => () => (_: LiftSession) => false) + eventExtractingTestRules.extractInlineJavaScript = true + + "LiftMerge when doing the final page merge" should { + "merge head segments in the page body in order into main head" in withLiftRules(testRules) { + val result = + testSession.merge( + + + + + + + + + +
+

+ + + +

+
+ + , + mockReq + ) + + (result \ "head" \ "_") === (Seq( + , + , + , + + ): NodeSeq) + } + + "merge tail segments in the page body in order at the end of the body" in withLiftRules(testRules) { + val result = + testSession.merge( + + + + + + + + + +
+

+ + + +

+
+ +

Thingies

+

More thingies

+ + , + mockReq + ) + + (result \ "body" \ "_").takeRight(3) === (Seq( + , + , + + ): NodeSeq) + } + + "not merge tail segments in the head" in withLiftRules(testRules) { + val result = + testSession.merge( + + + + + + + + + + + +
+

+ + + +

+
+ +

Thingies

+

More thingies

+ + , + mockReq + ) + + (result \ "body" \ "_").takeRight(3) === (Seq( + , + , + + ): NodeSeq) + } + + "normalize absolute link hrefs everywhere" in withLiftContext(testRules, testSession) { + val result = + testSession.merge( + + + + + + + + + + +
+

+ + + +

+
+ +

Thingies

+

More thingies

+ + , + mockReq + ) + + (result \\ "link").map(_ \@ "href") === + "/context-path/testlink" :: + "/context-path/testlink2" :: + "/context-path/testlink3" :: Nil + } + + "normalize absolute script srcs everywhere" in withLiftContext(testRules, testSession) { + val result = + testSession.merge( + + + + + + + + + + +
+

+ + + +

+
+ +

Thingies

+

More thingies

+ + , + mockReq + ) + + (result \\ "script").map(_ \@ "src") === + "/context-path/testscript" :: + "/context-path/testscript2" :: Nil + } + + "normalize absolute a hrefs everywhere" in withLiftContext(testRules, testSession) { + val result = + testSession.merge( + + + Booyan + + + Booyan + + Booyan + +
+ Booyan +

+ + Booyan + +

+
+ +

Thingies Booyan

+

More thingies

+ + , + mockReq + ) + + (result \\ "a").map(_ \@ "href") === + "/context-path/testa1" :: + "testa3" :: + "/context-path/testa2" :: + "testa4" :: + "/context-path/testa6" :: + "/context-path/testa5" :: Nil + } + + "normalize absolute form actions everywhere" in withLiftContext(testRules, testSession) { + val result = + testSession.merge( + + +
Booyan
+ + +
Booyan
+ +
Booyan
+ +
+
Booyan
+

+ +

Booyan
+ +

+
+ +

Thingies

Booyan

+

More thingies

+ + , + mockReq + ) + + (result \\ "form").map(_ \@ "action") === + "/context-path/testform1" :: + "testform3" :: + "/context-path/testform2" :: + "testform4" :: + "/context-path/testform6" :: + "/context-path/testform5" :: Nil + } + + "not rewrite script srcs anywhere" in withLiftContext(testRules, testSession) { + val result = + URLRewriter.doWith((_: String) => "rewritten") { + testSession.merge( + + + + + + + + +
+

+ +