Skip to content
Open
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ val common = library("common")
pekkoSlf4j,
pekkoSerializationJackson,
pekkoActorTyped,
supportInternationalisation
) ++ jackson,
)

Expand Down
30 changes: 30 additions & 0 deletions common/app/controllers/EmailSignupController.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package controllers

import com.gu.i18n.{CountryGroup, Country}
import com.typesafe.scalalogging.LazyLogging
import common.EmailSubsciptionMetrics._
import common.{GuLogging, ImplicitControllerExecutionContext, LinkTo}
Expand All @@ -12,6 +13,7 @@ import conf.switches.Switches.{
}
import model.Cached.{RevalidatableResult, WithoutRevalidationResult}
import model._
import net.liftweb.json.JObject
import play.api.data.Forms._
import play.api.data._
import play.api.data.format.Formats._
Expand Down Expand Up @@ -60,8 +62,30 @@ class EmailFormService(wsClient: WSClient, emailEmbedAgent: NewsletterSignupAgen
extends LazyLogging
with RemoteAddress {

private def getCountryAndRegion(
countryCode: String,
)(implicit request: Request[AnyContent]): (String, Option[String]) = {
val registrationLocation: String = CountryGroup.byFastlyCountryCode(countryCode).map(_.name).getOrElse("Other")
val registrationLocationState: Option[String] =
for {
countryCode <- Option(countryCode).filter(Set("US", "AU").contains)
country <- CountryGroup.countryByCode(countryCode)
stateCode <- request.headers.get("X-GU-GeoIP-Region")
stateName <- country.statesByCode.get(stateCode)
} yield stateName
(registrationLocation, registrationLocationState)
}

def submit(form: EmailForm)(implicit request: Request[AnyContent]): Future[WSResponse] = {
val consentMailerUrl = serviceUrl(form, emailEmbedAgent)
val countryCode = request.headers.get("X-GU-GeoLocation") match {
case Some(country) =>
country.replace("country:", "")
case None => "row"
}

val (registrationLocation, registrationLocationState) = getCountryAndRegion(countryCode)

val consentMailerPayload = JsObject(
Json
.obj(
Expand All @@ -70,6 +94,8 @@ class EmailFormService(wsClient: WSClient, emailEmbedAgent: NewsletterSignupAgen
"set-consents" -> form.marketing.filter(_ == true).map(_ => List("similar_guardian_products")),
"unset-consents" -> form.marketing.filter(_ == false).map(_ => List("similar_guardian_products")),
"browser-id" -> form.browserId,
"registrationLocation" -> registrationLocation,
"registrationLocationState" -> registrationLocationState,
)
.fields,
)
Expand All @@ -87,6 +113,8 @@ class EmailFormService(wsClient: WSClient, emailEmbedAgent: NewsletterSignupAgen
}

def submitWithMany(form: EmailFormManyNewsletters)(implicit request: Request[AnyContent]): Future[WSResponse] = {
val countryCode = request.headers.get("X-GU-GeoLocation").getOrElse("country:row").replace("country:", "")
val (registrationLocation, registrationLocationState) = getCountryAndRegion(countryCode)
val consentMailerPayload = JsObject(
Json
.obj(
Expand All @@ -96,6 +124,8 @@ class EmailFormService(wsClient: WSClient, emailEmbedAgent: NewsletterSignupAgen
"ref" -> form.ref,
"set-consents" -> form.marketing.filter(_ == true).map(_ => List("similar_guardian_products")),
"unset-consents" -> form.marketing.filter(_ == false).map(_ => List("similar_guardian_products")),
"registrationLocation" -> registrationLocation,
"registrationLocationState" -> registrationLocationState,
)
.fields,
)
Expand Down
234 changes: 234 additions & 0 deletions common/test/controllers/EmailFormServiceTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
package controllers

import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers._
import org.mockito.Mockito._
import org.scalatest.concurrent.ScalaFutures
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import org.scalatestplus.mockito.MockitoSugar
import play.api.libs.json._
import play.api.libs.ws._
import play.api.mvc.AnyContentAsEmpty
import play.api.test.FakeRequest
import services.newsletters.NewsletterSignupAgent
import test.WithTestExecutionContext

import scala.concurrent.Future

class EmailFormServiceTest
extends AnyWordSpec
with Matchers
with MockitoSugar
with ScalaFutures
with WithTestExecutionContext {

trait Fixture {
val wsClient: WSClient = mock[WSClient]
val wsRequest: WSRequest = mock[WSRequest]
val wsResponse: WSResponse = mock[WSResponse]
val newsletterSignupAgent: NewsletterSignupAgent = mock[NewsletterSignupAgent]

val service = new EmailFormService(wsClient, newsletterSignupAgent)

val singleNewsletterBaseForm: EmailForm = EmailForm(
email = "test@example.com",
listName = Some("the-long-read"),
marketing = None,
referrer = None,
ref = None,
refViewId = None,
browserId = None,
campaignCode = None,
googleRecaptchaResponse = None,
name = None,
)

val multipleNewslettersBaseForm: EmailFormManyNewsletters = EmailFormManyNewsletters(
email = "test@example.com",
listNames = Seq("the-long-read", "morning-briefing"),
marketing = None,
referrer = None,
ref = None,
refViewId = None,
campaignCode = None,
googleRecaptchaResponse = None,
name = None,
)

when(newsletterSignupAgent.getV2NewsletterByName(any[String])) thenReturn Left("test")
when(wsClient.url(any[String])) thenReturn wsRequest
when(wsRequest.withQueryStringParameters(any())) thenReturn wsRequest
when(wsRequest.addHttpHeaders(any())) thenReturn wsRequest
when(wsRequest.post(any[JsValue])(any[BodyWritable[JsValue]])) thenReturn Future.successful(wsResponse)
when(wsResponse.status) thenReturn 200
}

def capturePostedBody(wsRequest: WSRequest): JsObject = {
val captor: ArgumentCaptor[JsValue] = ArgumentCaptor.forClass(classOf[JsValue])
verify(wsRequest).post(captor.capture())(any())
captor.getValue.as[JsObject]
}

def registrationLocation(body: JsObject): String =
(body \ "registrationLocation").get.as[String]

def registrationLocationState(body: JsObject): JsValue =
(body \ "registrationLocationState").get

"EmailFormService.submit" when {

"getting registrationLocation from X-GU-GeoLocation header" should {

"use the country group name for a known country code" in new Fixture {
implicit val request: FakeRequest[AnyContentAsEmpty.type] =
FakeRequest().withHeaders("X-GU-GeoLocation" -> "country:GB")
service.submit(singleNewsletterBaseForm).futureValue
registrationLocation(capturePostedBody(wsRequest)) shouldBe "United Kingdom"
}

"use 'Other' for an unrecognised country code" in new Fixture {
implicit val request: FakeRequest[AnyContentAsEmpty.type] =
FakeRequest().withHeaders("X-GU-GeoLocation" -> "country:XX")
service.submit(singleNewsletterBaseForm).futureValue
registrationLocation(capturePostedBody(wsRequest)) shouldBe "Other"
}

"use 'Other' when the X-GU-GeoLocation header isn't present" in new Fixture {
implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest()
service.submit(singleNewsletterBaseForm).futureValue
registrationLocation(capturePostedBody(wsRequest)) shouldBe "Other"
}
}

"getting registrationLocationState from X-GU-GeoIP-Region header" should {

"get US state name from state code" in new Fixture {
implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders(
"X-GU-GeoLocation" -> "country:US",
"X-GU-GeoIP-Region" -> "CA",
)
service.submit(singleNewsletterBaseForm).futureValue
registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsString("California")
}

"get AU state name from state code" in new Fixture {
implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders(
"X-GU-GeoLocation" -> "country:AU",
"X-GU-GeoIP-Region" -> "NSW",
)
service.submit(singleNewsletterBaseForm).futureValue
registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsString("New South Wales")
}

"not be there (None) when the country is US but no state header is present" in new Fixture {
implicit val request: FakeRequest[AnyContentAsEmpty.type] =
FakeRequest().withHeaders("X-GU-GeoLocation" -> "country:US")
service.submit(singleNewsletterBaseForm).futureValue
registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsNull
}

"not be there (None) when the country is US but the state code is unrecognised" in new Fixture {
implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders(
"X-GU-GeoLocation" -> "country:US",
"X-GU-GeoIP-Region" -> "ZZ",
)
service.submit(singleNewsletterBaseForm).futureValue
registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsNull
}

"not be there (None) for a non-US/AU country even when a region header is present" in new Fixture {
implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders(
"X-GU-GeoLocation" -> "country:GB",
"X-GU-GeoIP-Region" -> "ENG",
)
service.submit(singleNewsletterBaseForm).futureValue
registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsNull
}

"not be there (None) when the X-GU-GeoLocation header is missing" in new Fixture {
implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest()
service.submit(singleNewsletterBaseForm).futureValue
registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsNull
}
}
}

"EmailFormService.submitWithMany" when {

"getting registrationLocation from X-GU-GeoLocation header" should {

"use the country group name for a known country code" in new Fixture {
implicit val request: FakeRequest[AnyContentAsEmpty.type] =
FakeRequest().withHeaders("X-GU-GeoLocation" -> "country:GB")
service.submitWithMany(multipleNewslettersBaseForm).futureValue
registrationLocation(capturePostedBody(wsRequest)) shouldBe "United Kingdom"
}

"use 'Other' for an unrecognised country code" in new Fixture {
implicit val request: FakeRequest[AnyContentAsEmpty.type] =
FakeRequest().withHeaders("X-GU-GeoLocation" -> "country:XX")
service.submitWithMany(multipleNewslettersBaseForm).futureValue
registrationLocation(capturePostedBody(wsRequest)) shouldBe "Other"
}

"use 'Other' when the X-GU-GeoLocation header isn't present" in new Fixture {
implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest()
service.submitWithMany(multipleNewslettersBaseForm).futureValue
registrationLocation(capturePostedBody(wsRequest)) shouldBe "Other"
}
}

"getting registrationLocationState from X-GU-GeoIP-Region header" should {

"get US state name from state code" in new Fixture {
implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders(
"X-GU-GeoLocation" -> "country:US",
"X-GU-GeoIP-Region" -> "CA",
)
service.submitWithMany(multipleNewslettersBaseForm).futureValue
registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsString("California")
}

"get AU state name from state code" in new Fixture {
implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders(
"X-GU-GeoLocation" -> "country:AU",
"X-GU-GeoIP-Region" -> "NSW",
)
service.submitWithMany(multipleNewslettersBaseForm).futureValue
registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsString("New South Wales")
}

"not be there (None) when the country is US but no state header is present" in new Fixture {
implicit val request: FakeRequest[AnyContentAsEmpty.type] =
FakeRequest().withHeaders("X-GU-GeoLocation" -> "country:US")
service.submitWithMany(multipleNewslettersBaseForm).futureValue
registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsNull
}

"not be there (None) when the country is US but the state code is unrecognised" in new Fixture {
implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders(
"X-GU-GeoLocation" -> "country:US",
"X-GU-GeoIP-Region" -> "ZZ",
)
service.submitWithMany(multipleNewslettersBaseForm).futureValue
registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsNull
}

"not be there (None) for a non-US/AU country even when a region header is present" in new Fixture {
implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders(
"X-GU-GeoLocation" -> "country:GB",
"X-GU-GeoIP-Region" -> "ENG",
)
service.submitWithMany(multipleNewslettersBaseForm).futureValue
registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsNull
}

"not be there (None) when the X-GU-GeoLocation header is missing" in new Fixture {
implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest()
service.submitWithMany(multipleNewslettersBaseForm).futureValue
registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsNull
}
}
}
}
Loading