From 47708d489d2a393e68093afc5e94ebdb62986db1 Mon Sep 17 00:00:00 2001
From: akwizgran <akwizgran@users.sourceforge.net>
Date: Tue, 10 Dec 2013 22:23:37 +0000
Subject: [PATCH] Added the ability to remove pseudonyms from the database.

---
 .../sf/briar/api/db/DatabaseComponent.java    |  15 +-
 .../api/db/LocalAuthorExistsException.java    |  10 ++
 .../api/db/NoSuchLocalAuthorException.java    |  11 ++
 .../api/db/event/LocalAuthorAddedEvent.java   |  17 +++
 .../api/db/event/LocalAuthorRemovedEvent.java |  17 +++
 briar-core/src/net/sf/briar/db/Database.java  |  40 ++++-
 .../sf/briar/db/DatabaseComponentImpl.java    | 142 +++++++++++++++---
 .../src/net/sf/briar/db/JdbcDatabase.java     |  66 +++++++-
 .../sf/briar/db/DatabaseComponentTest.java    |  66 +++++++-
 .../src/net/sf/briar/db/H2DatabaseTest.java   |  25 +++
 10 files changed, 368 insertions(+), 41 deletions(-)
 create mode 100644 briar-api/src/net/sf/briar/api/db/LocalAuthorExistsException.java
 create mode 100644 briar-api/src/net/sf/briar/api/db/NoSuchLocalAuthorException.java
 create mode 100644 briar-api/src/net/sf/briar/api/db/event/LocalAuthorAddedEvent.java
 create mode 100644 briar-api/src/net/sf/briar/api/db/event/LocalAuthorRemovedEvent.java

diff --git a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java
index 312dfd18ec..4e438d2781 100644
--- a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java
+++ b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java
@@ -56,7 +56,7 @@ public interface DatabaseComponent {
 	/** Stores an endpoint. */
 	void addEndpoint(Endpoint ep) throws DbException;
 
-	/** Stores a pseudonym that the user can use to sign messages. */
+	/** Stores a local pseudonym. */
 	void addLocalAuthor(LocalAuthor a) throws DbException;
 
 	/** Stores a locally generated group message. */
@@ -179,10 +179,10 @@ public interface DatabaseComponent {
 	 */
 	Map<ContactId, Long> getLastConnected() throws DbException;
 
-	/** Returns the pseudonym with the given ID. */
+	/** Returns the local pseudonym with the given ID. */
 	LocalAuthor getLocalAuthor(AuthorId a) throws DbException;
 
-	/** Returns all pseudonyms that the user can use to sign messages. */
+	/** Returns all local pseudonyms. */
 	Collection<LocalAuthor> getLocalAuthors() throws DbException;
 
 	/** Returns the local transport properties for all transports. */
@@ -295,6 +295,11 @@ public interface DatabaseComponent {
 	/** Removes a contact (and all associated state) from the database. */
 	void removeContact(ContactId c) throws DbException;
 
+	/**
+	 * Removes a local pseudonym (and all associated state) from the database.
+	 */
+	void removeLocalAuthor(AuthorId a) throws DbException;
+
 	/**
 	 * Removes a transport (and any associated configuration and local
 	 * properties) from the database.
@@ -302,8 +307,8 @@ public interface DatabaseComponent {
 	void removeTransport(TransportId t) throws DbException;
 
 	/**
-	 * Sets the connection reordering window for the given endoint in the given
-	 * rotation period.
+	 * Sets the connection reordering window for the given endpoint in the
+	 * given rotation period.
 	 */
 	void setConnectionWindow(ContactId c, TransportId t, long period,
 			long centre, byte[] bitmap) throws DbException;
diff --git a/briar-api/src/net/sf/briar/api/db/LocalAuthorExistsException.java b/briar-api/src/net/sf/briar/api/db/LocalAuthorExistsException.java
new file mode 100644
index 0000000000..52425d7e43
--- /dev/null
+++ b/briar-api/src/net/sf/briar/api/db/LocalAuthorExistsException.java
@@ -0,0 +1,10 @@
+package net.sf.briar.api.db;
+
+/**
+ * Thrown when a duplicate pseudonym is added to the database. This exception
+ * may occur due to concurrent updates and does not indicate a database error.
+ */
+public class LocalAuthorExistsException extends DbException {
+
+	private static final long serialVersionUID = -1483877298070151673L;
+}
diff --git a/briar-api/src/net/sf/briar/api/db/NoSuchLocalAuthorException.java b/briar-api/src/net/sf/briar/api/db/NoSuchLocalAuthorException.java
new file mode 100644
index 0000000000..d091fdaeb4
--- /dev/null
+++ b/briar-api/src/net/sf/briar/api/db/NoSuchLocalAuthorException.java
@@ -0,0 +1,11 @@
+package net.sf.briar.api.db;
+
+/**
+ * Thrown when a database operation is attempted for a pseudonym that is not in
+ * the database. This exception may occur due to concurrent updates and does
+ * not indicate a database error.
+ */
+public class NoSuchLocalAuthorException extends DbException {
+
+	private static final long serialVersionUID = 494398665376703860L;
+}
diff --git a/briar-api/src/net/sf/briar/api/db/event/LocalAuthorAddedEvent.java b/briar-api/src/net/sf/briar/api/db/event/LocalAuthorAddedEvent.java
new file mode 100644
index 0000000000..4b72402fa3
--- /dev/null
+++ b/briar-api/src/net/sf/briar/api/db/event/LocalAuthorAddedEvent.java
@@ -0,0 +1,17 @@
+package net.sf.briar.api.db.event;
+
+import net.sf.briar.api.AuthorId;
+
+/** An event that is broadcast when a pseudonym for the user is added. */
+public class LocalAuthorAddedEvent extends DatabaseEvent {
+
+	private final AuthorId authorId;
+
+	public LocalAuthorAddedEvent(AuthorId authorId) {
+		this.authorId = authorId;
+	}
+
+	public AuthorId getAuthorId() {
+		return authorId;
+	}
+}
diff --git a/briar-api/src/net/sf/briar/api/db/event/LocalAuthorRemovedEvent.java b/briar-api/src/net/sf/briar/api/db/event/LocalAuthorRemovedEvent.java
new file mode 100644
index 0000000000..0b5bd8849e
--- /dev/null
+++ b/briar-api/src/net/sf/briar/api/db/event/LocalAuthorRemovedEvent.java
@@ -0,0 +1,17 @@
+package net.sf.briar.api.db.event;
+
+import net.sf.briar.api.AuthorId;
+
+/** An event that is broadcast when a pseudonym for the user is removed. */
+public class LocalAuthorRemovedEvent extends DatabaseEvent {
+
+	private final AuthorId authorId;
+
+	public LocalAuthorRemovedEvent(AuthorId authorId) {
+		this.authorId = authorId;
+	}
+
+	public AuthorId getAuthorId() {
+		return authorId;
+	}
+}
diff --git a/briar-core/src/net/sf/briar/db/Database.java b/briar-core/src/net/sf/briar/db/Database.java
index 9df90f43f4..1a10cc82db 100644
--- a/briar-core/src/net/sf/briar/db/Database.java
+++ b/briar-core/src/net/sf/briar/db/Database.java
@@ -49,6 +49,8 @@ import net.sf.briar.api.transport.TemporarySecret;
  * <li> transport
  * <li> window
  * </ul>
+ * If table A has a foreign key pointing to table B, we get a read lock on A to
+ * read A, a write lock on A to write A, and write locks on A and B to write B.
  */
 interface Database<T> {
 
@@ -80,8 +82,8 @@ interface Database<T> {
 	 * Stores a contact with the given pseudonym, associated with the given
 	 * local pseudonym, and returns an ID for the contact.
 	 * <p>
-	 * Locking: contact write, retention write, subscription write, transport
-	 * write, window write.
+	 * Locking: contact write, message write, retention write,
+	 * subscription write, transport write, window write.
 	 */
 	ContactId addContact(T txn, Author remote, AuthorId local)
 			throws DbException;
@@ -103,9 +105,10 @@ interface Database<T> {
 			throws DbException;
 
 	/**
-	 * Stores a pseudonym that the user can use to sign messages.
+	 * Stores a local pseudonym.
 	 * <p>
-	 * Locking: contact write, identity write.
+	 * Locking: contact write, identity write, message write, retention write,
+	 * subscription write, transport write, window write.
 	 */
 	void addLocalAuthor(T txn, LocalAuthor a) throws DbException;
 
@@ -181,6 +184,13 @@ interface Database<T> {
 	 */
 	boolean containsContact(T txn, ContactId c) throws DbException;
 
+	/**
+	 * Returns true if the database contains the given local pseudonym.
+	 * <p>
+	 * Locking: identity read.
+	 */
+	boolean containsLocalAuthor(T txn, AuthorId a) throws DbException;
+
 	/**
 	 * Returns true if the database contains the given message.
 	 * <p>
@@ -246,6 +256,13 @@ interface Database<T> {
 	 */
 	Collection<Contact> getContacts(T txn) throws DbException;
 
+	/**
+	 * Returns all contacts associated with the given local pseudonym.
+	 * <p>
+	 * Locking: contact read.
+	 */
+	Collection<ContactId> getContacts(T txn, AuthorId a) throws DbException;
+
 	/**
 	 * Returns all endpoints.
 	 * <p>
@@ -291,14 +308,14 @@ interface Database<T> {
 	Map<ContactId, Long> getLastConnected(T txn) throws DbException;
 
 	/**
-	 * Returns the pseudonym with the given ID.
+	 * Returns the local pseudonym with the given ID.
 	 * <p>
-	 * Locking: identitiy read.
+	 * Locking: identity read.
 	 */
 	LocalAuthor getLocalAuthor(T txn, AuthorId a) throws DbException;
 
 	/**
-	 * Returns all pseudonyms that the user can use to sign messages.
+	 * Returns all local pseudonyms.
 	 * <p>
 	 * Locking: identity read.
 	 */
@@ -566,6 +583,15 @@ interface Database<T> {
 	 */
 	void removeContact(T txn, ContactId c) throws DbException;
 
+	/**
+	 * Removes the local pseudonym with the given ID (and all associated
+	 * state) from the database.
+	 * <p>
+	 * Locking: contact write, identity write, message write, retention write,
+	 * subscription write, transport write, window write.
+	 */
+	void removeLocalAuthor(T txn, AuthorId a) throws DbException;
+
 	/**
 	 * Removes a message (and all associated state) from the database.
 	 * <p>
diff --git a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
index df375151f6..d7c45f52a4 100644
--- a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
+++ b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
@@ -38,7 +38,9 @@ import net.sf.briar.api.db.ContactExistsException;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.db.GroupMessageHeader;
+import net.sf.briar.api.db.LocalAuthorExistsException;
 import net.sf.briar.api.db.NoSuchContactException;
+import net.sf.briar.api.db.NoSuchLocalAuthorException;
 import net.sf.briar.api.db.NoSuchMessageException;
 import net.sf.briar.api.db.NoSuchSubscriptionException;
 import net.sf.briar.api.db.NoSuchTransportException;
@@ -48,6 +50,8 @@ import net.sf.briar.api.db.event.ContactRemovedEvent;
 import net.sf.briar.api.db.event.DatabaseEvent;
 import net.sf.briar.api.db.event.DatabaseListener;
 import net.sf.briar.api.db.event.GroupMessageAddedEvent;
+import net.sf.briar.api.db.event.LocalAuthorAddedEvent;
+import net.sf.briar.api.db.event.LocalAuthorRemovedEvent;
 import net.sf.briar.api.db.event.LocalSubscriptionsUpdatedEvent;
 import net.sf.briar.api.db.event.LocalTransportsUpdatedEvent;
 import net.sf.briar.api.db.event.MessageExpiredEvent;
@@ -186,35 +190,47 @@ DatabaseCleaner.Callback {
 		ContactId c;
 		contactLock.writeLock().lock();
 		try {
-			retentionLock.writeLock().lock();
+			identityLock.readLock().lock();
 			try {
-				subscriptionLock.writeLock().lock();
+				messageLock.writeLock().lock();
 				try {
-					transportLock.writeLock().lock();
+					retentionLock.writeLock().lock();
 					try {
-						windowLock.writeLock().lock();
+						subscriptionLock.writeLock().lock();
 						try {
-							T txn = db.startTransaction();
+							transportLock.writeLock().lock();
 							try {
-								if(db.containsContact(txn, remote.getId()))
-									throw new ContactExistsException();
-								c = db.addContact(txn, remote, local);
-								db.commitTransaction(txn);
-							} catch(DbException e) {
-								db.abortTransaction(txn);
-								throw e;
+								windowLock.writeLock().lock();
+								try {
+									T txn = db.startTransaction();
+									try {
+										if(db.containsContact(txn, remote.getId()))
+											throw new ContactExistsException();
+										if(!db.containsLocalAuthor(txn, local))
+											throw new NoSuchLocalAuthorException();
+										c = db.addContact(txn, remote, local);
+										db.commitTransaction(txn);
+									} catch(DbException e) {
+										db.abortTransaction(txn);
+										throw e;
+									}
+								} finally {
+									windowLock.writeLock().unlock();
+								}
+							} finally {
+								transportLock.writeLock().unlock();
 							}
 						} finally {
-							windowLock.writeLock().unlock();
+							subscriptionLock.writeLock().unlock();
 						}
 					} finally {
-						transportLock.writeLock().unlock();
+						retentionLock.writeLock().unlock();
 					}
 				} finally {
-					subscriptionLock.writeLock().unlock();
+					messageLock.writeLock().unlock();
 				}
 			} finally {
-				retentionLock.writeLock().unlock();
+				identityLock.readLock().unlock();
 			}
 		} finally {
 			contactLock.writeLock().unlock();
@@ -263,13 +279,40 @@ DatabaseCleaner.Callback {
 		try {
 			identityLock.writeLock().lock();
 			try {
-				T txn = db.startTransaction();
+				messageLock.writeLock().lock();
 				try {
-					db.addLocalAuthor(txn, a);
-					db.commitTransaction(txn);
-				} catch(DbException e) {
-					db.abortTransaction(txn);
-					throw e;
+					retentionLock.writeLock().lock();
+					try {
+						subscriptionLock.writeLock().lock();
+						try {
+							transportLock.writeLock().lock();
+							try {
+								windowLock.writeLock().lock();
+								try {
+									T txn = db.startTransaction();
+									try {
+										if(db.containsLocalAuthor(txn, a.getId()))
+											throw new LocalAuthorExistsException();
+										db.addLocalAuthor(txn, a);
+										db.commitTransaction(txn);
+									} catch(DbException e) {
+										db.abortTransaction(txn);
+										throw e;
+									}
+								} finally {
+									windowLock.writeLock().unlock();
+								}
+							} finally {
+								transportLock.writeLock().unlock();
+							}
+						} finally {
+							subscriptionLock.writeLock().unlock();
+						}
+					} finally {
+						retentionLock.writeLock().unlock();
+					}
+				} finally {
+					messageLock.writeLock().unlock();
 				}
 			} finally {
 				identityLock.writeLock().unlock();
@@ -277,6 +320,7 @@ DatabaseCleaner.Callback {
 		} finally {
 			contactLock.writeLock().unlock();
 		}
+		callListeners(new LocalAuthorAddedEvent(a.getId()));
 	}
 
 	public void addLocalGroupMessage(Message m) throws DbException {
@@ -942,6 +986,8 @@ DatabaseCleaner.Callback {
 		try {
 			T txn = db.startTransaction();
 			try {
+				if(!db.containsLocalAuthor(txn, a))
+					throw new NoSuchLocalAuthorException();
 				LocalAuthor localAuthor = db.getLocalAuthor(txn, a);
 				db.commitTransaction(txn);
 				return localAuthor;
@@ -1643,6 +1689,58 @@ DatabaseCleaner.Callback {
 		callListeners(new ContactRemovedEvent(c));
 	}
 
+	public void removeLocalAuthor(AuthorId a) throws DbException {
+		Collection<ContactId> affected;
+		contactLock.writeLock().lock();
+		try {
+			identityLock.writeLock().lock();
+			try {
+				messageLock.writeLock().lock();
+				try {
+					retentionLock.writeLock().lock();
+					try {
+						subscriptionLock.writeLock().lock();
+						try {
+							transportLock.writeLock().lock();
+							try {
+								windowLock.writeLock().lock();
+								try {
+									T txn = db.startTransaction();
+									try {
+										if(!db.containsLocalAuthor(txn, a))
+											throw new NoSuchLocalAuthorException();
+										affected = db.getContacts(txn, a);
+										db.removeLocalAuthor(txn, a);
+										db.commitTransaction(txn);
+									} catch(DbException e) {
+										db.abortTransaction(txn);
+										throw e;
+									}
+								} finally {
+									windowLock.writeLock().unlock();
+								}
+							} finally {
+								transportLock.writeLock().unlock();
+							}
+						} finally {
+							subscriptionLock.writeLock().unlock();
+						}
+					} finally {
+						retentionLock.writeLock().unlock();
+					}
+				} finally {
+					messageLock.writeLock().unlock();
+				}
+			} finally {
+				identityLock.writeLock().unlock();
+			}
+		} finally {
+			contactLock.writeLock().unlock();
+		}
+		for(ContactId c : affected) callListeners(new ContactRemovedEvent(c));
+		callListeners(new LocalAuthorRemovedEvent(a));
+	}
+
 	public void removeTransport(TransportId t) throws DbException {
 		transportLock.writeLock().lock();
 		try {
diff --git a/briar-core/src/net/sf/briar/db/JdbcDatabase.java b/briar-core/src/net/sf/briar/db/JdbcDatabase.java
index f7de855305..2c432b5c66 100644
--- a/briar-core/src/net/sf/briar/db/JdbcDatabase.java
+++ b/briar-core/src/net/sf/briar/db/JdbcDatabase.java
@@ -60,6 +60,7 @@ import net.sf.briar.api.transport.TemporarySecret;
 abstract class JdbcDatabase implements Database<Connection> {
 
 	// Locking: identity
+	// Dependents: contact, message, retention, subscription, transport, window
 	private static final String CREATE_LOCAL_AUTHORS =
 			"CREATE TABLE localAuthors"
 					+ " (authorId HASH NOT NULL,"
@@ -81,10 +82,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " UNIQUE (authorId),"
 					+ " FOREIGN KEY (localAuthorId)"
 					+ " REFERENCES localAuthors (authorId)"
-					+ " ON DELETE RESTRICT)"; // Deletion not allowed
-
-	private static final String INDEX_CONTACTS_BY_AUTHOR =
-			"CREATE INDEX contactsByAuthor ON contacts (authorId)";
+					+ " ON DELETE CASCADE)";
 
 	// Locking: subscription
 	// Dependents: message
@@ -376,7 +374,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 			s = txn.createStatement();
 			s.executeUpdate(insertTypeNames(CREATE_LOCAL_AUTHORS));
 			s.executeUpdate(insertTypeNames(CREATE_CONTACTS));
-			s.executeUpdate(INDEX_CONTACTS_BY_AUTHOR);
 			s.executeUpdate(insertTypeNames(CREATE_GROUPS));
 			s.executeUpdate(insertTypeNames(CREATE_GROUP_VISIBILITIES));
 			s.executeUpdate(insertTypeNames(CREATE_CONTACT_GROUPS));
@@ -1000,6 +997,27 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public boolean containsLocalAuthor(Connection txn, AuthorId a)
+			throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT NULL FROM localAuthors WHERE authorId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, a.getBytes());
+			rs = ps.executeQuery();
+			boolean found = rs.next();
+			if(rs.next()) throw new DbStateException();
+			rs.close();
+			ps.close();
+			return found;
+		} catch(SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public boolean containsMessage(Connection txn, MessageId m)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -1228,6 +1246,28 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public Collection<ContactId> getContacts(Connection txn, AuthorId a)
+			throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT contactId FROM contacts"
+					+ " WHERE localAuthorId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, a.getBytes());
+			rs = ps.executeQuery();
+			List<ContactId> ids = new ArrayList<ContactId>();
+			while(rs.next()) ids.add(new ContactId(rs.getInt(1)));
+			rs.close();
+			ps.close();
+			return Collections.unmodifiableList(ids);
+		} catch(SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public Collection<Endpoint> getEndpoints(Connection txn)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -2543,6 +2583,22 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public void removeLocalAuthor(Connection txn, AuthorId a)
+			throws DbException {
+		PreparedStatement ps = null;
+		try {
+			String sql = "DELETE FROM localAuthors WHERE authorId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, a.getBytes());
+			int affected = ps.executeUpdate();
+			if(affected != 1) throw new DbStateException();
+			ps.close();
+		} catch(SQLException e) {
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public void removeMessage(Connection txn, MessageId m) throws DbException {
 		PreparedStatement ps = null;
 		try {
diff --git a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
index 6fcac54935..cc6a37ab37 100644
--- a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
+++ b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
@@ -25,12 +25,15 @@ import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.db.AckAndRequest;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.NoSuchContactException;
+import net.sf.briar.api.db.NoSuchLocalAuthorException;
 import net.sf.briar.api.db.NoSuchSubscriptionException;
 import net.sf.briar.api.db.NoSuchTransportException;
 import net.sf.briar.api.db.event.ContactAddedEvent;
 import net.sf.briar.api.db.event.ContactRemovedEvent;
 import net.sf.briar.api.db.event.DatabaseListener;
 import net.sf.briar.api.db.event.GroupMessageAddedEvent;
+import net.sf.briar.api.db.event.LocalAuthorAddedEvent;
+import net.sf.briar.api.db.event.LocalAuthorRemovedEvent;
 import net.sf.briar.api.db.event.LocalSubscriptionsUpdatedEvent;
 import net.sf.briar.api.db.event.PrivateMessageAddedEvent;
 import net.sf.briar.api.db.event.SubscriptionAddedEvent;
@@ -122,9 +125,9 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
 		final DatabaseListener listener = context.mock(DatabaseListener.class);
 		context.checking(new Expectations() {{
-			exactly(10).of(database).startTransaction();
+			exactly(11).of(database).startTransaction();
 			will(returnValue(txn));
-			exactly(10).of(database).commitTransaction(txn);
+			exactly(11).of(database).commitTransaction(txn);
 			// open()
 			oneOf(database).open();
 			will(returnValue(false));
@@ -134,10 +137,16 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(shutdown).addShutdownHook(with(any(Runnable.class)));
 			will(returnValue(shutdownHandle));
 			// addLocalAuthor(localAuthor)
+			oneOf(database).containsLocalAuthor(txn, localAuthorId);
+			will(returnValue(false));
 			oneOf(database).addLocalAuthor(txn, localAuthor);
+			oneOf(listener).eventOccurred(with(any(
+					LocalAuthorAddedEvent.class)));
 			// addContact(author, localAuthorId)
 			oneOf(database).containsContact(txn, authorId);
 			will(returnValue(false));
+			oneOf(database).containsLocalAuthor(txn, localAuthorId);
+			will(returnValue(true));
 			oneOf(database).addContact(txn, author, localAuthorId);
 			will(returnValue(contactId));
 			oneOf(listener).eventOccurred(with(any(ContactAddedEvent.class)));
@@ -180,6 +189,14 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			will(returnValue(true));
 			oneOf(database).removeContact(txn, contactId);
 			oneOf(listener).eventOccurred(with(any(ContactRemovedEvent.class)));
+			// removeLocalAuthor(localAuthorId)
+			oneOf(database).containsLocalAuthor(txn, localAuthorId);
+			will(returnValue(true));
+			oneOf(database).getContacts(txn, localAuthorId);
+			will(returnValue(Collections.emptyList()));
+			oneOf(database).removeLocalAuthor(txn, localAuthorId);
+			oneOf(listener).eventOccurred(with(any(
+					LocalAuthorRemovedEvent.class)));
 			// close()
 			oneOf(shutdown).removeShutdownHook(shutdownHandle);
 			oneOf(cleaner).stopCleaning();
@@ -202,6 +219,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		assertEquals(Arrays.asList(groupId), db.getSubscriptions());
 		db.unsubscribe(group);
 		db.removeContact(contactId);
+		db.removeLocalAuthor(localAuthorId);
 		db.removeListener(listener);
 		db.close();
 
@@ -512,6 +530,46 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		context.assertIsSatisfied();
 	}
 
+	@Test
+	public void testVariousMethodsThrowExceptionIfLocalAuthorIsMissing()
+			throws Exception {
+		Mockery context = new Mockery();
+		@SuppressWarnings("unchecked")
+		final Database<Object> database = context.mock(Database.class);
+		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
+		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
+		context.checking(new Expectations() {{
+			// Check whether the pseudonym is in the DB (which it's not)
+			exactly(3).of(database).startTransaction();
+			will(returnValue(txn));
+			exactly(3).of(database).containsLocalAuthor(txn, localAuthorId);
+			will(returnValue(false));
+			exactly(3).of(database).abortTransaction(txn);
+			// This is needed for addContact() to proceed
+			exactly(1).of(database).containsContact(txn, authorId);
+			will(returnValue(false));
+		}});
+		DatabaseComponent db = createDatabaseComponent(database, cleaner,
+				shutdown);
+
+		try {
+			db.addContact(author, localAuthorId);
+			fail();
+		} catch(NoSuchLocalAuthorException expected) {}
+
+		try {
+			db.getLocalAuthor(localAuthorId);
+			fail();
+		} catch(NoSuchLocalAuthorException expected) {}
+
+		try {
+			db.removeLocalAuthor(localAuthorId);
+			fail();
+		} catch(NoSuchLocalAuthorException expected) {}
+
+		context.assertIsSatisfied();
+	}
+
 	@Test
 	public void testVariousMethodsThrowExceptionIfSubscriptionIsMissing()
 			throws Exception {
@@ -571,6 +629,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			// addLocalAuthor(localAuthor)
 			oneOf(database).startTransaction();
 			will(returnValue(txn));
+			oneOf(database).containsLocalAuthor(txn, localAuthorId);
+			will(returnValue(false));
 			oneOf(database).addLocalAuthor(txn, localAuthor);
 			oneOf(database).commitTransaction(txn);
 			// addContact(author, localAuthorId)
@@ -578,6 +638,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			will(returnValue(txn));
 			oneOf(database).containsContact(txn, authorId);
 			will(returnValue(false));
+			oneOf(database).containsLocalAuthor(txn, localAuthorId);
+			will(returnValue(true));
 			oneOf(database).addContact(txn, author, localAuthorId);
 			will(returnValue(contactId));
 			oneOf(database).commitTransaction(txn);
diff --git a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
index c6719eab26..82e9ef580b 100644
--- a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
+++ b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
@@ -1715,6 +1715,31 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.close();
 	}
 
+	@Test
+	public void testGetContactsByLocalAuthorId() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a local author - no contacts should be associated
+		db.addLocalAuthor(txn, localAuthor);
+		Collection<ContactId> contacts = db.getContacts(txn, localAuthorId);
+		assertEquals(Collections.emptyList(), contacts);
+
+		// Add a contact associated with the local author
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
+		contacts = db.getContacts(txn, localAuthorId);
+		assertEquals(Collections.singletonList(contactId), contacts);
+
+		// Remove the local author - the contact should be removed
+		db.removeLocalAuthor(txn, localAuthorId);
+		contacts = db.getContacts(txn, localAuthorId);
+		assertEquals(Collections.emptyList(), contacts);
+		assertFalse(db.containsContact(txn, contactId));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
 	@Test
 	public void testExceptionHandling() throws Exception {
 		Database<Connection> db = open(false);
-- 
GitLab