/*
* Copyright (c) 2008-2017 the original author or authors.
*
* 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.cometd.client;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.cometd.bayeux.Channel;
import org.cometd.bayeux.Message;
import org.cometd.bayeux.client.ClientSession;
import org.cometd.bayeux.client.ClientSessionChannel;
import org.cometd.bayeux.server.BayeuxServer;
import org.cometd.bayeux.server.ConfigurableServerChannel;
import org.cometd.bayeux.server.ServerChannel;
import org.cometd.bayeux.server.ServerMessage;
import org.cometd.bayeux.server.ServerSession;
import org.cometd.client.transport.ClientTransport;
import org.cometd.server.DefaultSecurityPolicy;
import org.cometd.server.authorizer.GrantAuthorizer;
import org.junit.Assert;
import org.junit.Test;
public class SubscriptionFailureTest extends ClientServerTest {
@Test
public void testSubscriptionFailedOnClientRemovesListener() throws Exception {
startServer(null);
long maxNetworkDelay = 2000;
final long sleep = maxNetworkDelay + maxNetworkDelay / 2;
bayeux.getChannel(Channel.META_SUBSCRIBE).addListener(new ServerChannel.MessageListener() {
@Override
public boolean onMessage(ServerSession from, ServerChannel channel, ServerMessage.Mutable message) {
try {
Thread.sleep(sleep);
} catch (InterruptedException x) {
// Ignored
}
return true;
}
});
BayeuxClient client = newBayeuxClient();
client.setOption(ClientTransport.MAX_NETWORK_DELAY_OPTION, maxNetworkDelay);
client.handshake();
Assert.assertTrue(client.waitFor(5000, BayeuxClient.State.CONNECTED));
final CountDownLatch messageLatch = new CountDownLatch(1);
ClientSessionChannel.MessageListener messageCallback = new ClientSessionChannel.MessageListener() {
@Override
public void onMessage(ClientSessionChannel channel, Message message) {
messageLatch.countDown();
}
};
final CountDownLatch subscriptionLatch = new CountDownLatch(1);
ClientSessionChannel.MessageListener subscriptionCallback = new ClientSessionChannel.MessageListener() {
@Override
public void onMessage(ClientSessionChannel channel, Message message) {
if (!message.isSuccessful()) {
subscriptionLatch.countDown();
}
}
};
String channelName = "/echo";
client.getChannel(channelName).subscribe(messageCallback, subscriptionCallback);
Assert.assertTrue(subscriptionLatch.await(5, TimeUnit.SECONDS));
// Wait for subscription to happen on server.
Thread.sleep(sleep);
// Subscription has failed on client, but not on server.
// Publishing a message on server-side must not be notified on the client.
bayeux.getChannel(channelName).publish(null, "data");
Assert.assertFalse(messageLatch.await(1, TimeUnit.SECONDS));
disconnectBayeuxClient(client);
}
@Test
public void testSubscriptionFailedOnServerRemovesListener() throws Exception {
startServer(null);
String channelName = "/echo";
bayeux.createChannelIfAbsent(channelName, new ConfigurableServerChannel.Initializer() {
@Override
public void configureChannel(ConfigurableServerChannel channel) {
channel.addAuthorizer(GrantAuthorizer.GRANT_PUBLISH);
}
});
BayeuxClient client = newBayeuxClient();
client.handshake();
Assert.assertTrue(client.waitFor(5000, BayeuxClient.State.CONNECTED));
final CountDownLatch messageLatch = new CountDownLatch(1);
ClientSessionChannel.MessageListener messageCallback = new ClientSessionChannel.MessageListener() {
@Override
public void onMessage(ClientSessionChannel channel, Message message) {
messageLatch.countDown();
}
};
final CountDownLatch subscriptionLatch = new CountDownLatch(1);
ClientSessionChannel.MessageListener subscriptionCallback = new ClientSessionChannel.MessageListener() {
@Override
public void onMessage(ClientSessionChannel channel, Message message) {
if (!message.isSuccessful()) {
subscriptionLatch.countDown();
}
}
};
client.getChannel(channelName).subscribe(messageCallback, subscriptionCallback);
Assert.assertTrue(subscriptionLatch.await(5, TimeUnit.SECONDS));
// Force subscription on server.
bayeux.getChannel(channelName).subscribe(bayeux.getSession(client.getId()));
// Subscription has failed on client, but has been forced on server.
// Publishing a message on server-side must not be notified on the client.
bayeux.getChannel(channelName).publish(null, "data");
Assert.assertFalse(messageLatch.await(1, TimeUnit.SECONDS));
disconnectBayeuxClient(client);
}
@Test
public void testFailedSubscriptionDecrementsSubscriptionCount() throws Exception {
startServer(null);
final String channelName = "/count";
bayeux.setSecurityPolicy(new DefaultSecurityPolicy() {
@Override
public boolean canSubscribe(BayeuxServer server, ServerSession session, ServerChannel channel, ServerMessage message) {
boolean allowed = super.canSubscribe(server, session, channel, message);
Map<String, Object> ext = message.getExt();
return allowed && ext != null && (Boolean)ext.get("token");
}
});
BayeuxClient client = newBayeuxClient();
client.handshake();
Assert.assertTrue(client.waitFor(5000, BayeuxClient.State.CONNECTED));
final AtomicBoolean allowed = new AtomicBoolean();
client.addExtension(new ClientSession.Extension.Adapter() {
@Override
public boolean sendMeta(ClientSession session, Message.Mutable message) {
if (Channel.META_SUBSCRIBE.equals(message.getChannel()) &&
channelName.equals(message.get(Message.SUBSCRIPTION_FIELD))) {
message.getExt(true).put("token", allowed.get());
}
return true;
}
});
final CountDownLatch messageLatch = new CountDownLatch(1);
ClientSessionChannel.MessageListener messageCallback = new ClientSessionChannel.MessageListener() {
@Override
public void onMessage(ClientSessionChannel channel, Message message) {
messageLatch.countDown();
}
};
final CountDownLatch failedSubscription = new CountDownLatch(1);
ClientSessionChannel.MessageListener subscriptionCallback = new ClientSessionChannel.MessageListener() {
@Override
public void onMessage(ClientSessionChannel channel, Message message) {
if (!message.isSuccessful()) {
failedSubscription.countDown();
}
}
};
// First subscription fails, the subscription count should be
// decremented to zero so that a subsequent subscribe() could succeed.
client.getChannel(channelName).subscribe(messageCallback, subscriptionCallback);
Assert.assertTrue(failedSubscription.await(5, TimeUnit.SECONDS));
Assert.assertEquals(0, client.getChannel(channelName).getSubscribers().size());
Assert.assertEquals(0, bayeux.getChannel(channelName).getSubscribers().size());
// Now allow the subscription, we should be able to subscribe to the same channel.
allowed.set(true);
final CountDownLatch succeededSubscription = new CountDownLatch(1);
subscriptionCallback = new ClientSessionChannel.MessageListener() {
@Override
public void onMessage(ClientSessionChannel channel, Message message) {
if (message.isSuccessful()) {
succeededSubscription.countDown();
}
}
};
client.getChannel(channelName).subscribe(messageCallback, subscriptionCallback);
Assert.assertTrue(succeededSubscription.await(5, TimeUnit.SECONDS));
Assert.assertEquals(1, client.getChannel(channelName).getSubscribers().size());
Assert.assertEquals(1, bayeux.getChannel(channelName).getSubscribers().size());
// Make sure the message can be received.
bayeux.getChannel(channelName).publish(null, "data");
Assert.assertTrue(messageLatch.await(5, TimeUnit.SECONDS));
disconnectBayeuxClient(client);
}
}