Make stored messages REST accessible.

Add REST endpoints for retrieving and acknowledging pending
messges, beyond the WebSocket.

// FREEBIE
This commit is contained in:
Moxie Marlinspike
2015-04-15 16:19:07 -07:00
parent 53e7ffa311
commit db6ee8f687
14 changed files with 412 additions and 150 deletions

View File

@@ -20,9 +20,11 @@ import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import javax.ws.rs.core.MediaType;
@@ -45,8 +47,10 @@ public class FederatedControllerTest {
private static final String MULTI_DEVICE_RECIPIENT = "+14152222222";
private PushSender pushSender = mock(PushSender.class );
private ReceiptSender receiptSender = mock(ReceiptSender.class);
private FederatedClientManager federatedClientManager = mock(FederatedClientManager.class);
private AccountsManager accountsManager = mock(AccountsManager.class );
private MessagesManager messagesManager = mock(MessagesManager.class);
private RateLimiters rateLimiters = mock(RateLimiters.class );
private RateLimiter rateLimiter = mock(RateLimiter.class );
@@ -55,7 +59,7 @@ public class FederatedControllerTest {
private final ObjectMapper mapper = new ObjectMapper();
private final MessageController messageController = new MessageController(rateLimiters, pushSender, accountsManager, federatedClientManager);
private final MessageController messageController = new MessageController(rateLimiters, pushSender, receiptSender, accountsManager, messagesManager, federatedClientManager);
private final KeysControllerV2 keysControllerV2 = mock(KeysControllerV2.class);
@Rule

View File

@@ -3,6 +3,7 @@ package org.whispersystems.textsecuregcm.tests.controllers;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional;
import com.sun.jersey.api.client.ClientResponse;
import org.hamcrest.CoreMatchers;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -10,19 +11,26 @@ import org.whispersystems.textsecuregcm.controllers.MessageController;
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.MessageProtos;
import org.whispersystems.textsecuregcm.entities.MismatchedDevices;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.entities.StaleDevices;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import javax.ws.rs.core.MediaType;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -30,6 +38,7 @@ import io.dropwizard.testing.junit.ResourceTestRule;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;
@@ -42,8 +51,10 @@ public class MessageControllerTest {
private static final String MULTI_DEVICE_RECIPIENT = "+14152222222";
private final PushSender pushSender = mock(PushSender.class );
private final ReceiptSender receiptSender = mock(ReceiptSender.class);
private final FederatedClientManager federatedClientManager = mock(FederatedClientManager.class);
private final AccountsManager accountsManager = mock(AccountsManager.class );
private final MessagesManager messagesManager = mock(MessagesManager.class);
private final RateLimiters rateLimiters = mock(RateLimiters.class );
private final RateLimiter rateLimiter = mock(RateLimiter.class );
@@ -52,8 +63,8 @@ public class MessageControllerTest {
@Rule
public final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthenticator())
.addResource(new MessageController(rateLimiters, pushSender, accountsManager,
federatedClientManager))
.addResource(new MessageController(rateLimiters, pushSender, receiptSender, accountsManager,
messagesManager, federatedClientManager))
.build();
@@ -175,4 +186,75 @@ public class MessageControllerTest {
}
@Test
public synchronized void testGetMessages() throws Exception {
final long timestampOne = 313377;
final long timestampTwo = 313388;
List<OutgoingMessageEntity> messages = new LinkedList<OutgoingMessageEntity>() {{
add(new OutgoingMessageEntity(1L, MessageProtos.OutgoingMessageSignal.Type.CIPHERTEXT_VALUE, null, timestampOne, "+14152222222", 2, "hi there".getBytes()));
add(new OutgoingMessageEntity(2L, MessageProtos.OutgoingMessageSignal.Type.RECEIPT_VALUE, null, timestampTwo, "+14152222222", 2, null));
}};
when(messagesManager.getMessagesForDevice(eq(AuthHelper.VALID_NUMBER), eq(1L))).thenReturn(messages);
OutgoingMessageEntityList response =
resources.client().resource("/v1/messages/")
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.accept(MediaType.APPLICATION_JSON_TYPE)
.get(OutgoingMessageEntityList.class);
assertEquals(response.getMessages().size(), 2);
assertEquals(response.getMessages().get(0).getId(), 0);
assertEquals(response.getMessages().get(1).getId(), 0);
assertEquals(response.getMessages().get(0).getTimestamp(), timestampOne);
assertEquals(response.getMessages().get(1).getTimestamp(), timestampTwo);
}
@Test
public synchronized void testDeleteMessages() throws Exception {
long timestamp = System.currentTimeMillis();
when(messagesManager.delete(AuthHelper.VALID_NUMBER, 31337))
.thenReturn(Optional.of(new OutgoingMessageEntity(31337L,
MessageProtos.OutgoingMessageSignal.Type.CIPHERTEXT_VALUE,
null, timestamp,
"+14152222222", 1, "hi".getBytes())));
when(messagesManager.delete(AuthHelper.VALID_NUMBER, 31338))
.thenReturn(Optional.of(new OutgoingMessageEntity(31337L,
MessageProtos.OutgoingMessageSignal.Type.RECEIPT_VALUE,
null, System.currentTimeMillis(),
"+14152222222", 1, null)));
when(messagesManager.delete(AuthHelper.VALID_NUMBER, 31339))
.thenReturn(Optional.<OutgoingMessageEntity>absent());
ClientResponse response = resources.client().resource(String.format("/v1/messages/%d", 31337))
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.delete(ClientResponse.class);
assertThat("Good Response Code", response.getStatus(), is(equalTo(204)));
verify(receiptSender).sendReceipt(any(Account.class), eq("+14152222222"), eq(timestamp), eq(Optional.<String>absent()));
response = resources.client().resource(String.format("/v1/messages/%d", 31338))
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.delete(ClientResponse.class);
assertThat("Good Response Code", response.getStatus(), is(equalTo(204)));
verifyNoMoreInteractions(receiptSender);
response = resources.client().resource(String.format("/v1/messages/%d", 31339))
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.delete(ClientResponse.class);
assertThat("Good Response Code", response.getStatus(), is(equalTo(204)));
verifyNoMoreInteractions(receiptSender);
}
}

View File

@@ -10,6 +10,7 @@ import org.whispersystems.textsecuregcm.controllers.ReceiptController;
import org.whispersystems.textsecuregcm.entities.MessageProtos;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
@@ -32,12 +33,14 @@ public class ReceiptControllerTest {
private final FederatedClientManager federatedClientManager = mock(FederatedClientManager.class);
private final AccountsManager accountsManager = mock(AccountsManager.class );
private final ReceiptSender receiptSender = new ReceiptSender(accountsManager, pushSender, federatedClientManager);
private final ObjectMapper mapper = new ObjectMapper();
@Rule
public final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthenticator())
.addResource(new ReceiptController(accountsManager, federatedClientManager, pushSender))
.addResource(new ReceiptController(receiptSender))
.build();
@Before

View File

@@ -9,7 +9,9 @@ import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
@@ -57,12 +59,13 @@ public class WebSocketConnectionTest {
private static final Device device = mock(Device.class );
private static final UpgradeRequest upgradeRequest = mock(UpgradeRequest.class );
private static final PushSender pushSender = mock(PushSender.class);
private static final ReceiptSender receiptSender = mock(ReceiptSender.class);
@Test
public void testCredentials() throws Exception {
MessagesManager storedMessages = mock(MessagesManager.class);
WebSocketAccountAuthenticator webSocketAuthenticator = new WebSocketAccountAuthenticator(accountAuthenticator);
AuthenticatedConnectListener connectListener = new AuthenticatedConnectListener(accountsManager, pushSender, storedMessages, pubSubManager);
AuthenticatedConnectListener connectListener = new AuthenticatedConnectListener(accountsManager, pushSender, receiptSender, storedMessages, pubSubManager);
WebSocketSessionContext sessionContext = mock(WebSocketSessionContext.class);
when(accountAuthenticator.authenticate(eq(new BasicCredentials(VALID_USER, VALID_PASSWORD))))
@@ -98,10 +101,10 @@ public class WebSocketConnectionTest {
public void testOpen() throws Exception {
MessagesManager storedMessages = mock(MessagesManager.class);
List<Pair<Long, OutgoingMessageSignal>> outgoingMessages = new LinkedList<Pair<Long, OutgoingMessageSignal>> () {{
add(new Pair<>(1L, createMessage("sender1", 1111, false, "first")));
add(new Pair<>(2L, createMessage("sender1", 2222, false, "second")));
add(new Pair<>(3L, createMessage("sender2", 3333, false, "third")));
List<OutgoingMessageEntity> outgoingMessages = new LinkedList<OutgoingMessageEntity> () {{
add(createMessage(1L, "sender1", 1111, false, "first"));
add(createMessage(2L, "sender1", 2222, false, "second"));
add(createMessage(3L, "sender2", 3333, false, "third"));
}};
when(device.getId()).thenReturn(2L);
@@ -139,7 +142,7 @@ public class WebSocketConnectionTest {
});
WebsocketAddress websocketAddress = new WebsocketAddress(account.getNumber(), device.getId());
WebSocketConnection connection = new WebSocketConnection(accountsManager, pushSender, storedMessages,
WebSocketConnection connection = new WebSocketConnection(pushSender, receiptSender, storedMessages,
account, device, client);
connection.onDispatchSubscribed(websocketAddress.serialize());
@@ -154,25 +157,29 @@ public class WebSocketConnectionTest {
futures.get(0).setException(new IOException());
futures.get(2).setException(new IOException());
List<OutgoingMessageSignal> pending = new LinkedList<OutgoingMessageSignal>() {{
add(createMessage("sender1", 1111, false, "first"));
add(createMessage("sender2", 3333, false, "third"));
}};
// List<OutgoingMessageSignal> pending = new LinkedList<OutgoingMessageSignal>() {{
// add(createMessage("sender1", 1111, false, "first"));
// add(createMessage("sender2", 3333, false, "third"));
// }};
verify(pushSender, times(1)).sendMessage(eq(sender1), eq(sender1device), any(OutgoingMessageSignal.class));
verify(storedMessages, times(1)).delete(eq(2L));
// verify(pushSender, times(1)).sendMessage(eq(sender1), eq(sender1device), any(OutgoingMessageSignal.class));
connection.onDispatchUnsubscribed(websocketAddress.serialize());
verify(client).close(anyInt(), anyString());
}
private OutgoingMessageSignal createMessage(String sender, long timestamp, boolean receipt, String content) {
return OutgoingMessageSignal.newBuilder()
.setSource(sender)
.setSourceDevice(1)
.setType(receipt ? OutgoingMessageSignal.Type.RECEIPT_VALUE : OutgoingMessageSignal.Type.CIPHERTEXT_VALUE)
.setTimestamp(timestamp)
.setMessage(ByteString.copyFrom(content.getBytes()))
.build();
private OutgoingMessageEntity createMessage(long id, String sender, long timestamp, boolean receipt, String content) {
return new OutgoingMessageEntity(id, receipt ? OutgoingMessageSignal.Type.RECEIPT_VALUE : OutgoingMessageSignal.Type.CIPHERTEXT_VALUE,
null, timestamp, sender, 1, content.getBytes());
// return OutgoingMessageSignal.newBuilder()
// .setSource(sender)
// .setSourceDevice(1)
// .setType(receipt ? OutgoingMessageSignal.Type.RECEIPT_VALUE : OutgoingMessageSignal.Type.CIPHERTEXT_VALUE)
// .setTimestamp(timestamp)
// .setMessage(ByteString.copyFrom(content.getBytes()))
// .build();
}
}