This is an automated email from the ASF dual-hosted git repository. olli pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-scripting-groovy.git
commit 2e9cbd25fac67d7b49e68fd82a4bcbef81167c25 Author: Oliver Lietz <[email protected]> AuthorDate: Thu Dec 28 17:52:42 2017 +0100 SLING-7337 Add integration tests --- pom.xml | 125 +++++++++++++++++++++ .../scripting/groovy/it/GroovyTestSupport.java | 99 ++++++++++++++++ .../apache/sling/scripting/groovy/it/app/Page.java | 52 +++++++++ .../scripting/groovy/it/tests/AdaptToModelIT.java | 81 +++++++++++++ .../groovy/it/tests/GspScriptEngineFactoryIT.java | 72 ++++++++++++ .../sling/scripting/groovy/it/tests/SimpleIT.java | 81 +++++++++++++ .../resources/apps/groovy/page/adaptto/html.gsp | 31 +++++ .../resources/apps/groovy/page/simple/html.gsp | 28 +++++ src/test/resources/content/groovy.json | 18 +++ 9 files changed, 587 insertions(+) diff --git a/pom.xml b/pom.xml index a955284..45712b9 100644 --- a/pom.xml +++ b/pom.xml @@ -38,6 +38,7 @@ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <sling.java.version>8</sling.java.version> + <org.ops4j.pax.exam.version>4.11.0</org.ops4j.pax.exam.version> <groovy.version>2.4.13</groovy.version> </properties> @@ -65,10 +66,48 @@ </instructions> </configuration> </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-failsafe-plugin</artifactId> + <executions> + <execution> + <goals> + <goal>integration-test</goal> + <goal>verify</goal> + </goals> + </execution> + </executions> + <configuration> + <redirectTestOutputToFile>true</redirectTestOutputToFile> + <systemProperties> + <property> + <name>bundle.filename</name> + <value>${basedir}/target/${project.build.finalName}.jar</value> + </property> + </systemProperties> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.servicemix.tooling</groupId> + <artifactId>depends-maven-plugin</artifactId> + <executions> + <execution> + <goals> + <goal>generate-depends-file</goal> + </goals> + </execution> + </executions> + </plugin> </plugins> </build> <dependencies> + <!-- javax --> + <dependency> + <groupId>javax.inject</groupId> + <artifactId>javax.inject</artifactId> + <scope>test</scope> + </dependency> <!-- OSGi --> <dependency> <groupId>org.osgi</groupId> @@ -95,6 +134,13 @@ <artifactId>org.osgi.service.metatype.annotations</artifactId> <scope>provided</scope> </dependency> + <!-- Apache Felix --> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.framework</artifactId> + <version>5.6.10</version> + <scope>test</scope> + </dependency> <!-- Apache Sling --> <dependency> <groupId>org.apache.sling</groupId> @@ -104,16 +150,46 @@ </dependency> <dependency> <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.auth.core</artifactId> + <version>1.3.24</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.sling</groupId> <artifactId>org.apache.sling.commons.classloader</artifactId> <version>1.0.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.engine</artifactId> + <version>2.6.6</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.models.api</artifactId> + <version>1.3.6</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.resource.presence</artifactId> + <version>0.0.2</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.sling</groupId> <artifactId>org.apache.sling.scripting.api</artifactId> <version>2.1.0</version> <scope>provided</scope> </dependency> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.testing.paxexam</artifactId> + <version>0.0.5-SNAPSHOT</version> + <scope>provided</scope> + </dependency> <!-- Groovy --> <dependency> <groupId>org.codehaus.groovy</groupId> @@ -133,12 +209,61 @@ <version>${groovy.version}</version> <scope>provided</scope> </dependency> + <!-- jsoup --> + <dependency> + <groupId>org.jsoup</groupId> + <artifactId>jsoup</artifactId> + <version>1.10.2</version> + <scope>test</scope> + </dependency> <!-- logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <scope>provided</scope> </dependency> + <!-- testing --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.servicemix.bundles</groupId> + <artifactId>org.apache.servicemix.bundles.hamcrest</artifactId> + <version>1.3_1</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.ops4j.pax.exam</groupId> + <artifactId>pax-exam</artifactId> + <version>${org.ops4j.pax.exam.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.ops4j.pax.exam</groupId> + <artifactId>pax-exam-cm</artifactId> + <version>${org.ops4j.pax.exam.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.ops4j.pax.exam</groupId> + <artifactId>pax-exam-container-forked</artifactId> + <version>${org.ops4j.pax.exam.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.ops4j.pax.exam</groupId> + <artifactId>pax-exam-junit4</artifactId> + <version>${org.ops4j.pax.exam.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.ops4j.pax.exam</groupId> + <artifactId>pax-exam-link-mvn</artifactId> + <version>${org.ops4j.pax.exam.version}</version> + <scope>test</scope> + </dependency> </dependencies> </project> diff --git a/src/test/java/org/apache/sling/scripting/groovy/it/GroovyTestSupport.java b/src/test/java/org/apache/sling/scripting/groovy/it/GroovyTestSupport.java new file mode 100644 index 0000000..8aad7f5 --- /dev/null +++ b/src/test/java/org/apache/sling/scripting/groovy/it/GroovyTestSupport.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sling.scripting.groovy.it; + +import javax.inject.Inject; +import javax.script.ScriptEngineFactory; + +import aQute.bnd.osgi.Constants; +import org.apache.sling.api.servlets.ServletResolver; +import org.apache.sling.auth.core.AuthenticationSupport; +import org.apache.sling.engine.SlingRequestProcessor; +import org.apache.sling.testing.paxexam.TestSupport; +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.ProbeBuilder; +import org.ops4j.pax.exam.TestProbeBuilder; +import org.ops4j.pax.exam.util.Filter; +import org.osgi.service.http.HttpService; + +import static org.apache.sling.testing.paxexam.SlingOptions.slingModels; +import static org.apache.sling.testing.paxexam.SlingOptions.slingQuickstartOakTar; +import static org.apache.sling.testing.paxexam.SlingOptions.slingResourcePresence; +import static org.apache.sling.testing.paxexam.SlingOptions.slingScripting; +import static org.ops4j.pax.exam.CoreOptions.composite; +import static org.ops4j.pax.exam.CoreOptions.junitBundles; +import static org.ops4j.pax.exam.CoreOptions.mavenBundle; + +public class GroovyTestSupport extends TestSupport { + + @Inject + protected ServletResolver servletResolver; + + @Inject + protected SlingRequestProcessor slingRequestProcessor; + + @Inject + protected AuthenticationSupport authenticationSupport; + + @Inject + protected HttpService httpService; + + @Inject + @Filter(value = "(names=gsp)") + protected ScriptEngineFactory scriptEngineFactory; + + public Option baseConfiguration() { + return composite( + super.baseConfiguration(), + quickstart(), + // Sling Scripting Groovy + testBundle("bundle.filename"), + mavenBundle().groupId("org.codehaus.groovy").artifactId("groovy").versionAsInProject(), + mavenBundle().groupId("org.codehaus.groovy").artifactId("groovy-json").versionAsInProject(), + mavenBundle().groupId("org.codehaus.groovy").artifactId("groovy-templates").versionAsInProject(), + // testing + slingResourcePresence(), + mavenBundle().groupId("org.jsoup").artifactId("jsoup").versionAsInProject(), + mavenBundle().groupId("org.apache.servicemix.bundles").artifactId("org.apache.servicemix.bundles.hamcrest").versionAsInProject(), + junitBundles() + ); + } + + @ProbeBuilder + public TestProbeBuilder probeConfiguration(final TestProbeBuilder testProbeBuilder) { + testProbeBuilder.setHeader(Constants.EXPORT_PACKAGE, "org.apache.sling.scripting.groovy.it.app"); + testProbeBuilder.setHeader("Sling-Model-Packages", "org.apache.sling.scripting.groovy.it.app"); + testProbeBuilder.setHeader("Sling-Initial-Content", String.join(",", + "apps/groovy;path:=/apps/groovy;overwrite:=true;uninstall:=true", + "content;path:=/content;overwrite:=true;uninstall:=true" + )); + return testProbeBuilder; + } + + protected Option quickstart() { + final int httpPort = findFreePort(); + final String workingDirectory = workingDirectory(); + return composite( + slingQuickstartOakTar(workingDirectory, httpPort), + slingModels(), + slingScripting() + ); + } + +} diff --git a/src/test/java/org/apache/sling/scripting/groovy/it/app/Page.java b/src/test/java/org/apache/sling/scripting/groovy/it/app/Page.java new file mode 100644 index 0000000..03b03d5 --- /dev/null +++ b/src/test/java/org/apache/sling/scripting/groovy/it/app/Page.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sling.scripting.groovy.it.app; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.models.annotations.Model; +import org.apache.sling.models.annotations.injectorspecific.SlingObject; + +import static org.apache.sling.models.annotations.injectorspecific.InjectionStrategy.OPTIONAL; + +@Model(adaptables = {Resource.class, SlingHttpServletRequest.class}) +public class Page { + + @SlingObject + protected Resource resource; + + @SlingObject(injectionStrategy = OPTIONAL) + protected SlingHttpServletRequest request; + + public Page() { + } + + public String getName() { + return resource.getName(); + } + + public String getPath() { + return resource.getPath(); + } + + public String getTitle() { + return resource.getValueMap().get("title", String.class); + } + +} diff --git a/src/test/java/org/apache/sling/scripting/groovy/it/tests/AdaptToModelIT.java b/src/test/java/org/apache/sling/scripting/groovy/it/tests/AdaptToModelIT.java new file mode 100644 index 0000000..742bd23 --- /dev/null +++ b/src/test/java/org/apache/sling/scripting/groovy/it/tests/AdaptToModelIT.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sling.scripting.groovy.it.tests; + +import java.io.IOException; + +import javax.inject.Inject; + +import org.apache.sling.resource.presence.ResourcePresence; +import org.apache.sling.scripting.groovy.it.GroovyTestSupport; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.Configuration; +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerClass; +import org.ops4j.pax.exam.util.Filter; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.factoryConfiguration; + +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerClass.class) +public class AdaptToModelIT extends GroovyTestSupport { + + private Document document; + + @Inject + @Filter(value = "(path=/apps/groovy/page/adaptto/html.gsp)") + private ResourcePresence resourcePresence; + + @Configuration + public Option[] configuration() { + return new Option[]{ + baseConfiguration(), + factoryConfiguration("org.apache.sling.resource.presence.internal.ResourcePresenter") + .put("path", "/apps/groovy/page/adaptto/html.gsp") + .asOption(), + }; + } + + @Before + public void setup() throws IOException { + final String url = String.format("http://localhost:%s/groovy/adaptto.html", httpPort()); + document = Jsoup.connect(url).get(); + } + + @Test + public void testTitle() { + assertThat(document.title(), is("Sling Models adaptTo()")); + } + + @Test + public void testPageName() { + final Element name = document.getElementById("name"); + assertThat(name.text(), is("adaptto")); + } + +} diff --git a/src/test/java/org/apache/sling/scripting/groovy/it/tests/GspScriptEngineFactoryIT.java b/src/test/java/org/apache/sling/scripting/groovy/it/tests/GspScriptEngineFactoryIT.java new file mode 100644 index 0000000..aff0db4 --- /dev/null +++ b/src/test/java/org/apache/sling/scripting/groovy/it/tests/GspScriptEngineFactoryIT.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sling.scripting.groovy.it.tests; + +import org.apache.sling.scripting.groovy.it.GroovyTestSupport; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.Configuration; +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerClass; + +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerClass.class) +public class GspScriptEngineFactoryIT extends GroovyTestSupport { + + @Configuration + public Option[] configuration() { + return new Option[]{ + baseConfiguration() + }; + } + + @Test + public void testScriptEngineFactory() { + assertNotNull(scriptEngineFactory); + } + + @Test + public void testScriptEngineFactoryEngineName() { + assertThat(scriptEngineFactory.getEngineName(), is("Apache Sling Scripting Groovy")); + } + + @Test + public void testScriptEngineFactoryLanguageName() { + assertThat(scriptEngineFactory.getLanguageName(), is("Groovy Server Pages")); + } + + @Test + public void testScriptEngineFactoryLanguageVersion() { + assertThat(scriptEngineFactory.getLanguageVersion(), startsWith("2.4.")); + } + + @Test + public void testScriptEngineFactoryNames() { + assertThat(scriptEngineFactory.getNames(), hasItem("gsp")); + } + +} diff --git a/src/test/java/org/apache/sling/scripting/groovy/it/tests/SimpleIT.java b/src/test/java/org/apache/sling/scripting/groovy/it/tests/SimpleIT.java new file mode 100644 index 0000000..9f7a6e8 --- /dev/null +++ b/src/test/java/org/apache/sling/scripting/groovy/it/tests/SimpleIT.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sling.scripting.groovy.it.tests; + +import java.io.IOException; + +import javax.inject.Inject; + +import org.apache.sling.resource.presence.ResourcePresence; +import org.apache.sling.scripting.groovy.it.GroovyTestSupport; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.Configuration; +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerClass; +import org.ops4j.pax.exam.util.Filter; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.factoryConfiguration; + +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerClass.class) +public class SimpleIT extends GroovyTestSupport { + + private Document document; + + @Inject + @Filter(value = "(path=/apps/groovy/page/simple/html.gsp)") + private ResourcePresence resourcePresence; + + @Configuration + public Option[] configuration() { + return new Option[]{ + baseConfiguration(), + factoryConfiguration("org.apache.sling.resource.presence.internal.ResourcePresenter") + .put("path", "/apps/groovy/page/simple/html.gsp") + .asOption(), + }; + } + + @Before + public void setup() throws IOException { + final String url = String.format("http://localhost:%s/groovy/simple.html", httpPort()); + document = Jsoup.connect(url).get(); + } + + @Test + public void testTitle() { + assertThat(document.title(), is("groovy simple")); + } + + @Test + public void testPageName() { + final Element name = document.getElementById("name"); + assertThat(name.text(), is("simple")); + } + +} diff --git a/src/test/resources/apps/groovy/page/adaptto/html.gsp b/src/test/resources/apps/groovy/page/adaptto/html.gsp new file mode 100644 index 0000000..fb6eaea --- /dev/null +++ b/src/test/resources/apps/groovy/page/adaptto/html.gsp @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<!-- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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. +--> +<% + def page = request.adaptTo(org.apache.sling.scripting.groovy.it.app.Page.class) +%> +<html> +<head> + <meta charset="UTF-8"/> + <title>$page.title</title> +</head> +<body> +<span id="name">$page.name</span> +</body> +</html> diff --git a/src/test/resources/apps/groovy/page/simple/html.gsp b/src/test/resources/apps/groovy/page/simple/html.gsp new file mode 100644 index 0000000..e0a612e --- /dev/null +++ b/src/test/resources/apps/groovy/page/simple/html.gsp @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<!-- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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. +--> +<html> +<head> + <meta charset="UTF-8"/> + <title>$resource.valueMap.title</title> +</head> +<body> +<span id="name">$resource.name</span> +</body> +</html> diff --git a/src/test/resources/content/groovy.json b/src/test/resources/content/groovy.json new file mode 100644 index 0000000..6af671e --- /dev/null +++ b/src/test/resources/content/groovy.json @@ -0,0 +1,18 @@ +{ + "jcr:primaryType": "nt:unstructured", + "sling:resourceType": "groovy/page/main", + "sling:resourceSuperType": "groovy/page", + "title": "Apache Sling Scripting Groovy", + "simple": { + "jcr:primaryType": "nt:unstructured", + "sling:resourceType": "groovy/page/simple", + "sling:resourceSuperType": "groovy/page", + "title": "groovy simple" + }, + "adaptto": { + "jcr:primaryType": "nt:unstructured", + "sling:resourceType": "groovy/page/adaptto", + "sling:resourceSuperType": "groovy/page", + "title": "Sling Models adaptTo()" + } +} -- To stop receiving notification emails like this one, please contact "[email protected]" <[email protected]>.
