/*
* Copyright (C) 2009 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 com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import junit.framework.TestCase;
import org.apache.commons.codec.binary.Base64;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.jivesoftware.whack.ExternalComponentManager;
import org.waveprotocol.wave.federation.FederationErrorProto.FederationError;
import org.waveprotocol.wave.federation.FederationURICodec;
import org.waveprotocol.wave.federation.Proto.ProtocolAppliedWaveletDelta;
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.Proto.ProtocolWaveletDelta;
import org.waveprotocol.wave.model.id.WaveletName;
import org.waveprotocol.wave.waveserver.ProtocolHashedVersionFactory;
import org.waveprotocol.wave.waveserver.SubmitResultListener;
import org.waveprotocol.wave.waveserver.WaveletFederationListener;
import org.waveprotocol.wave.waveserver.WaveletFederationProvider;
import org.xmpp.component.Component;
import org.xmpp.packet.IQ;
import org.xmpp.packet.Message;
import org.xmpp.packet.Packet;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility functions and mock classes for testing XMPP Federation code.
*/
class XmppTestUtil extends TestCase {
static final String TEST_AUTHOR = "fozzie@initech-corp.com";
// TODO: Set correct hashes
static final String TEST_IQ_ID = "1-1";
static final long TEST_LENGTH_LIMIT = 300000;
static final String TEST_LOCAL_JID = "wave.initech-corp.com";
static final String TEST_LOCAL_DOMAIN = "initech-corp.com";
static final int TEST_OPERATIONS = 2;
static final String TEST_REMOTE_DOMAIN = "acmewave.com";
static final String TEST_REMOTE_PUBSUB_JID = "pubsub.acmewave.com";
static final String TEST_REMOTE_WAVE_JID = "wave.acmewave.com";
// TODO: Set correct hashes
// Proto.ProtocolHashedVersion.newBuilder().setVersion(12).setHistoryHash("hash-12").build();
static final long TEST_TIMESTAMP = 1234567890;
static final long TEST_TRUNCATED = 2300;
static final int TEST_VERSION = 1234;
private static final Pattern cDataPattern =
Pattern.compile("(.*!\\[CDATA\\[)(.*)(\\]\\].*)", Pattern.DOTALL
| Pattern.MULTILINE);
static final WaveletName waveletName =
WaveletName.of("initech-corp.com!a", "acmewave.com!b");
private static final FederationURICodec codec = new FederationURICodec();
private static final ProtocolHashedVersion versionZero =
ProtocolHashedVersion.newBuilder().setVersion(0).setHistoryHash(
ByteString.copyFromUtf8(codec.encode(waveletName))).build();
static final ProtocolHashedVersion TEST_START_VERSION = versionZero;
static final ProtocolHashedVersion TEST_END_VERSION =
ProtocolHashedVersionFactory.create(ByteString.copyFromUtf8("foo"),
TEST_START_VERSION, 25);
static final String TEST_HISTORY_HASH =
Base64Util.encode(TEST_END_VERSION.toByteArray());
// The historyHash isn't set. It's not passed across in the XMPP.
static final ProtocolHashedVersion TEST_COMMITTED =
ProtocolHashedVersion.newBuilder().setVersion(25).setHistoryHash(
ByteString.copyFromUtf8("")).build();
// TODO(arb): parameterise more of these canned responses.
static final String EXPECTED_SUBMIT_RESPONSE =
"\n<iq type=\"result\" id=\"1-1\" from=\"wave.initech-corp.com\""
+ " to=\"wave.acmewave.com\">\n"
+ " <pubsub xmlns=\"http://jabber.org/protocol/pubsub\">\n"
+ " <publish>\n"
+ " <item>\n"
+ " <submit-response"
+ " xmlns=\"http://waveprotocol.org/protocol/0.2/waveserver\""
+ " application-timestamp=\"1234567890\""
+ " operations-applied=\"2\">\n"
+ " <hashed-version history-hash=\""
+ "PFEbgrja3S/gehM3fT8P+YqbYIA=" + "\" version=\"25\"/>\n"
+ " </submit-response>\n" + " </item>\n"
+ " </publish>\n" + " </pubsub>\n" + "</iq>";
static final String EXPECTED_HISTORY_RESPONSE =
"\n<iq type=\"result\" id=\"1-1\" from=\"wave.initech-corp.com\""
+ " to=\"wave.acmewave.com\">\n"
+ " <pubsub xmlns=\"http://jabber.org/protocol/pubsub\">\n"
+ " <items>\n"
+ " <item>\n"
+ " <applied-delta"
+ " xmlns=\"http://waveprotocol.org/protocol/0.2/waveserver\">"
+ "<![CDATA[ignored]]></applied-delta>\n"
+ " </item>\n"
+ " <item>\n"
+ " <commit-notice"
+ " xmlns=\"http://waveprotocol.org/protocol/0.2/waveserver\""
+ " version=\"25\"/>\n"
+ " </item>\n"
+ " <item>\n"
+ " <history-truncated"
+ " xmlns=\"http://waveprotocol.org/protocol/0.2/waveserver\""
+ " version=\"2300\"/>\n" + " </item>\n"
+ " </items>\n" + " </pubsub>\n" + "</iq>";
private static final String TEST_FAKE_CERTIFICATE =
"CERTIFICATE OF PARTICIPATION";
private static final String TEST_FAKE_CERTIFICATE_2 = "BEST CHOCOLATE CAKE";
public static ByteString TEST_SIGNER_ID =
ByteString.copyFromUtf8("user@acmewave.com");
public void testNothing() {
/** Placeholder to shut up over eager test runners */
}
/**
* Asserts that two XML strings are equal, after replacing CDATA elements with
* a fixed string.
*
* @param expected the expected result
* @param actual the actual result
*/
static void assertEqualsWithoutCData(String expected, String actual) {
assertEquals(stripCData(expected), stripCData(actual));
}
/**
* Extracts the first CDATA from an XML string
*
* @param original the original string
* @return the extracted string, or null if no CDATA found.
*/
static String extractCData(String original) {
Matcher matcher = cDataPattern.matcher(original);
if (matcher.find()) {
return matcher.group(2);
} else {
return null;
}
}
/**
* Strips the first CDATA from an XML string.
*
* @param original the original string
* @return the stripped string.
*/
static String stripCData(String original) {
Matcher matcher = cDataPattern.matcher(original);
if (matcher.find()) {
return matcher.group(1) + matcher.group(3);
} else {
return original;
}
}
/**
* Turns an XML string into the correct subclass of Packet.
*
* @param xml the XML string
* @return an instance of a Packet subclass
*/
static Packet xmlToPacket(String xml) {
SAXReader reader = new SAXReader();
Element root;
try {
root = reader.read(xml).getRootElement();
} catch (DocumentException e) {
fail("invalid XML: " + xml);
return null;
}
String tag = root.getQName().getName();
if (tag.equals("message")) {
return new Message(root);
} else if (tag.equals("iq")) {
return new IQ(root);
} else {
fail("unsupported packet type: " + tag);
return null;
}
}
/**
* Methods that create and test standard canned protobuffers.
*/
/**
* Creates a ByteString representation of a ProtocolAppliedWaveletDelta for
* use in tests.
*
* @return the new PB
*/
static ByteString createTestAppliedWaveletDelta() {
ProtocolHashedVersion hashedVersion = createTestHistoryHashVersion();
ProtocolAppliedWaveletDelta.Builder appliedDelta =
ProtocolAppliedWaveletDelta.newBuilder();
appliedDelta.setHashedVersionAppliedAt(hashedVersion);
appliedDelta.setApplicationTimestamp(TEST_TIMESTAMP);
appliedDelta.setOperationsApplied(TEST_OPERATIONS);
ProtocolSignedDelta delta = createTestSignedDelta();
appliedDelta.setSignedOriginalDelta(delta);
return appliedDelta.build().toByteString();
}
/**
* Creates a ProtocolHashedVersion for use in tests.
*
* @return the new PB
*/
static ProtocolHashedVersion createTestHistoryHashVersion() {
return TEST_END_VERSION;
}
/**
* Creates a test ProtocolSignerInfo protobuffer.
*
* @return the new PB
*/
public static ProtocolSignerInfo createProtocolSignerInfo() {
ProtocolSignerInfo.Builder signer =
ProtocolSignerInfo.newBuilder();
signer.addCertificate(ByteString.copyFromUtf8(TEST_FAKE_CERTIFICATE));
signer.addCertificate(ByteString.copyFromUtf8(TEST_FAKE_CERTIFICATE_2));
signer.setDomain(TEST_LOCAL_DOMAIN);
signer.setHashAlgorithm(ProtocolSignerInfo.HashAlgorithm.SHA256);
return signer.build();
}
/**
* Creates a ProtocolSignedDelta PB for use in tests.
*
* @return the new PB
*/
static ProtocolSignedDelta createTestSignedDelta() {
ProtocolHashedVersion hashedVersion = createTestHistoryHashVersion();
ProtocolSignedDelta.Builder delta =
ProtocolSignedDelta.newBuilder();
ProtocolWaveletDelta.Builder waveletDelta =
ProtocolWaveletDelta.newBuilder();
waveletDelta.setAuthor(TEST_AUTHOR);
waveletDelta.setHashedVersion(hashedVersion);
delta.setDelta(waveletDelta.build().toByteString());
return delta.build();
}
/**
* Checks that the signed delta from the packet is the same as the one we
* created in createTestAppliedWaveletDelta.
*
* @param base64 base64 encoded PB.
*/
static void verifyTestAppliedWaveletDelta(String base64) {
try {
ProtocolAppliedWaveletDelta.parseFrom(Base64.decodeBase64(base64.getBytes()));
} catch (InvalidProtocolBufferException e) {
fail("String not valid base64 PB: '" + base64 + "'");
}
// TODO: implement field checks.
}
/**
* Checks that the signed delta from the packet is the same as the one we
* created in createTestSignedDelta.
*
* @param base64 base64 encoded PB.toByteArray()
*/
static void verifyTestSignedDelta(String base64) {
try {
ProtocolSignedDelta.parseFrom(Base64.decodeBase64(base64.getBytes()));
} catch (InvalidProtocolBufferException e) {
fail("String not valid base64 PB: '" + base64 + "'");
}
// TODO: implement field checks
}
/**
* Mock classes that record the parameters passed to interesting methods.
*/
static class MockDeltaSignerResponseListener implements
WaveletFederationProvider.DeltaSignerInfoResponseListener {
public ProtocolSignerInfo signerInfo = null;
public FederationError error = null;
public void onSuccess(ProtocolSignerInfo signerInfo) {
this.signerInfo = signerInfo;
}
public void onFailure(FederationError error) {
this.error = error;
}
}
/**
* A mock of XMPPDisco that allows controlled triggering of disco completion.
*/
static class MockDisco extends XmppDisco {
String remoteJID;
boolean discoStarted = false;
String savedRemoteDomain;
private SuccessFailCallback<String, String> callback;
private static final String TEST_DESCRIPTION = "Test Wave Server" ;
/**
* Constructor.
*
* @param scheduledExecutor an executor, used for retransmits.
*/
public MockDisco() {
super(TEST_DESCRIPTION);
}
@Override
public void discoverRemoteJid(String remoteDomain,
SuccessFailCallback<String, String> callback) {
discoStarted = true;
this.savedRemoteDomain = remoteDomain;
this.callback = callback;
}
void discoComplete() {
if (remoteJID == null) {
callback.onFailure("MockDisco fail");
} else {
callback.onSuccess(remoteJID);
}
}
void setRemoteJID(String jid) {
this.remoteJID = jid;
}
}
/**
* A mock ExternalComponentManager that tracks how many packets are sent
*/
static class MockExternalComponentManager extends ExternalComponentManager {
int packetsSent = 0;
public MockExternalComponentManager(String ip) {
super(ip);
}
@Override
public void sendPacket(Component component, Packet packet) {
packetsSent++;
}
}
/**
* A mock HistoryResponseListener that saves the passed arguments.
*/
static class MockHistoryResponseListener implements
WaveletFederationProvider.HistoryResponseListener {
public List<ByteString> savedDeltaList = null;
public ProtocolHashedVersion savedCommittedVersion = null;
public Long savedVersionTruncated = null;
public FederationError savedError = null;
@Override
public void onSuccess(List<ByteString> deltaList,
ProtocolHashedVersion lastCommittedVersion, long versionTruncatedAt) {
savedDeltaList = deltaList;
savedCommittedVersion = lastCommittedVersion;
savedVersionTruncated = versionTruncatedAt;
}
// @Override
// public void onSuccess(Set<Proto.ProtocolAppliedWaveletDelta> deltaSet,
// long lastCommittedVersion) {
// savedDeltaSet = deltaSet;
// savedCommittedVersion = lastCommittedVersion;
// }
@Override
public void onFailure(FederationError error) {
savedError = error;
}
}
/**
* A mock of WaveletFederationProvider that saves the arguments passed to it.
*/
static class MockProvider implements WaveletFederationProvider {
public WaveletName savedWaveletName;
public String savedDomain;
public ProtocolHashedVersion savedStartVersion;
public ProtocolHashedVersion savedEndVersion;
public long savedLengthLimit;
public HistoryResponseListener savedHistoryListener;
public ProtocolSignedDelta savedDelta;
public SubmitResultListener savedSubmitListener;
public ProtocolHashedVersion savedSignerRequestDelta = null;
public DeltaSignerInfoResponseListener savedGetSignerListener = null;
public PostSignerInfoResponseListener savedPostSignerResponseListener =
null;
public String savedPostedDomain = null;
public ProtocolSignerInfo savedSignerInfo = null;
/**
* Mock method that saves its results.
*/
@Override
public void requestHistory(WaveletName waveletName, String domain,
ProtocolHashedVersion startVersion,
ProtocolHashedVersion endVersion, long lengthLimit,
HistoryResponseListener listener) {
this.savedWaveletName = waveletName;
this.savedDomain = domain;
this.savedStartVersion = startVersion;
this.savedEndVersion = endVersion;
this.savedLengthLimit = lengthLimit;
this.savedHistoryListener = listener;
}
/**
* Mock method that saves its results.
*/
@Override
public void submitRequest(WaveletName waveletName,
ProtocolSignedDelta delta, SubmitResultListener listener) {
this.savedWaveletName = waveletName;
this.savedDelta = delta;
this.savedSubmitListener = listener;
}
@Override
public void getDeltaSignerInfo(ByteString signerId,
WaveletName waveletName,
ProtocolHashedVersion deltaEndVersion,
DeltaSignerInfoResponseListener listener) {
this.savedWaveletName = waveletName;
this.savedSignerRequestDelta = deltaEndVersion;
this.savedGetSignerListener = listener;
}
@Override
public void postSignerInfo(String destinationDomain,
ProtocolSignerInfo signerInfo,
PostSignerInfoResponseListener listener) {
this.savedPostedDomain = destinationDomain;
this.savedSignerInfo = signerInfo;
this.savedPostSignerResponseListener = listener;
}
}
/**
* A mock SubmitResultListener that saves it's arguments.
*/
static class MockSubmitResultListener implements SubmitResultListener {
public Integer savedOperationsApplied = null;
public ProtocolHashedVersion savedHashedVersion = null;
public Long savedTimestamp = null;
public FederationError savedError = null;
@Override
public void onSuccess(int operationsApplied,
ProtocolHashedVersion hashedVersionAfterApplication,
long applicationTimestamp) {
savedOperationsApplied = operationsApplied;
savedHashedVersion = hashedVersionAfterApplication;
savedTimestamp = applicationTimestamp;
}
@Override
public void onFailure(FederationError error) {
savedError = error;
}
}
/**
* A listener factory that returns a known instance of a MockWaveletListener.
*/
static class MockWaveletListenerFactory implements
WaveletFederationListener.Factory {
public MockWaveletListener mockWaveletListener;
public String savedDomain = null;
@Override
public WaveletFederationListener listenerForDomain(String domain) {
savedDomain = domain;
return mockWaveletListener;
}
}
/**
* A mock listener (Remote interface) that listens for update and commit
* messages and saves them.
*/
static class MockWaveletListener implements WaveletFederationListener {
WaveletName savedUpdateWaveletName = null;
WaveletName savedCommitWaveletName = null;
List<ByteString> savedDeltas = null;
ProtocolHashedVersion savedVersion = null;
public WaveletUpdateCallback savedCallback = null;
@Override
public void waveletDeltaUpdate(WaveletName waveletName,
List<ByteString> deltas, WaveletUpdateCallback callback) {
savedUpdateWaveletName = waveletName;
savedDeltas = deltas;
savedCallback = callback;
callback.onSuccess();
}
@Override
public void waveletCommitUpdate(WaveletName waveletName,
ProtocolHashedVersion version, WaveletUpdateCallback callback) {
savedUpdateWaveletName = waveletName;
savedCallback = callback;
callback.onSuccess();
}
}
static class MockPostSignerResponseListener implements
WaveletFederationProvider.PostSignerInfoResponseListener {
public FederationError savedError = null;
public boolean onSuccessCalled = false;
public void onSuccess() {
onSuccessCalled = true;
}
public void onFailure(FederationError error) {
this.savedError = error;
}
}
}