Use a fake EmailSender in tests

Instead of disabling email entirely, allow sent emails to be buffered
into a fake sender that can be inspected later.

To facilitate making assertions over emails, make EmailHeader
implementations more immutable, and implement
equals/hashCode/toString.

Change-Id: I483ea9f9216de1c5eb3635fa56f7ad9318c2bebf
This commit is contained in:
Dave Borowitz 2015-04-16 13:50:51 -07:00
parent 87295b6fbb
commit 0d398d79c9
6 changed files with 188 additions and 6 deletions

View File

@ -28,6 +28,7 @@ import com.google.gerrit.server.git.SubmoduleOp;
import com.google.gerrit.server.index.ChangeSchemas;
import com.google.gerrit.server.ssh.NoSshModule;
import com.google.gerrit.server.util.SocketUtil;
import com.google.gerrit.testutil.FakeEmailSender;
import com.google.gerrit.testutil.TempFileUtil;
import com.google.inject.Injector;
import com.google.inject.Key;
@ -115,6 +116,7 @@ public class GerritServer {
}
}
});
daemon.setEmailModuleForTesting(new FakeEmailSender.Module());
final File site;
ExecutorService daemonService = null;

View File

@ -152,6 +152,7 @@ public class Daemon extends SiteProgram {
private Path runFile;
private boolean test;
private AbstractModule luceneModule;
private Module emailModule;
private Runnable serverStarted;
@ -261,6 +262,11 @@ public class Daemon extends SiteProgram {
headless = true;
}
@VisibleForTesting
public void setEmailModuleForTesting(Module module) {
emailModule = module;
}
@VisibleForTesting
public void setLuceneModule(LuceneIndexModule m) {
luceneModule = m;
@ -322,7 +328,11 @@ public class Daemon extends SiteProgram {
modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
modules.add(new InternalAccountDirectory.Module());
modules.add(new DefaultCacheFactory.Module());
modules.add(new SmtpEmailSender.Module());
if (emailModule != null) {
modules.add(emailModule);
} else {
modules.add(new SmtpEmailSender.Module());
}
modules.add(new SignedTokenEmailTokenVerifier.Module());
modules.add(new PluginRestApiModule());
modules.add(new RestCacheAdminModule());

View File

@ -101,10 +101,12 @@ java_library(
'//lib:gwtorm',
'//lib:h2',
'//lib:junit',
'//lib/auto:auto-value',
'//lib/guice:guice',
'//lib/guice:guice-servlet',
'//lib/jgit:jgit',
'//lib/jgit:junit',
'//lib/log:api',
'//lib/log:impl_log4j',
'//lib/log:log4j',
],

View File

@ -14,6 +14,8 @@
package com.google.gerrit.server.mail;
import com.google.common.base.MoreObjects;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
@ -23,6 +25,7 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
public abstract class EmailHeader {
public abstract boolean isEmpty();
@ -30,7 +33,7 @@ public abstract class EmailHeader {
public abstract void write(Writer w) throws IOException;
public static class String extends EmailHeader {
private java.lang.String value;
private final java.lang.String value;
public String(java.lang.String v) {
value = v;
@ -53,6 +56,22 @@ public abstract class EmailHeader {
w.write(value);
}
}
@Override
public int hashCode() {
return Objects.hashCode(value);
}
@Override
public boolean equals(Object o) {
return (o instanceof String)
&& Objects.equals(value, ((String) o).value);
}
@Override
public java.lang.String toString() {
return MoreObjects.toStringHelper(this).addValue(value).toString();
}
}
static boolean needsQuotedPrintable(java.lang.String value) {
@ -113,7 +132,7 @@ public abstract class EmailHeader {
}
public static class Date extends EmailHeader {
private java.util.Date value;
private final java.util.Date value;
public Date(java.util.Date v) {
value = v;
@ -135,6 +154,22 @@ public abstract class EmailHeader {
fmt = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.ENGLISH);
w.write(fmt.format(value));
}
@Override
public int hashCode() {
return Objects.hashCode(value);
}
@Override
public boolean equals(Object o) {
return (o instanceof Date)
&& Objects.equals(value, ((Date) o).value);
}
@Override
public java.lang.String toString() {
return MoreObjects.toStringHelper(this).addValue(value).toString();
}
}
public static class AddressList extends EmailHeader {
@ -191,5 +226,21 @@ public abstract class EmailHeader {
needComma = true;
}
}
@Override
public int hashCode() {
return Objects.hashCode(list);
}
@Override
public boolean equals(Object o) {
return (o instanceof AddressList)
&& Objects.equals(list, ((AddressList) o).list);
}
@Override
public java.lang.String toString() {
return MoreObjects.toStringHelper(this).addValue(list).toString();
}
}
}

View File

@ -0,0 +1,119 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed 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 com.google.gerrit.testutil;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.mail.Address;
import com.google.gerrit.server.mail.EmailHeader;
import com.google.gerrit.server.mail.EmailSender;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
/**
* Email sender implementation that records messages in memory.
* <p>
* This class is mostly threadsafe. The only exception is that not all {@link
* EmailHeader} subclasses are immutable. In particular, if a caller holds a
* reference to an {@code AddressList} and mutates it after sending, the message
* returned by {@link #getMessages()} may or may not reflect mutations.
*/
@Singleton
public class FakeEmailSender implements EmailSender {
private static final Logger log =
LoggerFactory.getLogger(FakeEmailSender.class);
public static class Module extends AbstractModule {
@Override
public void configure() {
bind(EmailSender.class).to(FakeEmailSender.class);
}
}
@AutoValue
public abstract static class Message {
private static Message create(Address from, Collection<Address> rcpt,
Map<String, EmailHeader> headers, String body) {
return new AutoValue_FakeEmailSender_Message(from,
ImmutableList.copyOf(rcpt), ImmutableMap.copyOf(headers), body);
}
public abstract Address from();
public abstract ImmutableList<Address> rcpt();
public abstract ImmutableMap<String, EmailHeader> headers();
public abstract String body();
}
private final WorkQueue workQueue;
private final List<Message> messages;
@Inject
FakeEmailSender(WorkQueue workQueue) {
this.workQueue = workQueue;
messages = Collections.synchronizedList(new ArrayList<Message>());
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public boolean canEmail(String address) {
return true;
}
@Override
public void send(Address from, Collection<Address> rcpt,
Map<String, EmailHeader> headers, String body) throws EmailException {
messages.add(Message.create(from, rcpt, headers, body));
}
public ImmutableList<Message> getMessages() {
waitForEmails();
synchronized (messages) {
return ImmutableList.copyOf(messages);
}
}
private void waitForEmails() {
// TODO(dborowitz): This is brittle; consider forcing emails to use
// a single thread in tests (tricky because most callers just use the
// default executor).
for (WorkQueue.Task<?> task : workQueue.getTasks()) {
if (task.toString().contains("send-email")) {
try {
task.get();
} catch (ExecutionException | InterruptedException e) {
log.warn("error finishing email task", e);
}
}
}
}
}

View File

@ -46,7 +46,6 @@ import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.index.ChangeSchemas;
import com.google.gerrit.server.index.IndexModule.IndexType;
import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
import com.google.gerrit.server.mail.SmtpEmailSender;
import com.google.gerrit.server.patch.DiffExecutor;
import com.google.gerrit.server.schema.DataSourceType;
import com.google.gerrit.server.schema.SchemaCreator;
@ -88,7 +87,6 @@ public class InMemoryModule extends FactoryModule {
cfg.setString("gerrit", null, "allProjects", "Test-Projects");
cfg.setString("user", null, "name", "Gerrit Code Review");
cfg.setString("user", null, "email", "gerrit@localhost");
cfg.setBoolean("sendemail", null, "enable", false);
cfg.setString("cache", null, "directory", null);
cfg.setString("index", null, "type", "lucene");
cfg.setBoolean("index", "lucene", "testInmemory", true);
@ -179,7 +177,7 @@ public class InMemoryModule extends FactoryModule {
}
});
install(new DefaultCacheFactory.Module());
install(new SmtpEmailSender.Module());
install(new FakeEmailSender.Module());
install(new SignedTokenEmailTokenVerifier.Module());
IndexType indexType = null;