This is an automated email from the ASF dual-hosted git repository. rombert pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-paxexam-util.git
commit 392f3d2fe7c958717531b0c0b2aa199e1025e84d Author: Bertrand Delacretaz <[email protected]> AuthorDate: Fri Jan 24 10:46:07 2014 +0000 SLING-2788 - move sling-pax-util under testing git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1560938 13f79535-47bb-0310-9956-ffa450edef68 --- pom.xml | 145 +++++++++++++ .../apache/sling/paxexam/util/SlingPaxOptions.java | 224 +++++++++++++++++++++ .../sling/paxexam/util/SlingRepositoryTest.java | 72 +++++++ .../apache/sling/paxexam/util/SlingSetupTest.java | 190 +++++++++++++++++ 4 files changed, 631 insertions(+) diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..34d3673 --- /dev/null +++ b/pom.xml @@ -0,0 +1,145 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- 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. --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.apache.sling</groupId> + <artifactId>sling</artifactId> + <version>16</version> + <relativePath>../../parent/pom.xml</relativePath> + </parent> + + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.paxexam.util</artifactId> + <version>1.0-SNAPSHOT</version> + <packaging>jar</packaging> + + <name>Sling Pax Exam Utilities</name> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <exam.version>3.0.3</exam.version> + <url.version>1.5.2</url.version> + <pax.exam.log.level>INFO</pax.exam.log.level> + </properties> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <systemProperties> + <property> + <name>pax.exam.log.level</name> + <value>${pax.exam.log.level}</value> + </property> + <property> + <name>java.protocol.handler.pkgs</name> + <value>org.ops4j.pax.url</value> + </property> + </systemProperties> + </configuration> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.compendium</artifactId> + <version>4.2.0</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.ops4j.pax.exam</groupId> + <artifactId>pax-exam-container-native</artifactId> + <version>${exam.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.ops4j.pax.exam</groupId> + <artifactId>pax-exam-junit4</artifactId> + <version>${exam.version}</version> + </dependency> + <dependency> + <groupId>org.ops4j.pax.exam</groupId> + <artifactId>pax-exam-link-mvn</artifactId> + <version>${exam.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.ops4j.pax.url</groupId> + <artifactId>pax-url-aether</artifactId> + <version>${url.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.framework</artifactId> + <version>4.2.1</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>maven-launchpad-plugin</artifactId> + <version>2.2.1-SNAPSHOT</version> + <type>maven-plugin</type> + </dependency> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.launchpad.api</artifactId> + <version>1.1.0</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.api</artifactId> + <version>2.4.2</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.jcr.api</artifactId> + <version>2.1.0</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.settings</artifactId> + <version>1.3.0</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>javax.jcr</groupId> + <artifactId>jcr</artifactId> + <version>2.0</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <scope>test</scope> + </dependency> + </dependencies> +</project> diff --git a/src/main/java/org/apache/sling/paxexam/util/SlingPaxOptions.java b/src/main/java/org/apache/sling/paxexam/util/SlingPaxOptions.java new file mode 100644 index 0000000..9b1dcef --- /dev/null +++ b/src/main/java/org/apache/sling/paxexam/util/SlingPaxOptions.java @@ -0,0 +1,224 @@ +/* + * 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.paxexam.util; + +import static org.ops4j.pax.exam.CoreOptions.junitBundles; +import static org.ops4j.pax.exam.CoreOptions.mavenBundle; +import static org.ops4j.pax.exam.CoreOptions.systemProperty; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.apache.sling.maven.projectsupport.BundleListUtils; +import org.apache.sling.maven.projectsupport.bundlelist.v1_0_0.Bundle; +import org.apache.sling.maven.projectsupport.bundlelist.v1_0_0.BundleList; +import org.apache.sling.maven.projectsupport.bundlelist.v1_0_0.StartLevel; +import org.ops4j.pax.exam.CoreOptions; +import org.ops4j.pax.exam.options.CompositeOption; +import org.ops4j.pax.exam.options.DefaultCompositeOption; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Pax exam options and utilities to test Sling applications + * The basic idea is to get a vanilla Sling launchpad instance + * setup with a minimal amount of boilerplate code. + * See {@link SlingSetupTest} for an example. + * */ +public class SlingPaxOptions { + private static final Logger log = LoggerFactory.getLogger(SlingPaxOptions.class); + public static final int DEFAULT_SLING_START_LEVEL = 30; + public static final String PROP_TELNET_PORT = "osgi.shell.telnet.port"; + public static final String PROP_HTTP_PORT = "org.osgi.service.http.port"; + public static final String DEFAULT_RUN_MODES = "jackrabbit"; + + private static int getAvailablePort() { + int result = Integer.MIN_VALUE; + try { + final ServerSocket s = new ServerSocket(0); + result = s.getLocalPort(); + s.close(); + } catch(IOException ignore) { + } + return result; + } + + /** Get run modes to use for our tests, as set by the sling.run.modes property */ + public static Collection<String> getTestRunModes() { + final String runModes = System.getProperty("sling.run.modes", DEFAULT_RUN_MODES); + return Arrays.asList(runModes.split(",")); + } + + /** @param launchpadVersion null means use the latest */ + public static CompositeOption defaultLaunchpadOptions(String launchpadVersion) { + final String paxLogLevel = System.getProperty("pax.exam.log.level", "INFO"); + + final int slingStartLevel = DEFAULT_SLING_START_LEVEL; + final String telnetPort = System.getProperty(PROP_TELNET_PORT, String.valueOf(getAvailablePort())); + final String httpPort = System.getProperty(PROP_HTTP_PORT, String.valueOf(getAvailablePort())); + + log.info("{}={}", PROP_TELNET_PORT, telnetPort); + log.info("{}={}", PROP_HTTP_PORT, httpPort); + + return new DefaultCompositeOption( + junitBundles(), + systemProperty( "org.ops4j.pax.logging.DefaultServiceLog.level" ).value(paxLogLevel), + SlingPaxOptions.felixRemoteShellBundles(), + SlingPaxOptions.slingBootstrapBundles(), + SlingPaxOptions.slingLaunchpadBundles(launchpadVersion), + CoreOptions.frameworkStartLevel(slingStartLevel), + CoreOptions.frameworkProperty(PROP_TELNET_PORT).value(telnetPort), + CoreOptions.frameworkProperty(PROP_HTTP_PORT).value(httpPort) + ); + } + + public static CompositeOption slingBundleList(String groupId, String artifactId, String version, String type, String classifier) { + + final DefaultCompositeOption result = new DefaultCompositeOption(); + + final String paxUrl = new StringBuilder() + .append("mvn:") + .append(groupId) + .append("/") + .append(artifactId) + .append("/") + .append(version == null ? "" : version) + .append("/") + .append(type == null ? "" : type) + .append("/") + .append(classifier == null ? "" : classifier) + .toString(); + + // TODO BundleList should take an InputStream - for now copy to a tmp file for parsing + log.info("Getting bundle list {}", paxUrl); + File tmp = null; + final Collection<String> testRunModes = getTestRunModes(); + try { + tmp = dumpMvnUrlToTmpFile(paxUrl); + final BundleList list = BundleListUtils.readBundleList(tmp); + int counter = 0; + for(StartLevel s : list.getStartLevels()) { + + // Start level < 0 means bootstrap in our bundle lists + final int startLevel = s.getStartLevel() < 0 ? 1 : s.getStartLevel(); + + for(Bundle b : s.getBundles()) { + counter++; + + // TODO need better fragment detection + // (but pax exam should really detect that by itself?) + final List<String> KNOWN_FRAGMENTS = new ArrayList<String>(); + KNOWN_FRAGMENTS.add("org.apache.sling.extensions.webconsolebranding"); + final boolean isFragment = b.getArtifactId().contains("fragment") || KNOWN_FRAGMENTS.contains(b.getArtifactId()); + + // Ignore bundles with run modes that do not match ours + final String bundleRunModes = b.getRunModes(); + if(bundleRunModes != null && bundleRunModes.length() > 0) { + boolean active = false; + for(String m : bundleRunModes.split(",")) { + if(testRunModes.contains(m)) { + active = true; + break; + } + } + if(!active) { + log.info("Ignoring bundle {} as none of its run modes [{}] are active in this test run {}", + new Object[] { b.getArtifactId(), bundleRunModes, testRunModes} ); + continue; + } + } + + if(isFragment) { + result.add(mavenBundle(b.getGroupId(), b.getArtifactId(), b.getVersion()).noStart()); + } else if(startLevel == 0){ + result.add(mavenBundle(b.getGroupId(), b.getArtifactId(), b.getVersion())); + } else { + result.add(mavenBundle(b.getGroupId(), b.getArtifactId(), b.getVersion()).startLevel(startLevel)); + } + + log.info("Bundle added: {}/{}/{}", new Object [] { b.getGroupId(), b.getArtifactId(), b.getVersion()}); + } + } + log.info("Got {} bundles from {}", counter, paxUrl); + } catch(Exception e) { + throw new RuntimeException("Error getting bundle list " + paxUrl, e); + } finally { + if(tmp != null) { + tmp.delete(); + } + } + + return result; + } + + public static CompositeOption slingBootstrapBundles() { + return new DefaultCompositeOption( + mavenBundle("org.apache.felix", "org.apache.felix.http.jetty", "2.2.0"), + + // TODO: why is this needed? + mavenBundle("org.apache.sling", "org.apache.sling.launchpad.api", "1.1.0") + ); + } + + public static CompositeOption slingLaunchpadBundles(String version) { + return slingBundleList("org.apache.sling", "org.apache.sling.launchpad", version, "xml", "bundlelist"); + } + + /** @param version can be null, to use default */ + public static CompositeOption felixRemoteShellBundles() { + final String gogoVersion = "0.10.0"; + return new DefaultCompositeOption( + mavenBundle().groupId("org.apache.felix").artifactId("org.apache.felix.gogo.runtime").version(gogoVersion), + mavenBundle().groupId("org.apache.felix").artifactId("org.apache.felix.gogo.shell").version(gogoVersion), + mavenBundle().groupId("org.apache.felix").artifactId("org.apache.felix.gogo.command").version(gogoVersion), + mavenBundle().groupId("org.apache.felix").artifactId("org.apache.felix.shell.remote").version("1.1.2") + ); + } + + private static File dumpMvnUrlToTmpFile(String mvnUrl) throws IOException { + final URL url = new URL(mvnUrl); + final InputStream is = new BufferedInputStream(url.openStream()); + + final File tmp = File.createTempFile(SlingPaxOptions.class.getName(), "xml"); + log.debug("Copying bundle list contents to {}", tmp.getAbsolutePath()); + tmp.deleteOnExit(); + final OutputStream os = new BufferedOutputStream(new FileOutputStream(tmp)); + try { + final byte [] buffer = new byte[16384]; + int len = 0; + while( (len = is.read(buffer, 0, buffer.length)) > 0) { + os.write(buffer, 0, len); + } + os.flush(); + } finally { + os.close(); + is.close(); + } + + return tmp; + } +} \ No newline at end of file diff --git a/src/test/java/org/apache/sling/paxexam/util/SlingRepositoryTest.java b/src/test/java/org/apache/sling/paxexam/util/SlingRepositoryTest.java new file mode 100644 index 0000000..c1a14c8 --- /dev/null +++ b/src/test/java/org/apache/sling/paxexam/util/SlingRepositoryTest.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.paxexam.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import javax.inject.Inject; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.sling.jcr.api.SlingRepository; +import org.junit.Test; +import org.junit.runner.RunWith; +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; + +/** Verify that our tests have access to a functional Sling instance, + * and demonstrate how a simple test is setup. + * + * To create a test like this that runs against a full Sling launchpad + * instance, one only needs the pax exam setup in the pom and a test + * like this one that runs with @RunWith PaxExam, that provides a + * Configuration method and can access services or the BundleContext + * using @Inject. + */ +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerClass.class) +public class SlingRepositoryTest { + @Inject + private SlingRepository repository; + + @org.ops4j.pax.exam.Configuration + public Option[] config() { + return SlingPaxOptions.defaultLaunchpadOptions("7-SNAPSHOT").getOptions(); + } + + @Test + public void testNameDescriptor() { + // We could use JUnit categories to select tests, as we + // do in our integration, but let's avoid a dependency on + // that in this module + if(System.getProperty("sling.run.modes", "").contains("oak")) { + assertEquals("Apache Jackrabbit Oak", repository.getDescriptor("jcr.repository.name")); + } else { + assertEquals("Jackrabbit", repository.getDescriptor("jcr.repository.name")); + } + } + + @Test + public void testLogin() throws RepositoryException { + final Session s = repository.loginAdministrative(null); + assertNotNull(s); + s.logout(); + } + } \ No newline at end of file diff --git a/src/test/java/org/apache/sling/paxexam/util/SlingSetupTest.java b/src/test/java/org/apache/sling/paxexam/util/SlingSetupTest.java new file mode 100644 index 0000000..cc7cfe2 --- /dev/null +++ b/src/test/java/org/apache/sling/paxexam/util/SlingSetupTest.java @@ -0,0 +1,190 @@ +/* + * 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.paxexam.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import org.junit.Test; +import org.junit.runner.RunWith; +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.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; + +/** Verify that we get a working Sling launchpad with what SlingPaxOptions provide */ +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerClass.class) +public class SlingSetupTest { + + @Inject + private BundleContext bundleContext; + + @org.ops4j.pax.exam.Configuration + public Option[] config() { + return SlingPaxOptions.defaultLaunchpadOptions("7-SNAPSHOT").getOptions(); + } + + private void assertBundleActive(String symbolicName) { + assertEquals("Expecting bundle to be active:" + symbolicName, Bundle.ACTIVE, getBundleState(symbolicName)); + } + + private boolean isFragment(Bundle b) { + return b.getHeaders().get("Fragment-Host") != null; + } + + private Bundle getBundle(String symbolicName) { + for(Bundle b : bundleContext.getBundles()) { + if(symbolicName.equals(b.getSymbolicName())) { + return b; + } + } + return null; + } + /** @return bundle state, UNINSTALLED if absent */ + private int getBundleState(String symbolicName) { + return getBundleState(getBundle(symbolicName)); + } + + /** @return bundle state, UNINSTALLED if absent, ACTIVE */ + private int getBundleState(Bundle b) { + if(b == null) { + return Bundle.UNINSTALLED; + } else if(isFragment(b)) { + return Bundle.ACTIVE; + } else { + return b.getState(); + } + } + + @Test + public void testBundleContext() { + assertNotNull("Expecting BundleContext to be set", bundleContext); + } + + @Test + public void testSlingBundles() { + final String [] bundles = { + "org.apache.sling.adapter", + "org.apache.sling.api", + "org.apache.sling.auth.core", + "org.apache.sling.auth.form", + "org.apache.sling.auth.openid", + "org.apache.sling.auth.selector", + "org.apache.sling.bundleresource.impl", + "org.apache.sling.commons.classloader", + "org.apache.sling.commons.json", + "org.apache.sling.commons.log", + "org.apache.sling.commons.logservice", + "org.apache.sling.commons.mime", + "org.apache.sling.commons.osgi", + "org.apache.sling.commons.scheduler", + "org.apache.sling.commons.threads", + "org.apache.sling.discovery.api", + "org.apache.sling.discovery.impl", + "org.apache.sling.discovery.support", + "org.apache.sling.engine", + "org.apache.sling.event", + "org.apache.sling.extensions.explorer", + "org.apache.sling.extensions.groovy", + "org.apache.sling.extensions.threaddump", + "org.apache.sling.extensions.webconsolebranding", + "org.apache.sling.extensions.webconsolesecurityprovider", + "org.apache.sling.fragment.transaction", + "org.apache.sling.fragment.ws", + "org.apache.sling.fragment.xml", + "org.apache.sling.fsresource", + "org.apache.sling.installer.console", + "org.apache.sling.installer.core", + "org.apache.sling.installer.factory.configuration", + "org.apache.sling.installer.provider.file", + "org.apache.sling.installer.provider.jcr", + "org.apache.sling.jcr.jcr-wrapper", + "org.apache.sling.jcr.api", + "org.apache.sling.jcr.base", + "org.apache.sling.jcr.classloader", + "org.apache.sling.jcr.contentloader", + "org.apache.sling.jcr.davex", + "org.apache.sling.jcr.jackrabbit.accessmanager", + "org.apache.sling.jcr.jackrabbit.server", + "org.apache.sling.jcr.jackrabbit.usermanager", + "org.apache.sling.jcr.ocm", + "org.apache.sling.jcr.registration", + "org.apache.sling.jcr.resource", + "org.apache.sling.jcr.webconsole", + "org.apache.sling.jcr.webdav", + "org.apache.sling.launchpad.content", + "org.apache.sling.launchpad.installer", + "org.apache.sling.resourceresolver", + "org.apache.sling.scripting.api", + "org.apache.sling.scripting.core", + "org.apache.sling.scripting.javascript", + "org.apache.sling.scripting.jsp", + "org.apache.sling.scripting.jsp.taglib", + "org.apache.sling.servlets.get", + "org.apache.sling.servlets.post", + "org.apache.sling.servlets.resolver", + "org.apache.sling.settings" + }; + + final List<String> missing = new ArrayList<String>(); + for(String bundleName : bundles) { + final int state = getBundleState(bundleName); + if(state != Bundle.ACTIVE) { + missing.add(bundleName + " (state=" + state + ")"); + } + } + + if(!missing.isEmpty()) { + fail("Some required bundles are missing or inactive:" + missing); + } + } + + @Test + public void testSlingServices() { + assertBundleActive("org.apache.sling.commons.mime"); + assertBundleActive("org.apache.sling.engine"); + + final String [] services = { + "org.apache.sling.engine.SlingRequestProcessor", + "org.apache.sling.commons.mime.MimeTypeService", + "org.apache.sling.jcr.api.SlingRepository" + }; + + final List<String> missing = new ArrayList<String>(); + for(String svc : services) { + final ServiceReference<?> ref = bundleContext.getServiceReference(svc); + if(ref == null) { + missing.add(svc); + } else { + bundleContext.ungetService(ref); + } + } + if(!missing.isEmpty()) { + fail("Some required services are missing:" + missing); + } + } +} \ No newline at end of file -- To stop receiving notification emails like this one, please contact "[email protected]" <[email protected]>.
