Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions PubNub/Core/PubNub+Core.m
Original file line number Diff line number Diff line change
Expand Up @@ -302,12 +302,8 @@ - (void)copyWithConfiguration:(PNConfiguration *)configuration
[self cancelSubscribeOperations];

BOOL uuidChanged = ![configuration.userID isEqualToString:self.configuration.userID];
BOOL authKeyChanged = ((self.configuration.authKey && !configuration.authKey) ||
(!self.configuration.authKey && configuration.authKey) ||
(configuration.authKey && self.configuration.authKey &&
![configuration.authKey isEqualToString:self.configuration.authKey]));

if (uuidChanged || authKeyChanged) {

if (uuidChanged) {
[self unsubscribeFromChannels:self.subscriberManager.channels
groups:self.subscriberManager.channelGroups
withPresence:YES
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,102 @@ - (void)testContinuationUsesLongPollWhenStateIsConnected {
}


#pragma mark - Tests :: copyWithConfiguration unsubscribe behavior

/// Verify that `copyWithConfiguration:completion:` does **not** trigger an unsubscribe when only `authKey` changes.
///
/// When a token expires and is refreshed, the client should seamlessly continue its subscription on the new instance
/// without sending a leave request with the old (possibly expired) credentials. The new client inherits subscriber state
/// and resubscribes with the fresh auth key automatically.
- (void)testCopyWithConfigurationDoesNotUnsubscribeWhenOnlyAuthKeyChanges {
[self.client.subscriberManager addChannels:@[@"test-channel"]];
self.client.subscriberManager.currentState = PNTestConnectedSubscriberState;

id clientMock = [self mockForObject:self.client];

OCMReject([clientMock unsubscribeFromChannels:[OCMArg any]
groups:[OCMArg any]
withPresence:YES
queryParameters:[OCMArg any]
completion:[OCMArg any]]);

PNConfiguration *newConfig = [self.client.configuration copy];
newConfig.authKey = @"new-auth-token";

XCTestExpectation *completionExpectation = [self expectationWithDescription:@"copyWithConfiguration: should complete"];

// Set up transport that will succeed for the new client's subscribe.
PNURLSessionTransport *transport = [PNURLSessionTransport new];
PNTransportConfiguration *transportConfig = [PNTransportConfiguration new];
transportConfig.maximumConnections = 1;
[transport setupWithConfiguration:transportConfig];

NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration ephemeralSessionConfiguration];
sessionConfig.protocolClasses = @[[PNSlowSubscribeProtocol class]];
NSOperationQueue *opQueue = [NSOperationQueue new];
opQueue.maxConcurrentOperationCount = 1;
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:nil delegateQueue:opQueue];
[transport setValue:session forKey:@"session"];

[self.client copyWithConfiguration:newConfig completion:^(PubNub *client) {
client.subscriptionNetwork = transport;
[completionExpectation fulfill];
}];

// Complete the new client's subscribe request so the completion fires.
[self waitForCondition:^BOOL {
return [PNSlowSubscribeProtocol pendingCount] > 0;
} withTimeout:3.0 description:@"New client's subscribe request should be in-flight"];
[PNSlowSubscribeProtocol completeAllPendingWithSuccess];

[self waitForExpectationsWithTimeout:1.0 handler:nil];

OCMVerifyAll(clientMock);
}

/// Verify that `copyWithConfiguration:completion:` **does** trigger an unsubscribe when `userID` changes.
///
/// A user identity change means the server-side presence state (join/leave) is tied to the old identity.
/// The old client must send a leave so presence accurately reflects that the old user departed, before the new client
/// subscribes under the new identity.
- (void)testCopyWithConfigurationUnsubscribesWhenUserIDChanges {
[self.client.subscriberManager addChannels:@[@"test-channel"]];
self.client.subscriberManager.currentState = PNTestConnectedSubscriberState;

__block BOOL unsubscribeCalled = NO;
id clientMock = [self mockForObject:self.client];

OCMStub([clientMock unsubscribeFromChannels:[OCMArg any]
groups:[OCMArg any]
withPresence:YES
queryParameters:[OCMArg any]
completion:([OCMArg invokeBlockWithArgs:[NSNull null], nil])])
.andDo(^(__unused NSInvocation *invocation) {
unsubscribeCalled = YES;
});

PNConfiguration *newConfig = [self.client.configuration copy];
newConfig.userID = @"different-user-id";

XCTestExpectation *completionExpectation = [self expectationWithDescription:@"copyWithConfiguration: should complete"];

[self.client copyWithConfiguration:newConfig completion:^(PubNub *client) {
[completionExpectation fulfill];
}];

// Complete the new client's subscribe request.
[self waitForCondition:^BOOL {
return [PNSlowSubscribeProtocol pendingCount] > 0;
} withTimeout:3.0 description:@"New client's subscribe request should be in-flight"];
[PNSlowSubscribeProtocol completeAllPendingWithSuccess];

[self waitForExpectationsWithTimeout:1.0 handler:nil];

XCTAssertTrue(unsubscribeCalled,
@"copyWithConfiguration: should unsubscribe from old identity when userID changes");
}


#pragma mark - Helpers


Expand Down
Loading