mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-11 18:29:05 +01:00
Implement AvatarManager with unit and integration tests
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
package org.briarproject.briar.api.avatar;
|
||||
|
||||
import org.briarproject.bramble.api.contact.Contact;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.ClientId;
|
||||
import org.briarproject.briar.api.media.Attachment;
|
||||
import org.briarproject.briar.api.media.AttachmentHeader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@NotNullByDefault
|
||||
public interface AvatarManager {
|
||||
|
||||
/**
|
||||
* The unique ID of the avatar client.
|
||||
*/
|
||||
ClientId CLIENT_ID = new ClientId("org.briarproject.briar.avatar");
|
||||
|
||||
/**
|
||||
* The current major version of the avatar client.
|
||||
*/
|
||||
int MAJOR_VERSION = 0;
|
||||
|
||||
/**
|
||||
* The current minor version of the avatar client.
|
||||
*/
|
||||
int MINOR_VERSION = 0;
|
||||
|
||||
/**
|
||||
* Store a new profile image represented by the given InputStream
|
||||
* and share it with all contacts.
|
||||
*/
|
||||
AttachmentHeader addAvatar(String contentType, InputStream in)
|
||||
throws DbException, IOException;
|
||||
|
||||
/**
|
||||
* Returns the current known profile image header for the given contact
|
||||
* or null if none is known.
|
||||
*/
|
||||
@Nullable
|
||||
AttachmentHeader getAvatarHeader(Contact c) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns our current profile image header or null if none has been added.
|
||||
*/
|
||||
@Nullable
|
||||
AttachmentHeader getMyAvatarHeader() throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the profile image attachment for the given header.
|
||||
*/
|
||||
Attachment getAvatar(AttachmentHeader h) throws DbException;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.briarproject.briar.api.avatar.event;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.briar.api.media.AttachmentHeader;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
/**
|
||||
* An event that is broadcast when a new avatar is received.
|
||||
*/
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class AvatarUpdatedEvent extends Event {
|
||||
|
||||
private final ContactId contactId;
|
||||
private final AttachmentHeader attachmentHeader;
|
||||
|
||||
public AvatarUpdatedEvent(ContactId contactId,
|
||||
AttachmentHeader attachmentHeader) {
|
||||
this.contactId = contactId;
|
||||
this.attachmentHeader = attachmentHeader;
|
||||
}
|
||||
|
||||
public ContactId getContactId() {
|
||||
return contactId;
|
||||
}
|
||||
|
||||
public AttachmentHeader getAttachmentHeader() {
|
||||
return attachmentHeader;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.briarproject.briar.avatar;
|
||||
|
||||
interface AvatarConstants {
|
||||
|
||||
// Message type constants
|
||||
int MSG_TYPE_UPDATE = 0;
|
||||
|
||||
// Metadata keys for groups
|
||||
String GROUP_KEY_CONTACT_ID = "contactId";
|
||||
|
||||
// Message metadata keys
|
||||
String MSG_KEY_VERSION = "version";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
package org.briarproject.briar.avatar;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.client.ClientHelper;
|
||||
import org.briarproject.bramble.api.contact.Contact;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.contact.ContactManager.ContactHook;
|
||||
import org.briarproject.bramble.api.data.BdfDictionary;
|
||||
import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.data.MetadataParser;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.Metadata;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.identity.AuthorId;
|
||||
import org.briarproject.bramble.api.identity.IdentityManager;
|
||||
import org.briarproject.bramble.api.identity.LocalAuthor;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager.OpenDatabaseHook;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.Group;
|
||||
import org.briarproject.bramble.api.sync.Group.Visibility;
|
||||
import org.briarproject.bramble.api.sync.GroupFactory;
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.bramble.api.sync.InvalidMessageException;
|
||||
import org.briarproject.bramble.api.sync.Message;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.api.sync.validation.IncomingMessageHook;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.api.versioning.ClientVersioningManager;
|
||||
import org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook;
|
||||
import org.briarproject.briar.api.avatar.AvatarManager;
|
||||
import org.briarproject.briar.api.avatar.event.AvatarUpdatedEvent;
|
||||
import org.briarproject.briar.api.media.Attachment;
|
||||
import org.briarproject.briar.api.media.AttachmentHeader;
|
||||
import org.briarproject.briar.api.media.FileTooBigException;
|
||||
import org.briarproject.briar.api.media.InvalidAttachmentException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
|
||||
import static org.briarproject.bramble.util.IoUtils.copyAndClose;
|
||||
import static org.briarproject.briar.avatar.AvatarConstants.GROUP_KEY_CONTACT_ID;
|
||||
import static org.briarproject.briar.avatar.AvatarConstants.MSG_KEY_VERSION;
|
||||
import static org.briarproject.briar.avatar.AvatarConstants.MSG_TYPE_UPDATE;
|
||||
import static org.briarproject.briar.media.MediaConstants.MSG_KEY_CONTENT_TYPE;
|
||||
import static org.briarproject.briar.media.MediaConstants.MSG_KEY_DESCRIPTOR_LENGTH;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class AvatarManagerImpl implements AvatarManager, OpenDatabaseHook, ContactHook,
|
||||
ClientVersioningHook, IncomingMessageHook {
|
||||
|
||||
private final DatabaseComponent db;
|
||||
private final IdentityManager identityManager;
|
||||
private final ClientHelper clientHelper;
|
||||
private final ClientVersioningManager clientVersioningManager;
|
||||
private final MetadataParser metadataParser;
|
||||
private final GroupFactory groupFactory;
|
||||
private final Clock clock;
|
||||
|
||||
@Inject
|
||||
AvatarManagerImpl(
|
||||
DatabaseComponent db,
|
||||
IdentityManager identityManager,
|
||||
ClientHelper clientHelper,
|
||||
ClientVersioningManager clientVersioningManager,
|
||||
MetadataParser metadataParser,
|
||||
GroupFactory groupFactory,
|
||||
Clock clock) {
|
||||
this.db = db;
|
||||
this.identityManager = identityManager;
|
||||
this.clientHelper = clientHelper;
|
||||
this.clientVersioningManager = clientVersioningManager;
|
||||
this.metadataParser = metadataParser;
|
||||
this.groupFactory = groupFactory;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDatabaseOpened(Transaction txn) throws DbException {
|
||||
// Create our avatar group if necessary
|
||||
LocalAuthor a = identityManager.getLocalAuthor(txn);
|
||||
Group ourGroup = getGroup(a.getId());
|
||||
if (db.containsGroup(txn, ourGroup.getId())) return;
|
||||
db.addGroup(txn, ourGroup);
|
||||
|
||||
// Set things up for any pre-existing contacts
|
||||
for (Contact c : db.getContacts(txn)) addingContact(txn, c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addingContact(Transaction txn, Contact c) throws DbException {
|
||||
// Create a group to share with the contact
|
||||
Group theirGroup = getGroup(c.getAuthor().getId());
|
||||
db.addGroup(txn, theirGroup);
|
||||
// Attach the contact ID to the group
|
||||
BdfDictionary d = new BdfDictionary();
|
||||
d.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
|
||||
try {
|
||||
clientHelper.mergeGroupMetadata(txn, theirGroup.getId(), d);
|
||||
} catch (FormatException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
// Apply the client's visibility to our and their group
|
||||
Group ourGroup = getOurGroup(txn);
|
||||
Visibility client = clientVersioningManager.getClientVisibility(txn,
|
||||
c.getId(), CLIENT_ID, MAJOR_VERSION);
|
||||
db.setGroupVisibility(txn, c.getId(), ourGroup.getId(), client);
|
||||
db.setGroupVisibility(txn, c.getId(), theirGroup.getId(), client);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removingContact(Transaction txn, Contact c) throws DbException {
|
||||
db.removeGroup(txn, getGroup(c.getAuthor().getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClientVisibilityChanging(Transaction txn, Contact c,
|
||||
Visibility v) throws DbException {
|
||||
// Apply the client's visibility to our and the contact group
|
||||
Group ourGroup = getOurGroup(txn);
|
||||
Group theirGroup = getGroup(c.getAuthor().getId());
|
||||
db.setGroupVisibility(txn, c.getId(), ourGroup.getId(), v);
|
||||
db.setGroupVisibility(txn, c.getId(), theirGroup.getId(), v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean incomingMessage(Transaction txn, Message m, Metadata meta)
|
||||
throws DbException, InvalidMessageException {
|
||||
try {
|
||||
// Find the latest update, if any
|
||||
BdfDictionary d = metadataParser.parse(meta);
|
||||
LatestUpdate latest = findLatest(txn, m.getGroupId());
|
||||
if (latest != null) {
|
||||
if (d.getLong(MSG_KEY_VERSION) > latest.version) {
|
||||
// This update is newer - delete the previous update
|
||||
db.deleteMessage(txn, latest.messageId);
|
||||
db.deleteMessageMetadata(txn, latest.messageId);
|
||||
} else {
|
||||
// We've already received a newer update - delete this one
|
||||
db.deleteMessage(txn, m.getId());
|
||||
db.deleteMessageMetadata(txn, m.getId());
|
||||
return false; // don't broadcast update
|
||||
}
|
||||
}
|
||||
ContactId contactId = getContactId(txn, m.getGroupId());
|
||||
String contentType = d.getString(MSG_KEY_CONTENT_TYPE);
|
||||
AttachmentHeader a = new AttachmentHeader(m.getId(), contentType);
|
||||
txn.attach(new AvatarUpdatedEvent(contactId, a));
|
||||
} catch (FormatException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttachmentHeader addAvatar(String contentType, InputStream in)
|
||||
throws DbException, IOException {
|
||||
// find latest avatar
|
||||
GroupId groupId;
|
||||
LatestUpdate latest;
|
||||
Transaction txn = db.startTransaction(true);
|
||||
try {
|
||||
groupId = getOurGroup(txn).getId();
|
||||
// TODO this might not be the latest anymore at the end of this method
|
||||
// Can we run everything in one transaction? Probably not.
|
||||
latest = findLatest(txn, groupId);
|
||||
db.commitTransaction(txn);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
long version = latest == null ? 0 : latest.version + 1;
|
||||
// 0.0: Message Type, Version, Content-Type
|
||||
// TODO do we need to add the message type explicitly?
|
||||
BdfList list = BdfList.of(MSG_TYPE_UPDATE, version, contentType);
|
||||
byte[] descriptor = clientHelper.toByteArray(list);
|
||||
// add BdfList and stream content to body
|
||||
ByteArrayOutputStream bodyOut = new ByteArrayOutputStream();
|
||||
bodyOut.write(descriptor);
|
||||
copyAndClose(in, bodyOut);
|
||||
if (bodyOut.size() > MAX_MESSAGE_BODY_LENGTH)
|
||||
throw new FileTooBigException();
|
||||
// assemble message
|
||||
byte[] body = bodyOut.toByteArray();
|
||||
long timestamp = clock.currentTimeMillis();
|
||||
Message m = clientHelper.createMessage(groupId, timestamp, body);
|
||||
// add metadata to message
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
meta.put(MSG_KEY_VERSION, version);
|
||||
meta.put(MSG_KEY_CONTENT_TYPE, contentType);
|
||||
meta.put(MSG_KEY_DESCRIPTOR_LENGTH, descriptor.length);
|
||||
// send message
|
||||
db.transaction(false, txn2 -> {
|
||||
if (latest != null) {
|
||||
// delete previous update
|
||||
db.deleteMessage(txn2, latest.messageId);
|
||||
db.deleteMessageMetadata(txn2, latest.messageId);
|
||||
}
|
||||
clientHelper.addLocalMessage(txn2, m, meta, true, false);
|
||||
});
|
||||
return new AttachmentHeader(m.getId(), contentType);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public AttachmentHeader getAvatarHeader(Contact c) throws DbException {
|
||||
try {
|
||||
Group g = getGroup(c.getAuthor().getId());
|
||||
return db.transactionWithNullableResult(true, txn ->
|
||||
getAvatarHeader(txn, g.getId())
|
||||
);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public AttachmentHeader getMyAvatarHeader() throws DbException {
|
||||
try {
|
||||
return db.transactionWithNullableResult(true, txn -> {
|
||||
Group g = getOurGroup(txn);
|
||||
return getAvatarHeader(txn, g.getId());
|
||||
});
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private AttachmentHeader getAvatarHeader(Transaction txn, GroupId groupId)
|
||||
throws DbException, FormatException {
|
||||
LatestUpdate latest = findLatest(txn, groupId);
|
||||
if (latest == null) return null;
|
||||
return new AttachmentHeader(latest.messageId, latest.contentType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Attachment getAvatar(AttachmentHeader h) throws DbException {
|
||||
MessageId m = h.getMessageId();
|
||||
byte[] body = clientHelper.getMessage(m).getBody();
|
||||
try {
|
||||
BdfDictionary meta = clientHelper.getMessageMetadataAsDictionary(m);
|
||||
String contentType = meta.getString(MSG_KEY_CONTENT_TYPE);
|
||||
if (!contentType.equals(h.getContentType()))
|
||||
throw new InvalidAttachmentException();
|
||||
int offset = meta.getLong(MSG_KEY_DESCRIPTOR_LENGTH).intValue();
|
||||
return new Attachment(h, new ByteArrayInputStream(body, offset,
|
||||
body.length - offset));
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private LatestUpdate findLatest(Transaction txn, GroupId g)
|
||||
throws DbException, FormatException {
|
||||
Map<MessageId, BdfDictionary> metadata =
|
||||
clientHelper.getMessageMetadataAsDictionary(txn, g);
|
||||
for (Map.Entry<MessageId, BdfDictionary> e : metadata.entrySet()) {
|
||||
BdfDictionary meta = e.getValue();
|
||||
long version = meta.getLong(MSG_KEY_VERSION);
|
||||
String contentType = meta.getString(MSG_KEY_CONTENT_TYPE);
|
||||
return new LatestUpdate(e.getKey(), version, contentType);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ContactId getContactId(Transaction txn, GroupId g)
|
||||
throws DbException {
|
||||
try {
|
||||
BdfDictionary meta =
|
||||
clientHelper.getGroupMetadataAsDictionary(txn, g);
|
||||
return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue());
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Group getOurGroup(Transaction txn) throws DbException {
|
||||
LocalAuthor a = identityManager.getLocalAuthor(txn);
|
||||
return getGroup(a.getId());
|
||||
}
|
||||
|
||||
private Group getGroup(AuthorId authorId) {
|
||||
return groupFactory
|
||||
.createGroup(CLIENT_ID, MAJOR_VERSION, authorId.getBytes());
|
||||
}
|
||||
|
||||
private static class LatestUpdate {
|
||||
|
||||
private final MessageId messageId;
|
||||
private final long version;
|
||||
private final String contentType;
|
||||
|
||||
private LatestUpdate(MessageId messageId, long version,
|
||||
String contentType) {
|
||||
this.messageId = messageId;
|
||||
this.version = version;
|
||||
this.contentType = contentType;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.briarproject.briar.avatar;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactManager;
|
||||
import org.briarproject.bramble.api.data.BdfReaderFactory;
|
||||
import org.briarproject.bramble.api.data.MetadataEncoder;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.sync.validation.ValidationManager;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.api.versioning.ClientVersioningManager;
|
||||
import org.briarproject.briar.api.avatar.AvatarManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
|
||||
import static org.briarproject.briar.api.avatar.AvatarManager.CLIENT_ID;
|
||||
import static org.briarproject.briar.api.avatar.AvatarManager.MAJOR_VERSION;
|
||||
import static org.briarproject.briar.api.avatar.AvatarManager.MINOR_VERSION;
|
||||
|
||||
@Module
|
||||
public class AvatarModule {
|
||||
|
||||
public static class EagerSingletons {
|
||||
@Inject
|
||||
AvatarValidator avatarValidator;
|
||||
@Inject
|
||||
AvatarManager avatarManager;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
AvatarValidator provideAvatarValidator(ValidationManager validationManager,
|
||||
BdfReaderFactory bdfReaderFactory, MetadataEncoder metadataEncoder,
|
||||
Clock clock) {
|
||||
AvatarValidator introductionValidator =
|
||||
new AvatarValidator(bdfReaderFactory, metadataEncoder, clock);
|
||||
validationManager.registerMessageValidator(CLIENT_ID, MAJOR_VERSION,
|
||||
introductionValidator);
|
||||
return introductionValidator;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
AvatarManager provideAvatarManager(
|
||||
LifecycleManager lifecycleManager,
|
||||
ContactManager contactManager,
|
||||
ValidationManager validationManager,
|
||||
ClientVersioningManager clientVersioningManager,
|
||||
AvatarManagerImpl avatarManager) {
|
||||
lifecycleManager.registerOpenDatabaseHook(avatarManager);
|
||||
contactManager.registerContactHook(avatarManager);
|
||||
validationManager.registerIncomingMessageHook(CLIENT_ID,
|
||||
MAJOR_VERSION, avatarManager);
|
||||
clientVersioningManager.registerClient(CLIENT_ID,
|
||||
MAJOR_VERSION, MINOR_VERSION, avatarManager);
|
||||
return avatarManager;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package org.briarproject.briar.avatar;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.data.BdfDictionary;
|
||||
import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.data.BdfReader;
|
||||
import org.briarproject.bramble.api.data.BdfReaderFactory;
|
||||
import org.briarproject.bramble.api.data.MetadataEncoder;
|
||||
import org.briarproject.bramble.api.db.Metadata;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.Group;
|
||||
import org.briarproject.bramble.api.sync.InvalidMessageException;
|
||||
import org.briarproject.bramble.api.sync.Message;
|
||||
import org.briarproject.bramble.api.sync.MessageContext;
|
||||
import org.briarproject.bramble.api.sync.validation.MessageValidator;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.briar.media.CountingInputStream;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE;
|
||||
import static org.briarproject.bramble.util.ValidationUtils.checkLength;
|
||||
import static org.briarproject.bramble.util.ValidationUtils.checkSize;
|
||||
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_CONTENT_TYPE_BYTES;
|
||||
import static org.briarproject.briar.avatar.AvatarConstants.MSG_KEY_VERSION;
|
||||
import static org.briarproject.briar.avatar.AvatarConstants.MSG_TYPE_UPDATE;
|
||||
import static org.briarproject.briar.media.MediaConstants.MSG_KEY_CONTENT_TYPE;
|
||||
import static org.briarproject.briar.media.MediaConstants.MSG_KEY_DESCRIPTOR_LENGTH;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class AvatarValidator implements MessageValidator {
|
||||
|
||||
private final BdfReaderFactory bdfReaderFactory;
|
||||
private final MetadataEncoder metadataEncoder;
|
||||
private final Clock clock;
|
||||
|
||||
AvatarValidator(BdfReaderFactory bdfReaderFactory,
|
||||
MetadataEncoder metadataEncoder, Clock clock) {
|
||||
this.bdfReaderFactory = bdfReaderFactory;
|
||||
this.metadataEncoder = metadataEncoder;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageContext validateMessage(Message m, Group g)
|
||||
throws InvalidMessageException {
|
||||
// Reject the message if it's too far in the future
|
||||
long now = clock.currentTimeMillis();
|
||||
if (m.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) {
|
||||
throw new InvalidMessageException(
|
||||
"Timestamp is too far in the future");
|
||||
}
|
||||
try {
|
||||
InputStream in = new ByteArrayInputStream(m.getBody());
|
||||
CountingInputStream countIn =
|
||||
new CountingInputStream(in, MAX_MESSAGE_BODY_LENGTH);
|
||||
BdfReader reader = bdfReaderFactory.createReader(countIn);
|
||||
BdfList list = reader.readList();
|
||||
long bytesRead = countIn.getBytesRead();
|
||||
BdfDictionary d = validateUpdate(list, bytesRead);
|
||||
Metadata meta = metadataEncoder.encode(d);
|
||||
return new MessageContext(meta);
|
||||
} catch (IOException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private BdfDictionary validateUpdate(BdfList body, long descriptorLength)
|
||||
throws FormatException {
|
||||
// 0.0: Message Type, Version, Content-Type
|
||||
checkSize(body, 3);
|
||||
// Message Type
|
||||
long messageType = body.getLong(0);
|
||||
if (messageType != MSG_TYPE_UPDATE) throw new FormatException();
|
||||
// Version
|
||||
long version = body.getLong(1);
|
||||
if (version < 0) throw new FormatException();
|
||||
// Content-Type
|
||||
String contentType = body.getString(2);
|
||||
checkLength(contentType, 1, MAX_CONTENT_TYPE_BYTES);
|
||||
|
||||
// Return the metadata
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
meta.put(MSG_KEY_VERSION, version);
|
||||
meta.put(MSG_KEY_CONTENT_TYPE, contentType);
|
||||
meta.put(MSG_KEY_DESCRIPTOR_LENGTH, descriptorLength);
|
||||
return meta;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
package org.briarproject.briar.avatar;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.client.ClientHelper;
|
||||
import org.briarproject.bramble.api.contact.Contact;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.data.BdfDictionary;
|
||||
import org.briarproject.bramble.api.data.BdfEntry;
|
||||
import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.data.MetadataParser;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.EventAction;
|
||||
import org.briarproject.bramble.api.db.Metadata;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.identity.AuthorId;
|
||||
import org.briarproject.bramble.api.identity.IdentityManager;
|
||||
import org.briarproject.bramble.api.identity.LocalAuthor;
|
||||
import org.briarproject.bramble.api.sync.Group;
|
||||
import org.briarproject.bramble.api.sync.Group.Visibility;
|
||||
import org.briarproject.bramble.api.sync.GroupFactory;
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.bramble.api.sync.InvalidMessageException;
|
||||
import org.briarproject.bramble.api.sync.Message;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.api.versioning.ClientVersioningManager;
|
||||
import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||
import org.briarproject.bramble.test.DbExpectations;
|
||||
import org.briarproject.briar.api.avatar.event.AvatarUpdatedEvent;
|
||||
import org.briarproject.briar.api.media.AttachmentHeader;
|
||||
import org.jmock.Expectations;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
||||
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
||||
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
|
||||
import static org.briarproject.bramble.test.TestUtils.getContact;
|
||||
import static org.briarproject.bramble.test.TestUtils.getGroup;
|
||||
import static org.briarproject.bramble.test.TestUtils.getLocalAuthor;
|
||||
import static org.briarproject.bramble.test.TestUtils.getMessage;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
||||
import static org.briarproject.bramble.util.StringUtils.getRandomString;
|
||||
import static org.briarproject.briar.api.avatar.AvatarManager.CLIENT_ID;
|
||||
import static org.briarproject.briar.api.avatar.AvatarManager.MAJOR_VERSION;
|
||||
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_CONTENT_TYPE_BYTES;
|
||||
import static org.briarproject.briar.avatar.AvatarConstants.GROUP_KEY_CONTACT_ID;
|
||||
import static org.briarproject.briar.avatar.AvatarConstants.MSG_KEY_VERSION;
|
||||
import static org.briarproject.briar.avatar.AvatarConstants.MSG_TYPE_UPDATE;
|
||||
import static org.briarproject.briar.media.MediaConstants.MSG_KEY_CONTENT_TYPE;
|
||||
import static org.briarproject.briar.media.MediaConstants.MSG_KEY_DESCRIPTOR_LENGTH;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
public class AvatarManagerImplTest extends BrambleMockTestCase {
|
||||
|
||||
private final DatabaseComponent db = context.mock(DatabaseComponent.class);
|
||||
private final IdentityManager identityManager =
|
||||
context.mock(IdentityManager.class);
|
||||
private final ClientHelper clientHelper = context.mock(ClientHelper.class);
|
||||
private final ClientVersioningManager clientVersioningManager =
|
||||
context.mock(ClientVersioningManager.class);
|
||||
private final MetadataParser metadataParser =
|
||||
context.mock(MetadataParser.class);
|
||||
private final GroupFactory groupFactory = context.mock(GroupFactory.class);
|
||||
private final Clock clock = context.mock(Clock.class);
|
||||
|
||||
private final Group localGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
|
||||
private final GroupId localGroupId = localGroup.getId();
|
||||
private final LocalAuthor localAuthor = getLocalAuthor();
|
||||
private final Contact contact = getContact();
|
||||
private final Group contactGroup = getGroup(CLIENT_ID, MAJOR_VERSION, 32);
|
||||
private final GroupId contactGroupId = contactGroup.getId();
|
||||
private final Message ourMsg = getMessage(localGroupId);
|
||||
private final Message contactMsg = getMessage(contactGroupId);
|
||||
private final Metadata meta = new Metadata();
|
||||
private final String contentType = getRandomString(MAX_CONTENT_TYPE_BYTES);
|
||||
private final BdfDictionary metaDict = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_VERSION, 1),
|
||||
new BdfEntry(MSG_KEY_CONTENT_TYPE, contentType)
|
||||
);
|
||||
|
||||
private final AvatarManagerImpl avatarManager =
|
||||
new AvatarManagerImpl(db, identityManager, clientHelper,
|
||||
clientVersioningManager, metadataParser, groupFactory,
|
||||
clock);
|
||||
|
||||
@Test
|
||||
public void testOpenDatabaseHook() throws DbException, FormatException {
|
||||
Transaction txn = new Transaction(null, false);
|
||||
|
||||
// local group already exists, so nothing more to do
|
||||
expectCreateGroup(localAuthor.getId(), localGroup);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(identityManager).getLocalAuthor(txn);
|
||||
will(returnValue(localAuthor));
|
||||
oneOf(db).containsGroup(txn, localGroupId);
|
||||
will(returnValue(true));
|
||||
}});
|
||||
avatarManager.onDatabaseOpened(txn);
|
||||
|
||||
// local group does not exist, so we need to set things up for contacts
|
||||
expectCreateGroup(localAuthor.getId(), localGroup);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(identityManager).getLocalAuthor(txn);
|
||||
will(returnValue(localAuthor));
|
||||
oneOf(db).containsGroup(txn, localGroupId);
|
||||
will(returnValue(false));
|
||||
oneOf(db).addGroup(txn, localGroup);
|
||||
oneOf(db).getContacts(txn);
|
||||
will(returnValue(Collections.singletonList(contact)));
|
||||
}});
|
||||
expectAddingContact(txn, contact, SHARED);
|
||||
avatarManager.onDatabaseOpened(txn);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddingContact() throws DbException, FormatException {
|
||||
Transaction txn = new Transaction(null, false);
|
||||
|
||||
expectAddingContact(txn, contact, INVISIBLE);
|
||||
avatarManager.addingContact(txn, contact);
|
||||
|
||||
Contact contact2 = getContact();
|
||||
expectAddingContact(txn, contact2, VISIBLE);
|
||||
avatarManager.addingContact(txn, contact2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemovingContact() throws DbException {
|
||||
Transaction txn = new Transaction(null, false);
|
||||
|
||||
expectCreateGroup(contact.getAuthor().getId(), contactGroup);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(db).removeGroup(txn, contactGroup);
|
||||
}});
|
||||
|
||||
avatarManager.removingContact(txn, contact);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnClientVisibilityChanging() throws DbException {
|
||||
Transaction txn = new Transaction(null, false);
|
||||
|
||||
expectGetOurGroup(txn);
|
||||
expectCreateGroup(contact.getAuthor().getId(), contactGroup);
|
||||
expectSetGroupVisibility(txn, contact.getId(), localGroupId, VISIBLE);
|
||||
expectSetGroupVisibility(txn, contact.getId(), contactGroupId, VISIBLE);
|
||||
avatarManager.onClientVisibilityChanging(txn, contact, VISIBLE);
|
||||
|
||||
expectGetOurGroup(txn);
|
||||
expectCreateGroup(contact.getAuthor().getId(), contactGroup);
|
||||
expectSetGroupVisibility(txn, contact.getId(), localGroupId, SHARED);
|
||||
expectSetGroupVisibility(txn, contact.getId(), contactGroupId, SHARED);
|
||||
avatarManager.onClientVisibilityChanging(txn, contact, SHARED);
|
||||
|
||||
expectGetOurGroup(txn);
|
||||
expectCreateGroup(contact.getAuthor().getId(), contactGroup);
|
||||
expectSetGroupVisibility(txn, contact.getId(), localGroupId, INVISIBLE);
|
||||
expectSetGroupVisibility(txn, contact.getId(), contactGroupId,
|
||||
INVISIBLE);
|
||||
avatarManager.onClientVisibilityChanging(txn, contact, INVISIBLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstIncomingMessage()
|
||||
throws DbException, InvalidMessageException, FormatException {
|
||||
Transaction txn = new Transaction(null, false);
|
||||
BdfDictionary d = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_CONTENT_TYPE, contentType)
|
||||
);
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(metadataParser).parse(meta);
|
||||
will(returnValue(d));
|
||||
}});
|
||||
expectFindLatest(txn, contactGroupId, new MessageId(getRandomId()),
|
||||
null);
|
||||
expectGetContactId(txn, contactGroupId, contact.getId());
|
||||
|
||||
assertFalse(avatarManager.incomingMessage(txn, contactMsg, meta));
|
||||
assertEquals(1, txn.getActions().size());
|
||||
Event event = ((EventAction) txn.getActions().get(0)).getEvent();
|
||||
AvatarUpdatedEvent avatarUpdatedEvent = (AvatarUpdatedEvent) event;
|
||||
assertEquals(contactMsg.getId(),
|
||||
avatarUpdatedEvent.getAttachmentHeader().getMessageId());
|
||||
assertEquals(contact.getId(), avatarUpdatedEvent.getContactId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNewerIncomingMessage()
|
||||
throws DbException, InvalidMessageException, FormatException {
|
||||
Transaction txn = new Transaction(null, false);
|
||||
BdfDictionary d = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_VERSION, 1),
|
||||
new BdfEntry(MSG_KEY_CONTENT_TYPE, contentType)
|
||||
);
|
||||
MessageId latestMsgId = new MessageId(getRandomId());
|
||||
BdfDictionary latest = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_VERSION, 0),
|
||||
new BdfEntry(MSG_KEY_CONTENT_TYPE, contentType)
|
||||
);
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(metadataParser).parse(meta);
|
||||
will(returnValue(d));
|
||||
// delete old "latest" message
|
||||
oneOf(db).deleteMessage(txn, latestMsgId);
|
||||
oneOf(db).deleteMessageMetadata(txn, latestMsgId);
|
||||
}});
|
||||
expectFindLatest(txn, contactGroupId, latestMsgId, latest);
|
||||
expectGetContactId(txn, contactGroupId, contact.getId());
|
||||
|
||||
assertFalse(avatarManager.incomingMessage(txn, contactMsg, meta));
|
||||
|
||||
// event to broadcast
|
||||
assertEquals(1, txn.getActions().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteOlderIncomingMessage()
|
||||
throws DbException, InvalidMessageException, FormatException {
|
||||
Transaction txn = new Transaction(null, false);
|
||||
BdfDictionary d = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_VERSION, 0),
|
||||
new BdfEntry(MSG_KEY_CONTENT_TYPE, contentType)
|
||||
);
|
||||
MessageId latestMsgId = new MessageId(getRandomId());
|
||||
BdfDictionary latest = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_VERSION, 1),
|
||||
new BdfEntry(MSG_KEY_CONTENT_TYPE, contentType)
|
||||
);
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(metadataParser).parse(meta);
|
||||
will(returnValue(d));
|
||||
// delete older incoming message
|
||||
oneOf(db).deleteMessage(txn, contactMsg.getId());
|
||||
oneOf(db).deleteMessageMetadata(txn, contactMsg.getId());
|
||||
}});
|
||||
expectFindLatest(txn, contactGroupId, latestMsgId, latest);
|
||||
|
||||
assertFalse(avatarManager.incomingMessage(txn, contactMsg, meta));
|
||||
|
||||
// no event to broadcast
|
||||
assertEquals(0, txn.getActions().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddAvatar() throws Exception {
|
||||
byte[] avatarBytes = getRandomBytes(42);
|
||||
InputStream inputStream = new ByteArrayInputStream(avatarBytes);
|
||||
Transaction txn = new Transaction(null, true);
|
||||
Transaction txn2 = new Transaction(null, false);
|
||||
long latestVersion = metaDict.getLong(MSG_KEY_VERSION);
|
||||
long version = latestVersion + 1;
|
||||
BdfList list = BdfList.of(MSG_TYPE_UPDATE, version, contentType);
|
||||
long now = System.currentTimeMillis();
|
||||
Message newMsg = getMessage(localGroupId);
|
||||
BdfDictionary newMeta = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_VERSION, version),
|
||||
new BdfEntry(MSG_KEY_CONTENT_TYPE, contentType),
|
||||
new BdfEntry(MSG_KEY_DESCRIPTOR_LENGTH, 0)
|
||||
);
|
||||
context.checking(new DbExpectations() {{
|
||||
oneOf(db).startTransaction(true);
|
||||
will(returnValue(txn));
|
||||
oneOf(db).commitTransaction(txn);
|
||||
oneOf(db).endTransaction(txn);
|
||||
oneOf(clientHelper).toByteArray(list);
|
||||
oneOf(clock).currentTimeMillis();
|
||||
will(returnValue(now));
|
||||
oneOf(clientHelper).createMessage(with(equal(localGroupId)),
|
||||
with(equal(now)), with(any(byte[].class)));
|
||||
will(returnValue(newMsg));
|
||||
oneOf(db).transaction(with(false), withDbRunnable(txn2));
|
||||
oneOf(db).deleteMessage(txn2, ourMsg.getId());
|
||||
oneOf(db).deleteMessageMetadata(txn2, ourMsg.getId());
|
||||
oneOf(clientHelper)
|
||||
.addLocalMessage(txn2, newMsg, newMeta, true, false);
|
||||
}});
|
||||
expectGetOurGroup(txn);
|
||||
expectFindLatest(txn, localGroupId, ourMsg.getId(), metaDict);
|
||||
|
||||
AttachmentHeader header =
|
||||
avatarManager.addAvatar(contentType, inputStream);
|
||||
assertEquals(newMsg.getId(), header.getMessageId());
|
||||
assertEquals(contentType, header.getContentType());
|
||||
}
|
||||
|
||||
private void expectGetContactId(Transaction txn, GroupId groupId,
|
||||
ContactId contactId) throws DbException, FormatException {
|
||||
BdfDictionary d = BdfDictionary
|
||||
.of(new BdfEntry(GROUP_KEY_CONTACT_ID, contactId.getInt()));
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(clientHelper).getGroupMetadataAsDictionary(txn, groupId);
|
||||
will(returnValue(d));
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectFindLatest(Transaction txn, GroupId groupId,
|
||||
MessageId messageId, @Nullable BdfDictionary d)
|
||||
throws DbException, FormatException {
|
||||
Map<MessageId, BdfDictionary> map = new HashMap<>();
|
||||
if (d != null) map.put(messageId, d);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(clientHelper)
|
||||
.getMessageMetadataAsDictionary(txn, groupId);
|
||||
will(returnValue(map));
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectSetGroupVisibility(Transaction txn, ContactId contactId,
|
||||
GroupId groupId, Visibility v) throws DbException {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(db).setGroupVisibility(txn, contactId, groupId, v);
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectAddingContact(Transaction txn, Contact c, Visibility v)
|
||||
throws DbException, FormatException {
|
||||
BdfDictionary groupMeta = BdfDictionary.of(
|
||||
new BdfEntry(GROUP_KEY_CONTACT_ID, c.getId().getInt())
|
||||
);
|
||||
expectGetOurGroup(txn);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(groupFactory).createGroup(CLIENT_ID, MAJOR_VERSION,
|
||||
c.getAuthor().getId().getBytes());
|
||||
will(returnValue(contactGroup));
|
||||
oneOf(db).addGroup(txn, contactGroup);
|
||||
oneOf(clientHelper)
|
||||
.mergeGroupMetadata(txn, contactGroupId, groupMeta);
|
||||
oneOf(clientVersioningManager)
|
||||
.getClientVisibility(txn, c.getId(), CLIENT_ID,
|
||||
MAJOR_VERSION);
|
||||
will(returnValue(v));
|
||||
}});
|
||||
expectSetGroupVisibility(txn, c.getId(), localGroupId, v);
|
||||
expectSetGroupVisibility(txn, c.getId(), contactGroupId, v);
|
||||
}
|
||||
|
||||
private void expectGetOurGroup(Transaction txn) throws DbException {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(identityManager).getLocalAuthor(txn);
|
||||
will(returnValue(localAuthor));
|
||||
}});
|
||||
expectCreateGroup(localAuthor.getId(), localGroup);
|
||||
}
|
||||
|
||||
private void expectCreateGroup(AuthorId authorId, Group group) {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(groupFactory)
|
||||
.createGroup(CLIENT_ID, MAJOR_VERSION, authorId.getBytes());
|
||||
will(returnValue(group));
|
||||
}});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package org.briarproject.briar.avatar;
|
||||
|
||||
import org.briarproject.bramble.test.TestDatabaseConfigModule;
|
||||
import org.briarproject.briar.api.avatar.AvatarManager;
|
||||
import org.briarproject.briar.api.media.Attachment;
|
||||
import org.briarproject.briar.api.media.AttachmentHeader;
|
||||
import org.briarproject.briar.test.BriarIntegrationTest;
|
||||
import org.briarproject.briar.test.BriarIntegrationTestComponent;
|
||||
import org.briarproject.briar.test.DaggerBriarIntegrationTestComponent;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
|
||||
import static org.briarproject.bramble.util.IoUtils.copyAndClose;
|
||||
import static org.briarproject.bramble.util.StringUtils.getRandomString;
|
||||
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_CONTENT_TYPE_BYTES;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
public class AvatarManagerIntegrationTest
|
||||
extends BriarIntegrationTest<BriarIntegrationTestComponent> {
|
||||
|
||||
private AvatarManager avatarManager0, avatarManager1;
|
||||
|
||||
private final String contentType = getRandomString(MAX_CONTENT_TYPE_BYTES);
|
||||
|
||||
@Before
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
avatarManager0 = c0.getAvatarManager();
|
||||
avatarManager1 = c1.getAvatarManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createComponents() {
|
||||
BriarIntegrationTestComponent component =
|
||||
DaggerBriarIntegrationTestComponent.builder().build();
|
||||
BriarIntegrationTestComponent.Helper.injectEagerSingletons(component);
|
||||
component.inject(this);
|
||||
|
||||
c0 = DaggerBriarIntegrationTestComponent.builder()
|
||||
.testDatabaseConfigModule(new TestDatabaseConfigModule(t0Dir))
|
||||
.build();
|
||||
BriarIntegrationTestComponent.Helper.injectEagerSingletons(c0);
|
||||
|
||||
c1 = DaggerBriarIntegrationTestComponent.builder()
|
||||
.testDatabaseConfigModule(new TestDatabaseConfigModule(t1Dir))
|
||||
.build();
|
||||
BriarIntegrationTestComponent.Helper.injectEagerSingletons(c1);
|
||||
|
||||
c2 = DaggerBriarIntegrationTestComponent.builder()
|
||||
.testDatabaseConfigModule(new TestDatabaseConfigModule(t2Dir))
|
||||
.build();
|
||||
BriarIntegrationTestComponent.Helper.injectEagerSingletons(c2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddingAndSyncAvatars() throws Exception {
|
||||
// Both contacts don't have avatars
|
||||
assertNull(avatarManager0.getMyAvatarHeader());
|
||||
assertNull(avatarManager1.getMyAvatarHeader());
|
||||
|
||||
// Both contacts don't see avatars for each other
|
||||
assertNull(avatarManager0.getAvatarHeader(contact1From0));
|
||||
assertNull(avatarManager1.getAvatarHeader(contact0From1));
|
||||
|
||||
// 0 adds avatar
|
||||
byte[] avatar0bytes = getRandomBytes(42);
|
||||
InputStream avatar0inputStream = new ByteArrayInputStream(avatar0bytes);
|
||||
AttachmentHeader header0 =
|
||||
avatarManager0.addAvatar(contentType, avatar0inputStream);
|
||||
assertEquals(contentType, header0.getContentType());
|
||||
|
||||
// 0 sees their own avatar
|
||||
header0 = avatarManager0.getMyAvatarHeader();
|
||||
assertNotNull(header0);
|
||||
assertEquals(contentType, header0.getContentType());
|
||||
assertNotNull(header0.getMessageId());
|
||||
|
||||
// 0 can retrieve their own avatar
|
||||
Attachment attachment0 = avatarManager0.getAvatar(header0);
|
||||
assertEquals(contentType, attachment0.getHeader().getContentType());
|
||||
assertStreamMatches(avatar0bytes, attachment0.getStream());
|
||||
|
||||
// send the avatar from 0 to 1
|
||||
sync0To1(1, true);
|
||||
|
||||
// 1 also sees 0's avatar now
|
||||
AttachmentHeader header0From1 =
|
||||
avatarManager1.getAvatarHeader(contact0From1);
|
||||
assertNotNull(header0From1);
|
||||
assertEquals(contentType, header0From1.getContentType());
|
||||
assertNotNull(header0From1.getMessageId());
|
||||
|
||||
// 1 can retrieve 0's avatar
|
||||
Attachment attachment0From1 = avatarManager1.getAvatar(header0From1);
|
||||
assertEquals(contentType,
|
||||
attachment0From1.getHeader().getContentType());
|
||||
assertStreamMatches(avatar0bytes, attachment0From1.getStream());
|
||||
|
||||
// 1 also adds avatar
|
||||
String contentType1 = getRandomString(MAX_CONTENT_TYPE_BYTES);
|
||||
byte[] avatar1bytes = getRandomBytes(42);
|
||||
InputStream avatar1inputStream = new ByteArrayInputStream(avatar1bytes);
|
||||
avatarManager1.addAvatar(contentType1, avatar1inputStream);
|
||||
|
||||
// send the avatar from 1 to 0
|
||||
sync1To0(1, true);
|
||||
|
||||
// 0 sees 1's avatar now
|
||||
AttachmentHeader header1From0 =
|
||||
avatarManager0.getAvatarHeader(contact1From0);
|
||||
assertNotNull(header1From0);
|
||||
assertEquals(contentType1, header1From0.getContentType());
|
||||
assertNotNull(header1From0.getMessageId());
|
||||
|
||||
// 0 can retrieve 1's avatar
|
||||
Attachment attachment1From0 = avatarManager0.getAvatar(header1From0);
|
||||
assertEquals(contentType1,
|
||||
attachment1From0.getHeader().getContentType());
|
||||
assertStreamMatches(avatar1bytes, attachment1From0.getStream());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdatingAvatars() throws Exception {
|
||||
// 0 adds avatar
|
||||
byte[] avatar0bytes = getRandomBytes(42);
|
||||
InputStream avatar0inputStream = new ByteArrayInputStream(avatar0bytes);
|
||||
avatarManager0.addAvatar(contentType, avatar0inputStream);
|
||||
|
||||
// 0 can retrieve their own avatar
|
||||
AttachmentHeader header0 = avatarManager0.getMyAvatarHeader();
|
||||
assertNotNull(header0);
|
||||
Attachment attachment0 = avatarManager0.getAvatar(header0);
|
||||
assertStreamMatches(avatar0bytes, attachment0.getStream());
|
||||
|
||||
// send the avatar from 0 to 1
|
||||
sync0To1(1, true);
|
||||
|
||||
// 1 only sees 0's avatar
|
||||
AttachmentHeader header0From1 =
|
||||
avatarManager1.getAvatarHeader(contact0From1);
|
||||
assertNotNull(header0From1);
|
||||
Attachment attachment0From1 = avatarManager1.getAvatar(header0From1);
|
||||
assertStreamMatches(avatar0bytes, attachment0From1.getStream());
|
||||
|
||||
// 0 adds a new avatar
|
||||
byte[] avatar0bytes2 = getRandomBytes(42);
|
||||
InputStream avatar0inputStream2 =
|
||||
new ByteArrayInputStream(avatar0bytes2);
|
||||
avatarManager0.addAvatar(contentType, avatar0inputStream2);
|
||||
|
||||
// 0 now only sees their new avatar
|
||||
header0 = avatarManager0.getMyAvatarHeader();
|
||||
assertNotNull(header0);
|
||||
attachment0 = avatarManager0.getAvatar(header0);
|
||||
assertStreamMatches(avatar0bytes2, attachment0.getStream());
|
||||
|
||||
// send the new avatar from 0 to 1
|
||||
sync0To1(1, true);
|
||||
|
||||
// 1 only sees 0's new avatar
|
||||
header0From1 =
|
||||
avatarManager1.getAvatarHeader(contact0From1);
|
||||
assertNotNull(header0From1);
|
||||
attachment0From1 = avatarManager1.getAvatar(header0From1);
|
||||
assertStreamMatches(avatar0bytes2, attachment0From1.getStream());
|
||||
}
|
||||
|
||||
private void assertStreamMatches(byte[] bytes, InputStream inputStream) {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
copyAndClose(inputStream, outputStream);
|
||||
assertArrayEquals(bytes, outputStream.toByteArray());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package org.briarproject.briar.avatar;
|
||||
|
||||
import org.briarproject.bramble.api.data.BdfDictionary;
|
||||
import org.briarproject.bramble.api.data.BdfEntry;
|
||||
import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.data.BdfReader;
|
||||
import org.briarproject.bramble.api.data.BdfReaderFactory;
|
||||
import org.briarproject.bramble.api.data.MetadataEncoder;
|
||||
import org.briarproject.bramble.api.db.Metadata;
|
||||
import org.briarproject.bramble.api.sync.Group;
|
||||
import org.briarproject.bramble.api.sync.InvalidMessageException;
|
||||
import org.briarproject.bramble.api.sync.Message;
|
||||
import org.briarproject.bramble.api.sync.MessageContext;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||
import org.jmock.Expectations;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE;
|
||||
import static org.briarproject.bramble.test.TestUtils.getClientId;
|
||||
import static org.briarproject.bramble.test.TestUtils.getGroup;
|
||||
import static org.briarproject.bramble.test.TestUtils.getMessage;
|
||||
import static org.briarproject.bramble.util.StringUtils.getRandomString;
|
||||
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_CONTENT_TYPE_BYTES;
|
||||
import static org.briarproject.briar.avatar.AvatarConstants.MSG_KEY_VERSION;
|
||||
import static org.briarproject.briar.avatar.AvatarConstants.MSG_TYPE_UPDATE;
|
||||
import static org.briarproject.briar.media.MediaConstants.MSG_KEY_CONTENT_TYPE;
|
||||
import static org.briarproject.briar.media.MediaConstants.MSG_KEY_DESCRIPTOR_LENGTH;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class AvatarValidatorTest extends BrambleMockTestCase {
|
||||
|
||||
private final BdfReaderFactory bdfReaderFactory =
|
||||
context.mock(BdfReaderFactory.class);
|
||||
private final MetadataEncoder metadataEncoder =
|
||||
context.mock(MetadataEncoder.class);
|
||||
private final Clock clock = context.mock(Clock.class);
|
||||
private final BdfReader reader = context.mock(BdfReader.class);
|
||||
|
||||
private final Group group = getGroup(getClientId(), 123);
|
||||
private final Message message = getMessage(group.getId());
|
||||
private final long now = message.getTimestamp() + 1000;
|
||||
private final String contentType = getRandomString(MAX_CONTENT_TYPE_BYTES);
|
||||
private final long version = System.currentTimeMillis();
|
||||
private final BdfDictionary meta = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_VERSION, version),
|
||||
new BdfEntry(MSG_KEY_CONTENT_TYPE, contentType),
|
||||
// Descriptor length is zero as the test doesn't read from the
|
||||
// counting input stream
|
||||
new BdfEntry(MSG_KEY_DESCRIPTOR_LENGTH, 0L)
|
||||
);
|
||||
|
||||
private final AvatarValidator validator =
|
||||
new AvatarValidator(bdfReaderFactory, metadataEncoder, clock);
|
||||
|
||||
@Test(expected = InvalidMessageException.class)
|
||||
public void testRejectsFarFutureTimestamp() throws Exception {
|
||||
expectCheckTimestamp(message.getTimestamp() - MAX_CLOCK_DIFFERENCE - 1);
|
||||
|
||||
validator.validateMessage(message, group);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException.class)
|
||||
public void testRejectsEmptyBody() throws Exception {
|
||||
expectCheckTimestamp(now);
|
||||
expectParseList(new BdfList());
|
||||
|
||||
validator.validateMessage(message, group);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException.class)
|
||||
public void testRejectsTooShortBody() throws Exception {
|
||||
expectCheckTimestamp(now);
|
||||
expectParseList(BdfList.of(MSG_TYPE_UPDATE, version));
|
||||
|
||||
validator.validateMessage(message, group);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException.class)
|
||||
public void testRejectsUnknownMessageType() throws Exception {
|
||||
expectCheckTimestamp(now);
|
||||
expectParseList(BdfList.of(MSG_TYPE_UPDATE + 1, version, contentType));
|
||||
|
||||
validator.validateMessage(message, group);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException.class)
|
||||
public void testRejectsNonLongVersion() throws Exception {
|
||||
expectCheckTimestamp(now);
|
||||
expectParseList(BdfList.of(MSG_TYPE_UPDATE, "foo", contentType));
|
||||
|
||||
validator.validateMessage(message, group);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException.class)
|
||||
public void testRejectsNonStringContentType() throws Exception {
|
||||
expectCheckTimestamp(now);
|
||||
expectParseList(BdfList.of(MSG_TYPE_UPDATE, version, 1337));
|
||||
|
||||
validator.validateMessage(message, group);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException.class)
|
||||
public void testRejectsEmptyContentType() throws Exception {
|
||||
expectCheckTimestamp(now);
|
||||
expectParseList(BdfList.of(MSG_TYPE_UPDATE, version, ""));
|
||||
|
||||
validator.validateMessage(message, group);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException.class)
|
||||
public void testRejectsTooLongContentType() throws Exception {
|
||||
String contentType = getRandomString(MAX_CONTENT_TYPE_BYTES + 1);
|
||||
|
||||
expectCheckTimestamp(now);
|
||||
expectParseList(BdfList.of(MSG_TYPE_UPDATE, version, contentType));
|
||||
|
||||
validator.validateMessage(message, group);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException.class)
|
||||
public void testRejectsTooLongBody() throws Exception {
|
||||
expectCheckTimestamp(now);
|
||||
expectParseList(BdfList.of(MSG_TYPE_UPDATE, version, contentType, 1));
|
||||
|
||||
validator.validateMessage(message, group);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException.class)
|
||||
public void testRejectsNegativeVersion() throws Exception {
|
||||
expectCheckTimestamp(now);
|
||||
expectParseList(BdfList.of(MSG_TYPE_UPDATE, -1, contentType));
|
||||
|
||||
validator.validateMessage(message, group);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAcceptsUpdateMessage() throws Exception {
|
||||
testAcceptsUpdateMessage(
|
||||
BdfList.of(MSG_TYPE_UPDATE, version, contentType), meta);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAcceptsZeroVersion() throws Exception {
|
||||
BdfList body = BdfList.of(MSG_TYPE_UPDATE, 0L, contentType);
|
||||
BdfDictionary meta = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_VERSION, 0L),
|
||||
new BdfEntry(MSG_KEY_CONTENT_TYPE, contentType),
|
||||
new BdfEntry(MSG_KEY_DESCRIPTOR_LENGTH, 0L)
|
||||
);
|
||||
testAcceptsUpdateMessage(body, meta);
|
||||
}
|
||||
|
||||
private void testAcceptsUpdateMessage(BdfList body, BdfDictionary meta)
|
||||
throws Exception {
|
||||
expectCheckTimestamp(now);
|
||||
expectParseList(body);
|
||||
expectEncodeMetadata(meta);
|
||||
|
||||
MessageContext result = validator.validateMessage(message, group);
|
||||
assertEquals(0, result.getDependencies().size());
|
||||
}
|
||||
|
||||
private void expectCheckTimestamp(long now) {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(clock).currentTimeMillis();
|
||||
will(returnValue(now));
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectParseList(BdfList body) throws Exception {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(bdfReaderFactory).createReader(with(any(InputStream.class)));
|
||||
will(returnValue(reader));
|
||||
oneOf(reader).readList();
|
||||
will(returnValue(body));
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectEncodeMetadata(BdfDictionary meta) throws Exception {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(metadataEncoder).encode(meta);
|
||||
will(returnValue(new Metadata()));
|
||||
}});
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package org.briarproject.briar.introduction;
|
||||
|
||||
import org.briarproject.bramble.BrambleCoreModule;
|
||||
import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
|
||||
import org.briarproject.briar.avatar.AvatarModule;
|
||||
import org.briarproject.briar.blog.BlogModule;
|
||||
import org.briarproject.briar.client.BriarClientModule;
|
||||
import org.briarproject.briar.forum.ForumModule;
|
||||
@@ -19,6 +20,7 @@ import dagger.Component;
|
||||
@Component(modules = {
|
||||
BrambleCoreIntegrationTestModule.class,
|
||||
BrambleCoreModule.class,
|
||||
AvatarModule.class,
|
||||
BlogModule.class,
|
||||
BriarClientModule.class,
|
||||
ForumModule.class,
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.briarproject.bramble.api.identity.IdentityManager;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.properties.TransportPropertyManager;
|
||||
import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
|
||||
import org.briarproject.briar.api.avatar.AvatarManager;
|
||||
import org.briarproject.briar.api.blog.BlogFactory;
|
||||
import org.briarproject.briar.api.blog.BlogManager;
|
||||
import org.briarproject.briar.api.blog.BlogSharingManager;
|
||||
@@ -24,6 +25,7 @@ import org.briarproject.briar.api.messaging.MessagingManager;
|
||||
import org.briarproject.briar.api.messaging.PrivateMessageFactory;
|
||||
import org.briarproject.briar.api.privategroup.PrivateGroupManager;
|
||||
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager;
|
||||
import org.briarproject.briar.avatar.AvatarModule;
|
||||
import org.briarproject.briar.blog.BlogModule;
|
||||
import org.briarproject.briar.client.BriarClientModule;
|
||||
import org.briarproject.briar.forum.ForumModule;
|
||||
@@ -41,6 +43,7 @@ import dagger.Component;
|
||||
@Component(modules = {
|
||||
BrambleCoreIntegrationTestModule.class,
|
||||
BrambleCoreModule.class,
|
||||
AvatarModule.class,
|
||||
BlogModule.class,
|
||||
BriarClientModule.class,
|
||||
ForumModule.class,
|
||||
@@ -55,6 +58,8 @@ public interface BriarIntegrationTestComponent
|
||||
|
||||
void inject(BriarIntegrationTest<BriarIntegrationTestComponent> init);
|
||||
|
||||
void inject(AvatarModule.EagerSingletons init);
|
||||
|
||||
void inject(BlogModule.EagerSingletons init);
|
||||
|
||||
void inject(ForumModule.EagerSingletons init);
|
||||
@@ -75,6 +80,8 @@ public interface BriarIntegrationTestComponent
|
||||
|
||||
IdentityManager getIdentityManager();
|
||||
|
||||
AvatarManager getAvatarManager();
|
||||
|
||||
ClientHelper getClientHelper();
|
||||
|
||||
ContactManager getContactManager();
|
||||
@@ -117,6 +124,7 @@ public interface BriarIntegrationTestComponent
|
||||
BriarIntegrationTestComponent c) {
|
||||
BrambleCoreIntegrationTestEagerSingletons.Helper
|
||||
.injectEagerSingletons(c);
|
||||
c.inject(new AvatarModule.EagerSingletons());
|
||||
c.inject(new BlogModule.EagerSingletons());
|
||||
c.inject(new ForumModule.EagerSingletons());
|
||||
c.inject(new GroupInvitationModule.EagerSingletons());
|
||||
|
||||
Reference in New Issue
Block a user