FIRMessagingConnectionTest.m 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. /*
  2. * Copyright 2020 Google LLC
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import <OCMock/OCMock.h>
  17. #import <XCTest/XCTest.h>
  18. #import "FirebaseMessaging/Sources/FIRMessagingClient.h"
  19. #import "FirebaseMessaging/Sources/FIRMessagingConnection.h"
  20. #import "FirebaseMessaging/Sources/FIRMessagingDataMessageManager.h"
  21. #import "FirebaseMessaging/Sources/FIRMessagingRmqManager.h"
  22. #import "FirebaseMessaging/Sources/FIRMessagingSecureSocket.h"
  23. #import "FirebaseMessaging/Sources/FIRMessagingUtilities.h"
  24. #import "FirebaseMessaging/Sources/Protos/GtalkCore.pbobjc.h"
  25. #import "FirebaseMessaging/Tests/UnitTests/FIRMessagingFakeConnection.h"
  26. static NSString *const kDeviceAuthId = @"123456";
  27. static NSString *const kSecretToken = @"56789";
  28. // used to verify if we are sending in the right proto or not.
  29. // set it to negative value to disable this check
  30. static FIRMessagingProtoTag currentProtoSendTag;
  31. @interface FIRMessagingSecureSocket ()
  32. @property(nonatomic, readwrite, assign) FIRMessagingSecureSocketState state;
  33. @end
  34. @interface FIRMessagingSecureSocket (test_FIRMessagingConnection)
  35. - (void)_successconnectToHost:(NSString *)host port:(NSUInteger)port onRunLoop:(NSRunLoop *)runLoop;
  36. - (void)_fakeSuccessfulSocketConnect;
  37. @end
  38. @implementation FIRMessagingSecureSocket (test_FIRMessagingConnection)
  39. - (void)_successconnectToHost:(NSString *)host
  40. port:(NSUInteger)port
  41. onRunLoop:(NSRunLoop *)runLoop {
  42. // created ports, opened streams
  43. // invoke callback async
  44. [self _fakeSuccessfulSocketConnect];
  45. }
  46. - (void)_fakeSuccessfulSocketConnect {
  47. self.state = kFIRMessagingSecureSocketOpen;
  48. [self.delegate secureSocketDidConnect:self];
  49. }
  50. @end
  51. // make sure these are defined in FIRMessagingConnection
  52. @interface FIRMessagingConnection () <FIRMessagingSecureSocketDelegate>
  53. @property(nonatomic, readwrite, assign) int64_t lastLoginServerTimestamp;
  54. @property(nonatomic, readwrite, assign) int lastStreamIdAcked;
  55. @property(nonatomic, readwrite, assign) int inStreamId;
  56. @property(nonatomic, readwrite, assign) int outStreamId;
  57. @property(nonatomic, readwrite, strong) FIRMessagingSecureSocket *socket;
  58. @property(nonatomic, readwrite, strong) NSMutableArray *unackedS2dIds;
  59. @property(nonatomic, readwrite, strong) NSMutableDictionary *ackedS2dMap;
  60. @property(nonatomic, readwrite, strong) NSMutableArray *d2sInfos;
  61. - (void)setupConnectionSocket;
  62. - (void)connectToSocket:(FIRMessagingSecureSocket *)socket;
  63. - (NSTimeInterval)connectionTimeoutInterval;
  64. - (void)sendHeartbeatPing;
  65. @end
  66. @interface FIRMessagingConnectionTest : XCTestCase
  67. @property(nonatomic, readwrite, assign) BOOL didSuccessfullySendData;
  68. @property(nonatomic, readwrite, strong) NSUserDefaults *userDefaults;
  69. @property(nonatomic, readwrite, strong) FIRMessagingConnection *fakeConnection;
  70. @property(nonatomic, readwrite, strong) id mockClient;
  71. @property(nonatomic, readwrite, strong) id mockConnection;
  72. @property(nonatomic, readwrite, strong) id mockRmq;
  73. @property(nonatomic, readwrite, strong) id mockDataMessageManager;
  74. @end
  75. @implementation FIRMessagingConnectionTest
  76. - (void)setUp {
  77. [super setUp];
  78. _userDefaults = [[NSUserDefaults alloc] init];
  79. _mockRmq = OCMClassMock([FIRMessagingRmqManager class]);
  80. _mockDataMessageManager = OCMClassMock([FIRMessagingDataMessageManager class]);
  81. // fake connection is only used to simulate the socket behavior
  82. _fakeConnection =
  83. [[FIRMessagingFakeConnection alloc] initWithAuthID:kDeviceAuthId
  84. token:kSecretToken
  85. host:[FIRMessagingFakeConnection fakeHost]
  86. port:[FIRMessagingFakeConnection fakePort]
  87. runLoop:[NSRunLoop currentRunLoop]
  88. rmq2Manager:_mockRmq
  89. fcmManager:_mockDataMessageManager];
  90. _mockClient = OCMClassMock([FIRMessagingClient class]);
  91. _fakeConnection.delegate = _mockClient;
  92. _mockConnection = OCMPartialMock(_fakeConnection);
  93. _didSuccessfullySendData = NO;
  94. }
  95. - (void)tearDown {
  96. [self.fakeConnection teardown];
  97. [super tearDown];
  98. }
  99. - (void)testInitialConnectionNotConnected {
  100. XCTAssertEqual(kFIRMessagingConnectionNotConnected, [self.fakeConnection state]);
  101. }
  102. - (void)testSuccessfulSocketConnection {
  103. [self.fakeConnection signIn];
  104. // should be connected now
  105. XCTAssertEqual(kFIRMessagingConnectionConnected, self.fakeConnection.state);
  106. XCTAssertEqual(0, self.fakeConnection.lastStreamIdAcked);
  107. XCTAssertEqual(0, self.fakeConnection.inStreamId);
  108. XCTAssertEqual(0, self.fakeConnection.ackedS2dMap.count);
  109. XCTAssertEqual(0, self.fakeConnection.unackedS2dIds.count);
  110. [self stubSocketDisconnect:self.fakeConnection.socket];
  111. }
  112. - (void)testSignInAndThenSignOut {
  113. [self.fakeConnection signIn];
  114. [self stubSocketDisconnect:self.fakeConnection.socket];
  115. [self.fakeConnection signOut];
  116. XCTAssertEqual(kFIRMessagingSecureSocketClosed, self.fakeConnection.socket.state);
  117. }
  118. - (void)testSuccessfulSignIn {
  119. [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
  120. XCTAssertEqual(self.fakeConnection.state, kFIRMessagingConnectionSignedIn);
  121. XCTAssertEqual(self.fakeConnection.outStreamId, 2);
  122. XCTAssertTrue(self.didSuccessfullySendData);
  123. }
  124. - (void)testSignOut_whenSignedIn {
  125. [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
  126. // should be signed in now
  127. id mockSocket = self.fakeConnection.socket;
  128. [self.fakeConnection signOut];
  129. XCTAssertEqual(self.fakeConnection.state, kFIRMessagingConnectionNotConnected);
  130. XCTAssertEqual(self.fakeConnection.outStreamId, 3);
  131. XCTAssertNil([(FIRMessagingSecureSocket *)mockSocket delegate]);
  132. XCTAssertTrue(self.didSuccessfullySendData);
  133. OCMVerify([mockSocket sendData:[OCMArg any]
  134. withTag:kFIRMessagingProtoTagClose
  135. rmqId:[OCMArg isNil]]);
  136. }
  137. - (void)testReceiveCloseProto {
  138. [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
  139. id mockSocket = self.fakeConnection.socket;
  140. GtalkClose *close = [[GtalkClose alloc] init];
  141. [self.fakeConnection secureSocket:mockSocket
  142. didReceiveData:[close data]
  143. withTag:kFIRMessagingProtoTagClose];
  144. XCTAssertEqual(self.fakeConnection.state, kFIRMessagingConnectionNotConnected);
  145. XCTAssertTrue(self.didSuccessfullySendData);
  146. }
  147. - (void)testLoginRequest {
  148. XCTAssertEqual(kFIRMessagingConnectionNotConnected, [self.fakeConnection state]);
  149. [self.fakeConnection setupConnectionSocket];
  150. id socketMock = OCMPartialMock(self.fakeConnection.socket);
  151. self.fakeConnection.socket = socketMock;
  152. [[[socketMock stub] andDo:^(NSInvocation *invocation) {
  153. [socketMock _fakeSuccessfulSocketConnect];
  154. }] connectToHost:[FIRMessagingFakeConnection fakeHost]
  155. port:[FIRMessagingFakeConnection fakePort]
  156. onRunLoop:[OCMArg any]];
  157. [[[socketMock stub] andCall:@selector(_sendData:withTag:rmqId:) onObject:self]
  158. // do nothing
  159. sendData:[OCMArg any]
  160. withTag:kFIRMessagingProtoTagLoginRequest
  161. rmqId:[OCMArg isNil]];
  162. // swizzle disconnect socket
  163. [[[socketMock stub] andCall:@selector(_disconnectSocket) onObject:self] disconnect];
  164. currentProtoSendTag = kFIRMessagingProtoTagLoginRequest;
  165. // send login request
  166. [self.fakeConnection connectToSocket:socketMock];
  167. // verify login request sent
  168. XCTAssertEqual(1, self.fakeConnection.outStreamId);
  169. XCTAssertTrue(self.didSuccessfullySendData);
  170. [socketMock verify];
  171. }
  172. - (void)testLoginRequest_withPendingMessagesInRmq {
  173. // TODO: add fake messages to rmq and test login request with them
  174. }
  175. - (void)testLoginRequest_withSuccessfulResponse {
  176. [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
  177. OCMVerify([self.mockClient didLoginWithConnection:[OCMArg isEqual:self.fakeConnection]]);
  178. // should send a heartbeat ping too
  179. XCTAssertEqual(self.fakeConnection.outStreamId, 2);
  180. // update for the received login response proto
  181. XCTAssertEqual(self.fakeConnection.inStreamId, 1);
  182. // did send data during login
  183. XCTAssertTrue(self.didSuccessfullySendData);
  184. }
  185. - (void)testConnectionTimeout {
  186. XCTAssertEqual(kFIRMessagingConnectionNotConnected, [self.fakeConnection state]);
  187. [self.fakeConnection setupConnectionSocket];
  188. id socketMock = OCMPartialMock(self.fakeConnection.socket);
  189. self.fakeConnection.socket = socketMock;
  190. [[[socketMock stub] andDo:^(NSInvocation *invocation) {
  191. [socketMock _fakeSuccessfulSocketConnect];
  192. }] connectToHost:[FIRMessagingFakeConnection fakeHost]
  193. port:[FIRMessagingFakeConnection fakePort]
  194. onRunLoop:[OCMArg any]];
  195. [self.fakeConnection connectToSocket:socketMock];
  196. XCTAssertEqual(self.fakeConnection.state, kFIRMessagingConnectionConnected);
  197. GtalkLoginResponse *response = [[GtalkLoginResponse alloc] init];
  198. [response setId_p:@""];
  199. // connection timeout has been scheduled
  200. // should disconnect since we wait for more time
  201. XCTestExpectation *disconnectExpectation =
  202. [self expectationWithDescription:@"FCM connection should timeout without receiving "
  203. @"any data for a timeout interval"];
  204. [[[socketMock stub] andDo:^(NSInvocation *invocation) {
  205. [self _disconnectSocket];
  206. [disconnectExpectation fulfill];
  207. }] disconnect];
  208. // simulate connection receiving login response
  209. [self.fakeConnection secureSocket:socketMock
  210. didReceiveData:[response data]
  211. withTag:kFIRMessagingProtoTagLoginResponse];
  212. [self waitForExpectationsWithTimeout:2.0
  213. handler:^(NSError *error) {
  214. XCTAssertNil(error);
  215. }];
  216. [socketMock verify];
  217. XCTAssertEqual(self.fakeConnection.state, kFIRMessagingConnectionNotConnected);
  218. }
  219. - (void)testDataMessageReceive {
  220. [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
  221. GtalkDataMessageStanza *stanza = [[GtalkDataMessageStanza alloc] init];
  222. [stanza setCategory:@"special"];
  223. [stanza setFrom:@"xyz"];
  224. [self.fakeConnection secureSocket:self.fakeConnection.socket
  225. didReceiveData:[stanza data]
  226. withTag:kFIRMessagingProtoTagDataMessageStanza];
  227. OCMVerify([self.mockClient connectionDidRecieveMessage:[OCMArg checkWithBlock:^BOOL(id obj) {
  228. GtalkDataMessageStanza *message = (GtalkDataMessageStanza *)obj;
  229. return [[message category] isEqual:@"special"] &&
  230. [[message from] isEqual:@"xyz"];
  231. }]]);
  232. // did send data while login
  233. XCTAssertTrue(self.didSuccessfullySendData);
  234. }
  235. - (void)testDataMessageReceiveWithInvalidTag {
  236. [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
  237. GtalkDataMessageStanza *stanza = [[GtalkDataMessageStanza alloc] init];
  238. BOOL didCauseException = NO;
  239. @try {
  240. [self.fakeConnection secureSocket:self.fakeConnection.socket
  241. didReceiveData:[stanza data]
  242. withTag:kFIRMessagingProtoTagInvalid];
  243. } @catch (NSException *exception) {
  244. didCauseException = YES;
  245. } @finally {
  246. }
  247. XCTAssertFalse(didCauseException);
  248. }
  249. - (void)testDataMessageReceiveWithTagThatDoesntEquateToClass {
  250. [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
  251. GtalkDataMessageStanza *stanza = [[GtalkDataMessageStanza alloc] init];
  252. BOOL didCauseException = NO;
  253. int8_t tagWhichDoesntEquateToClass = INT8_MAX;
  254. @try {
  255. [self.fakeConnection secureSocket:self.fakeConnection.socket
  256. didReceiveData:[stanza data]
  257. withTag:tagWhichDoesntEquateToClass];
  258. } @catch (NSException *exception) {
  259. didCauseException = YES;
  260. } @finally {
  261. }
  262. XCTAssertFalse(didCauseException);
  263. }
  264. - (void)testHeartbeatSend {
  265. [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection]; // outstreamId should be 2
  266. XCTAssertEqual(self.fakeConnection.outStreamId, 2);
  267. [self.fakeConnection sendHeartbeatPing];
  268. id mockSocket = self.fakeConnection.socket;
  269. OCMVerify([mockSocket sendData:[OCMArg any]
  270. withTag:kFIRMessagingProtoTagHeartbeatPing
  271. rmqId:[OCMArg isNil]]);
  272. XCTAssertEqual(self.fakeConnection.outStreamId, 3);
  273. // did send data
  274. XCTAssertTrue(self.didSuccessfullySendData);
  275. }
  276. - (void)testHeartbeatReceived {
  277. [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
  278. XCTAssertEqual(self.fakeConnection.outStreamId, 2);
  279. GtalkHeartbeatPing *ping = [[GtalkHeartbeatPing alloc] init];
  280. [self.fakeConnection secureSocket:self.fakeConnection.socket
  281. didReceiveData:[ping data]
  282. withTag:kFIRMessagingProtoTagHeartbeatPing];
  283. XCTAssertEqual(self.fakeConnection.inStreamId, 2);
  284. id mockSocket = self.fakeConnection.socket;
  285. OCMVerify([mockSocket sendData:[OCMArg any]
  286. withTag:kFIRMessagingProtoTagHeartbeatAck
  287. rmqId:[OCMArg isNil]]);
  288. XCTAssertEqual(self.fakeConnection.outStreamId, 3);
  289. // did send data
  290. XCTAssertTrue(self.didSuccessfullySendData);
  291. }
  292. // TODO: Add tests for Selective/Stream ACK's
  293. #pragma mark - Stubs
  294. - (void)_disconnectSocket {
  295. self.fakeConnection.socket.state = kFIRMessagingSecureSocketClosed;
  296. }
  297. - (void)_sendData:(NSData *)data withTag:(int8_t)tag rmqId:(NSString *)rmqId {
  298. NSLog(@"FIRMessaging Socket: Send data with Tag: %d rmq: %@", tag, rmqId);
  299. if (currentProtoSendTag > 0) {
  300. XCTAssertEqual(tag, currentProtoSendTag);
  301. }
  302. self.didSuccessfullySendData = YES;
  303. }
  304. #pragma mark - Private Helpers
  305. /**
  306. * Stub socket disconnect to prevent spurious assert. Since we mock the socket object being
  307. * used by the connection, while we teardown the client we also disconnect the socket to tear
  308. * it down. Since we are using mock sockets we need to stub the `disconnect` to prevent some
  309. * assertions from taking place.
  310. * The `_disconectSocket` has the gist of the actual socket disconnect without any assertions.
  311. */
  312. - (void)stubSocketDisconnect:(id)mockSocket {
  313. [[[mockSocket stub] andCall:@selector(_disconnectSocket) onObject:self] disconnect];
  314. [mockSocket verify];
  315. }
  316. - (void)mockSuccessfulSignIn {
  317. XCTAssertEqual(kFIRMessagingConnectionNotConnected, [self.fakeConnection state]);
  318. [self.fakeConnection setupConnectionSocket];
  319. id socketMock = OCMPartialMock(self.fakeConnection.socket);
  320. self.fakeConnection.socket = socketMock;
  321. [[[socketMock stub] andDo:^(NSInvocation *invocation) {
  322. [socketMock _fakeSuccessfulSocketConnect];
  323. }] connectToHost:[FIRMessagingFakeConnection fakeHost]
  324. port:[FIRMessagingFakeConnection fakePort]
  325. onRunLoop:[OCMArg any]];
  326. [[[socketMock stub] andCall:@selector(_sendData:withTag:rmqId:) onObject:self]
  327. // do nothing
  328. sendData:[OCMArg any]
  329. withTag:kFIRMessagingProtoTagLoginRequest
  330. rmqId:[OCMArg isNil]];
  331. // send login request
  332. currentProtoSendTag = kFIRMessagingProtoTagLoginRequest;
  333. [self.fakeConnection connectToSocket:socketMock];
  334. GtalkLoginResponse *response = [[GtalkLoginResponse alloc] init];
  335. [response setId_p:@""];
  336. // simulate connection receiving login response
  337. [self.fakeConnection secureSocket:socketMock
  338. didReceiveData:[response data]
  339. withTag:kFIRMessagingProtoTagLoginResponse];
  340. OCMVerify([self.mockClient didLoginWithConnection:[OCMArg isEqual:self.fakeConnection]]);
  341. // should receive data
  342. XCTAssertTrue(self.didSuccessfullySendData);
  343. // should send a heartbeat ping too
  344. XCTAssertEqual(self.fakeConnection.outStreamId, 2);
  345. // update for the received login response proto
  346. XCTAssertEqual(self.fakeConnection.inStreamId, 1);
  347. }
  348. - (void)setupSuccessfulLoginRequestWithConnection:(FIRMessagingConnection *)fakeConnection {
  349. [fakeConnection setupConnectionSocket];
  350. id socketMock = OCMPartialMock(fakeConnection.socket);
  351. fakeConnection.socket = socketMock;
  352. [[[socketMock stub] andDo:^(NSInvocation *invocation) {
  353. [socketMock _fakeSuccessfulSocketConnect];
  354. }] connectToHost:[FIRMessagingFakeConnection fakeHost]
  355. port:[FIRMessagingFakeConnection fakePort]
  356. onRunLoop:[OCMArg any]];
  357. [[[socketMock stub] andCall:@selector(_sendData:withTag:rmqId:) onObject:self]
  358. // do nothing
  359. sendData:[OCMArg any]
  360. withTag:kFIRMessagingProtoTagLoginRequest
  361. rmqId:[OCMArg isNil]];
  362. // swizzle disconnect socket
  363. [[[socketMock stub] andCall:@selector(_disconnectSocket) onObject:self] disconnect];
  364. // send login request
  365. currentProtoSendTag = kFIRMessagingProtoTagLoginRequest;
  366. [fakeConnection connectToSocket:socketMock];
  367. GtalkLoginResponse *response = [[GtalkLoginResponse alloc] init];
  368. [response setId_p:@""];
  369. // simulate connection receiving login response
  370. [fakeConnection secureSocket:socketMock
  371. didReceiveData:[response data]
  372. withTag:kFIRMessagingProtoTagLoginResponse];
  373. }
  374. @end