blob: bbc4d0578775e3ddad6be5457d7dac5d240e5e1a [file] [log] [blame]
// Copyright (C) 2014 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.ericsson.gerrit.plugins.eventslog.sql;
import static com.ericsson.gerrit.plugins.eventslog.sql.SQLTable.TABLE_NAME;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.ericsson.gerrit.plugins.eventslog.EventsLogConfig;
import com.ericsson.gerrit.plugins.eventslog.MalformedQueryException;
import com.ericsson.gerrit.plugins.eventslog.ServiceUnavailableException;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.Project.NameKey;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.events.ProjectEvent;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gson.Gson;
import com.google.inject.Provider;
import com.zaxxer.hikari.HikariConfig;
import java.io.IOException;
import java.net.ConnectException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@RunWith(MockitoJUnitRunner.class)
public class SQLStoreTest {
private static final Logger log = LoggerFactory.getLogger(SQLStoreTest.class);
private static final String TEST_URL = "jdbc:h2:mem:" + TABLE_NAME;
private static final String TEST_LOCAL_URL = "jdbc:h2:mem:test";
private static final String TEST_OPTIONS = "DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false";
private static final String TERM_CONN_MSG = "terminating connection";
private static final String MSG = "message";
private static final String GENERIC_QUERY = "SELECT * FROM " + TABLE_NAME;
private static final boolean PROJECT_VISIBLE_TO_USER = true;
private static final boolean PROJECT_NOT_VISIBLE_TO_USER = false;
@Mock private ProjectControl.GenericFactory pcFactoryMock;
@Mock private Provider<CurrentUser> userProviderMock;
@Mock private EventsLogConfig cfgMock;
@Mock private EventsLogCleaner logCleanerMock;
private SQLClient eventsDb;
private SQLClient localEventsDb;
private SQLStore store;
private HikariConfig config;
private ScheduledThreadPoolExecutor poolMock;
private Statement stat;
@Rule public TemporaryFolder testFolder = new TemporaryFolder();
@Before
public void setUp() throws SQLException {
config = new HikariConfig();
config.addDataSourceProperty("DB_CLOSE_DELAY", "-1");
config.addDataSourceProperty("DATABASE_TO_UPPER", "false");
Connection conn = DriverManager.getConnection(TEST_URL + ";" + TEST_OPTIONS);
stat = conn.createStatement();
poolMock = new PoolMock();
when(cfgMock.getMaxAge()).thenReturn(5);
when(cfgMock.getLocalStorePath()).thenReturn(testFolder.getRoot().toPath());
}
private void tearDown() throws Exception {
stat.execute("DROP TABLE " + TABLE_NAME);
store.stop();
}
private void setUpClient() {
config.setJdbcUrl(TEST_URL);
eventsDb = new SQLClient(config);
config.setJdbcUrl(TEST_LOCAL_URL);
localEventsDb = new SQLClient(config);
store =
new SQLStore(
pcFactoryMock,
userProviderMock,
cfgMock,
eventsDb,
localEventsDb,
poolMock,
logCleanerMock);
store.start();
}
private void setUpClientMock() throws SQLException {
eventsDb = mock(SQLClient.class);
localEventsDb = mock(SQLClient.class);
when(localEventsDb.dbExists()).thenReturn(true);
}
@Test
public void storeThenQueryVisible() throws Exception {
MockEvent mockEvent = setUpMocks(PROJECT_VISIBLE_TO_USER);
store.storeEvent(mockEvent);
List<String> events = store.queryChangeEvents(GENERIC_QUERY);
String json = new Gson().toJson(mockEvent);
assertThat(events).containsExactly(json).inOrder();
tearDown();
}
@Test
public void storeThenQueryNotVisible() throws Exception {
MockEvent mockEvent = setUpMocks(PROJECT_NOT_VISIBLE_TO_USER);
store.storeEvent(mockEvent);
List<String> events = store.queryChangeEvents(GENERIC_QUERY);
assertThat(events).isEmpty();
tearDown();
}
private MockEvent setUpMocks(boolean isVisible) throws NoSuchProjectException, IOException {
MockEvent mockEvent = new MockEvent();
ProjectControl pcMock = mock(ProjectControl.class);
CurrentUser userMock = mock(CurrentUser.class);
when(userProviderMock.get()).thenReturn(userMock);
when(pcFactoryMock.controlFor(mockEvent.getProjectNameKey(), userMock)).thenReturn(pcMock);
when(pcMock.isVisible()).thenReturn(isVisible);
setUpClient();
return mockEvent;
}
@Test(expected = MalformedQueryException.class)
public void throwBadRequestTriggerOnBadQuery() throws Exception {
setUpClient();
String badQuery = "bad query";
store.queryChangeEvents(badQuery);
}
@Test
public void notReturnEventOfNonExistingProject() throws Exception {
MockEvent mockEvent = new MockEvent();
Project.NameKey projectMock = mock(Project.NameKey.class);
CurrentUser userMock = mock(CurrentUser.class);
when(userProviderMock.get()).thenReturn(userMock);
NameKey projectNameKey = mockEvent.getProjectNameKey();
doThrow(new NoSuchProjectException(projectMock))
.when(pcFactoryMock)
.controlFor(projectNameKey, userMock);
setUpClient();
store.storeEvent(mockEvent);
List<String> events = store.queryChangeEvents(GENERIC_QUERY);
assertThat(events).isEmpty();
verify(logCleanerMock).removeProjectEventsAsync(mockEvent.project);
tearDown();
}
@Test
public void notReturnEventWithNoVisibilityInfo() throws Exception {
MockEvent mockEvent = new MockEvent();
CurrentUser userMock = mock(CurrentUser.class);
when(userProviderMock.get()).thenReturn(userMock);
NameKey projectNameKey = mockEvent.getProjectNameKey();
doThrow(new IOException()).when(pcFactoryMock).controlFor(projectNameKey, userMock);
setUpClient();
store.storeEvent(mockEvent);
List<String> events = store.queryChangeEvents(GENERIC_QUERY);
assertThat(events).isEmpty();
tearDown();
}
@Test
public void retryOnConnectException() throws Exception {
MockEvent mockEvent = new MockEvent();
when(cfgMock.getMaxTries()).thenReturn(3);
Throwable[] exceptions = new Throwable[3];
Arrays.fill(exceptions, new SQLException(new ConnectException()));
setUpClientMock();
doThrow(exceptions).doNothing().when(eventsDb).storeEvent(mockEvent);
doThrow(exceptions).doNothing().when(eventsDb).queryOne();
store =
new SQLStore(
pcFactoryMock,
userProviderMock,
cfgMock,
eventsDb,
localEventsDb,
poolMock,
logCleanerMock);
store.start();
store.storeEvent(mockEvent);
verify(eventsDb, times(3)).storeEvent(mockEvent);
verify(localEventsDb).storeEvent(mockEvent);
}
@Test
public void retryOnMessage() throws Exception {
MockEvent mockEvent = new MockEvent();
when(cfgMock.getMaxTries()).thenReturn(3);
Throwable[] exceptions = new Throwable[3];
Arrays.fill(exceptions, new SQLException(TERM_CONN_MSG));
setUpClientMock();
doThrow(exceptions).doNothing().when(eventsDb).storeEvent(mockEvent);
doThrow(exceptions).doNothing().when(eventsDb).queryOne();
store =
new SQLStore(
pcFactoryMock,
userProviderMock,
cfgMock,
eventsDb,
localEventsDb,
poolMock,
logCleanerMock);
store.start();
store.storeEvent(mockEvent);
verify(eventsDb, times(3)).storeEvent(mockEvent);
verify(localEventsDb).storeEvent(mockEvent);
}
@Test
public void noRetryOnMessage() throws Exception {
MockEvent mockEvent = new MockEvent();
when(cfgMock.getMaxTries()).thenReturn(3);
setUpClientMock();
doThrow(new SQLException(MSG)).when(eventsDb).storeEvent(mockEvent);
store =
new SQLStore(
pcFactoryMock,
userProviderMock,
cfgMock,
eventsDb,
localEventsDb,
poolMock,
logCleanerMock);
store.start();
store.storeEvent(mockEvent);
verify(eventsDb, times(1)).storeEvent(mockEvent);
}
@Test
public void noRetryOnZeroMaxTries() throws Exception {
MockEvent mockEvent = new MockEvent();
when(cfgMock.getMaxTries()).thenReturn(0);
Throwable[] exceptions = new Throwable[3];
Arrays.fill(exceptions, new SQLException(new ConnectException()));
setUpClientMock();
doThrow(exceptions).doNothing().when(eventsDb).storeEvent(mockEvent);
doThrow(exceptions).doNothing().when(eventsDb).queryOne();
store =
new SQLStore(
pcFactoryMock,
userProviderMock,
cfgMock,
eventsDb,
localEventsDb,
poolMock,
logCleanerMock);
store.start();
store.storeEvent(mockEvent);
verify(eventsDb, times(1)).storeEvent(mockEvent);
}
@Test(expected = ServiceUnavailableException.class)
public void throwSQLExceptionIfNotOnline() throws Exception {
MockEvent mockEvent = new MockEvent();
setUpClientMock();
doThrow(new SQLException(new ConnectException())).when(eventsDb).createDBIfNotCreated();
doThrow(new SQLException()).when(eventsDb).queryOne();
store =
new SQLStore(
pcFactoryMock,
userProviderMock,
cfgMock,
eventsDb,
localEventsDb,
poolMock,
logCleanerMock);
store.start();
store.storeEvent(mockEvent);
store.queryChangeEvents(GENERIC_QUERY);
}
@Test
public void restoreFromLocalAndRemoveUnfoundProjectEvents() throws Exception {
MockEvent mockEvent = new MockEvent();
MockEvent mockEvent2 = new MockEvent("proj");
MockEvent mockEvent3 = new MockEvent("unfound");
ProjectControl pc = mock(ProjectControl.class);
NoSuchProjectException e = mock(NoSuchProjectException.class);
CurrentUser userMock = mock(CurrentUser.class);
when(userProviderMock.get()).thenReturn(userMock);
when(pcFactoryMock.controlFor((mockEvent.getProjectNameKey()), userMock)).thenReturn(pc);
when(pcFactoryMock.controlFor((mockEvent2.getProjectNameKey()), userMock)).thenReturn(pc);
when(pc.isVisible()).thenReturn(true);
doThrow(e).when(pcFactoryMock).controlFor((mockEvent3.getProjectNameKey()), userMock);
config.setJdbcUrl(TEST_URL);
eventsDb = new SQLClient(config);
config.setJdbcUrl(TEST_LOCAL_URL);
localEventsDb = new SQLClient(config);
store =
new SQLStore(
pcFactoryMock,
userProviderMock,
cfgMock,
eventsDb,
localEventsDb,
poolMock,
logCleanerMock);
localEventsDb.createDBIfNotCreated();
localEventsDb.storeEvent(mockEvent);
localEventsDb.storeEvent(mockEvent2);
localEventsDb.storeEvent(mockEvent3);
store.start();
List<String> events = store.queryChangeEvents(GENERIC_QUERY);
Gson gson = new Gson();
String json = gson.toJson(mockEvent);
String json2 = gson.toJson(mockEvent2);
assertThat(events).containsExactly(json, json2).inOrder();
tearDown();
}
@Test
public void offlineUponStart() throws Exception {
setUpClientMock();
doThrow(new SQLException(new ConnectException())).when(eventsDb).createDBIfNotCreated();
doThrow(new SQLException()).when(eventsDb).queryOne();
store =
new SQLStore(
pcFactoryMock,
userProviderMock,
cfgMock,
eventsDb,
localEventsDb,
poolMock,
logCleanerMock);
store.start();
verify(localEventsDb).createDBIfNotCreated();
}
@Test
public void storeLocalOffline() throws Exception {
MockEvent mockEvent = new MockEvent();
setUpClientMock();
doThrow(new SQLException(new ConnectException())).when(eventsDb).createDBIfNotCreated();
doThrow(new SQLException()).when(eventsDb).queryOne();
store =
new SQLStore(
pcFactoryMock,
userProviderMock,
cfgMock,
eventsDb,
localEventsDb,
poolMock,
logCleanerMock);
store.start();
store.storeEvent(mockEvent);
verify(localEventsDb).storeEvent(mockEvent);
}
@Test
public void storeLocalOfflineAfterNoRetry() throws Exception {
MockEvent mockEvent = new MockEvent();
setUpClientMock();
when(cfgMock.getMaxTries()).thenReturn(0);
doThrow(new SQLException(new ConnectException())).when(eventsDb).createDBIfNotCreated();
doThrow(new SQLException()).when(eventsDb).queryOne();
store =
new SQLStore(
pcFactoryMock,
userProviderMock,
cfgMock,
eventsDb,
localEventsDb,
poolMock,
logCleanerMock);
store.start();
store.storeEvent(mockEvent);
verify(localEventsDb).storeEvent(mockEvent);
}
/**
* For this test we expect that if we can connect to main database, then we should come back
* online and try setting up again. We just want to make sure that restoreEventsFromLocal gets
* called, so verifying that getLocalDBFile is called is sufficient.
*/
@Test
public void testConnectionTask() throws Exception {
config.setJdbcUrl(TEST_URL);
eventsDb = new SQLClient(config);
localEventsDb = mock(SQLClient.class);
when(localEventsDb.dbExists()).thenReturn(true);
when(localEventsDb.getAll()).thenReturn(ImmutableList.of(mock(SQLEntry.class)));
store =
new SQLStore(
pcFactoryMock,
userProviderMock,
cfgMock,
eventsDb,
localEventsDb,
poolMock,
logCleanerMock);
store.start();
poolMock.scheduleWithFixedDelay(store.new CheckConnectionTask(), 0, 0, TimeUnit.MILLISECONDS);
verify(localEventsDb, times(2)).removeOldEvents(0);
}
@Test
public void checkConnectionAndRestoreCopyLocal() throws Exception {
checkConnectionAndRestore(true);
}
@Test
public void checkConnectionAndRestoreNoCopyLocal() throws Exception {
checkConnectionAndRestore(false);
}
private void checkConnectionAndRestore(boolean copy) throws Exception {
MockEvent mockEvent = new MockEvent();
eventsDb = mock(SQLClient.class);
config.setJdbcUrl(TEST_LOCAL_URL);
localEventsDb = new SQLClient(config);
localEventsDb.createDBIfNotCreated();
localEventsDb.storeEvent(mockEvent);
doThrow(new SQLException(new ConnectException()))
.doNothing()
.when(eventsDb)
.createDBIfNotCreated();
if (copy) {
when(cfgMock.getCopyLocal()).thenReturn(true);
}
store =
new SQLStore(
pcFactoryMock,
userProviderMock,
cfgMock,
eventsDb,
localEventsDb,
poolMock,
logCleanerMock);
store.start();
verify(eventsDb).queryOne();
verify(eventsDb).storeEvent(any(String.class), any(Timestamp.class), any(String.class));
List<SQLEntry> entries = localEventsDb.getAll();
assertThat(entries).isEmpty();
}
public class MockEvent extends ProjectEvent {
public String project = "mock project";
MockEvent() {
super("mock event");
}
MockEvent(String project) {
this();
this.project = project;
}
@Override
public Project.NameKey getProjectNameKey() {
return new Project.NameKey(project);
}
}
class PoolMock extends ScheduledThreadPoolExecutor {
PoolMock() {
super(1);
}
@Override
public ScheduledFuture<?> scheduleWithFixedDelay(
Runnable command, long initialDelay, long delay, TimeUnit unit) {
log.info(command.toString());
command.run();
return null;
}
}
}