Este es un corto post mostrando algo que hice en mi trabajo y que me pareció interesante.
En mi trabajo estamos desarrollando pruebas end to end (e2e) de multiples servicios. Entre los varios features que queríamos probar estaba la opción de resetear la contraseña. Este feature, muy común en varios sitios web, sigue un patrón similar:
El usuario olvida su contraseña. Se dirige a un formulario específico para esta situación e introduce la dirección de correo electrónico con la que se había registrado.
Un correo es enviado al usuario conteniendo un enlace especial para resetear la contraseña.
El usuario se loguea a su cuenta de correo y da click en el enlace contenido en el correo.
El enlace lo dirige a un formulario dónde va a poder introducir una nueva contraseña.
Una vez hecho esto la contraseña del usuario ha sido actualizada.
Nada fuera de lo ordinario.
La idea es hacer un test de todo el flujo, específicamente de los servicios del back-end que soportan esto. Estos son dos:
- Iniciar reseteo de contraseña: Inicia el reseteo de contraseña dada una dirección de correo electrónico. Tiene como efecto secundario el envío de un correo con un enlace que contiene un token especial.
- Modificar la contraseña con token especial: Actualiza la contraseña de una cuenta dados: la nueva contraseña, la dirección de correo electrónico y un token especial (enviado en el correo).
Pero para testear el flujo completo deberíamos usar el primer servicio para iniciar el “reseteo”, después de alguna forma “abrir” la bandeja de entrada del correo del usuario, extraer el token y usarlo para consumir el segundo servicio. Así que la parte no obvia es ¿cómo podemos lograr ese “abrir la bandeja de entrada”?
Voy a mostrar como lo hicimos y como encontramos que reactive extensions nos podría servir para lograrlo.
Primero cabe decir que para enviar correos usamos SES de Amazon. Nuestro código usa el SDK que realmente solo hace una solicitud HTTP a un servidor de Amazon. Además los tests e2e se ejecutan en un proceso distinto al servicio que queríamos testear.
Decidimos hacer lo siguiente: los tests levantan un servidor embebido que simula ser
SES, a este lo llamarémos FakeSES. Digamos que este servicio se levantó en el puerto
9999
. Entonces levantamos el servicio que querémos probar haciéndole pensar que SES
se encuentra en <ip del _host_ que corre los tests>:9999
.
Nada fuera del otro mundo hasta ahora.
Ahora, gracias a que los tests levantan FakeSES deberían poder inspeccionar las solicitudes de envíos de correos que le llegan. El problema sin embargo es que los tests se ejecutan por un lado mientras que FakeSES se debe ejecutar al mismo tiempo mientras atiende solicitudes. ¿Cómo conectar ambas cosas?
Aquí es dónde entra reactive extensions. Lo que deseamos hacer es ofrecerle a los
tests una vista de las solicitudes de envío que le llegan a FakeSES. ¡Un Observable
encajaría muy bien acá! Digamos que este es nuestro código del servidor falso:
case class SendEmailRequest(subject: String, body: String, destination: String)
class FakeSES {
private def requestToSendEmailRequest(request: HTTPRequest): SendEmailRequest =
??? // no es relevante
private val successResponse: HTTPResponse =
??? // no es relevante
private def handleRequest(request: HTTPRequest, response: HTTPResponse): Response = {
successResponse}
// *************************
// Alguna lógica que levanta
// el servidor embebido y
// simula ser SES usando la
// función `handleRequest`
// *************************
}
Entonces idealmente queremos que además tenga una propiedad que nos permita leer las solicitudes de envíos de correos, sin importar en qué momento llegaron o llegarán:
class FakeSES {
// ... lo mismo que antes
val sendEmailRequests: Observable[SendEmailRequest] = ???
}
La pregunta es ¿como inicializar esta variable?
Podemos hacer lo siguiente:
- Inicializamos una variable de tipo
BehaviorSubject[SendEmailRequest]
. - Cada vez que llega una solicitud “escribimos” esta variable.
- Exponemos solamente el lado de “lectura”: es decir los que usen instancias
de esta clase van a poder ver el
Observable
pero no elBehaviorSubject
.
class FakeSES {
// ... lo mismo que antes
private val sendEmailRequestsSubject = BehaviorSubject[SendEmailRequest]
private def handleRequest(request: HTTPRequest, response: HTTPResponse): Response = {
.onNext(requestToSendEmailRequest(request))
sendEmailRequestsSubject
successResponse}
val sendEmailRequests: Observable[SendEmailRequest] = sendEmailRequestsSubject
}
Dado que BehaviorSubject
es una subclase de Observable
podemos hacer este último paso
(si han visto los futuros y promesas sabrán que por ahí hay una idea similar)-
Ahora veamos como se escribiría un test:
import org.scalatest.concurrent.ScalaFutures
import org.scalatest._
import scala.concurrent.duration._
import scala.concurrent.Await
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
class ResetPasswordTest
extends WordSpec
with MustMatchers
with WordSpec
with BeforeAndAfterAll {
val fakeSES = new FakeSES()
def beforeAll() = fakeSES.start()
def afterAll() = fakeSES.stop()
val email = "some@email.com"
val oldPassword = "asdfdgfad"
val newPassword = "pas5w0rd"
def firstResetPasswordEmail(): Future[SendEmailRequest] =
.sendEmailRequests
fakeSES.filter(_.destination == email)
.filter(_.subject == "Forgot your password?")
.first
.toBlocking
.toFuture
// un cliente del servicio que queremos testear
val service = ???
def extractResetToken(email: SendEmailRequest): String =
??? // no es relevante
"The client" should {
"execute the request password flow" in {
.createUser(email, oldPassword)
service.startPasswordReset(email)
serviceval resetEmail = Await.result(firstResetPasswordEmail(), 1.second)
val resetToken = extractResetToken(resetEmail)
.resetPassword(email, resetToken, newPassword).statusCode mustBe 200
service.login(email, newPassword).statusCode mustBe 200
service.login(email, oldPassword).statusCode mustBe 403
service}
}
}
Aquí la función firstResetPasswordEmail
nos permite filtrar el primer correo
que nos interesa. Cabe notar que este código funciona sin importar si llegan
otros correos (como por ejemplo un correo de bienvenida apenas el usuario se
registra), sin importar en qué orden lleguen, etc… . Esto gracias a la naturaleza
declarativa de reactive extensions.
Y eso fue todo. Todavía no he encontrado un uso en “producción” de reactive extensions pero para esto me pareció útil.