/** * 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) { } } }