Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit bbb890d

Browse files
authoredJun 29, 2022
Add support for user signin and server to user messages (#327)
1 parent 53de835 commit bbb890d

File tree

17 files changed

+707
-250
lines changed

17 files changed

+707
-250
lines changed
 

‎src/main/java/com/pusher/client/Pusher.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import com.pusher.client.connection.ConnectionEventListener;
1919
import com.pusher.client.connection.ConnectionState;
2020
import com.pusher.client.connection.impl.InternalConnection;
21+
import com.pusher.client.user.User;
22+
import com.pusher.client.user.impl.InternalUser;
2123
import com.pusher.client.util.Factory;
2224

2325
/**
@@ -42,6 +44,7 @@ public class Pusher implements Client {
4244
private final InternalConnection connection;
4345
private final ChannelManager channelManager;
4446
private final Factory factory;
47+
private final InternalUser user;
4548

4649
/**
4750
* Creates a new instance of Pusher.
@@ -102,11 +105,17 @@ public Pusher(final String apiKey, final PusherOptions pusherOptions) {
102105

103106
this.pusherOptions = pusherOptions;
104107
this.factory = factory;
105-
connection = factory.getConnection(apiKey, this.pusherOptions);
108+
connection = factory.getConnection(apiKey, this.pusherOptions, this::handleEvent);
106109
channelManager = factory.getChannelManager();
110+
user = factory.newUser(connection, pusherOptions.getUserAuthenticator());
107111
channelManager.setConnection(connection);
108112
}
109113

114+
private void handleEvent(String event, String wholeMessage) {
115+
user.handleEvent(event, wholeMessage);
116+
channelManager.onMessage(event, wholeMessage);
117+
}
118+
110119
/* Connection methods */
111120

112121
/**
@@ -192,6 +201,28 @@ public void disconnect() {
192201
}
193202
}
194203

204+
/**
205+
*
206+
* @return The {@link com.pusher.client.user.User} associated with this Pusher connection.
207+
*/
208+
public User user() {
209+
return user;
210+
}
211+
212+
/**
213+
* Signs in on the Pusher connection as the current user.
214+
*
215+
* <p>
216+
* Requires {@link PusherOptions#setUserAuthenticator} to have been called.
217+
* </p>
218+
*
219+
* @throws IllegalStateException if no {@link UserAuthenticator} has been set.
220+
*/
221+
public void signin() {
222+
throwExceptionIfNoUserAuthenticatorHasBeenSet();
223+
user.signin();
224+
}
225+
195226
/* Subscription methods */
196227

197228
/**
@@ -378,6 +409,13 @@ private void throwExceptionIfNoChannelAuthorizerHasBeenSet() {
378409
}
379410
}
380411

412+
private void throwExceptionIfNoUserAuthenticatorHasBeenSet() {
413+
if (pusherOptions.getUserAuthenticator() == null) {
414+
throw new IllegalStateException(
415+
"Cannot sign in because no UserAuthenticator has been set. Call PusherOptions.setUserAuthenticator() before connecting to Pusher");
416+
}
417+
}
418+
381419
/**
382420
*
383421
* @param channelName The name of the public channel to be retrieved
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
package com.pusher.client.channel.impl;
2+
3+
import java.util.HashMap;
4+
import java.util.HashSet;
5+
import java.util.LinkedHashMap;
6+
import java.util.Map;
7+
import java.util.Set;
8+
9+
import com.google.gson.Gson;
10+
11+
import com.google.gson.GsonBuilder;
12+
import com.pusher.client.channel.*;
13+
import com.pusher.client.channel.impl.message.SubscribeMessage;
14+
import com.pusher.client.channel.impl.message.UnsubscribeMessage;
15+
import com.pusher.client.util.Factory;
16+
17+
public abstract class BaseChannel implements InternalChannel {
18+
protected final Gson GSON;
19+
private static final String INTERNAL_EVENT_PREFIX = "pusher_internal:";
20+
protected static final String SUBSCRIPTION_SUCCESS_EVENT = "pusher_internal:subscription_succeeded";
21+
private Set<SubscriptionEventListener> globalListeners = new HashSet<SubscriptionEventListener>();
22+
private final Map<String, Set<SubscriptionEventListener>> eventNameToListenerMap = new HashMap<String, Set<SubscriptionEventListener>>();
23+
protected volatile ChannelState state = ChannelState.INITIAL;
24+
private ChannelEventListener eventListener;
25+
private final Factory factory;
26+
private final Object lock = new Object();
27+
28+
public BaseChannel(final Factory factory) {
29+
GsonBuilder gsonBuilder = new GsonBuilder();
30+
gsonBuilder.registerTypeAdapter(PusherEvent.class, new PusherEventDeserializer());
31+
GSON = gsonBuilder.create();
32+
this.factory = factory;
33+
}
34+
35+
/* Channel implementation */
36+
37+
@Override
38+
abstract public String getName();
39+
40+
@Override
41+
public void bind(final String eventName, final SubscriptionEventListener listener) {
42+
validateArguments(eventName, listener);
43+
44+
synchronized (lock) {
45+
Set<SubscriptionEventListener> listeners = eventNameToListenerMap.get(eventName);
46+
if (listeners == null) {
47+
listeners = new HashSet<SubscriptionEventListener>();
48+
eventNameToListenerMap.put(eventName, listeners);
49+
}
50+
listeners.add(listener);
51+
}
52+
}
53+
54+
@Override
55+
public void bindGlobal(SubscriptionEventListener listener) {
56+
validateArguments("", listener);
57+
58+
synchronized(lock) {
59+
globalListeners.add(listener);
60+
}
61+
}
62+
63+
@Override
64+
public void unbind(String eventName, SubscriptionEventListener listener) {
65+
validateArguments(eventName, listener);
66+
67+
synchronized (lock) {
68+
final Set<SubscriptionEventListener> listeners = eventNameToListenerMap.get(eventName);
69+
if (listeners != null) {
70+
listeners.remove(listener);
71+
if (listeners.isEmpty()) {
72+
eventNameToListenerMap.remove(eventName);
73+
}
74+
}
75+
}
76+
}
77+
78+
@Override
79+
public void unbindGlobal(SubscriptionEventListener listener) {
80+
validateArguments("", listener);
81+
82+
synchronized(lock) {
83+
if (globalListeners != null) {
84+
globalListeners.remove(listener);
85+
}
86+
}
87+
}
88+
89+
@Override
90+
public boolean isSubscribed() {
91+
return state == ChannelState.SUBSCRIBED;
92+
}
93+
94+
/* InternalChannel implementation */
95+
96+
@Override
97+
public String toSubscribeMessage() {
98+
return GSON.toJson(new SubscribeMessage(getName()));
99+
}
100+
101+
@Override
102+
public String toUnsubscribeMessage() {
103+
return GSON.toJson(new UnsubscribeMessage(getName()));
104+
}
105+
106+
@Override
107+
public PusherEvent prepareEvent(String event, String message) {
108+
return GSON.fromJson(message, PusherEvent.class);
109+
}
110+
111+
@Override
112+
public void onMessage(String event, String message) {
113+
if (event.equals(SUBSCRIPTION_SUCCESS_EVENT)) {
114+
updateState(ChannelState.SUBSCRIBED);
115+
} else {
116+
final Set<SubscriptionEventListener> listeners = getInterestedListeners(event);
117+
if (listeners != null) {
118+
final PusherEvent pusherEvent = prepareEvent(event, message);
119+
if (pusherEvent != null) {
120+
for (final SubscriptionEventListener listener : listeners) {
121+
factory.queueOnEventThread(new Runnable() {
122+
@Override
123+
public void run() {
124+
listener.onEvent(pusherEvent);
125+
}
126+
});
127+
}
128+
}
129+
}
130+
}
131+
}
132+
133+
@Override
134+
public void updateState(ChannelState state) {
135+
this.state = state;
136+
137+
if (state == ChannelState.SUBSCRIBED && eventListener != null) {
138+
factory.queueOnEventThread(new Runnable() {
139+
@Override
140+
public void run() {
141+
eventListener.onSubscriptionSucceeded(getName());
142+
}
143+
});
144+
}
145+
}
146+
147+
@Override
148+
public void setEventListener(final ChannelEventListener listener) {
149+
eventListener = listener;
150+
}
151+
152+
@Override
153+
public ChannelEventListener getEventListener() {
154+
return eventListener;
155+
}
156+
157+
/* Comparable implementation */
158+
159+
@Override
160+
public int compareTo(final InternalChannel other) {
161+
return getName().compareTo(other.getName());
162+
}
163+
164+
/* Implementation detail */
165+
166+
@Override
167+
public String toString() {
168+
return String.format("[Channel: name=%s]", getName());
169+
}
170+
171+
private void validateArguments(final String eventName, final SubscriptionEventListener listener) {
172+
173+
if (eventName == null) {
174+
throw new IllegalArgumentException("Cannot bind or unbind to channel " + getName() + " with a null event name");
175+
}
176+
177+
if (listener == null) {
178+
throw new IllegalArgumentException("Cannot bind or unbind to channel " + getName() + " with a null listener");
179+
}
180+
181+
if (eventName.startsWith(INTERNAL_EVENT_PREFIX)) {
182+
throw new IllegalArgumentException("Cannot bind or unbind channel " + getName()
183+
+ " with an internal event name such as " + eventName);
184+
}
185+
}
186+
187+
protected Set<SubscriptionEventListener> getInterestedListeners(String event) {
188+
synchronized (lock) {
189+
Set<SubscriptionEventListener> listeners = new HashSet<SubscriptionEventListener>();
190+
191+
final Set<SubscriptionEventListener> sharedListeners =
192+
eventNameToListenerMap.get(event);
193+
194+
if (sharedListeners != null ) {
195+
listeners.addAll(sharedListeners);
196+
}
197+
if (!globalListeners.isEmpty()) {
198+
listeners.addAll(globalListeners);
199+
}
200+
201+
if (listeners.isEmpty()){
202+
return null;
203+
}
204+
205+
return listeners;
206+
}
207+
}
208+
}
Lines changed: 2 additions & 205 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,12 @@
11
package com.pusher.client.channel.impl;
22

3-
import java.util.HashMap;
4-
import java.util.HashSet;
5-
import java.util.LinkedHashMap;
6-
import java.util.Map;
7-
import java.util.Set;
8-
9-
import com.google.gson.Gson;
10-
11-
import com.google.gson.GsonBuilder;
12-
import com.pusher.client.channel.*;
13-
import com.pusher.client.channel.impl.message.SubscribeMessage;
14-
import com.pusher.client.channel.impl.message.UnsubscribeMessage;
153
import com.pusher.client.util.Factory;
164

17-
public class ChannelImpl implements InternalChannel {
18-
protected final Gson GSON;
19-
private static final String INTERNAL_EVENT_PREFIX = "pusher_internal:";
20-
protected static final String SUBSCRIPTION_SUCCESS_EVENT = "pusher_internal:subscription_succeeded";
5+
public class ChannelImpl extends BaseChannel {
216
protected final String name;
22-
private Set<SubscriptionEventListener> globalListeners = new HashSet<SubscriptionEventListener>();
23-
private final Map<String, Set<SubscriptionEventListener>> eventNameToListenerMap = new HashMap<String, Set<SubscriptionEventListener>>();
24-
protected volatile ChannelState state = ChannelState.INITIAL;
25-
private ChannelEventListener eventListener;
26-
private final Factory factory;
27-
private final Object lock = new Object();
287

298
public ChannelImpl(final String channelName, final Factory factory) {
30-
GsonBuilder gsonBuilder = new GsonBuilder();
31-
gsonBuilder.registerTypeAdapter(PusherEvent.class, new PusherEventDeserializer());
32-
GSON = gsonBuilder.create();
9+
super(factory);
3310
if (channelName == null) {
3411
throw new IllegalArgumentException("Cannot subscribe to a channel with a null name");
3512
}
@@ -44,198 +21,18 @@ public ChannelImpl(final String channelName, final Factory factory) {
4421
}
4522

4623
name = channelName;
47-
this.factory = factory;
4824
}
4925

50-
/* Channel implementation */
51-
5226
@Override
5327
public String getName() {
5428
return name;
5529
}
56-
57-
@Override
58-
public void bind(final String eventName, final SubscriptionEventListener listener) {
59-
60-
validateArguments(eventName, listener);
61-
62-
synchronized (lock) {
63-
Set<SubscriptionEventListener> listeners = eventNameToListenerMap.get(eventName);
64-
if (listeners == null) {
65-
listeners = new HashSet<SubscriptionEventListener>();
66-
eventNameToListenerMap.put(eventName, listeners);
67-
}
68-
listeners.add(listener);
69-
}
70-
}
71-
72-
@Override
73-
public void bindGlobal(SubscriptionEventListener listener) {
74-
validateArguments("", listener);
75-
76-
synchronized(lock) {
77-
globalListeners.add(listener);
78-
}
79-
}
80-
81-
@Override
82-
public void unbind(final String eventName, final SubscriptionEventListener listener) {
83-
84-
validateArguments(eventName, listener);
85-
86-
synchronized (lock) {
87-
final Set<SubscriptionEventListener> listeners = eventNameToListenerMap.get(eventName);
88-
if (listeners != null) {
89-
listeners.remove(listener);
90-
if (listeners.isEmpty()) {
91-
eventNameToListenerMap.remove(eventName);
92-
}
93-
}
94-
}
95-
}
96-
97-
@Override
98-
public void unbindGlobal(SubscriptionEventListener listener) {
99-
validateArguments("", listener);
100-
101-
synchronized(lock) {
102-
if (globalListeners != null) {
103-
globalListeners.remove(listener);
104-
}
105-
}
106-
}
107-
108-
@Override
109-
public boolean isSubscribed() {
110-
return state == ChannelState.SUBSCRIBED;
111-
}
112-
113-
/* InternalChannel implementation */
114-
115-
@Override
116-
public PusherEvent prepareEvent(String event, String message) {
117-
return GSON.fromJson(message, PusherEvent.class);
118-
}
119-
120-
@Override
121-
public void onMessage(final String event, final String message) {
122-
123-
if (event.equals(SUBSCRIPTION_SUCCESS_EVENT)) {
124-
updateState(ChannelState.SUBSCRIBED);
125-
} else {
126-
final Set<SubscriptionEventListener> listeners = getInterestedListeners(event);
127-
if (listeners != null) {
128-
final PusherEvent pusherEvent = prepareEvent(event, message);
129-
if (pusherEvent != null) {
130-
for (final SubscriptionEventListener listener : listeners) {
131-
factory.queueOnEventThread(new Runnable() {
132-
@Override
133-
public void run() {
134-
listener.onEvent(pusherEvent);
135-
}
136-
});
137-
}
138-
}
139-
}
140-
}
141-
}
142-
143-
144-
@Override
145-
public String toSubscribeMessage() {
146-
return GSON.toJson(new SubscribeMessage(name));
147-
}
148-
149-
@Override
150-
public String toUnsubscribeMessage() {
151-
return GSON.toJson(
152-
new UnsubscribeMessage(name));
153-
}
154-
155-
@Override
156-
public void updateState(final ChannelState state) {
157-
158-
this.state = state;
159-
160-
if (state == ChannelState.SUBSCRIBED && eventListener != null) {
161-
factory.queueOnEventThread(new Runnable() {
162-
@Override
163-
public void run() {
164-
eventListener.onSubscriptionSucceeded(ChannelImpl.this.getName());
165-
}
166-
});
167-
}
168-
}
169-
170-
/* Comparable implementation */
171-
172-
@Override
173-
public void setEventListener(final ChannelEventListener listener) {
174-
eventListener = listener;
175-
}
176-
177-
@Override
178-
public ChannelEventListener getEventListener() {
179-
return eventListener;
180-
}
181-
182-
@Override
183-
public int compareTo(final InternalChannel other) {
184-
return getName().compareTo(other.getName());
185-
}
186-
187-
/* implementation detail */
188-
18930
@Override
19031
public String toString() {
19132
return String.format("[Public Channel: name=%s]", name);
19233
}
19334

194-
19535
protected String[] getDisallowedNameExpressions() {
19636
return new String[] { "^private-.*", "^presence-.*" };
19737
}
198-
199-
private void validateArguments(final String eventName, final SubscriptionEventListener listener) {
200-
201-
if (eventName == null) {
202-
throw new IllegalArgumentException("Cannot bind or unbind to channel " + name + " with a null event name");
203-
}
204-
205-
if (listener == null) {
206-
throw new IllegalArgumentException("Cannot bind or unbind to channel " + name + " with a null listener");
207-
}
208-
209-
if (eventName.startsWith(INTERNAL_EVENT_PREFIX)) {
210-
throw new IllegalArgumentException("Cannot bind or unbind channel " + name
211-
+ " with an internal event name such as " + eventName);
212-
}
213-
214-
if (state == ChannelState.UNSUBSCRIBED) {
215-
throw new IllegalStateException(
216-
"Cannot bind or unbind to events on a channel that has been unsubscribed. Call Pusher.subscribe() to resubscribe to this channel");
217-
}
218-
}
219-
220-
protected Set<SubscriptionEventListener> getInterestedListeners(String event) {
221-
synchronized (lock) {
222-
Set<SubscriptionEventListener> listeners = new HashSet<SubscriptionEventListener>();
223-
224-
final Set<SubscriptionEventListener> sharedListeners =
225-
eventNameToListenerMap.get(event);
226-
227-
if (sharedListeners != null ) {
228-
listeners.addAll(sharedListeners);
229-
}
230-
if (!globalListeners.isEmpty()) {
231-
listeners.addAll(globalListeners);
232-
}
233-
234-
if (listeners.isEmpty()){
235-
return null;
236-
}
237-
238-
return listeners;
239-
}
240-
}
24138
}

‎src/main/java/com/pusher/client/channel/impl/PrivateChannelImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ private String authorize() {
8282
return authResponse.getAuth();
8383
}
8484
} catch (JsonSyntaxException e) {
85-
throw new AuthorizationFailureException("Unable to parse response from Authorizer");
85+
throw new AuthorizationFailureException("Unable to parse response from ChannelAuthorizer");
8686
}
8787
}
8888

‎src/main/java/com/pusher/client/channel/impl/message/SubscribeMessage.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import java.util.HashMap;
44
import java.util.Map;
5-
import java.util.TreeMap;
65

76
public class SubscribeMessage {
87
private String event = "pusher:subscribe";

‎src/main/java/com/pusher/client/connection/websocket/WebSocketConnection.java

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.util.concurrent.ConcurrentHashMap;
1111
import java.util.concurrent.Future;
1212
import java.util.concurrent.TimeUnit;
13+
import java.util.function.BiConsumer;
1314
import java.util.logging.Logger;
1415

1516
import javax.net.ssl.SSLException;
@@ -28,7 +29,6 @@ public class WebSocketConnection implements InternalConnection, WebSocketListene
2829
private static final Logger log = Logger.getLogger(WebSocketConnection.class.getName());
2930
private static final Gson GSON = new Gson();
3031

31-
private static final String INTERNAL_EVENT_PREFIX = "pusher:";
3232
private static final String PING_EVENT_SERIALIZED = "{\"event\": \"pusher:ping\"}";
3333

3434
private final Factory factory;
@@ -38,27 +38,29 @@ public class WebSocketConnection implements InternalConnection, WebSocketListene
3838
private final Proxy proxy;
3939
private final int maxReconnectionAttempts;
4040
private final int maxReconnectionGap;
41+
private final BiConsumer<String, String> eventHandler;
4142

4243
private volatile ConnectionState state = ConnectionState.DISCONNECTED;
4344
private WebSocketClientWrapper underlyingConnection;
4445
private String socketId;
4546
private int reconnectAttempts = 0;
4647

47-
4848
public WebSocketConnection(
4949
final String url,
5050
final long activityTimeout,
5151
final long pongTimeout,
5252
int maxReconnectionAttempts,
5353
int maxReconnectionGap,
5454
final Proxy proxy,
55+
final BiConsumer<String, String> eventHandler,
5556
final Factory factory) throws URISyntaxException {
5657
webSocketUri = new URI(url);
5758
activityTimer = new ActivityTimer(activityTimeout, pongTimeout);
5859
this.maxReconnectionAttempts = maxReconnectionAttempts;
5960
this.maxReconnectionGap = maxReconnectionGap;
6061
this.proxy = proxy;
6162
this.factory = factory;
63+
this.eventHandler = eventHandler;
6264

6365
for (final ConnectionState state : ConnectionState.values()) {
6466
eventListeners.put(state, Collections.newSetFromMap(new ConcurrentHashMap<ConnectionEventListener, Boolean>()));
@@ -170,21 +172,12 @@ public void run() {
170172
}
171173

172174
private void handleEvent(final String event, final String wholeMessage) {
173-
if (event.startsWith(INTERNAL_EVENT_PREFIX)) {
174-
handleInternalEvent(event, wholeMessage);
175-
}
176-
else {
177-
factory.getChannelManager().onMessage(event, wholeMessage);
178-
}
179-
}
180-
181-
private void handleInternalEvent(final String event, final String wholeMessage) {
182175
if (event.equals("pusher:connection_established")) {
183176
handleConnectionMessage(wholeMessage);
184-
}
185-
else if (event.equals("pusher:error")) {
177+
} else if (event.equals("pusher:error")) {
186178
handleError(wholeMessage);
187179
}
180+
eventHandler.accept(event, wholeMessage);
188181
}
189182

190183
@SuppressWarnings("rawtypes")
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.pusher.client.user;
2+
3+
import com.pusher.client.channel.SubscriptionEventListener;
4+
5+
/**
6+
* An object that represents a user on a Pusher connection. An implementation of this
7+
* interface is returned when you call {@link com.pusher.client.Pusher#user()}.
8+
*/
9+
public interface User {
10+
/**
11+
*
12+
* @return The user id of the signed in user. Null if no user is signed in;
13+
*/
14+
String userId();
15+
16+
/**
17+
* Binds a {@link SubscriptionEventListener} to an event. The
18+
* {@link SubscriptionEventListener} will be notified whenever the specified
19+
* event is received for this user.
20+
*
21+
* @param eventName
22+
* The name of the event to listen to.
23+
* @param listener
24+
* A listener to receive notifications when the event is
25+
* received.
26+
* @throws IllegalArgumentException
27+
* If either of the following are true:
28+
* <ul>
29+
* <li>The name of the event is null.</li>
30+
* <li>The {@link SubscriptionEventListener} is null.</li>
31+
* </ul>
32+
*/
33+
void bind(String eventName, SubscriptionEventListener listener);
34+
35+
/**
36+
* Binds a {@link SubscriptionEventListener} to all events. The
37+
* {@link SubscriptionEventListener} will be notified whenever an
38+
* event is received for this user.
39+
*
40+
* @param listener
41+
* A listener to receive notifications when the event is
42+
* received.
43+
* @throws IllegalArgumentException
44+
* If the {@link SubscriptionEventListener} is null.
45+
*/
46+
void bindGlobal(SubscriptionEventListener listener);
47+
48+
/**
49+
* <p>
50+
* Unbinds a previously bound {@link SubscriptionEventListener} from an
51+
* event. The {@link SubscriptionEventListener} will no longer be notified
52+
* whenever the specified event is received for this user.
53+
* </p>
54+
*
55+
* @param eventName
56+
* The name of the event to stop listening to.
57+
* @param listener
58+
* The listener to unbind from the event.
59+
* @throws IllegalArgumentException
60+
* If either of the following are true:
61+
* <ul>
62+
* <li>The name of the event is null.</li>
63+
* <li>The {@link SubscriptionEventListener} is null.</li>
64+
* </ul>
65+
*/
66+
void unbind(String eventName, SubscriptionEventListener listener);
67+
68+
/**
69+
* <p>
70+
* Unbinds a previously bound {@link SubscriptionEventListener} from global
71+
* events. The {@link SubscriptionEventListener} will no longer be notified
72+
* whenever the any event is received for this user.
73+
* </p>
74+
*
75+
* @param listener
76+
* The listener to unbind from the event.
77+
* @throws IllegalArgumentException
78+
* If the {@link SubscriptionEventListener} is null.
79+
*/
80+
void unbindGlobal(SubscriptionEventListener listener);
81+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package com.pusher.client.user.impl;
2+
3+
import com.google.gson.Gson;
4+
import com.google.gson.GsonBuilder;
5+
import com.google.gson.JsonSyntaxException;
6+
7+
import com.pusher.client.UserAuthenticator;
8+
import com.pusher.client.AuthenticationFailureException;
9+
import com.pusher.client.channel.PusherEvent;
10+
import com.pusher.client.channel.PusherEventDeserializer;
11+
import com.pusher.client.channel.SubscriptionEventListener;
12+
import com.pusher.client.channel.impl.ChannelManager;
13+
import com.pusher.client.connection.ConnectionEventListener;
14+
import com.pusher.client.connection.ConnectionState;
15+
import com.pusher.client.connection.ConnectionStateChange;
16+
import com.pusher.client.connection.impl.InternalConnection;
17+
import com.pusher.client.connection.impl.message.AuthenticationResponse;
18+
import com.pusher.client.connection.impl.message.SigninMessage;
19+
import com.pusher.client.user.User;
20+
import com.pusher.client.util.Factory;
21+
22+
import java.util.Map;
23+
import java.util.logging.Logger;
24+
25+
public class InternalUser implements User {
26+
private static final Gson GSON;
27+
private static final Logger log = Logger.getLogger(User.class.getName());
28+
29+
static {
30+
GsonBuilder gsonBuilder = new GsonBuilder();
31+
gsonBuilder.registerTypeAdapter(PusherEvent.class, new PusherEventDeserializer());
32+
GSON = gsonBuilder.create();
33+
}
34+
35+
private static class ConnectionStateChangeHandler implements ConnectionEventListener {
36+
private InternalUser user;
37+
38+
public ConnectionStateChangeHandler(InternalUser user) {
39+
this.user = user;
40+
}
41+
42+
@Override
43+
public void onConnectionStateChange(ConnectionStateChange change) {
44+
switch (change.getCurrentState()) {
45+
case CONNECTED:
46+
user.attemptSignin();
47+
break;
48+
case CONNECTING:
49+
case DISCONNECTED:
50+
user.disconnect();
51+
break;
52+
default:
53+
// NOOP
54+
}
55+
}
56+
57+
@Override
58+
public void onError(String message, String code, Exception e) {
59+
log.warning(message);
60+
}
61+
}
62+
63+
private final InternalConnection connection;
64+
private final UserAuthenticator userAuthenticator;
65+
private final ChannelManager channelManager;
66+
private boolean signinRequested;
67+
private ServerToUserChannel serverToUserChannel;
68+
private String userId;
69+
70+
public InternalUser(InternalConnection connection, UserAuthenticator userAuthenticator, Factory factory) {
71+
this.connection = connection;
72+
this.userAuthenticator = userAuthenticator;
73+
this.channelManager = factory.getChannelManager();
74+
this.signinRequested = false;
75+
this.serverToUserChannel = new ServerToUserChannel(this, factory);
76+
77+
connection.bind(ConnectionState.ALL, new ConnectionStateChangeHandler(this));
78+
}
79+
80+
public void signin() throws AuthenticationFailureException {
81+
if (signinRequested || userId != null) {
82+
return;
83+
}
84+
85+
signinRequested = true;
86+
attemptSignin();
87+
}
88+
89+
public void handleEvent(String event, String wholeMessage) {
90+
if (event.equals("pusher:signin_success")) {
91+
onSigninSuccess(GSON.fromJson(wholeMessage, PusherEvent.class));
92+
}
93+
}
94+
95+
private void attemptSignin() throws AuthenticationFailureException {
96+
if (!signinRequested || userId != null) {
97+
return;
98+
}
99+
100+
if (connection.getState() != ConnectionState.CONNECTED) {
101+
// Signin will be attempted when the connection is connected
102+
return;
103+
}
104+
105+
AuthenticationResponse authenticationResponse = getAuthenticationResponse();
106+
connection.sendMessage(authenticationResponseToSigninMessage(authenticationResponse));
107+
}
108+
109+
private static String authenticationResponseToSigninMessage(AuthenticationResponse authenticationResponse) {
110+
return GSON.toJson(new SigninMessage(
111+
authenticationResponse.getAuth(),
112+
authenticationResponse.getUserData()
113+
));
114+
}
115+
116+
private AuthenticationResponse getAuthenticationResponse() throws AuthenticationFailureException {
117+
String response = userAuthenticator.authenticate(connection.getSocketId());
118+
try {
119+
AuthenticationResponse authenticationResponse = GSON.fromJson(response, AuthenticationResponse.class);
120+
if (authenticationResponse.getAuth() == null || authenticationResponse.getUserData() == null) {
121+
throw new AuthenticationFailureException("Didn't receive all the fields expected from the UserAuthenticator. Expected auth and user_data");
122+
}
123+
return authenticationResponse;
124+
} catch (JsonSyntaxException e) {
125+
throw new AuthenticationFailureException("Unable to parse response from AuthenticationResponse");
126+
}
127+
}
128+
129+
private void onSigninSuccess(PusherEvent event) {
130+
try {
131+
String userData = (String) GSON.fromJson(event.getData(), Map.class).get("user_data");
132+
userId = (String) GSON.fromJson(userData, Map.class).get("id");
133+
} catch (Exception e) {
134+
log.severe("Failed parsing user data after signin");
135+
return;
136+
}
137+
138+
if (userId == null) {
139+
log.severe("User data doesn't contain an id");
140+
return;
141+
}
142+
channelManager.subscribeTo(serverToUserChannel, null);
143+
}
144+
145+
private void disconnect() {
146+
channelManager.unsubscribeFrom(serverToUserChannel.getName());
147+
userId = null;
148+
}
149+
150+
@Override
151+
public String userId() {
152+
return userId;
153+
}
154+
155+
@Override
156+
public void bind(String eventName, SubscriptionEventListener listener) {
157+
serverToUserChannel.bind(eventName, listener);
158+
}
159+
160+
@Override
161+
public void bindGlobal(SubscriptionEventListener listener) {
162+
serverToUserChannel.bindGlobal(listener);
163+
}
164+
165+
@Override
166+
public void unbind(String eventName, SubscriptionEventListener listener) {
167+
serverToUserChannel.unbind(eventName, listener);
168+
}
169+
170+
@Override
171+
public void unbindGlobal(SubscriptionEventListener listener) {
172+
serverToUserChannel.unbindGlobal(listener);
173+
}
174+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.pusher.client.user.impl;
2+
3+
import com.pusher.client.channel.impl.BaseChannel;
4+
import com.pusher.client.user.User;
5+
import com.pusher.client.util.Factory;
6+
7+
class ServerToUserChannel extends BaseChannel {
8+
private User user;
9+
10+
public ServerToUserChannel(User user, Factory factory) {
11+
super(factory);
12+
13+
this.user = user;
14+
}
15+
16+
@Override
17+
public String getName() {
18+
String userId = user.userId();
19+
if (userId == null) {
20+
throw new IllegalStateException("User id is null in ServerToUserChannel");
21+
}
22+
return "#server-to-user-" + user.userId();
23+
}
24+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.pusher.client.connection.impl.message;
2+
3+
import com.google.gson.annotations.SerializedName;
4+
5+
public class AuthenticationResponse {
6+
private String auth;
7+
8+
@SerializedName("user_data")
9+
private String userData;
10+
11+
public String getAuth() {
12+
return auth;
13+
}
14+
15+
public String getUserData() {
16+
return userData;
17+
}
18+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.pusher.client.connection.impl.message;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
public class SigninMessage {
7+
private String event = "pusher:signin";
8+
private Map<String, String> data = new HashMap<>();
9+
10+
public SigninMessage(String auth, String userData) {
11+
data.put("auth", auth);
12+
data.put("user_data", userData);
13+
}
14+
}

‎src/main/java/com/pusher/client/util/Factory.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
import java.util.concurrent.Executors;
88
import java.util.concurrent.ScheduledExecutorService;
99
import java.util.concurrent.ThreadFactory;
10+
import java.util.function.BiConsumer;
1011

1112
import javax.net.ssl.SSLException;
1213

1314
import com.pusher.client.ChannelAuthorizer;
1415
import com.pusher.client.PusherOptions;
16+
import com.pusher.client.user.impl.InternalUser;
1517
import com.pusher.client.UserAuthenticator;
1618
import com.pusher.client.Pusher;
1719
import com.pusher.client.channel.impl.ChannelImpl;
@@ -51,7 +53,7 @@ public class Factory {
5153
private ScheduledExecutorService timers;
5254
private static final Object eventLock = new Object();
5355

54-
public synchronized InternalConnection getConnection(final String apiKey, final PusherOptions options) {
56+
public synchronized InternalConnection getConnection(final String apiKey, final PusherOptions options, BiConsumer<String, String> eventHandler) {
5557
if (connection == null) {
5658
try {
5759
connection = new WebSocketConnection(
@@ -61,6 +63,7 @@ public synchronized InternalConnection getConnection(final String apiKey, final
6163
options.getMaxReconnectionAttempts(),
6264
options.getMaxReconnectGapInSeconds(),
6365
options.getProxy(),
66+
eventHandler,
6467
this);
6568
}
6669
catch (final URISyntaxException e) {
@@ -103,6 +106,10 @@ public PresenceChannelImpl newPresenceChannel(final InternalConnection connectio
103106
return new PresenceChannelImpl(connection, channelName, channelAuthorizer, this);
104107
}
105108

109+
public InternalUser newUser(InternalConnection connection, UserAuthenticator userAuthenticator) {
110+
return new InternalUser(connection, userAuthenticator, this);
111+
}
112+
106113
public synchronized ChannelManager getChannelManager() {
107114
if (channelManager == null) {
108115
channelManager = new ChannelManager(this);

‎src/test/java/com/pusher/client/EndToEndTest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import java.net.Proxy;
1313
import java.net.URI;
14+
import java.util.function.BiConsumer;
1415

1516
import org.junit.After;
1617
import org.junit.Before;
@@ -60,7 +61,7 @@ public void setUp() throws Exception {
6061
pusherOptions = new PusherOptions().setChannelAuthorizer(mockChannelAuthorizer).setUseTLS(false);
6162

6263
connection = new WebSocketConnection(pusherOptions.buildUrl(API_KEY), ACTIVITY_TIMEOUT, PONG_TIMEOUT, pusherOptions.getMaxReconnectionAttempts(),
63-
pusherOptions.getMaxReconnectGapInSeconds(), proxy, factory);
64+
pusherOptions.getMaxReconnectGapInSeconds(), proxy, (event, wholeMessage) -> {}, factory);
6465

6566
doAnswer(new Answer() {
6667
@Override
@@ -84,7 +85,7 @@ public WebSocketClientWrapper answer(final InvocationOnMock invocation) throws T
8485
}
8586
});
8687

87-
when(factory.getConnection(API_KEY, pusherOptions)).thenReturn(connection);
88+
when(factory.getConnection(eq(API_KEY), eq(pusherOptions), any(BiConsumer.class))).thenReturn(connection);
8889

8990
when(factory.getChannelManager()).thenAnswer(new Answer<ChannelManager>() {
9091
@Override

‎src/test/java/com/pusher/client/PusherTest.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@
2121
import com.pusher.client.connection.ConnectionEventListener;
2222
import com.pusher.client.connection.ConnectionState;
2323
import com.pusher.client.connection.impl.InternalConnection;
24+
import com.pusher.client.user.impl.InternalUser;
2425
import com.pusher.client.util.Factory;
2526
import com.pusher.client.util.HttpChannelAuthorizer;
27+
import com.pusher.client.util.HttpUserAuthenticator;
28+
29+
import java.util.function.BiConsumer;
2630

2731
@RunWith(MockitoJUnitRunner.class)
2832
public class PusherTest {
@@ -35,6 +39,7 @@ public class PusherTest {
3539
private Pusher pusher;
3640
private PusherOptions options;
3741
private ChannelAuthorizer channelAuthorizer;
42+
private @Mock InternalUser mockUser;
3843
private @Mock InternalConnection mockConnection;
3944
private @Mock ChannelManager mockChannelManager;
4045
private @Mock ConnectionEventListener mockConnectionEventListener;
@@ -49,15 +54,16 @@ public class PusherTest {
4954
@Before
5055
public void setUp() {
5156
channelAuthorizer = new HttpChannelAuthorizer("http://www.example.com");
52-
options = new PusherOptions().setChannelAuthorizer(channelAuthorizer);
57+
options = new PusherOptions().setChannelAuthorizer(channelAuthorizer).setUserAuthenticator(new HttpUserAuthenticator("http://user-auth.com"));
5358

54-
when(factory.getConnection(eq(API_KEY), any(PusherOptions.class))).thenReturn(mockConnection);
59+
when(factory.getConnection(eq(API_KEY), any(PusherOptions.class), any(BiConsumer.class))).thenReturn(mockConnection);
5560
when(factory.getChannelManager()).thenReturn(mockChannelManager);
5661
when(factory.newPublicChannel(PUBLIC_CHANNEL_NAME)).thenReturn(mockPublicChannel);
5762
when(factory.newPrivateChannel(mockConnection, PRIVATE_CHANNEL_NAME, channelAuthorizer))
5863
.thenReturn(mockPrivateChannel);
5964
when(factory.newPresenceChannel(mockConnection, PRESENCE_CHANNEL_NAME, channelAuthorizer)).thenReturn(
6065
mockPresenceChannel);
66+
when(factory.newUser(eq(mockConnection), any(UserAuthenticator.class))).thenReturn(mockUser);
6167
doAnswer(new Answer() {
6268
@Override
6369
public Object answer(InvocationOnMock invocation) {
@@ -310,4 +316,17 @@ public void testUnsubscribeDelegatesCallToTheChannelManager() {
310316
verify(mockChannelManager).unsubscribeFrom(PUBLIC_CHANNEL_NAME);
311317
}
312318

319+
@Test(expected = IllegalStateException.class)
320+
public void testSigninThrowsIfUserAuthenticatorIsNotSet() {
321+
options.setUserAuthenticator(null);
322+
pusher = new Pusher(API_KEY, options);
323+
pusher.signin();
324+
}
325+
326+
@Test
327+
public void testSigninCallsUserSignin() {
328+
doNothing().when(mockUser).signin();
329+
pusher.signin();
330+
verify(mockUser).signin();
331+
}
313332
}

‎src/test/java/com/pusher/client/channel/impl/ChannelImplTest.java

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -223,19 +223,6 @@ public void testUpdateStateToSubscribedNotifiesListenerThatSubscriptionSucceeded
223223
verify(mockListener).onSubscriptionSucceeded(getChannelName());
224224
}
225225

226-
@Test(expected = IllegalStateException.class)
227-
public void testBindWhenInUnsubscribedStateThrowsException() {
228-
channel.updateState(ChannelState.UNSUBSCRIBED);
229-
channel.bind(EVENT_NAME, mockListener);
230-
}
231-
232-
@Test(expected = IllegalStateException.class)
233-
public void testUnbindWhenInUnsubscribedStateThrowsException() {
234-
channel.bind(EVENT_NAME, mockListener);
235-
channel.updateState(ChannelState.UNSUBSCRIBED);
236-
channel.unbind(EVENT_NAME, mockListener);
237-
}
238-
239226
/* end of tests */
240227

241228
/**

‎src/test/java/com/pusher/client/connection/websocket/WebSocketConnectionTest.java

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.net.Proxy;
88
import java.net.URI;
99
import java.net.URISyntaxException;
10+
import java.util.function.BiConsumer;
1011
import java.util.concurrent.ScheduledExecutorService;
1112
import java.util.concurrent.ScheduledThreadPoolExecutor;
1213
import java.util.concurrent.TimeUnit;
@@ -21,7 +22,6 @@
2122
import org.mockito.runners.MockitoJUnitRunner;
2223
import org.mockito.stubbing.Answer;
2324

24-
import com.pusher.client.channel.impl.ChannelManager;
2525
import com.pusher.client.connection.ConnectionEventListener;
2626
import com.pusher.client.connection.ConnectionState;
2727
import com.pusher.client.connection.ConnectionStateChange;
@@ -43,7 +43,7 @@ public class WebSocketConnectionTest {
4343
private static final Proxy PROXY = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxyaddress", 80));
4444

4545
@Mock
46-
private ChannelManager mockChannelManager;
46+
private BiConsumer<String, String> mockEventHandler;
4747
@Mock
4848
private WebSocketClientWrapper mockUnderlyingConnection;
4949
@Mock
@@ -57,7 +57,6 @@ public class WebSocketConnectionTest {
5757

5858
@Before
5959
public void setUp() throws URISyntaxException, SSLException {
60-
when(factory.getChannelManager()).thenReturn(mockChannelManager);
6160
when(factory.newWebSocketClientWrapper(any(URI.class), any(Proxy.class), any(WebSocketConnection.class))).thenReturn(
6261
mockUnderlyingConnection);
6362
doAnswer(new Answer() {
@@ -70,15 +69,15 @@ public Object answer(InvocationOnMock invocation) {
7069
}).when(factory).queueOnEventThread(any(Runnable.class));
7170
when(factory.getTimers()).thenReturn(new DoNothingExecutor());
7271

73-
connection = new WebSocketConnection(URL, ACTIVITY_TIMEOUT, PONG_TIMEOUT, MAX_RECONNECTION_ATTEMPTS, MAX_GAP, PROXY, factory);
72+
connection = new WebSocketConnection(URL, ACTIVITY_TIMEOUT, PONG_TIMEOUT, MAX_RECONNECTION_ATTEMPTS, MAX_GAP, PROXY, mockEventHandler, factory);
7473
connection.bind(ConnectionState.ALL, mockEventListener);
7574
}
7675

7776
@Test
7877
public void testUnbindingWhenNotAlreadyBoundReturnsFalse() throws URISyntaxException {
7978
final ConnectionEventListener listener = mock(ConnectionEventListener.class);
8079
final WebSocketConnection connection = new WebSocketConnection(URL, ACTIVITY_TIMEOUT, PONG_TIMEOUT, MAX_RECONNECTION_ATTEMPTS, MAX_GAP,
81-
PROXY, factory);
80+
PROXY, (event, wholeMessage) -> {}, factory);
8281
final boolean unbound = connection.unbind(ConnectionState.ALL, listener);
8382
assertEquals(false, unbound);
8483
}
@@ -87,7 +86,7 @@ public void testUnbindingWhenNotAlreadyBoundReturnsFalse() throws URISyntaxExcep
8786
public void testUnbindingWhenBoundReturnsTrue() throws URISyntaxException {
8887
final ConnectionEventListener listener = mock(ConnectionEventListener.class);
8988
final WebSocketConnection connection = new WebSocketConnection(URL, ACTIVITY_TIMEOUT, PONG_TIMEOUT, MAX_RECONNECTION_ATTEMPTS, MAX_GAP,
90-
PROXY, factory);
89+
PROXY, (event, wholeMessage) -> {}, factory);
9190

9291
connection.bind(ConnectionState.ALL, listener);
9392

@@ -126,7 +125,7 @@ public void testConnectDoesNotCallConnectOnUnderlyingConnectionIfAlreadyInConnec
126125
@Test
127126
public void testListenerDoesNotReceiveConnectingEventIfItIsOnlyBoundToTheConnectedEvent() throws URISyntaxException {
128127
connection = new WebSocketConnection(URL, ACTIVITY_TIMEOUT, PONG_TIMEOUT, MAX_RECONNECTION_ATTEMPTS, MAX_GAP,
129-
PROXY, factory);
128+
PROXY, (event, wholeMessage) -> {}, factory);
130129
connection.bind(ConnectionState.CONNECTED, mockEventListener);
131130
connection.connect();
132131

@@ -198,12 +197,12 @@ public void testSendMessageWhenWebSocketLibraryThrowsExceptionRaisesErrorEvent()
198197
}
199198

200199
@Test
201-
public void testReceiveUserMessagePassesMessageToChannelManager() {
200+
public void testReceiveUserMessagePassesMessageToEventHandler() {
202201
connect();
203202

204203
connection.onMessage(INCOMING_MESSAGE);
205204

206-
verify(mockChannelManager).onMessage(EVENT_NAME, INCOMING_MESSAGE);
205+
verify(mockEventHandler).accept(EVENT_NAME, INCOMING_MESSAGE);
207206
}
208207

209208
@Test
@@ -228,7 +227,7 @@ public void testOnCloseCallbackUpdatesStateToDisconnectedWhenPreviousStateIsDisc
228227
@Test
229228
public void testOnCloseCallbackDoesNotCallListenerIfItIsNotBoundToDisconnectedEvent() throws URISyntaxException {
230229
connection = new WebSocketConnection(URL, ACTIVITY_TIMEOUT, PONG_TIMEOUT, MAX_RECONNECTION_ATTEMPTS, MAX_GAP,
231-
PROXY, factory);
230+
PROXY, (event, wholeMessage) -> {}, factory);
232231
connection.bind(ConnectionState.CONNECTED, mockEventListener);
233232

234233
connection.connect();
@@ -370,7 +369,7 @@ public Object answer(InvocationOnMock invocation) {
370369
}).when(scheduledExecutorService).schedule(any(Runnable.class), any(Long.class), any(TimeUnit.class));
371370

372371
// Reconnect a single time (maxReconnectionAttempts = 1)
373-
connection = new WebSocketConnection(URL, ACTIVITY_TIMEOUT, PONG_TIMEOUT, 1, MAX_GAP, PROXY, factory);
372+
connection = new WebSocketConnection(URL, ACTIVITY_TIMEOUT, PONG_TIMEOUT, 1, MAX_GAP, PROXY, (event, wholeMessage) -> {}, factory);
374373

375374
connection.connect();
376375

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package com.pusher.client.user.impl;
2+
3+
import static org.junit.Assert.*;
4+
import static org.mockito.Mockito.*;
5+
6+
import org.junit.Before;
7+
import org.junit.Test;
8+
import org.junit.runner.RunWith;
9+
import org.mockito.Mock;
10+
import org.mockito.invocation.InvocationOnMock;
11+
import org.mockito.runners.MockitoJUnitRunner;
12+
import org.mockito.stubbing.Answer;
13+
14+
import com.pusher.client.UserAuthenticator;
15+
import com.pusher.client.AuthenticationFailureException;
16+
import com.pusher.client.channel.SubscriptionEventListener;
17+
import com.pusher.client.channel.impl.ChannelManager;
18+
import com.pusher.client.connection.impl.InternalConnection;
19+
import com.pusher.client.connection.ConnectionState;
20+
import com.pusher.client.util.Factory;
21+
22+
@RunWith(MockitoJUnitRunner.class)
23+
public class InternalUserTest {
24+
private static final String socketId = "123";
25+
private static final String authenticationResponse = "{\"auth\": \"123:456\", \"user_data\":\"{\\\"id\\\": \\\"someid\\\"}\"}";
26+
private static final String authenticationResponseMalformed = "{}";
27+
private static final String signinSuccessEvent = "{\"event\": \"pusher:signin_success\", \"data\": \"{\\\"user_data\\\": \\\"{\\\\\\\"id\\\\\\\":\\\\\\\"1\\\\\\\"}\\\"}\"}";
28+
private static final String signinSuccessEventMissingId = "{\"event\": \"pusher:signin_success\", \"data\": \"{\\\"user_data\\\": \\\"{}\\\"}\"}";
29+
private static final String signinSuccessEventMalformed = "{\"event\": \"pusher:signin_success\", \"data\": \"{}\"}";
30+
31+
private InternalUser user;
32+
private @Mock UserAuthenticator mockUserAuthenticator;
33+
private @Mock InternalConnection mockConnection;
34+
private @Mock ChannelManager mockChannelManager;
35+
private @Mock Factory mockFactory;
36+
private @Mock SubscriptionEventListener mockEventListener;
37+
38+
@Before
39+
public void setUp() {
40+
when(mockConnection.getSocketId()).thenReturn(socketId);
41+
when(mockFactory.getChannelManager()).thenReturn(mockChannelManager);
42+
user = new InternalUser(mockConnection, mockUserAuthenticator, mockFactory);
43+
}
44+
45+
@Test
46+
public void testSigninWhenNotConnected() {
47+
when(mockConnection.getState()).thenReturn(ConnectionState.DISCONNECTED);
48+
user.signin();
49+
verify(mockUserAuthenticator, never()).authenticate(any(String.class));
50+
verify(mockConnection, never()).sendMessage(any(String.class));
51+
}
52+
53+
@Test
54+
public void testSigninWhenConnected() {
55+
when(mockConnection.getState()).thenReturn(ConnectionState.CONNECTED);
56+
when(mockUserAuthenticator.authenticate(socketId)).thenReturn(authenticationResponse);
57+
user.signin();
58+
verify(mockConnection).sendMessage(any(String.class));
59+
}
60+
61+
@Test(expected = AuthenticationFailureException.class)
62+
public void testSigninMalformedResponse() {
63+
when(mockConnection.getState()).thenReturn(ConnectionState.CONNECTED);
64+
when(mockUserAuthenticator.authenticate(socketId)).thenReturn(authenticationResponseMalformed);
65+
user.signin();
66+
}
67+
68+
@Test
69+
public void testHandleEventSigninSuccessEvent() {
70+
user.handleEvent("pusher:signin_success", signinSuccessEvent);
71+
assertEquals(user.userId(), "1");
72+
verify(mockChannelManager).subscribeTo(any(ServerToUserChannel.class), eq(null));
73+
}
74+
75+
@Test
76+
public void testHandleEventSigninSuccessEventMissingId() {
77+
user.handleEvent("pusher:signin_success", signinSuccessEventMissingId);
78+
assertNull(user.userId());
79+
verify(mockChannelManager, never()).subscribeTo(any(ServerToUserChannel.class), eq(null));
80+
}
81+
82+
@Test
83+
public void testHandleEventSigninSuccessEventMalformed() {
84+
user.handleEvent("pusher:signin_success", signinSuccessEventMalformed);
85+
assertNull(user.userId());
86+
verify(mockChannelManager, never()).subscribeTo(any(ServerToUserChannel.class), eq(null));
87+
}
88+
89+
@Test
90+
public void testSigninWhenSignedIn() {
91+
when(mockConnection.getState()).thenReturn(ConnectionState.CONNECTED);
92+
user.handleEvent("pusher:signin_success", signinSuccessEvent);
93+
assertEquals(user.userId(), "1");
94+
user.signin();
95+
verify(mockUserAuthenticator, never()).authenticate(any(String.class));
96+
verify(mockConnection, never()).sendMessage(any(String.class));
97+
}
98+
}

0 commit comments

Comments
 (0)
Please sign in to comment.