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-testing-email.git
commit f5de0a002534fffcbebc8e805ba25199a060d140 Author: Robert Munteanu <[email protected]> AuthorDate: Fri Jun 9 14:36:36 2017 +0000 SLING-6949 - Create testing utilities for email-enabled applications Initial commit of a bundle which starts an SMTP server inside a Sling application and then exposes the received messages via HTTP. git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1798227 13f79535-47bb-0310-9956-ffa450edef68 --- pom.xml | 123 ++++++++++++++++++ .../sling/testing/email/impl/EMailServlet.java | 109 ++++++++++++++++ .../testing/email/impl/SmtpServerWrapper.java | 114 +++++++++++++++++ .../sling/testing/email/impl/EmailServletTest.java | 142 +++++++++++++++++++++ .../testing/email/impl/SmtpServerWrapperTest.java | 61 +++++++++ 5 files changed, 549 insertions(+) diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..68e43b8 --- /dev/null +++ b/pom.xml @@ -0,0 +1,123 @@ +<?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>30</version> + <relativePath /> + </parent> + + <artifactId>org.apache.sling.testing.email</artifactId> + <version>0.9.0-SNAPSHOT</version> + <packaging>bundle</packaging> + + <name>Apache Sling Testing Email Support</name> + <description> + Contains utilities that assist in validating email-enabled OSGi applications. + </description> + <scm> + <connection>scm:svn:http://svn.apache.org/repos/asf/sling/trunk/testing/email</connection> + <developerConnection> scm:svn:https://svn.apache.org/repos/asf/sling/trunk/testing/email</developerConnection> + <url>http://svn.apache.org/viewvc/sling/trunk/testing/email</url> + </scm> + + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <instructions> + <!-- + subetha-smpt and subetha-wiser are not OSGi bundles + Since the potential for reuse inside the runtime is quite small, + let bnd embed the needed packages inside this bundle + + Also embed the felix-utils JSON Writer + --> + <Conditional-Package>org.subethamail.*,org.apache.felix.utils.json</Conditional-Package> + </instructions> + </configuration> + </plugin> + </plugins> + </build> + <dependencies> + <dependency> + <groupId>org.subethamail</groupId> + <artifactId>subethasmtp-wiser</artifactId> + <version>1.2</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>javax.servlet-api</artifactId> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>osgi.core</artifactId> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.service.http.whiteboard</artifactId> + <version>1.0.0</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.utils</artifactId> + <version>1.9.0</version> + <scope>provided</scope> + </dependency> + + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>hamcrest-library</artifactId> + <version>1.3</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.testing.clients</artifactId> + <version>1.1.0</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.testing.sling-mock</artifactId> + <version>2.2.12</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.jayway.jsonpath</groupId> + <artifactId>json-path</artifactId> + <version>2.2.0</version> + <scope>test</scope> + </dependency> + </dependencies> +</project> diff --git a/src/main/java/org/apache/sling/testing/email/impl/EMailServlet.java b/src/main/java/org/apache/sling/testing/email/impl/EMailServlet.java new file mode 100644 index 0000000..25ff6ac --- /dev/null +++ b/src/main/java/org/apache/sling/testing/email/impl/EMailServlet.java @@ -0,0 +1,109 @@ +/* + * 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.testing.email.impl; + +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN; + +import java.io.IOException; +import java.util.Enumeration; + +import javax.mail.Header; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.felix.utils.json.JSONWriter; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * Exposes emails stored by the {@link SmtpServerWrapper} through an HTTP API + * + * <p>The entry points for the servlet are: + * + * <ol> + * <li><tt>GET /system/sling/testing/email/config</tt>, which returns + * a JSON object containing the configuration properties of the {@link SmtpServerWrapper}</li> + * <li><tt>GET /system/sling/testing/email/messages</tt>, which returns the messages + * currently held by the {@link SmtpServerWrapper}</li> + * </ol> + */ +@Component(service = Servlet.class, + property = { + HTTP_WHITEBOARD_SERVLET_PATTERN + "=/system/sling/testing/email", + HTTP_WHITEBOARD_CONTEXT_SELECT + "=(" + HTTP_WHITEBOARD_CONTEXT_NAME + "=org.osgi.service.http)" + }) +public class EMailServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Reference + private SmtpServerWrapper wiser; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + resp.setContentType("application/json"); + JSONWriter w = new JSONWriter(resp.getWriter()); + + String action = ( req.getPathInfo() == null || req.getPathInfo().isEmpty() ) ? "messages" : req.getPathInfo().substring(1); + + switch (action) { + case "messages": + w.object(); + w.key("messages"); + w.array(); + for ( MimeMessage msg : wiser.getMessages() ) { + w.object(); + try { + Enumeration<?> headers = msg.getAllHeaders(); + while ( headers.hasMoreElements()) { + Header header = (Header) headers.nextElement(); + w.key(header.getName()).value(header.getValue()); + } + + w.key("-Content-").value(msg.getContent()); + + } catch (MessagingException e) { + throw new ServletException("Failed retrieving message data", e); + } + w.endObject(); + } + w.endArray(); + w.endObject(); + break; + + case "config": + w.object(); + w.key("bindPort").value(wiser.getEffectiveBindPort()); + w.endObject(); + break; + + default: + resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); + break; + } + + + } +} diff --git a/src/main/java/org/apache/sling/testing/email/impl/SmtpServerWrapper.java b/src/main/java/org/apache/sling/testing/email/impl/SmtpServerWrapper.java new file mode 100644 index 0000000..046b626 --- /dev/null +++ b/src/main/java/org/apache/sling/testing/email/impl/SmtpServerWrapper.java @@ -0,0 +1,114 @@ +/* + * 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.testing.email.impl; + +import java.lang.reflect.Field; +import java.net.ServerSocket; +import java.util.ArrayList; +import java.util.List; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; + +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.Designate; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; +import org.subethamail.smtp.server.SMTPServer; +import org.subethamail.wiser.Wiser; +import org.subethamail.wiser.WiserMessage; + +// we want the component to be immediate since it is usable outside the OSGi service registry +// via SMTP connections +@Component(service = SmtpServerWrapper.class, immediate = true) +@Designate(ocd = SmtpServerWrapper.Config.class) +public class SmtpServerWrapper { + + @ObjectClassDefinition(name="Apache Sling Testing SMTP Server Wrapper") + public @interface Config { + + @AttributeDefinition(name="Bind port", description="The port on which the server will bind. A value of 0 requests binding on any available port ") + int bind_port() default 0; + } + + // wiser is not thread-safe so guard access to the instance + private final Object sync = new Object(); + private Wiser wiser; + private int effectiveBindPort; + + @Activate + public void activate(Config cfg) throws ReflectiveOperationException { + + int bindPort; + + synchronized (sync) { + wiser = new Wiser(); + wiser.setPort(cfg.bind_port()); + wiser.start(); + bindPort = cfg.bind_port() == 0 ? reflectiveGetEffectiveBindPort(wiser.getServer()) : cfg.bind_port(); + } + + effectiveBindPort = bindPort; + } + + private int reflectiveGetEffectiveBindPort(SMTPServer server) throws ReflectiveOperationException { + + // we control the version of Wiser used so there is no risk of an exception here + Field field = SMTPServer.class.getDeclaredField("serverSocket"); + field.setAccessible(true); + ServerSocket socket = (ServerSocket) field.get(server); + + return socket.getLocalPort(); + } + + @Deactivate + public void deactivate() { + + synchronized (sync) { + wiser.stop(); + } + } + + public int getEffectiveBindPort() { + + return effectiveBindPort; + } + + public void clearMessages() { + + synchronized (sync) { + wiser.getMessages().clear(); + } + } + + public List<MimeMessage> getMessages() { + + try { + List<MimeMessage> messages = new ArrayList<>(); + synchronized (sync) { + for ( WiserMessage message : wiser.getMessages()) { + messages.add(message.getMimeMessage()); + } + } + return messages; + } catch (MessagingException e) { + throw new RuntimeException("Failed converting to " + MimeMessage.class.getName(), e); + } + } +} diff --git a/src/test/java/org/apache/sling/testing/email/impl/EmailServletTest.java b/src/test/java/org/apache/sling/testing/email/impl/EmailServletTest.java new file mode 100644 index 0000000..f07d2d4 --- /dev/null +++ b/src/test/java/org/apache/sling/testing/email/impl/EmailServletTest.java @@ -0,0 +1,142 @@ +/* + * 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.testing.email.impl; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +import javax.mail.Message.RecipientType; +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.internet.AddressException; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; + +import org.apache.sling.servlethelpers.MockSlingHttpServletRequest; +import org.apache.sling.servlethelpers.MockSlingHttpServletResponse; +import org.apache.sling.testing.clients.util.PortAllocator; +import org.apache.sling.testing.mock.sling.junit.SlingContext; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import com.jayway.jsonpath.JsonPath; + +public class EmailServletTest { + + @Rule + public SlingContext ctx = new SlingContext(); + private int bindPort; + private EMailServlet servlet; + + @Before + public void prepare() { + + bindPort = new PortAllocator().allocatePort(); + + ctx.registerInjectActivateService(new SmtpServerWrapper(), Collections.singletonMap("bind.port", bindPort)); + + servlet = ctx.registerInjectActivateService(new EMailServlet()); + } + + @Test + public void getBindPort() throws ServletException, IOException { + + // SLING-6947 + MockSlingHttpServletRequest request = new MockSlingHttpServletRequest(ctx.resourceResolver()) { + @Override + public String getPathInfo() { + return "/config"; + } + }; + + MockSlingHttpServletResponse response = new MockSlingHttpServletResponse(); + servlet.service(request, response); + + assertEquals("response.status", HttpServletResponse.SC_OK, response.getStatus()); + + // SLING-6948 + byte[] out = response.getOutputAsString().getBytes(); + int configuredPort = JsonPath.read(new ByteArrayInputStream(out), "$.bindPort"); + + assertThat("bindPort", configuredPort, equalTo(bindPort)); + } + + @Test + public void getMessages() throws ServletException, IOException, MessagingException { + + String subject1 = "Test email"; + String body1 = "A long message \r\nbody"; + sendEmail(subject1, body1); + + String subject2 = "Verification email"; + String body2 = "A shorter message body"; + sendEmail(subject2, body2); + + // SLING-6947 + MockSlingHttpServletRequest request = new MockSlingHttpServletRequest(ctx.resourceResolver()) { + @Override + public String getPathInfo() { + return "/messages"; + } + }; + + MockSlingHttpServletResponse response = new MockSlingHttpServletResponse(); + servlet.service(request, response); + + assertEquals("response.status", HttpServletResponse.SC_OK, response.getStatus()); + + // SLING-6948 + byte[] out = response.getOutputAsString().getBytes(); + List<String> subjects = JsonPath.read(new ByteArrayInputStream(out), "$.messages[*].Subject"); + + assertThat("subjects.size", subjects, hasSize(2)); + assertThat("subjects", subjects, Matchers.hasItems(subject1, subject2)); + + String readBody = JsonPath.read(new ByteArrayInputStream(out), "$.messages[0].['-Content-']"); + assertThat("body", readBody, equalTo(body1)); + } + + private void sendEmail(String subject, String body) throws MessagingException, AddressException { + + Properties mailProps = new Properties(); + mailProps.put("mail.smtp.host", "localhost"); + mailProps.put("mail.smtp.port", String.valueOf(bindPort)); + + Session mailSession = Session.getInstance(mailProps); + + MimeMessage msg = new MimeMessage(mailSession); + msg.setFrom(new InternetAddress("sender@localhost")); + msg.addRecipient(RecipientType.TO, new InternetAddress("receiver@localhost")); + msg.setSubject(subject); + msg.setText(body); + + Transport.send(msg); + } +} diff --git a/src/test/java/org/apache/sling/testing/email/impl/SmtpServerWrapperTest.java b/src/test/java/org/apache/sling/testing/email/impl/SmtpServerWrapperTest.java new file mode 100644 index 0000000..14e28a8 --- /dev/null +++ b/src/test/java/org/apache/sling/testing/email/impl/SmtpServerWrapperTest.java @@ -0,0 +1,61 @@ +/* + * 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.testing.email.impl; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.junit.Assert.assertThat; + +import java.util.Collections; + +import org.apache.sling.testing.clients.util.PortAllocator; +import org.apache.sling.testing.mock.osgi.junit.OsgiContext; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; + +public class SmtpServerWrapperTest { + + @Rule public OsgiContext ctx = new OsgiContext(); + + private SmtpServerWrapper wrapper; + + @After + public void cleanUp() { + if ( wrapper != null ) { + wrapper.deactivate(); + } + } + + @Test + public void startupWithPreconfiguredPort() throws ReflectiveOperationException { + + final int configuredPort = new PortAllocator().allocatePort(); + + wrapper = ctx.registerInjectActivateService(new SmtpServerWrapper(), Collections.singletonMap("bind.port", configuredPort)); + + assertThat("bindPort", wrapper.getEffectiveBindPort(), equalTo(configuredPort)); + } + + @Test + public void startupWithRandomPort() throws ReflectiveOperationException { + + wrapper = ctx.registerInjectActivateService(new SmtpServerWrapper(), Collections.singletonMap("bind.port", 0)); + + assertThat("bindPort", wrapper.getEffectiveBindPort(), greaterThan(0)); + } +} -- To stop receiving notification emails like this one, please contact "[email protected]" <[email protected]>.
