package org.testcontainers.junit;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.Uninterruptibles;
import com.mongodb.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.rabbitmq.client.*;
import org.bson.Document;
import org.junit.*;
import org.rnorth.ducttape.RetryCountExceededException;
import org.rnorth.ducttape.unreliables.Unreliables;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.Base58;
import org.testcontainers.utility.TestEnvironment;
import java.io.*;
import java.net.Socket;
import java.time.Duration;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.rnorth.visibleassertions.VisibleAssertions.*;
import static org.testcontainers.containers.BindMode.READ_ONLY;
/**
* Tests for GenericContainerRules
*/
public class GenericContainerRuleTest {
private static final int REDIS_PORT = 6379;
private static final String RABBIQMQ_TEST_EXCHANGE = "TestExchange";
private static final String RABBITMQ_TEST_ROUTING_KEY = "TestRoutingKey";
private static final String RABBITMQ_TEST_MESSAGE = "Hello world";
private static final int RABBITMQ_PORT = 5672;
private static final int MONGO_PORT = 27017;
/*
* Test data setup
*/
@BeforeClass
public static void setupContent() throws FileNotFoundException {
File contentFolder = new File(System.getProperty("user.home") + "/.tmp-test-container");
contentFolder.mkdir();
writeStringToFile(contentFolder, "file", "Hello world!");
}
/**
* Redis
*/
@ClassRule
public static GenericContainer redis = new GenericContainer("redis:3.0.2")
.withExposedPorts(REDIS_PORT);
/**
* RabbitMQ
*/
@ClassRule
public static GenericContainer rabbitMq = new GenericContainer("rabbitmq:3.5.3")
.withExposedPorts(RABBITMQ_PORT);
/**
* MongoDB
*/
@ClassRule
public static GenericContainer mongo = new GenericContainer("mongo:3.1.5")
.withExposedPorts(MONGO_PORT);
/**
* Pass an environment variable to the container, then run a shell script that exposes the variable in a quick and
* dirty way for testing.
*/
@ClassRule
public static GenericContainer alpineEnvVar = new GenericContainer("alpine:3.2")
.withExposedPorts(80)
.withEnv("MAGIC_NUMBER", "42")
.withCommand("/bin/sh", "-c", "while true; do echo \"$MAGIC_NUMBER\" | nc -l -p 80; done");
/**
* Pass environment variables to the container, then run a shell script that exposes the variables in a quick and
* dirty way for testing.
*/
@ClassRule
public static GenericContainer alpineEnvVarFromMap = new GenericContainer("alpine:3.2")
.withExposedPorts(80)
.withEnv(ImmutableMap.of(
"FIRST", "42",
"SECOND", "50"
))
.withCommand("/bin/sh", "-c", "while true; do echo \"$FIRST and $SECOND\" | nc -l -p 80; done");
/**
* Map a file on the classpath to a file in the container, and then expose the content for testing.
*/
@ClassRule
public static GenericContainer alpineClasspathResource = new GenericContainer("alpine:3.2")
.withExposedPorts(80)
.withClasspathResourceMapping("mappable-resource/test-resource.txt", "/content.txt", READ_ONLY)
.withCommand("/bin/sh", "-c", "while true; do cat /content.txt | nc -l -p 80; done");
/**
* Create a container with an extra host entry and expose the content of /etc/hosts for testing.
*/
@ClassRule
public static GenericContainer alpineExtrahost = new GenericContainer("alpine:3.2")
.withExposedPorts(80)
.withExtraHost("somehost", "192.168.1.10")
.withCommand("/bin/sh", "-c", "while true; do cat /etc/hosts | nc -l -p 80; done");
// @Test
// public void simpleRedisTest() {
// String ipAddress = redis.getContainerIpAddress();
// Integer port = redis.getMappedPort(REDIS_PORT);
//
// // Use Redisson to obtain a List that is backed by Redis
// Config redisConfig = new Config();
// redisConfig.useSingleServer().setAddress(ipAddress + ":" + port);
//
// Redisson redisson = Redisson.create(redisConfig);
//
// List<String> testList = redisson.getList("test");
// testList.add("foo");
// testList.add("bar");
// testList.add("baz");
//
// List<String> testList2 = redisson.getList("test");
// assertEquals("The list contains the expected number of items (redis is working!)", 3, testList2.size());
// assertTrue("The list contains an item that was put in (redis is working!)", testList2.contains("foo"));
// assertTrue("The list contains an item that was put in (redis is working!)", testList2.contains("bar"));
// assertTrue("The list contains an item that was put in (redis is working!)", testList2.contains("baz"));
// }
@Test
public void simpleRabbitMqTest() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(rabbitMq.getContainerIpAddress());
factory.setPort(rabbitMq.getMappedPort(RABBITMQ_PORT));
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(RABBIQMQ_TEST_EXCHANGE, "direct", true);
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, RABBIQMQ_TEST_EXCHANGE, RABBITMQ_TEST_ROUTING_KEY);
// Set up a consumer on the queue
final boolean[] messageWasReceived = new boolean[1];
channel.basicConsume(queueName, false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
messageWasReceived[0] = Arrays.equals(body, RABBITMQ_TEST_MESSAGE.getBytes());
}
});
// post a message
channel.basicPublish(RABBIQMQ_TEST_EXCHANGE, RABBITMQ_TEST_ROUTING_KEY, null, RABBITMQ_TEST_MESSAGE.getBytes());
// check the message was received
assertTrue("The message was received", Unreliables.retryUntilSuccess(5, TimeUnit.SECONDS, () -> {
if (!messageWasReceived[0]) {
throw new IllegalStateException("Message not received yet");
}
return true;
}));
}
@Test
public void simpleMongoDbTest() {
MongoClient mongoClient = new MongoClient(mongo.getContainerIpAddress(), mongo.getMappedPort(MONGO_PORT));
MongoDatabase database = mongoClient.getDatabase("test");
MongoCollection<Document> collection = database.getCollection("testCollection");
Document doc = new Document("name", "foo")
.append("value", 1);
collection.insertOne(doc);
Document doc2 = collection.find(new Document("name", "foo")).first();
assertEquals("A record can be inserted into and retrieved from MongoDB", 1, doc2.get("value"));
}
@Test
public void environmentAndCustomCommandTest() throws IOException {
String line = getReaderForContainerPort80(alpineEnvVar).readLine();
assertEquals("An environment variable can be passed into a command", "42", line);
}
@Test
public void environmentFromMapTest() throws IOException {
String line = getReaderForContainerPort80(alpineEnvVarFromMap).readLine();
assertEquals("Environment variables can be passed into a command from a map", "42 and 50", line);
}
@Test
public void customClasspathResourceMappingTest() throws IOException {
// Note: This functionality doesn't work if you are running your build inside a Docker container;
// in that case this test will fail.
String line = getReaderForContainerPort80(alpineClasspathResource).readLine();
assertEquals("Resource on the classpath can be mapped using calls to withClasspathResourceMapping", "FOOBAR", line);
}
@Test
public void exceptionThrownWhenMappedPortNotFound() throws IOException {
assertThrows("When the requested port is not mapped, getMappedPort() throws an exception",
IllegalArgumentException.class,
() -> {
return redis.getMappedPort(666);
});
}
protected static void writeStringToFile(File contentFolder, String filename, String string) throws FileNotFoundException {
File file = new File(contentFolder, filename);
PrintStream printStream = new PrintStream(new FileOutputStream(file));
printStream.println(string);
printStream.close();
}
@Test @Ignore //TODO investigate intermittent failures
public void failFastWhenContainerHaltsImmediately() throws Exception {
long startingTimeMs = System.currentTimeMillis();
final GenericContainer failsImmediately = new GenericContainer("alpine:3.2")
.withCommand("/bin/sh", "-c", "return false")
.withMinimumRunningDuration(Duration.ofMillis(100));
try {
assertThrows(
"When we start a container that halts immediately, an exception is thrown",
RetryCountExceededException.class,
() -> {
failsImmediately.start();
return null;
});
// Check how long it took, to verify that we ARE bailing out early.
// Want to strike a balance here; too short and this test will fail intermittently
// on slow systems and/or due to GC variation, too long and we won't properly test
// what we're intending to test.
int allowedSecondsToFailure =
GenericContainer.CONTAINER_RUNNING_TIMEOUT_SEC / 2;
long completedTimeMs = System.currentTimeMillis();
assertTrue("container should not take long to start up",
completedTimeMs - startingTimeMs < 1000L * allowedSecondsToFailure);
} finally {
failsImmediately.stop();
}
}
@Test
public void testExecInContainer() throws Exception {
// The older "lxc" execution driver doesn't support "exec". At the time of writing (2016/03/29),
// that's the case for CircleCI.
// Once they resolve the issue, this clause can be removed.
Assume.assumeTrue(TestEnvironment.dockerExecutionDriverSupportsExec());
final GenericContainer.ExecResult result = redis.execInContainer("redis-cli", "role");
assertTrue("Output for \"redis-cli role\" command should start with \"master\"", result.getStdout().startsWith("master"));
assertEquals("Stderr for \"redis-cli role\" command should be empty", "", result.getStderr());
// We expect to reach this point for modern Docker versions.
}
@Test
public void extraHostTest() throws IOException {
BufferedReader br = getReaderForContainerPort80(alpineExtrahost);
// read hosts file from container
StringBuffer hosts = new StringBuffer();
String line = br.readLine();
while (line != null) {
hosts.append(line);
hosts.append("\n");
line = br.readLine();
}
Matcher matcher = Pattern.compile("^192.168.1.10\\s.*somehost", Pattern.MULTILINE).matcher(hosts.toString());
assertTrue("The hosts file of container contains extra host", matcher.find());
}
@Test
public void createContainerCmdHookTest() {
// Use random name to avoid the conflicts between the tests
String randomName = Base58.randomString(5);
try(
GenericContainer container = new GenericContainer<>("redis:3.0.2")
.withCommand("redis-server", "--help")
.withCreateContainerCmdModifier(cmd -> cmd.withName("overrideMe"))
// Preserves the order
.withCreateContainerCmdModifier(cmd -> cmd.withName(randomName))
// Allows to override pre-configured values by GenericContainer
.withCreateContainerCmdModifier(cmd -> cmd.withCmd("redis-server", "--port", "6379"))
) {
container.start();
assertEquals("Name is configured", "/" + randomName, container.getContainerInfo().getName());
assertEquals("Command is configured", "[redis-server, --port, 6379]", Arrays.toString(container.getContainerInfo().getConfig().getCmd()));
}
}
private BufferedReader getReaderForContainerPort80(GenericContainer container) {
return Unreliables.retryUntilSuccess(10, TimeUnit.SECONDS, () -> {
Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
Socket socket = new Socket(container.getContainerIpAddress(), container.getMappedPort(80));
return new BufferedReader(new InputStreamReader(socket.getInputStream()));
});
}
}