/**
* Copyright (C) 2014 Stratio (http://stratio.com)
*
* 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.stratio.ingestion.sink.cassandra;
import static org.fest.assertions.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.anyListOf;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import java.io.FileNotFoundException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.apache.flume.Channel;
import org.apache.flume.Context;
import org.apache.flume.Event;
import org.apache.flume.EventDeliveryException;
import org.apache.flume.Sink;
import org.apache.flume.Transaction;
import org.apache.flume.conf.ConfigurationException;
import org.apache.flume.event.EventBuilder;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.junit.Rule;
import org.junit.Test;
import org.junit.Ignore;
import org.junit.rules.ExpectedException;
import com.datastax.driver.core.Cluster;
import com.datastax.driver.core.ColumnMetadata;
import com.datastax.driver.core.ConsistencyLevel;
import com.datastax.driver.core.DataType;
import com.datastax.driver.core.Session;
import com.datastax.driver.core.TableMetadata;
import com.datastax.driver.core.exceptions.DriverException;
import com.datastax.driver.core.exceptions.DriverInternalError;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
public class TestCassandraSink {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void confWithMissingTablesFails() {
final CassandraSink sink = new CassandraSink();
final Context context = new Context();
thrown.expect(ConfigurationException.class);
thrown.expectMessage("tables is mandatory");
sink.configure(context);
}
@Test
public void confWithBadHostsFails() {
final CassandraSink sink = new CassandraSink();
final Context context = new Context();
context.put("tables", "keyspace.table");
context.put("hosts", "localhost:9badport");
thrown.expect(ConfigurationException.class);
thrown.expectMessage("Could not parse host: localhost:9badport");
thrown.expectCause(new CauseMatcher(IllegalArgumentException.class));
sink.configure(context);
}
@Test
public void confMissingCqlFileFails() {
final CassandraSink sink = new CassandraSink();
final Context context = new Context();
context.put("tables", "keyspace.table");
context.put("cqlFile", "/NOT/FOUND/MY.CQL");
thrown.expect(ConfigurationException.class);
thrown.expectMessage("Cannot read CQL file: /NOT/FOUND/MY.CQL");
thrown.expectCause(new CauseMatcher(FileNotFoundException.class));
sink.configure(context);
}
@Ignore @Test
public void processEmtpyChannel() throws EventDeliveryException {
final CassandraSink sink = new CassandraSink();
final Channel channel = mock(Channel.class);
final Transaction tx = mock(Transaction.class);
final CassandraTable table = mock(CassandraTable.class);
final Context ctx = new Context();
ctx.put("tables", "keyspace.table");
sink.configure(ctx);
sink.tables = Collections.singletonList(table);
sink.setChannel(channel);
when(channel.getTransaction()).thenReturn(tx);
when(channel.take()).thenReturn(null);
assertThat(sink.process()).isEqualTo(Sink.Status.READY);
verifyZeroInteractions(table);
}
@Ignore @Test
public void processOneEvent() throws EventDeliveryException {
final CassandraSink sink = new CassandraSink();
final Channel channel = mock(Channel.class);
final Transaction tx = mock(Transaction.class);
final CassandraTable table = mock(CassandraTable.class);
final Context ctx = new Context();
ctx.put("tables", "keyspace.table");
sink.configure(ctx);
sink.tables = Collections.singletonList(table);
sink.setChannel(channel);
when(channel.getTransaction()).thenReturn(tx);
final Event event = EventBuilder.withBody(new byte[0], ImmutableMap.of("id", "1", "col", "text"));
when(channel.take()).thenReturn(event).thenReturn(null);
assertThat(sink.process()).isEqualTo(Sink.Status.READY);
verify(table).save(ImmutableList.of(event));
verifyNoMoreInteractions(table);
verify(tx).begin();
verify(tx).commit();
verify(tx).close();
}
@Test
public void processOneEventWithBatchSizeOne() throws EventDeliveryException {
final CassandraSink sink = new CassandraSink();
final Channel channel = mock(Channel.class);
final Transaction tx = mock(Transaction.class);
final CassandraTable table = mock(CassandraTable.class);
final Context ctx = new Context();
ctx.put("tables", "keyspace.table");
ctx.put("batchSize", "1");
sink.configure(ctx);
sink.tables = Collections.singletonList(table);
sink.setChannel(channel);
when(channel.getTransaction()).thenReturn(tx);
final Event event = EventBuilder.withBody(new byte[0], ImmutableMap.of("id", "1", "col", "text"));
when(channel.take()).thenReturn(event).thenReturn(null);
assertThat(sink.process()).isEqualTo(Sink.Status.READY);
verify(table).save(ImmutableList.of(event));
verifyNoMoreInteractions(table);
verify(tx).begin();
verify(tx).commit();
verify(tx).close();
}
@Ignore @Test
public void processDriverException() throws EventDeliveryException {
final CassandraSink sink = new CassandraSink();
final Channel channel = mock(Channel.class);
final Transaction tx = mock(Transaction.class);
final CassandraTable table = mock(CassandraTable.class);
final Context ctx = new Context();
ctx.put("tables", "keyspace.table");
sink.configure(ctx);
sink.tables = Collections.singletonList(table);
sink.setChannel(channel);
when(channel.getTransaction()).thenReturn(tx);
final Event event = EventBuilder.withBody(new byte[0], ImmutableMap.of("id", "1", "col", "text"));
when(channel.take()).thenReturn(event).thenReturn(null);
doThrow(DriverException.class).when(table).save(anyListOf(Event.class));
boolean hasThrown = false;
try {
sink.process();
} catch (EventDeliveryException ex) {
hasThrown = true;
if (!(ex.getCause() instanceof DriverException)) {
fail("Did not throw inner DriverException: " + ex);
}
}
verify(tx).begin();
verify(tx).rollback();
verify(tx).close();
verifyNoMoreInteractions(tx);
if (!hasThrown) {
fail("Did not throw exception");
}
}
@Ignore @Test
public void processDriverExceptionWithRollbackException() throws EventDeliveryException {
final CassandraSink sink = new CassandraSink();
final Channel channel = mock(Channel.class);
final Transaction tx = mock(Transaction.class);
final CassandraTable table = mock(CassandraTable.class);
final Context ctx = new Context();
ctx.put("tables", "keyspace.table");
sink.configure(ctx);
sink.tables = Collections.singletonList(table);
sink.setChannel(channel);
when(channel.getTransaction()).thenReturn(tx);
final Event event = EventBuilder.withBody(new byte[0], ImmutableMap.of("id", "1", "col", "text"));
when(channel.take()).thenReturn(event).thenReturn(null);
doThrow(DriverException.class).when(table).save(anyListOf(Event.class));
doThrow(RuntimeException.class).when(tx).rollback();
boolean hasThrown = false;
try {
sink.process();
} catch (EventDeliveryException ex) {
hasThrown = true;
if (!(ex.getCause() instanceof DriverException)) {
fail("Did not throw inner DriverException: " + ex);
}
}
verify(tx).begin();
verify(tx).rollback();
verify(tx).close();
verifyNoMoreInteractions(tx);
if (!hasThrown) {
fail("Did not throw exception");
}
}
@Ignore @Test
public void processIllegalArgumentException() throws EventDeliveryException {
final CassandraSink sink = new CassandraSink();
final Channel channel = mock(Channel.class);
final Transaction tx = mock(Transaction.class);
final CassandraTable table = mock(CassandraTable.class);
final Context ctx = new Context();
ctx.put("tables", "keyspace.table");
sink.configure(ctx);
sink.tables = Collections.singletonList(table);
sink.setChannel(channel);
when(channel.getTransaction()).thenReturn(tx);
final Event event = EventBuilder.withBody(new byte[0], ImmutableMap.of("id", "1", "col", "text"));
when(channel.take()).thenReturn(event).thenReturn(null);
doThrow(IllegalArgumentException.class).when(table).save(anyListOf(Event.class));
boolean hasThrown = false;
try {
sink.process();
} catch (EventDeliveryException ex) {
hasThrown = true;
if (!(ex.getCause() instanceof IllegalArgumentException)) {
fail("Did not throw inner IllegalArgumentException: " + ex);
}
}
verify(tx).begin();
verify(tx).rollback();
verify(tx).close();
verifyNoMoreInteractions(tx);
if (!hasThrown) {
fail("Did not throw exception");
}
}
@Test
public void processRuntimeException() throws EventDeliveryException {
final CassandraSink sink = new CassandraSink();
final Channel channel = mock(Channel.class);
final Transaction tx = mock(Transaction.class);
final CassandraTable table = mock(CassandraTable.class);
final Context ctx = new Context();
ctx.put("tables", "keyspace.table");
sink.configure(ctx);
sink.tables = Collections.singletonList(table);
sink.setChannel(channel);
when(channel.getTransaction()).thenReturn(tx);
final Event event = EventBuilder.withBody(new byte[0], ImmutableMap.of("id", "1", "col", "text"));
when(channel.take()).thenReturn(event).thenReturn(null);
doThrow(RuntimeException.class).when(table).save(anyListOf(Event.class));
boolean hasThrown = false;
try {
sink.process();
} catch (RuntimeException ex) {
hasThrown = true;
}
verify(tx).begin();
verify(tx).rollback();
verify(tx).close();
verifyNoMoreInteractions(tx);
if (!hasThrown) {
fail("Did not throw exception");
}
}
@Test
public void stop() {
final CassandraSink sink = new CassandraSink();
final Channel channel = mock(Channel.class);
final Session session = mock(Session.class);
final Cluster cluster = mock(Cluster.class);
final Context ctx = new Context();
ctx.put("tables", "keyspace.table");
sink.configure(ctx);
sink.setChannel(channel);
sink.session = session;
sink.cluster = cluster;
sink.stop();
verify(session).isClosed();
verify(session).close();
verifyNoMoreInteractions(session);
verify(cluster).isClosed();
verify(cluster).close();
verifyNoMoreInteractions(cluster);
}
@Test
public void stopWithDriverInternalException() {
final CassandraSink sink = new CassandraSink();
final Channel channel = mock(Channel.class);
final Session session = mock(Session.class);
final Cluster cluster = mock(Cluster.class);
final Context ctx = new Context();
ctx.put("tables", "keyspace.table");
sink.configure(ctx);
sink.setChannel(channel);
sink.session = session;
sink.cluster = cluster;
doThrow(DriverInternalError.class).when(session).close();
doThrow(DriverInternalError.class).when(cluster).close();
sink.stop();
verify(session).isClosed();
verify(session).close();
verifyNoMoreInteractions(session);
verify(cluster).isClosed();
verify(cluster).close();
verifyNoMoreInteractions(cluster);
}
@Test
public void thatParseWorksOnIgnoreCase() throws EventDeliveryException {
final CassandraSink sink = new CassandraSink();
final Channel channel = mock(Channel.class);
final Transaction tx = mock(Transaction.class);
final Session session = mock(Session.class);
final ConsistencyLevel consistencyLevel = ConsistencyLevel.QUORUM;
String bodyColumn = null;
TableMetadata tableMetadata = mock(TableMetadata.class);
// we want to test the method CassandraTable.parse so...we don't mock it
final CassandraTable tableWithoutIgnoreCase = new CassandraTable(session, tableMetadata, consistencyLevel,
bodyColumn);
// create a context without ignore case
final Context ctx = new Context();
ctx.put("tables", "keyspace.table");
sink.configure(ctx);
sink.tables = Collections.singletonList(tableWithoutIgnoreCase);
sink.setChannel(channel);
when(channel.getTransaction()).thenReturn(tx);
// mock table metadata
mockTableMetadataWithIdAndNameColumns(tableMetadata);
// put event names in upper case
final Event event = EventBuilder.withBody(new byte[0], ImmutableMap.of("ID", "1", "NAME", "text"));
// parsed result should be empty
final Map<String, Object> parsedResult = tableWithoutIgnoreCase.parse(event);
assertThat(parsedResult).isEmpty();
// now with ignore case --> should be some results
final CassandraTable tableWitIgnoreCase = new CassandraTable(session, tableMetadata, consistencyLevel,
bodyColumn, true);
ctx.put("ignoreCase", "true");
final Map<String, Object> parsedResultIgnoreCase = tableWitIgnoreCase.parse(event);
assertThat(parsedResultIgnoreCase).isNotEmpty();
assertThat(parsedResultIgnoreCase.get("id")).isNotNull();
}
private void mockTableMetadataWithIdAndNameColumns(TableMetadata tableMetadata) {
ColumnMetadata colId = mock(ColumnMetadata.class);
when(colId.getName()).thenReturn("id");
when(colId.getType()).thenReturn(DataType.text());
ColumnMetadata colName = mock(ColumnMetadata.class);
when(colName.getName()).thenReturn("name");
when(colName.getType()).thenReturn(DataType.text());
List<ColumnMetadata> listOfColumns = ImmutableList.of(colId, colName);
when(tableMetadata.getColumns()).thenReturn(listOfColumns);
}
static class CauseMatcher extends BaseMatcher<Throwable> {
final Class<?> cause;
public CauseMatcher(final Class<?> cause) {
this.cause = cause;
}
@Override
public boolean matches(Object item) {
return item != null && cause.equals(item.getClass());
}
@Override
public void describeTo(Description description) {
}
}
}