mirror of
https://github.com/kidfromjupiter/nearby.git
synced 2026-09-14 22:56:12 -04:00
Refactor the GNCBLEL2CAPServer.
PiperOrigin-RevId: 745855701
This commit is contained in:
committed by
Copybara-Service
parent
7fc2066be0
commit
675e4f738f
@@ -27,4 +27,5 @@ typedef NS_ERROR_ENUM(GNCBLEErrorDomain, GNCBLEError){
|
||||
GNCBLEErrorAlreadyDiscoveringSpecifiedCharacteristics,
|
||||
GNCBLEErrorAlreadyReadingCharacteristic,
|
||||
GNCBLEErrorAlreadyScanning,
|
||||
GNCBLEErrorL2CAPListeningOnQueueNil,
|
||||
};
|
||||
|
||||
@@ -15,8 +15,17 @@
|
||||
#import <CoreBluetooth/CoreBluetooth.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@protocol GNCPeripheralManager;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
* Completion handler for starting listening for an L2CAP channel.
|
||||
*
|
||||
* @param error The cause of the failure, or @c nil if no error occurred.
|
||||
*/
|
||||
typedef void (^GNCStartListeningL2CAPChannelCompletionHandler)(NSError *_Nullable error);
|
||||
|
||||
/**
|
||||
* An object that publishes the @c PSM value.
|
||||
*
|
||||
@@ -28,11 +37,27 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@property(atomic, readonly) CBL2CAPPSM PSM;
|
||||
|
||||
/**
|
||||
* Retrieves the L2CAP channel.
|
||||
* Initializes the L2CAP server.
|
||||
*
|
||||
* @return The L2CAP channel, or nil if the channel is not available.
|
||||
* @param peripheralManager The peripheral manager to use. If @c nil, a default peripheral manager
|
||||
* will be used.
|
||||
* @param queue The queue to use. If @c nil, then by default it will be DISPATCH_QUEUE_SERIAL.
|
||||
*/
|
||||
- (nullable CBL2CAPChannel *)getChannel;
|
||||
- (instancetype)initWithPeripheralManager:(nullable id<GNCPeripheralManager>)peripheralManager
|
||||
queue:(nullable dispatch_queue_t)queue;
|
||||
|
||||
/**
|
||||
* Starts listening for an L2CAP channel.
|
||||
*
|
||||
* @param completionHandler The completion handler to call when the L2CAP channel is started.
|
||||
*/
|
||||
- (void)startListeningChannelWithCompletionHandler:
|
||||
(GNCStartListeningL2CAPChannelCompletionHandler)completionHandler;
|
||||
|
||||
/**
|
||||
* Closes the L2CAP channel.
|
||||
*/
|
||||
- (void)close;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
static char *const kGNCBLEL2CAPServerQueueLabel = "com.nearby.GNCBLEL2CAPServer";
|
||||
static char *const kGNCBLEL2CAPServerQueueLabel = "com.google.nearby.GNCBLEL2CAPServer";
|
||||
|
||||
@interface GNCBLEL2CAPServer () <GNCPeripheralManagerDelegate>
|
||||
@property(atomic, readwrite) CBL2CAPPSM PSM;
|
||||
@@ -32,37 +32,67 @@ static char *const kGNCBLEL2CAPServerQueueLabel = "com.nearby.GNCBLEL2CAPServer"
|
||||
@implementation GNCBLEL2CAPServer {
|
||||
dispatch_queue_t _queue;
|
||||
id<GNCPeripheralManager> _peripheralManager;
|
||||
NSString *_serviceID;
|
||||
GNCStartListeningL2CAPChannelCompletionHandler _startListeningL2CAPChannelcompletionHandler;
|
||||
|
||||
// The L2CAP channel that is used to send and receive data.
|
||||
CBL2CAPChannel *_channel;
|
||||
CBL2CAPChannel *_l2CAPChannel;
|
||||
|
||||
/// Whether start call has been performed when the peripheral was off.
|
||||
BOOL _alreadyStartedWhenPeripheralPoweredOff;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_queue = dispatch_queue_create(kGNCBLEL2CAPServerQueueLabel, DISPATCH_QUEUE_SERIAL);
|
||||
_peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:nil queue:_queue];
|
||||
// Set for @c GNCPeripheralManager to be able to forward callbacks.
|
||||
_peripheralManager.peripheralDelegate = self;
|
||||
}
|
||||
return self;
|
||||
return [self initWithPeripheralManager:nil queue:nil];
|
||||
}
|
||||
|
||||
// This is private and should only be used for tests. The provided peripheral manager must call
|
||||
// delegate methods on the main queue.
|
||||
- (instancetype)initWithPeripheralManager:(nullable id<GNCPeripheralManager>)peripheralManager {
|
||||
- (instancetype)initWithPeripheralManager:(nullable id<GNCPeripheralManager>)peripheralManager
|
||||
queue:(nullable dispatch_queue_t)queue {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_queue = dispatch_get_main_queue();
|
||||
_peripheralManager = peripheralManager;
|
||||
// Set for @c GNCPeripheralManager to be able to forward callbacks.
|
||||
_peripheralManager.peripheralDelegate = self;
|
||||
_queue = queue ?: dispatch_queue_create(kGNCBLEL2CAPServerQueueLabel, DISPATCH_QUEUE_SERIAL);
|
||||
if (peripheralManager) {
|
||||
_peripheralManager = peripheralManager;
|
||||
// Set for @c GNCPeripheralManager to be able to forward callbacks.
|
||||
_peripheralManager.peripheralDelegate = self;
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (nullable CBL2CAPChannel *)getChannel {
|
||||
return _channel;
|
||||
- (void)startListeningChannelWithCompletionHandler:
|
||||
(GNCStartListeningL2CAPChannelCompletionHandler)completionHandler {
|
||||
_startListeningL2CAPChannelcompletionHandler = [completionHandler copy];
|
||||
if (!_queue) {
|
||||
_startListeningL2CAPChannelcompletionHandler([NSError
|
||||
errorWithDomain:GNCBLEErrorDomain
|
||||
code:GNCBLEErrorL2CAPListeningOnQueueNil
|
||||
userInfo:nil]);
|
||||
return;
|
||||
}
|
||||
if (!_peripheralManager) {
|
||||
// Lazy initialization to avoid system dialog on app startup before pairing.
|
||||
_peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:nil
|
||||
queue:_queue
|
||||
options:nil];
|
||||
|
||||
// Set for @c GNCPeripheralManager to be able to forward callbacks.
|
||||
_peripheralManager.peripheralDelegate = self;
|
||||
}
|
||||
|
||||
if (_peripheralManager.state == CBManagerStatePoweredOn) {
|
||||
// Bluetooth link is already encrypted, however encryption is not required here to avoid getting
|
||||
// insufficient authentication errors due to initialization order.
|
||||
[_peripheralManager publishL2CAPChannelWithEncryption:NO];
|
||||
} else {
|
||||
GTMLoggerInfo(@"[NEARBY] Peripheral must be on to start, waiting.");
|
||||
_alreadyStartedWhenPeripheralPoweredOff = YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)close {
|
||||
[self shutDown];
|
||||
}
|
||||
|
||||
#pragma mark - GNCPeripheralManagerDelegate
|
||||
@@ -70,10 +100,18 @@ static char *const kGNCBLEL2CAPServerQueueLabel = "com.nearby.GNCBLEL2CAPServer"
|
||||
- (void)gnc_peripheralManagerDidUpdateState:(id<GNCPeripheralManager>)peripheral {
|
||||
dispatch_assert_queue(_queue);
|
||||
if (_peripheralManager.state == CBManagerStatePoweredOn) {
|
||||
[_peripheralManager publishL2CAPChannelWithEncryption:NO];
|
||||
if (_alreadyStartedWhenPeripheralPoweredOff) {
|
||||
// Only setup once so that toggling Bluetooth does not cause an L2CAP channel to be
|
||||
// published every time since a new instance is created for each new channel.
|
||||
_alreadyStartedWhenPeripheralPoweredOff = NO;
|
||||
|
||||
// Bluetooth link is already encrypted, however encryption is not required here to avoid
|
||||
// getting insufficient authentication errors due to initialization order.
|
||||
[_peripheralManager publishL2CAPChannelWithEncryption:NO];
|
||||
}
|
||||
}
|
||||
if (_peripheralManager.state == CBManagerStatePoweredOff) {
|
||||
[_peripheralManager unpublishL2CAPChannel:_PSM];
|
||||
[self shutDown];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,19 +119,27 @@ static char *const kGNCBLEL2CAPServerQueueLabel = "com.nearby.GNCBLEL2CAPServer"
|
||||
didPublishL2CAPChannel:(CBL2CAPPSM)PSM
|
||||
error:(nullable NSError *)error {
|
||||
dispatch_assert_queue(_queue);
|
||||
GTMLoggerDebug(@"[NEARBY] didPublishL2CAPChannel with PSM: %@", @(PSM));
|
||||
if (error) {
|
||||
GTMLoggerError(@"Failed to publish L2CAP channel: %@", error);
|
||||
GTMLoggerError(@"[NEARBY] Failed to publish L2CAP channel: %@", error);
|
||||
if (_startListeningL2CAPChannelcompletionHandler) {
|
||||
_startListeningL2CAPChannelcompletionHandler(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
GTMLoggerInfo(@"Published L2CAP channel with PSM: %d", PSM);
|
||||
_PSM = PSM;
|
||||
if (_startListeningL2CAPChannelcompletionHandler) {
|
||||
_startListeningL2CAPChannelcompletionHandler(nil);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)gnc_peripheralManager:(id<GNCPeripheralManager>)peripheral
|
||||
didUnpublishL2CAPChannel:(CBL2CAPPSM)PSM
|
||||
error:(NSError *)error {
|
||||
dispatch_assert_queue(_queue);
|
||||
GTMLoggerDebug(@"[NEARBY] didUnpublishL2CAPChannel on PSM %@", @(PSM));
|
||||
if (error) {
|
||||
GTMLoggerError(@"Failed to unpublish L2CAP channel: %@", error);
|
||||
GTMLoggerError(@"[NEARBY] Failed to unpublish L2CAP channel: %@", error);
|
||||
}
|
||||
_PSM = 0;
|
||||
}
|
||||
@@ -102,50 +148,52 @@ static char *const kGNCBLEL2CAPServerQueueLabel = "com.nearby.GNCBLEL2CAPServer"
|
||||
didOpenL2CAPChannel:(nullable CBL2CAPChannel *)channel
|
||||
error:(nullable NSError *)error {
|
||||
dispatch_assert_queue(_queue);
|
||||
GTMLoggerDebug(@"[NEARBY] didOpenL2CAPChannel");
|
||||
if (error) {
|
||||
GTMLoggerError(@"Failed to open L2CAP channel: %@", error);
|
||||
GTMLoggerError(@"[NEARBY] Failed to open L2CAP channel: %@", error);
|
||||
return;
|
||||
}
|
||||
GTMLoggerInfo(@"Opened L2CAP channel with PSM: %d", channel.PSM);
|
||||
_channel = channel;
|
||||
// TODO: b/399815436 - Implement to wrap up l2cap channel.
|
||||
|
||||
// Cleanup older references.
|
||||
if (_l2CAPChannel) {
|
||||
// The device may establish a new L2CAP socket connection while the old socket is still
|
||||
// connected if sysproxy stopped for a reason other than Bluetooth disconnection. Closing the
|
||||
// channel here ensures the server and the client state is reset between the two
|
||||
// connection sessions.
|
||||
[self closeL2CAPChannel];
|
||||
}
|
||||
|
||||
_l2CAPChannel = channel;
|
||||
// TODO: b/399815436 - Implement to wrap up l2cap channel with |GNCBLEL2CAPStream|.
|
||||
}
|
||||
|
||||
#pragma mark - CBPeripheralManagerDelegate
|
||||
|
||||
- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral {
|
||||
dispatch_async(_queue, ^{
|
||||
[self gnc_peripheralManagerDidUpdateState:peripheral];
|
||||
});
|
||||
[self gnc_peripheralManagerDidUpdateState:peripheral];
|
||||
}
|
||||
|
||||
- (void)peripheralManager:(CBPeripheralManager *)peripheral
|
||||
didPublishL2CAPChannel:(CBL2CAPPSM)PSM
|
||||
error:(nullable NSError *)error {
|
||||
dispatch_async(_queue, ^{
|
||||
[self gnc_peripheralManager:peripheral didPublishL2CAPChannel:PSM error:error];
|
||||
});
|
||||
[self gnc_peripheralManager:peripheral didPublishL2CAPChannel:PSM error:error];
|
||||
}
|
||||
|
||||
- (void)peripheralManager:(CBPeripheralManager *)peripheral
|
||||
didUnpublishL2CAPChannel:(CBL2CAPPSM)PSM
|
||||
error:(nullable NSError *)error {
|
||||
dispatch_async(_queue, ^{
|
||||
[self gnc_peripheralManager:peripheral didUnpublishL2CAPChannel:PSM error:error];
|
||||
});
|
||||
[self gnc_peripheralManager:peripheral didUnpublishL2CAPChannel:PSM error:error];
|
||||
}
|
||||
|
||||
- (void)peripheralManager:(CBPeripheralManager *)peripheral
|
||||
didOpenL2CAPChannel:(nullable CBL2CAPChannel *)channel
|
||||
error:(nullable NSError *)error {
|
||||
dispatch_async(_queue, ^{
|
||||
[self gnc_peripheralManager:peripheral didOpenL2CAPChannel:channel error:error];
|
||||
});
|
||||
[self gnc_peripheralManager:peripheral didOpenL2CAPChannel:channel error:error];
|
||||
}
|
||||
|
||||
#pragma mark Private
|
||||
|
||||
- (void)tearDown {
|
||||
- (void)shutDown {
|
||||
if (_PSM > 0) {
|
||||
[_peripheralManager unpublishL2CAPChannel:_PSM];
|
||||
}
|
||||
@@ -153,6 +201,10 @@ static char *const kGNCBLEL2CAPServerQueueLabel = "com.nearby.GNCBLEL2CAPServer"
|
||||
_peripheralManager = nil;
|
||||
}
|
||||
|
||||
- (void)closeL2CAPChannel {
|
||||
_l2CAPChannel = nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -30,7 +30,6 @@ objc_library(
|
||||
"GNCBLEGATTServerTest.m",
|
||||
"GNCBLEL2CAPFakeInputOutputStream.h",
|
||||
"GNCBLEL2CAPFakeInputOutputStream.m",
|
||||
"GNCBLEL2CAPServer+Testing.h",
|
||||
"GNCBLEL2CAPServerTest.m",
|
||||
"GNCBLEL2CAPStreamTest.m",
|
||||
"GNCBLEMedium+Testing.h",
|
||||
|
||||
@@ -18,58 +18,102 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "internal/platform/implementation/apple/Tests/GNCBLEL2CAPServer+Testing.h"
|
||||
#import "internal/platform/implementation/apple/Tests/GNCFakePeripheralManager.h"
|
||||
|
||||
static const int kPSM = 192;
|
||||
|
||||
@interface GNCBLEL2CAPServerTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation GNCBLEL2CAPServerTest
|
||||
|
||||
#pragma mark - Publish L2CAP channel
|
||||
#pragma mark Tests
|
||||
|
||||
- (void)testPublishL2CAPChannel {
|
||||
- (void)testPublishL2CAPChannelWhenStartListeningChannel {
|
||||
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
|
||||
|
||||
fakePeripheralManager.PSM = kPSM;
|
||||
GNCBLEL2CAPServer *l2capServer =
|
||||
[[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager];
|
||||
|
||||
[[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager
|
||||
queue:dispatch_get_main_queue()];
|
||||
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
|
||||
|
||||
XCTAssertEqual([l2capServer PSM], kPSM);
|
||||
XCTAssertNotNil([l2capServer getChannel]);
|
||||
[l2capServer startListeningChannelWithCompletionHandler:^(NSError *error) {
|
||||
XCTAssertEqual(error, nil);
|
||||
XCTAssertEqual([l2capServer PSM], fakePeripheralManager.PSM);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testFailedToPublishL2CAPChannel {
|
||||
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
|
||||
|
||||
fakePeripheralManager.didPublishL2CAPChannelError = [NSError errorWithDomain:@"fake"
|
||||
code:0
|
||||
userInfo:nil];
|
||||
fakePeripheralManager.PSM = kPSM;
|
||||
GNCBLEL2CAPServer *l2capServer =
|
||||
[[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager];
|
||||
|
||||
[[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager
|
||||
queue:dispatch_get_main_queue()];
|
||||
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
|
||||
|
||||
XCTAssertNotEqual([l2capServer PSM], kPSM);
|
||||
XCTAssertNil([l2capServer getChannel]);
|
||||
[l2capServer startListeningChannelWithCompletionHandler:^(NSError *error) {
|
||||
XCTAssertEqual(error, fakePeripheralManager.didPublishL2CAPChannelError);
|
||||
XCTAssertEqual([l2capServer PSM], 0);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testUnPublishL2CAPChannel {
|
||||
- (void)testPoweredOffUnpublishesChannel {
|
||||
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
|
||||
|
||||
fakePeripheralManager.PSM = kPSM;
|
||||
GNCBLEL2CAPServer *l2capServer =
|
||||
[[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager];
|
||||
[[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager
|
||||
queue:dispatch_get_main_queue()];
|
||||
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
|
||||
|
||||
[l2capServer startListeningChannelWithCompletionHandler:^(NSError *error) {
|
||||
XCTAssertEqual(error, nil);
|
||||
XCTAssertEqual([l2capServer PSM], fakePeripheralManager.PSM);
|
||||
}];
|
||||
|
||||
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOff];
|
||||
|
||||
[self waitForExpectations:@[ fakePeripheralManager.unpublishExpectation ] timeout:0.0];
|
||||
}
|
||||
|
||||
- (void)testStartPeripheralManagerInitiallyOff {
|
||||
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
|
||||
GNCBLEL2CAPServer *l2capServer =
|
||||
[[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager
|
||||
queue:dispatch_get_main_queue()];
|
||||
|
||||
[l2capServer startListeningChannelWithCompletionHandler:^(NSError *error) {
|
||||
XCTAssertEqual(error, nil);
|
||||
XCTAssertEqual([l2capServer PSM], fakePeripheralManager.PSM);
|
||||
}];
|
||||
|
||||
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
|
||||
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOff];
|
||||
}
|
||||
|
||||
- (void)testClose {
|
||||
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
|
||||
GNCBLEL2CAPServer *l2capServer =
|
||||
[[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager
|
||||
queue:dispatch_get_main_queue()];
|
||||
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
|
||||
|
||||
[l2capServer startListeningChannelWithCompletionHandler:^(NSError *error) {
|
||||
XCTAssertEqual(error, nil);
|
||||
XCTAssertEqual([l2capServer PSM], fakePeripheralManager.PSM);
|
||||
}];
|
||||
|
||||
[l2capServer close];
|
||||
|
||||
XCTAssertEqual([l2capServer PSM], 0);
|
||||
}
|
||||
|
||||
- (void)testCloseDoesNotUnpublishesChannelIfNotConnected {
|
||||
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
|
||||
fakePeripheralManager.unpublishExpectation.inverted = YES;
|
||||
GNCBLEL2CAPServer *l2capServer =
|
||||
[[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager
|
||||
queue:dispatch_get_main_queue()];
|
||||
|
||||
[l2capServer close];
|
||||
|
||||
[self waitForExpectations:@[ fakePeripheralManager.unpublishExpectation ] timeout:1.0];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -44,6 +44,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
/** Expectation fulfilled when peripheral responds to a request with an error. */
|
||||
@property(nonatomic, readonly) XCTestExpectation *respondToRequestErrorExpectation;
|
||||
|
||||
/** Expectation fulfilled when peripheral unpublishes an L2CAP channel. */
|
||||
@property(nonatomic, readonly) XCTestExpectation *unpublishExpectation;
|
||||
|
||||
/**
|
||||
* Similates an @c addService: error.
|
||||
*
|
||||
|
||||
@@ -72,10 +72,13 @@
|
||||
initWithDescription:@"Fulfilled when peripheral responds to a request with success."];
|
||||
_respondToRequestErrorExpectation = [[XCTestExpectation alloc]
|
||||
initWithDescription:@"Fulfilled when peripheral responds to a request with an error."];
|
||||
_unpublishExpectation = [[XCTestExpectation alloc]
|
||||
initWithDescription:@"Fulfilled when L2CAP channel is unpublished."];
|
||||
_isAdvertising = NO;
|
||||
_state = CBManagerStateUnknown;
|
||||
_advertisementData = nil;
|
||||
_services = [[NSMutableArray alloc] init];
|
||||
_PSM = 192;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -141,6 +144,7 @@
|
||||
[peripheralDelegate gnc_peripheralManager:self
|
||||
didUnpublishL2CAPChannel:_PSM
|
||||
error:_didUnPublishL2CAPChannelError];
|
||||
[_unpublishExpectation fulfill];
|
||||
}
|
||||
|
||||
#pragma mark - Testing Helpers
|
||||
|
||||
Reference in New Issue
Block a user