1/*
2 *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
3 *
4 *  Use of this source code is governed by a BSD-style license
5 *  that can be found in the LICENSE file in the root of the source
6 *  tree. An additional intellectual property rights grant can be found
7 *  in the file PATENTS.  All contributing project authors may
8 *  be found in the AUTHORS file in the root of the source tree.
9 */
10
11#import <Foundation/Foundation.h>
12#import <OCMock/OCMock.h>
13
14#import "ARDAppClient+Internal.h"
15#import "ARDJoinResponse+Internal.h"
16#import "ARDMessageResponse+Internal.h"
17#import "ARDSDPUtils.h"
18#import "RTCMediaConstraints.h"
19#import "RTCPeerConnectionFactory.h"
20#import "RTCSessionDescription.h"
21
22#include "webrtc/base/gunit.h"
23#include "webrtc/base/ssladapter.h"
24
25// These classes mimic XCTest APIs, to make eventual conversion to XCTest
26// easier. Conversion will happen once XCTest is supported well on build bots.
27@interface ARDTestExpectation : NSObject
28
29@property(nonatomic, readonly) NSString *description;
30@property(nonatomic, readonly) BOOL isFulfilled;
31
32- (instancetype)initWithDescription:(NSString *)description;
33- (void)fulfill;
34
35@end
36
37@implementation ARDTestExpectation
38
39@synthesize description = _description;
40@synthesize isFulfilled = _isFulfilled;
41
42- (instancetype)initWithDescription:(NSString *)description {
43  if (self = [super init]) {
44    _description = description;
45  }
46  return self;
47}
48
49- (void)fulfill {
50  _isFulfilled = YES;
51}
52
53@end
54
55@interface ARDTestCase : NSObject
56
57- (ARDTestExpectation *)expectationWithDescription:(NSString *)description;
58- (void)waitForExpectationsWithTimeout:(NSTimeInterval)timeout
59                               handler:(void (^)(NSError *error))handler;
60
61@end
62
63@implementation ARDTestCase {
64  NSMutableArray *_expectations;
65}
66
67- (instancetype)init {
68  if (self = [super init]) {
69   _expectations = [NSMutableArray array];
70  }
71  return self;
72}
73
74- (ARDTestExpectation *)expectationWithDescription:(NSString *)description {
75  ARDTestExpectation *expectation =
76      [[ARDTestExpectation alloc] initWithDescription:description];
77  [_expectations addObject:expectation];
78  return expectation;
79}
80
81- (void)waitForExpectationsWithTimeout:(NSTimeInterval)timeout
82                               handler:(void (^)(NSError *error))handler {
83  NSDate *startDate = [NSDate date];
84  while (![self areExpectationsFulfilled]) {
85    NSTimeInterval duration = [[NSDate date] timeIntervalSinceDate:startDate];
86    if (duration > timeout) {
87      NSAssert(NO, @"Expectation timed out.");
88      break;
89    }
90    [[NSRunLoop currentRunLoop]
91        runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
92  }
93  handler(nil);
94}
95
96- (BOOL)areExpectationsFulfilled {
97  for (ARDTestExpectation *expectation in _expectations) {
98    if (!expectation.isFulfilled) {
99      return NO;
100    }
101  }
102  return YES;
103}
104
105@end
106
107@interface ARDAppClientTest : ARDTestCase
108@end
109
110@implementation ARDAppClientTest
111
112#pragma mark - Mock helpers
113
114- (id)mockRoomServerClientForRoomId:(NSString *)roomId
115                           clientId:(NSString *)clientId
116                        isInitiator:(BOOL)isInitiator
117                           messages:(NSArray *)messages
118                     messageHandler:
119    (void (^)(ARDSignalingMessage *))messageHandler {
120  id mockRoomServerClient =
121      [OCMockObject mockForProtocol:@protocol(ARDRoomServerClient)];
122
123  // Successful join response.
124  ARDJoinResponse *joinResponse = [[ARDJoinResponse alloc] init];
125  joinResponse.result = kARDJoinResultTypeSuccess;
126  joinResponse.roomId = roomId;
127  joinResponse.clientId = clientId;
128  joinResponse.isInitiator = isInitiator;
129  joinResponse.messages = messages;
130
131  // Successful message response.
132  ARDMessageResponse *messageResponse = [[ARDMessageResponse alloc] init];
133  messageResponse.result = kARDMessageResultTypeSuccess;
134
135  // Return join response from above on join.
136  [[[mockRoomServerClient stub] andDo:^(NSInvocation *invocation) {
137    __unsafe_unretained void (^completionHandler)(ARDJoinResponse *response,
138                                                  NSError *error);
139    [invocation getArgument:&completionHandler atIndex:3];
140    completionHandler(joinResponse, nil);
141  }] joinRoomWithRoomId:roomId isLoopback:NO completionHandler:[OCMArg any]];
142
143  // Return message response from above on join.
144  [[[mockRoomServerClient stub] andDo:^(NSInvocation *invocation) {
145    __unsafe_unretained ARDSignalingMessage *message;
146    __unsafe_unretained void (^completionHandler)(ARDMessageResponse *response,
147                                                  NSError *error);
148    [invocation getArgument:&message atIndex:2];
149    [invocation getArgument:&completionHandler atIndex:5];
150    messageHandler(message);
151    completionHandler(messageResponse, nil);
152  }] sendMessage:[OCMArg any]
153            forRoomId:roomId
154             clientId:clientId
155    completionHandler:[OCMArg any]];
156
157  // Do nothing on leave.
158  [[[mockRoomServerClient stub] andDo:^(NSInvocation *invocation) {
159    __unsafe_unretained void (^completionHandler)(NSError *error);
160    [invocation getArgument:&completionHandler atIndex:4];
161    if (completionHandler) {
162      completionHandler(nil);
163    }
164  }] leaveRoomWithRoomId:roomId
165                clientId:clientId
166       completionHandler:[OCMArg any]];
167
168  return mockRoomServerClient;
169}
170
171- (id)mockSignalingChannelForRoomId:(NSString *)roomId
172                           clientId:(NSString *)clientId
173                     messageHandler:
174    (void (^)(ARDSignalingMessage *message))messageHandler {
175  id mockSignalingChannel =
176      [OCMockObject niceMockForProtocol:@protocol(ARDSignalingChannel)];
177  [[mockSignalingChannel stub] registerForRoomId:roomId clientId:clientId];
178  [[[mockSignalingChannel stub] andDo:^(NSInvocation *invocation) {
179    __unsafe_unretained ARDSignalingMessage *message;
180    [invocation getArgument:&message atIndex:2];
181    messageHandler(message);
182  }] sendMessage:[OCMArg any]];
183  return mockSignalingChannel;
184}
185
186- (id)mockTURNClient {
187  id mockTURNClient =
188      [OCMockObject mockForProtocol:@protocol(ARDTURNClient)];
189  [[[mockTURNClient stub] andDo:^(NSInvocation *invocation) {
190    // Don't return anything in TURN response.
191    __unsafe_unretained void (^completionHandler)(NSArray *turnServers,
192                                                  NSError *error);
193    [invocation getArgument:&completionHandler atIndex:2];
194    completionHandler([NSArray array], nil);
195  }] requestServersWithCompletionHandler:[OCMArg any]];
196  return mockTURNClient;
197}
198
199- (ARDAppClient *)createAppClientForRoomId:(NSString *)roomId
200                                  clientId:(NSString *)clientId
201                               isInitiator:(BOOL)isInitiator
202                                  messages:(NSArray *)messages
203                            messageHandler:
204    (void (^)(ARDSignalingMessage *message))messageHandler
205                          connectedHandler:(void (^)(void))connectedHandler {
206  id turnClient = [self mockTURNClient];
207  id signalingChannel = [self mockSignalingChannelForRoomId:roomId
208                                                   clientId:clientId
209                                             messageHandler:messageHandler];
210  id roomServerClient =
211      [self mockRoomServerClientForRoomId:roomId
212                                 clientId:clientId
213                              isInitiator:isInitiator
214                                 messages:messages
215                           messageHandler:messageHandler];
216  id delegate =
217      [OCMockObject niceMockForProtocol:@protocol(ARDAppClientDelegate)];
218  [[[delegate stub] andDo:^(NSInvocation *invocation) {
219    connectedHandler();
220  }] appClient:[OCMArg any] didChangeConnectionState:RTCICEConnectionConnected];
221
222  return [[ARDAppClient alloc] initWithRoomServerClient:roomServerClient
223                                       signalingChannel:signalingChannel
224                                             turnClient:turnClient
225                                               delegate:delegate];
226}
227
228// Tests that an ICE connection is established between two ARDAppClient objects
229// where one is set up as a caller and the other the answerer. Network
230// components are mocked out and messages are relayed directly from object to
231// object. It's expected that both clients reach the RTCICEConnectionConnected
232// state within a reasonable amount of time.
233- (void)testSession {
234  // Need block arguments here because we're setting up a callbacks before we
235  // create the clients.
236  ARDAppClient *caller = nil;
237  ARDAppClient *answerer = nil;
238  __block __weak ARDAppClient *weakCaller = nil;
239  __block __weak ARDAppClient *weakAnswerer = nil;
240  NSString *roomId = @"testRoom";
241  NSString *callerId = @"testCallerId";
242  NSString *answererId = @"testAnswererId";
243
244  ARDTestExpectation *callerConnectionExpectation =
245      [self expectationWithDescription:@"Caller PC connected."];
246  ARDTestExpectation *answererConnectionExpectation =
247      [self expectationWithDescription:@"Answerer PC connected."];
248
249  caller = [self createAppClientForRoomId:roomId
250                                 clientId:callerId
251                              isInitiator:YES
252                                 messages:[NSArray array]
253                           messageHandler:^(ARDSignalingMessage *message) {
254    ARDAppClient *strongAnswerer = weakAnswerer;
255    [strongAnswerer channel:strongAnswerer.channel didReceiveMessage:message];
256  } connectedHandler:^{
257    [callerConnectionExpectation fulfill];
258  }];
259  // TODO(tkchin): Figure out why DTLS-SRTP constraint causes thread assertion
260  // crash in Debug.
261  caller.defaultPeerConnectionConstraints = [[RTCMediaConstraints alloc] init];
262  weakCaller = caller;
263
264  answerer = [self createAppClientForRoomId:roomId
265                                   clientId:answererId
266                                isInitiator:NO
267                                   messages:[NSArray array]
268                             messageHandler:^(ARDSignalingMessage *message) {
269    ARDAppClient *strongCaller = weakCaller;
270    [strongCaller channel:strongCaller.channel didReceiveMessage:message];
271  } connectedHandler:^{
272    [answererConnectionExpectation fulfill];
273  }];
274  // TODO(tkchin): Figure out why DTLS-SRTP constraint causes thread assertion
275  // crash in Debug.
276  answerer.defaultPeerConnectionConstraints =
277      [[RTCMediaConstraints alloc] init];
278  weakAnswerer = answerer;
279
280  // Kick off connection.
281  [caller connectToRoomWithId:roomId isLoopback:NO isAudioOnly:NO];
282  [answerer connectToRoomWithId:roomId isLoopback:NO isAudioOnly:NO];
283  [self waitForExpectationsWithTimeout:20 handler:^(NSError *error) {
284    if (error) {
285      NSLog(@"Expectations error: %@", error);
286    }
287  }];
288}
289
290@end
291
292@interface ARDSDPUtilsTest : ARDTestCase
293- (void)testPreferVideoCodec;
294@end
295
296@implementation ARDSDPUtilsTest
297
298- (void)testPreferVideoCodec {
299  NSString *sdp = @("m=video 9 RTP/SAVPF 100 116 117 96 120\n"
300                    "a=rtpmap:120 H264/90000\n");
301  NSString *expectedSdp = @("m=video 9 RTP/SAVPF 120 100 116 117 96\n"
302                            "a=rtpmap:120 H264/90000\n");
303  RTCSessionDescription* desc =
304      [[RTCSessionDescription alloc] initWithType:@"offer" sdp:sdp];
305  RTCSessionDescription *h264Desc =
306      [ARDSDPUtils descriptionForDescription:desc
307                         preferredVideoCodec:@"H264"];
308  EXPECT_TRUE([h264Desc.description isEqualToString:expectedSdp]);
309}
310
311@end
312
313class SignalingTest : public ::testing::Test {
314 protected:
315  static void SetUpTestCase() {
316    rtc::InitializeSSL();
317  }
318  static void TearDownTestCase() {
319    rtc::CleanupSSL();
320  }
321};
322
323TEST_F(SignalingTest, SessionTest) {
324  @autoreleasepool {
325    ARDAppClientTest *test = [[ARDAppClientTest alloc] init];
326    [test testSession];
327  }
328}
329
330TEST_F(SignalingTest, SDPTest) {
331  @autoreleasepool {
332    ARDSDPUtilsTest *test = [[ARDSDPUtilsTest alloc] init];
333    [test testPreferVideoCodec];
334  }
335}
336
337
338