forked from typelevel/skunk
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Startup handlers for unsupported authentication schemes, and a ne…
…ato simulator to test it.
- Loading branch information
Showing
7 changed files
with
222 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
modules/core/src/main/scala/exception/UnsupportedAuthenticationSchemeExceptio.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// Copyright (c) 2018-2020 by Rob Norris | ||
// This software is licensed under the MIT License (MIT). | ||
// For more information see LICENSE or https://opensource.org/licenses/MIT | ||
|
||
package skunk.exception | ||
|
||
import skunk.net.message.BackendMessage | ||
|
||
class UnsupportedAuthenticationSchemeException( | ||
message: BackendMessage | ||
) extends SkunkException( | ||
sql = None, | ||
message = s"Unsupported authentication scheme.", | ||
hint = Some( | ||
s"""|The server requested `$message`, but Skunk currently only supports `trust` and `password` (md5). | ||
|""".stripMargin | ||
) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
119 changes: 119 additions & 0 deletions
119
modules/tests/src/test/scala/simulation/SimulatedMessageSocket.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
// Copyright (c) 2018-2020 by Rob Norris | ||
// This software is licensed under the MIT License (MIT). | ||
// For more information see LICENSE or https://opensource.org/licenses/MIT | ||
|
||
package tests | ||
package simulation | ||
|
||
import cats._ | ||
import cats.implicits._ | ||
import skunk.net.message.FrontendMessage | ||
import cats.free.Free | ||
import scala.collection.immutable.Queue | ||
import skunk.net.MessageSocket | ||
import skunk.net.message.BackendMessage | ||
import cats.effect._ | ||
import cats.effect.concurrent.Ref | ||
import skunk.util.Origin | ||
import skunk.exception.ProtocolError | ||
|
||
object SimulatedMessageSocket { | ||
|
||
// Monadic DSL for writing a simulated Postgres server. We're using an explicit Yoneda encoding | ||
// here so we can define a functor instance below, which is necessary if we want to run this | ||
// program step by step (which we do). | ||
sealed trait Step[+A] | ||
case class Respond[A](m: BackendMessage, k: Unit => A) extends Step[A] | ||
case class Expect[A](h: PartialFunction[FrontendMessage, A]) extends Step[A] | ||
object Step { | ||
implicit val FunctorStep: Functor[Step] = | ||
new Functor[Step] { | ||
def map[A,B](fa: Step[A])(f: A => B): Step[B] = | ||
fa match { | ||
case Respond(ms, k) => Respond(ms, k andThen f) | ||
case Expect(h) => Expect(h andThen f) | ||
} | ||
} | ||
} | ||
|
||
// To ensure that simulations all terminate cleanly, we will provide a value that must be | ||
// returned but has no public constructor, so you can only construct it with `halt` below. | ||
sealed trait Halt | ||
private object Halt extends Halt | ||
|
||
// Smart constructors for our DSL. | ||
def respond(m: BackendMessage): Free[Step, Unit] = Free.liftF(Respond(m, identity)) | ||
def expect[A](h: PartialFunction[FrontendMessage, A]): Free[Step, A] = Free.liftF(Expect(h)) | ||
def flatExpect[A](h: PartialFunction[FrontendMessage, Free[Step, A]]): Free[Step, A] = expect(h).flatten | ||
def halt: Free[Step, Halt] = expect { case _ => Halt } | ||
|
||
// Our server runtime consists of a queue of outgoing messages, plus a continuation that consumes | ||
// an incoming message and computes the next continuation. | ||
case class MockState(queue: Queue[BackendMessage], expect: Expect[Free[Step, _]]) | ||
|
||
// To advance the machine we run it until we reach an Expect node. If we reach a Respond then we | ||
// enqueue its message and continue to the next step. If we reach a terminal node then the | ||
// simulation has ended and we can't process any more messages. | ||
def advance(m: Free[Step, _], q: Queue[BackendMessage]): Either[String, MockState] = | ||
m.resume match { | ||
case Left(Respond(m, k)) => advance(k(()), q :+ m) | ||
case Left(Expect(h)) => MockState(q, Expect(h)).asRight | ||
case Right(_) => "The simulation has ended.".asLeft | ||
} | ||
|
||
// To receive a message we dequeue from our state. Because `advance` above enqueues eagerly it is | ||
// impossible to miss messages. If there are no pending messages we're stuck. | ||
def receive(ms: MockState): Either[String, (BackendMessage, MockState)] = | ||
ms.queue.dequeueOption match { | ||
case Some((m, q)) => (m, ms.copy(queue = q)).asRight | ||
case None => "No pending messages.".asLeft | ||
} | ||
|
||
// To send a message we pass it to the continuation and compute the next one. | ||
def send(m: FrontendMessage, ms: MockState): Either[String, MockState] = | ||
advance(ms.expect.h(m), ms.queue) | ||
|
||
// Ok so now we have what we need to construct a MessageSocket whose soul is a simulation defined | ||
// with the DSL above. We'll advance the machine first, ensuring that there's at least one | ||
// Expect node inside, otherwise we're done early. This will never happen because users aren't | ||
// able to construct a value of type `Halt` so it's kind of academic. Anyway we'll stuff our | ||
// initial MockState into a Ref and then we consult it when we `send` and `receive`, using the | ||
// standard ref-state-machine pattern. This is pretty cool really. | ||
def apply(m: Free[Step, Halt]): IO[MessageSocket[IO]] = | ||
advance(m, Queue.empty).leftMap(new IllegalStateException(_)).liftTo[IO].flatMap(Ref[IO].of).map { ref => | ||
new MessageSocket[IO] { | ||
|
||
def receive: IO[BackendMessage] = | ||
ref.modify { ms => | ||
SimulatedMessageSocket.receive(ms) match { | ||
case Right((m, ms)) => (ms, m.pure[IO]) | ||
case Left(err) => (ms, IO.raiseError(new IllegalStateException(err))) | ||
} | ||
} .flatten | ||
|
||
def send(message: FrontendMessage): IO[Unit] = | ||
ref.modify { ms => | ||
SimulatedMessageSocket.send(message, ms) match { | ||
case Right(ms) => (ms, IO.unit) | ||
case Left(err) => (ms, IO.raiseError(new IllegalStateException(err))) | ||
} | ||
} .flatten | ||
|
||
def history(max: Int): IO[List[Either[Any,Any]]] = | ||
Nil.pure[IO] // not implemeneted | ||
|
||
def expect[B](f: PartialFunction[BackendMessage,B])(implicit or: Origin): IO[B] = | ||
receive.flatMap { msg => | ||
f.lift(msg) match { | ||
case Some(b) => b.pure[IO] | ||
case None => IO.raiseError(new ProtocolError(msg, or)) | ||
} | ||
} | ||
|
||
def flatExpect[B](f: PartialFunction[BackendMessage,IO[B]])(implicit or: Origin): IO[B] = | ||
expect(f).flatten | ||
|
||
} | ||
} | ||
|
||
} |
69 changes: 69 additions & 0 deletions
69
modules/tests/src/test/scala/simulation/SimulatedStartupTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
// Copyright (c) 2018-2020 by Rob Norris | ||
// This software is licensed under the MIT License (MIT). | ||
// For more information see LICENSE or https://opensource.org/licenses/MIT | ||
|
||
package tests | ||
package simulation | ||
|
||
import skunk.net.message._ | ||
import skunk.net.protocol.Startup | ||
import skunk.net.protocol.Exchange | ||
import natchez.Trace.Implicits.noop | ||
import cats.effect.IO | ||
import cats.implicits._ | ||
import skunk.exception.UnsupportedAuthenticationSchemeException | ||
import cats.free.Free | ||
import skunk.exception.PostgresErrorException | ||
|
||
case object SimulatedStartupTest extends ffstest.FTest { | ||
import SimulatedMessageSocket._ | ||
|
||
test("immediate server error") { | ||
|
||
// A trivial simulation that responds to StartupMessage with an ErrorMessage. | ||
val simulation: Free[Step, Halt] = | ||
flatExpect { | ||
case StartupMessage(_, _) => | ||
respond(ErrorResponse(Map('M' -> "Nope", 'S' -> "ERROR", 'C' -> "123"))) *> | ||
halt | ||
} | ||
|
||
for { | ||
ex <- Exchange[IO] | ||
ms <- SimulatedMessageSocket(simulation) | ||
s = Startup[IO](implicitly, ex, ms, implicitly) | ||
e <- s.apply("bob", "db", None).assertFailsWith[PostgresErrorException] | ||
_ <- assertEqual("message", e.message, "Nope.") | ||
} yield ("ok") | ||
|
||
} | ||
|
||
List( | ||
AuthenticationCleartextPassword, | ||
AuthenticationGSS, | ||
AuthenticationKerberosV5, | ||
AuthenticationSASL(List("Foo", "Bar")), | ||
AuthenticationSCMCredential, | ||
AuthenticationSSPI, | ||
).foreach { msg => | ||
test(s"unsupported authentication scheme: $msg") { | ||
|
||
// A trivial simulation that only handles the first exchange of the Startup protocol. | ||
val simulation: Free[Step, Halt] = | ||
flatExpect { | ||
case StartupMessage(_, _) => | ||
respond(msg) *> | ||
halt | ||
} | ||
|
||
for { | ||
ex <- Exchange[IO] | ||
ms <- SimulatedMessageSocket(simulation) | ||
s = Startup[IO](implicitly, ex, ms, implicitly) | ||
_ <- s.apply("bob", "db", None).assertFailsWith[UnsupportedAuthenticationSchemeException] | ||
} yield ("ok") | ||
|
||
} | ||
} | ||
|
||
} |