/** * Copyright 2010 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.waveprotocol.wave.federation.xmpp; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableList; import com.google.protobuf.ByteString; import junit.framework.TestCase; import org.dom4j.Element; import org.mockito.ArgumentCaptor; import org.waveprotocol.wave.federation.ProtocolHashedVersionFactory; import org.waveprotocol.wave.federation.WaveletFederationListener; import org.waveprotocol.wave.federation.WaveletFederationProvider; import org.waveprotocol.wave.federation.FederationErrorProto.FederationError; import org.waveprotocol.wave.federation.Proto.ProtocolHashedVersion; import org.waveprotocol.wave.federation.Proto.ProtocolSignedDelta; import org.waveprotocol.wave.federation.Proto.ProtocolSignerInfo; import org.waveprotocol.wave.federation.WaveletFederationListener.WaveletUpdateCallback; import org.waveprotocol.wave.federation.WaveletFederationProvider.DeltaSignerInfoResponseListener; import org.waveprotocol.wave.federation.WaveletFederationProvider.HistoryResponseListener; import org.waveprotocol.wave.federation.WaveletFederationProvider.PostSignerInfoResponseListener; import org.waveprotocol.wave.federation.WaveletFederationProvider.SubmitResultListener; import org.waveprotocol.wave.model.id.WaveId; import org.waveprotocol.wave.model.id.WaveletId; import org.waveprotocol.wave.model.id.WaveletName; import org.waveprotocol.wave.model.id.URIEncoderDecoder.EncodingException; import org.xmpp.packet.IQ; import org.xmpp.packet.Message; import org.xmpp.packet.PacketError; import java.util.List; /** * Tests for {@link XmppFederationRemote}. * * TODO(thorogood,arb): This class actually test round-trips sent from an * XmppFederationRemote to a XmppFederationHost. * * @author arb@google.com (Anthony Baxter) * @author thorogood@google.com (Sam Thorogood) */ public class XmppFederationRemoteTest extends TestCase { private final static String LOCAL_DOMAIN = "acmewave.com"; private final static String LOCAL_JID = "wave." + LOCAL_DOMAIN; private final static String REMOTE_DOMAIN = "initech-corp.com"; private final static String REMOTE_JID = "wave." + REMOTE_DOMAIN; private final static WaveletName REMOTE_WAVELET = WaveletName.of(WaveId.of(REMOTE_DOMAIN, "wave"), WaveletId.of(REMOTE_DOMAIN, "wavelet")); private final static ProtocolHashedVersion START_VERSION = ProtocolHashedVersionFactory.createVersionZero(REMOTE_WAVELET); private final static ByteString DELTA_BYTESTRING = ByteString.copyFromUtf8("Irrelevant delta bytes"); private final static ProtocolHashedVersion VERSION_ONE = ProtocolHashedVersionFactory.create(DELTA_BYTESTRING, START_VERSION, 1); private final static ProtocolSignedDelta DUMMY_SIGNED_DELTA = ProtocolSignedDelta.newBuilder().setDelta(ByteString.copyFromUtf8("fake blahblah")).build(); private final static String TEST_ID = "1-1-sometestID"; private final static ByteString FAKE_SIGNER_ID = ByteString.copyFromUtf8("Hello Signer!"); private final static ProtocolSignerInfo FAKE_SIGNER_INFO = ProtocolSignerInfo.newBuilder() .setHashAlgorithm(ProtocolSignerInfo.HashAlgorithm.SHA256) .setDomain(REMOTE_DOMAIN) .addCertificate(ByteString.copyFromUtf8("Test certificate")).build(); private MockOutgoingPacketTransport transport; private WaveletFederationListener.Factory mockUpdateListenerFactory; private MockDisco disco; private XmppManager manager; private WaveletFederationProvider mockProvider; private WaveletFederationListener mockUpdateListener; // The remote represents the 'caller' for all unit tests in this class. private XmppFederationRemote remote; // The host represents the 'callee' for all unit tests in this class. private XmppFederationHost host; private static final String EXPECTED_RECEIPT_MESSAGE = "\n<message id=\"" + TEST_ID + "\" to=\"" + REMOTE_JID + "\"" + " from=\"" + LOCAL_JID + "\">\n" + " <received xmlns=\"urn:xmpp:receipts\"/>\n" + "</message>"; private static final String EXPECTED_SUBMIT_REQUEST; private static final String EXPECTED_HISTORY_REQUEST; static { try { String uri = XmppUtil.waveletNameCodec.waveletNameToURI(REMOTE_WAVELET); EXPECTED_SUBMIT_REQUEST = "\n<iq type=\"set\" id=\"" + TEST_ID + "\" from=\"" + LOCAL_JID + "\"" + " to=\"" + REMOTE_JID + "\">\n" + " <pubsub xmlns=\"http://jabber.org/protocol/pubsub\">\n" + " <publish node=\"wavelet\">\n" + " <item>\n" + " <submit-request xmlns=\"http://waveprotocol.org/protocol/0.2/waveserver\">\n" + " <delta wavelet-name=\"" + uri + "\">" + "<![CDATA[" + Base64Util.encode(DUMMY_SIGNED_DELTA) + "]]></delta>\n" + " </submit-request>\n" + " </item>\n" + " </publish>\n" + " </pubsub>\n" + "</iq>"; EXPECTED_HISTORY_REQUEST = "\n<iq type=\"get\" id=\"" + TEST_ID + "\" from=\"" + LOCAL_JID + "\"" + " to=\"" + REMOTE_JID + "\">\n" + " <pubsub xmlns=\"http://jabber.org/protocol/pubsub\">\n" + " <items node=\"wavelet\">\n" + " <delta-history xmlns=\"http://waveprotocol.org/protocol/0.2/waveserver\"" + " start-version=\"" + START_VERSION.getVersion() + "\"" + " start-version-hash=\"" + Base64Util.encode(START_VERSION.getHistoryHash()) + "\"" + " end-version=\"" + VERSION_ONE.getVersion() + "\"" + " end-version-hash=\"" + Base64Util.encode(VERSION_ONE.getHistoryHash()) + "\"" + " wavelet-name=\"" + uri + "\"/>\n" + " </items>\n" + " </pubsub>\n" + "</iq>"; } catch (EncodingException e) { throw new RuntimeException(e); } } @Override public void setUp() { XmppUtil.fakeUniqueId = TEST_ID; mockProvider = mock(WaveletFederationProvider.class); mockUpdateListener = mock(WaveletFederationListener.class); mockUpdateListenerFactory = mock(WaveletFederationListener.Factory.class); when(mockUpdateListenerFactory.listenerForDomain(eq(REMOTE_DOMAIN))) .thenReturn(mockUpdateListener); // Create mockDisco. It wants an XmppManager, but we don't need to set it here. disco = new MockDisco("irrelevant"); transport = new MockOutgoingPacketTransport(); remote = new XmppFederationRemote(mockUpdateListenerFactory, disco, LOCAL_JID); host = new XmppFederationHost(mockProvider, disco, REMOTE_JID); manager = new XmppManager(host, remote, disco, transport, LOCAL_JID); remote.setManager(manager); } /** * Tests that the constructor behaves as expected. */ public void testConstructor() { assertEquals(0, transport.packetsSent); } /** * Tests that a submit request from a local wave server is sent out to the * foreign federation host, and that the response from it is passed back to * the wave server. */ public void testSubmitRequest() { int OPS_APPLIED = 1; long TIMESTAMP_APPLIED = 123; ProtocolHashedVersion APPLIED_AT = ProtocolHashedVersion.newBuilder() .setVersion(VERSION_ONE.getVersion() + OPS_APPLIED) .setHistoryHash(ByteString.copyFromUtf8("blah")).build(); disco.testInjectInDomainToJidMap(REMOTE_DOMAIN, REMOTE_JID); SubmitResultListener listener = mock(SubmitResultListener.class); remote.submitRequest(REMOTE_WAVELET, DUMMY_SIGNED_DELTA, listener); verifyZeroInteractions(listener); assertEquals(1, transport.packetsSent); // Validate the outgoing request. IQ outgoingRequest = (IQ) transport.packets.poll(); assertEquals(EXPECTED_SUBMIT_REQUEST, outgoingRequest.toString()); // Send the outgoing request back to the manager, so it hooks up to the // Federation Host. manager.receivePacket(outgoingRequest); // Provide the remote's host with a dummy answer to verified input. ArgumentCaptor<SubmitResultListener> remoteListener = ArgumentCaptor.forClass(SubmitResultListener.class); verify(mockProvider) .submitRequest(eq(REMOTE_WAVELET), eq(DUMMY_SIGNED_DELTA), remoteListener.capture()); remoteListener.getValue().onSuccess(OPS_APPLIED, APPLIED_AT, TIMESTAMP_APPLIED); // Confirm that the packet has been sent back out over the transport. assertEquals(2, transport.packetsSent); IQ historyResponse = (IQ) transport.packets.poll(); manager.receivePacket(historyResponse); // Confirm that the success is finally delivered to the listener. verify(listener, never()).onFailure(any(FederationError.class)); verify(listener) .onSuccess(eq(OPS_APPLIED), any(ProtocolHashedVersion.class), eq(TIMESTAMP_APPLIED)); } /** * Tests that that a submit request sent out can properly process a resulting * error. */ public void testSubmitRequestError() { disco.testInjectInDomainToJidMap(REMOTE_DOMAIN, REMOTE_JID); SubmitResultListener listener = mock(SubmitResultListener.class); remote.submitRequest(REMOTE_WAVELET, DUMMY_SIGNED_DELTA, listener); verifyZeroInteractions(listener); assertEquals(1, transport.packetsSent); // Validate the outgoing request. IQ outgoingRequest = (IQ) transport.packets.poll(); assertEquals(EXPECTED_SUBMIT_REQUEST, outgoingRequest.toString()); // Return a confusing error response (<registration-required>). IQ errorResponse = IQ.createResultIQ(outgoingRequest); errorResponse.setError(PacketError.Condition.registration_required); manager.receivePacket(errorResponse); // Confirm error is passed through to the callback. ArgumentCaptor<FederationError> error = ArgumentCaptor.forClass(FederationError.class); verify(listener).onFailure(error.capture()); verify(listener, never()) .onSuccess(anyInt(), any(ProtocolHashedVersion.class), anyLong()); assertEquals(FederationError.Code.UNDEFINED_CONDITION, error.getValue().getErrorCode()); } /** * Tests that a submit request doesn't fall over if disco fails, but instead * passes an error back to the wave server. */ public void testSubmitRequestDiscoFailed() { disco.testInjectInDomainToJidMap(REMOTE_DOMAIN, null); SubmitResultListener listener = mock(SubmitResultListener.class); ProtocolSignedDelta signedDelta = ProtocolSignedDelta.newBuilder().setDelta(ByteString.copyFromUtf8("fake")).build(); remote.submitRequest(REMOTE_WAVELET, signedDelta, listener); verify(listener).onFailure(any(FederationError.class)); verify(listener, never()) .onSuccess(anyInt(), any(ProtocolHashedVersion.class), anyLong()); } /** * Tests that a history request from a local wave server is sent out to the * foreign federation host, and that the response from it is passed back to * the wave server. */ public void testHistoryRequest() { disco.testInjectInDomainToJidMap(REMOTE_DOMAIN, REMOTE_JID); // Send the outgoing request. Assert that a packet is sent and that no // callbacks have been invoked. HistoryResponseListener listener = mock(HistoryResponseListener.class); remote.requestHistory(REMOTE_WAVELET, REMOTE_DOMAIN, START_VERSION, VERSION_ONE, -1, listener); verifyZeroInteractions(listener); assertEquals(1, transport.packetsSent); // Validate the outgoing request. IQ outgoingRequest = (IQ) transport.packets.poll(); assertEquals(EXPECTED_HISTORY_REQUEST, outgoingRequest.toString()); // Send the outgoing request back to the manager, so it hooks up to the // Federation Host. manager.receivePacket(outgoingRequest); ArgumentCaptor<HistoryResponseListener> remoteListener = ArgumentCaptor.forClass(HistoryResponseListener.class); // TODO(thorogood): Note that the caller's JID is not the domain we expect // here - it is not actually the domain of the requester! verify(mockProvider).requestHistory(eq(REMOTE_WAVELET), eq(LOCAL_JID), eq(START_VERSION), eq(VERSION_ONE), anyInt(), remoteListener.capture()); remoteListener.getValue().onSuccess(ImmutableList.of(DELTA_BYTESTRING), VERSION_ONE, 0); // Confirm that the packet has been sent back out over the transport. assertEquals(2, transport.packetsSent); IQ historyResponse = (IQ) transport.packets.poll(); manager.receivePacket(historyResponse); // Confirm that the success is finally delivered to the listener. ArgumentCaptor<ProtocolHashedVersion> commitVersion = ArgumentCaptor.forClass(ProtocolHashedVersion.class); verify(listener, never()).onFailure(any(FederationError.class)); verify(listener).onSuccess(eq(ImmutableList.of(DELTA_BYTESTRING)), commitVersion.capture(), anyInt()); // Confirm that the returned commit notice matches the expected value. // TODO(thorogood): We don't transfer the history hash over the wire. assertEquals(VERSION_ONE.getVersion(), commitVersion.getValue().getVersion()); assertEquals(ByteString.EMPTY, commitVersion.getValue().getHistoryHash()); } /** * Helper method wrapping an unchecked mock conversion. */ @SuppressWarnings("unchecked") private static List<ByteString> anyListByteString() { return any(List.class); } /** * Tests that a submit request doesn't fall over if disco fails, but instead * passes an error back to the wave server. */ public void testHistoryRequestDiscoFailed() { disco.testInjectInDomainToJidMap(REMOTE_DOMAIN, null); HistoryResponseListener listener = mock(HistoryResponseListener.class); ProtocolSignedDelta signedDelta = ProtocolSignedDelta.newBuilder().setDelta(ByteString.copyFromUtf8("fake")).build(); remote.requestHistory(REMOTE_WAVELET, REMOTE_DOMAIN, START_VERSION, VERSION_ONE, -1, listener); verify(listener).onFailure(any(FederationError.class)); verify(listener, never()) .onSuccess(anyListByteString(), any(ProtocolHashedVersion.class), anyLong()); } /** * Test a successful get signer. */ public void testGetSigner() { disco.testInjectInDomainToJidMap(REMOTE_DOMAIN, REMOTE_JID); // Send the outgoing request. Assert that a packet is sent and that no // callbacks have been invoked. DeltaSignerInfoResponseListener listener = mock(DeltaSignerInfoResponseListener.class); remote.getDeltaSignerInfo(FAKE_SIGNER_ID, REMOTE_WAVELET, VERSION_ONE, listener); verifyZeroInteractions(listener); assertEquals(1, transport.packetsSent); // Validate the outgoing request. IQ outgoingRequest = (IQ) transport.packets.poll(); //assertEquals(EXPECTED_HISTORY_REQUEST, outgoingRequest.toString()); // Send the outgoing request back to the manager, so it hooks up to the // Federation Host. manager.receivePacket(outgoingRequest); ArgumentCaptor<DeltaSignerInfoResponseListener> remoteListener = ArgumentCaptor.forClass(DeltaSignerInfoResponseListener.class); verify(mockProvider).getDeltaSignerInfo(eq(FAKE_SIGNER_ID), eq(REMOTE_WAVELET), eq(VERSION_ONE), remoteListener.capture()); remoteListener.getValue().onSuccess(FAKE_SIGNER_INFO); // Confirm that the packet has been sent back out over the transport. assertEquals(2, transport.packetsSent); IQ historyResponse = (IQ) transport.packets.poll(); manager.receivePacket(historyResponse); // Confirm that the success is finally delivered to the listener. verify(listener, never()).onFailure(any(FederationError.class)); verify(listener).onSuccess(eq(FAKE_SIGNER_INFO)); } /** * Test a successful post signer. */ public void testPostSigner() { disco.testInjectInDomainToJidMap(REMOTE_DOMAIN, REMOTE_JID); // Send the outgoing request. Assert that a packet is sent and that no // callbacks have been invoked. PostSignerInfoResponseListener listener = mock(PostSignerInfoResponseListener.class); remote.postSignerInfo(REMOTE_DOMAIN, FAKE_SIGNER_INFO, listener); verifyZeroInteractions(listener); assertEquals(1, transport.packetsSent); // Validate the outgoing request. IQ outgoingRequest = (IQ) transport.packets.poll(); //assertEquals(EXPECTED_HISTORY_REQUEST, outgoingRequest.toString()); // Send the outgoing request back to the manager, so it hooks up to the // Federation Host. manager.receivePacket(outgoingRequest); ArgumentCaptor<PostSignerInfoResponseListener> remoteListener = ArgumentCaptor.forClass(PostSignerInfoResponseListener.class); verify(mockProvider).postSignerInfo(eq(REMOTE_DOMAIN), eq(FAKE_SIGNER_INFO), remoteListener.capture()); remoteListener.getValue().onSuccess(); // Confirm that the packet has been sent back out over the transport. assertEquals(2, transport.packetsSent); IQ historyResponse = (IQ) transport.packets.poll(); manager.receivePacket(historyResponse); // Confirm that the success is finally delivered to the listener. verify(listener, never()).onFailure(any(FederationError.class)); verify(listener).onSuccess(); } /** * Tests an update message containing both a delta and commit notice from a * foreign federation host is correctly decoded and passed to the Update * Listener Factory, and a response is sent as requested. */ public void testUpdate() throws EncodingException { Message updateMessage = new Message(); Element waveletUpdate = addWaveletUpdate(updateMessage, true); // request receipt waveletUpdate.addElement("applied-delta").addCDATA(Base64Util.encode(DELTA_BYTESTRING)); waveletUpdate.addElement("commit-notice") .addAttribute("version", String.valueOf(VERSION_ONE.getVersion())) .addAttribute("history-hash", Base64Util.encode(VERSION_ONE.getHistoryHash())); manager.receivePacket(updateMessage); ArgumentCaptor<WaveletUpdateCallback> deltaCallback = ArgumentCaptor.forClass(WaveletUpdateCallback.class); List<ByteString> expected = ImmutableList.of(DELTA_BYTESTRING); verify(mockUpdateListener).waveletDeltaUpdate(eq(REMOTE_WAVELET), eq(expected), deltaCallback.capture()); deltaCallback.getValue().onSuccess(); assertEquals(0, transport.packetsSent); // Callback has only been invoked once. ArgumentCaptor<WaveletUpdateCallback> commitCallback = ArgumentCaptor.forClass(WaveletUpdateCallback.class); verify(mockUpdateListener).waveletCommitUpdate(eq(REMOTE_WAVELET), eq(VERSION_ONE), commitCallback.capture()); commitCallback.getValue().onSuccess(); assertEquals(1, transport.packetsSent); // Callback has been invoked twice, expect receipt. assertEquals(EXPECTED_RECEIPT_MESSAGE, transport.lastPacketSent.toString()); } /** * Test that a single update message, where a receipt is not requested, is * correctly received and processed. */ public void testUpdateNoReceipt() throws EncodingException { Message updateMessage = new Message(); Element waveletUpdate = addWaveletUpdate(updateMessage, false); waveletUpdate.addElement("applied-delta").addCDATA(Base64Util.encode(DELTA_BYTESTRING)); manager.receivePacket(updateMessage); ArgumentCaptor<WaveletUpdateCallback> deltaCallback = ArgumentCaptor.forClass(WaveletUpdateCallback.class); List<ByteString> expected = ImmutableList.of(DELTA_BYTESTRING); verify(mockUpdateListener).waveletDeltaUpdate(eq(REMOTE_WAVELET), eq(expected), deltaCallback.capture()); deltaCallback.getValue().onSuccess(); assertEquals(0, transport.packetsSent); // Do not expect a callback. } /** * Add a single wavelet-update message to the given Message. Should (probably) * not be called twice on the same Message. */ private Element addWaveletUpdate(Message updateMessage, boolean requestReceipt) throws EncodingException { updateMessage.setFrom(REMOTE_JID); updateMessage.setTo(LOCAL_JID); updateMessage.setID(TEST_ID); if (requestReceipt) { updateMessage.addChildElement("request", XmppNamespace.NAMESPACE_XMPP_RECEIPTS); } Element event = updateMessage.addChildElement("event", XmppNamespace.NAMESPACE_PUBSUB_EVENT); Element waveletUpdate = event.addElement("items").addElement("item").addElement("wavelet-update"); waveletUpdate.addAttribute("wavelet-name", XmppUtil.waveletNameCodec.waveletNameToURI(REMOTE_WAVELET)); return waveletUpdate; } }