appcenter-sdk-apple/MobileCenter/MobileCenterTests/MSIngestionSenderTests.m

596 строки
22 KiB
Objective-C

/*
* Copyright (c) Microsoft Corporation. All rights reserved.
*/
#import "MSDevice.h"
#import "MSDevicePrivate.h"
#import "MSIngestionSender.h"
#import "MSHttpSenderPrivate.h"
#import "MSHttpTestUtil.h"
#import "MSMobileCenterErrors.h"
#import "MSMockLog.h"
#import "MSSenderCall.h"
#import "MSSenderDelegate.h"
#import "MobileCenter+Internal.h"
#import "OCMock.h"
#import <OCHamcrestIOS/OCHamcrestIOS.h>
#import <XCTest/XCTest.h>
static NSTimeInterval const kMSTestTimeout = 5.0;
static NSString *const kMSBaseUrl = @"https://test.com";
static NSString *const kMSAppSecret = @"mockAppSecret";
@interface MSIngestionSenderTests : XCTestCase
@property(nonatomic, strong) MSIngestionSender *sut;
@property(nonatomic, strong) id reachabilityMock;
@property(nonatomic) NetworkStatus currentNetworkStatus;
@end
@implementation MSIngestionSenderTests
- (void)setUp {
[super setUp];
NSDictionary *headers = @{
@"Content-Type" : @"application/json",
@"App-Secret" : @"myUnitTestAppSecret",
@"Install-ID" : MS_UUID_STRING
};
NSDictionary *queryStrings = @{ @"api_version" : @"1.0.0-preview20160914" };
// Mock reachability.
self.reachabilityMock = OCMClassMock([MS_Reachability class]);
self.currentNetworkStatus = ReachableViaWiFi;
OCMStub([self.reachabilityMock currentReachabilityStatus]).andDo(^(NSInvocation *invocation) {
NetworkStatus test = self.currentNetworkStatus;
[invocation setReturnValue:&test];
});
// sut: System under test
self.sut = [[MSIngestionSender alloc] initWithBaseUrl:kMSBaseUrl
apiPath:@"test-path"
headers:headers
queryStrings:queryStrings
reachability:self.reachabilityMock
retryIntervals:@[ @(0.5), @(1), @(1.5) ]];
}
- (void)tearDown {
[super tearDown];
[OHHTTPStubs removeAllStubs];
}
- (void)testSendBatchLogs {
// Stub http response
[MSHttpTestUtil stubHttp200Response];
NSString *containerId = @"1";
MSLogContainer *container = [self createLogContainerWithId:containerId];
__weak XCTestExpectation *expectation = [self expectationWithDescription:@"HTTP Response 200"];
[self.sut sendAsync:container
completionHandler:^(NSString *batchId, NSError *error, NSUInteger statusCode) {
XCTAssertNil(error);
XCTAssertEqual(containerId, batchId);
XCTAssertEqual(statusCode, MSHTTPCodesNo200OK);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:kMSTestTimeout
handler:^(NSError *_Nullable error) {
if (error) {
XCTFail(@"Expectation Failed with error: %@", error);
}
}];
}
- (void)testUnrecoverableError {
// Stub http response
[MSHttpTestUtil stubHttp404Response];
NSString *containerId = @"1";
MSLogContainer *container = [self createLogContainerWithId:containerId];
__weak XCTestExpectation *expectation = [self expectationWithDescription:@"HTTP Response 200"];
[self.sut sendAsync:container
completionHandler:^(NSString *batchId, NSError *error, NSUInteger statusCode) {
XCTAssertEqual(containerId, batchId);
XCTAssertEqual(statusCode, MSHTTPCodesNo404NotFound);
XCTAssertEqual(error.domain, kMSMCErrorDomain);
XCTAssertEqual(error.code, kMSMCConnectionHttpErrorCode);
XCTAssertEqual(error.localizedDescription, kMSMCConnectionHttpErrorDesc);
XCTAssertTrue([error.userInfo[kMSMCConnectionHttpCodeErrorKey] isEqual:@(MSHTTPCodesNo404NotFound)]);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:kMSTestTimeout
handler:^(NSError *_Nullable error) {
if (error) {
XCTFail(@"Expectation Failed with error: %@", error);
}
}];
}
- (void)testNetworkDown {
// If
[MSHttpTestUtil stubNetworkDownResponse];
XCTestExpectation *requestCompletedExcpectation = [self expectationWithDescription:@"Request completed."];
MSLogContainer *container = [self createLogContainerWithId:@"1"];
// Set a delegate for suspending event.
id delegateMock = OCMProtocolMock(@protocol(MSSenderDelegate));
OCMStub([delegateMock senderDidSuspend:self.sut]).andDo(^(NSInvocation *invocation) {
[requestCompletedExcpectation fulfill];
});
[self.sut addDelegate:delegateMock];
// When
[self.sut sendAsync:container
completionHandler:^(NSString *batchId, NSError *error, NSUInteger statusCode) {
// This should not be happening.
XCTFail(@"Completion handler should'nt be called on recoverable errors.");
}];
// Then
[self waitForExpectationsWithTimeout:kMSTestTimeout
handler:^(NSError *error) {
// The call must still be in the pending calls, intended to be retried later.
assertThatUnsignedLong(self.sut.pendingCalls.count, equalToInt(1));
// Sender must be suspended when network is down.
assertThatBool(self.sut.suspended, isTrue());
if (error) {
XCTFail(@"Expectation Failed with error: %@", error);
}
}];
}
- (void)testNetworkUpAgain {
// If
XCTestExpectation *requestCompletedExcpectation = [self expectationWithDescription:@"Request completed."];
__block NSInteger forwardedStatus;
__block NSError *forwardedError;
[MSHttpTestUtil stubHttp200Response];
MSLogContainer *container = [self createLogContainerWithId:@"1"];
// Set a delegate for suspending/resuming event.
id delegateMock = OCMProtocolMock(@protocol(MSSenderDelegate));
[self.sut addDelegate:delegateMock];
OCMStub([delegateMock senderDidSuspend:self.sut]).andDo(^(NSInvocation *invocation) {
// Send one batch now that the sender is suspended.
[self.sut sendAsync:container
completionHandler:^(NSString *batchId, NSError *error, NSUInteger statusCode) {
forwardedStatus = statusCode;
forwardedError = error;
[requestCompletedExcpectation fulfill];
}];
// When
// Simulate network up again.
[self simulateReachabilityChangedNotification:ReachableViaWiFi];
});
// Simulate network is down.
[self simulateReachabilityChangedNotification:NotReachable];
// Then
[self waitForExpectationsWithTimeout:kMSTestTimeout
handler:^(NSError *error) {
// The sender got resumed.
OCMVerify([delegateMock senderDidResume:self.sut]);
assertThatBool(self.sut.suspended, isFalse());
// The call as been removed.
assertThatUnsignedLong(self.sut.pendingCalls.count, equalToInt(0));
// Status codes and error must be the same.
assertThatLong(MSHTTPCodesNo200OK, equalToLong(forwardedStatus));
assertThat(forwardedError, nilValue());
if (error) {
XCTFail(@"Expectation Failed with error: %@", error);
}
}];
}
- (void)testTasksSuspendedOnSenderSuspended {
// If
XCTestExpectation *tasksListedExpectation = [self expectationWithDescription:@"URL Session tasks listed."];
__block NSArray<NSURLSessionDataTask *> *tasks;
[MSHttpTestUtil stubLongTimeOutResponse];
MSLogContainer *container1 = [self createLogContainerWithId:@"1"];
MSLogContainer *container2 = [self createLogContainerWithId:@"2"];
// Send logs
[self.sut sendAsync:container1
completionHandler:^(NSString *batchId, NSError *error, NSUInteger statusCode) {
XCTFail(@"Completion handler shouldn't be called as test will finish before the response timeout.");
}];
[self.sut sendAsync:container2
completionHandler:^(NSString *batchId, NSError *error, NSUInteger statusCode) {
XCTFail(@"Completion handler shouldn't be called as test will finish before the response timeout.");
}];
// When
[self.sut suspend];
[self.sut.session getTasksWithCompletionHandler:^(NSArray<NSURLSessionDataTask *> *_Nonnull dataTasks,
NSArray<NSURLSessionUploadTask *> *_Nonnull uploadTasks,
NSArray<NSURLSessionDownloadTask *> *_Nonnull downloadTasks) {
tasks = dataTasks;
[tasksListedExpectation fulfill];
}];
// Then
[self waitForExpectationsWithTimeout:kMSTestTimeout
handler:^(NSError *error) {
// Must be only two tasks
assertThatInteger(tasks.count, equalToInteger(2));
// Tasks must be suspended.
[tasks enumerateObjectsUsingBlock:^(__kindof NSURLSessionTask *_Nonnull task,
NSUInteger idx, BOOL *_Nonnull stop) {
assertThatInteger(task.state, equalToInteger(NSURLSessionTaskStateSuspended));
}];
// Sender must be suspended.
assertThatBool(self.sut.suspended, isTrue());
// Calls must still be in the pending calls, intended to be resumed later.
assertThatUnsignedLong(self.sut.pendingCalls.count, equalToInt(2));
if (error) {
XCTFail(@"Expectation Failed with error: %@", error);
}
}];
}
- (void)testTasksRunningOnSenderResumed {
// If
XCTestExpectation *tasksListedExpectation = [self expectationWithDescription:@"Container 1 sent."];
__block NSArray<NSURLSessionDataTask *> *tasks;
[MSHttpTestUtil stubLongTimeOutResponse];
MSLogContainer *container1 = [self createLogContainerWithId:@"1"];
MSLogContainer *container2 = [self createLogContainerWithId:@"2"];
// Send logs
[self.sut sendAsync:container1
completionHandler:^(NSString *batchId, NSError *error, NSUInteger statusCode) {
XCTFail(@"Completion handler shouldn't be called as test will finish before the response timeout.");
}];
[self.sut sendAsync:container2
completionHandler:^(NSString *batchId, NSError *error, NSUInteger statusCode) {
XCTFail(@"Completion handler shouldn't be called as test will finish before the response timeout.");
}];
[self.sut suspend];
// When
[self.sut resume];
[self.sut.session getTasksWithCompletionHandler:^(NSArray<NSURLSessionDataTask *> *_Nonnull dataTasks,
NSArray<NSURLSessionUploadTask *> *_Nonnull uploadTasks,
NSArray<NSURLSessionDownloadTask *> *_Nonnull downloadTasks) {
// Capture tasks state.
tasks = dataTasks;
[tasksListedExpectation fulfill];
}];
// Then
[self waitForExpectationsWithTimeout:kMSTestTimeout
handler:^(NSError *error) {
// Must be only two tasks
assertThatInteger(tasks.count, equalToInteger(2));
// Tasks must have been resumed.
[tasks enumerateObjectsUsingBlock:^(__kindof NSURLSessionDataTask *_Nonnull task,
NSUInteger idx, BOOL *_Nonnull stop) {
assertThatInteger(task.state, equalToInteger(NSURLSessionTaskStateRunning));
}];
// Sender must be suspended.
assertThatBool(self.sut.suspended, isFalse());
// Calls must still be in the pending calls, not yet timed out.
assertThatUnsignedLong(self.sut.pendingCalls.count, equalToInt(2));
if (error) {
XCTFail(@"Expectation Failed with error: %@", error);
}
}];
}
- (void)testRetryStoppedWhileSuspended {
// If
XCTestExpectation *responseReceivedExcpectation = [self expectationWithDescription:@"Request completed."];
NSString *containerId = @"1";
MSLogContainer *container = [self createLogContainerWithId:containerId];
// Mock the call to intercept the retry.
MSSenderCall *mockedCall = OCMPartialMock([[MSSenderCall alloc] initWithRetryIntervals:@[ @(UINT_MAX) ]]);
mockedCall.delegate = self.sut;
mockedCall.data = container;
mockedCall.callId = container.batchId;
mockedCall.completionHandler = nil;
OCMStub([mockedCall sender:self.sut callCompletedWithStatus:MSHTTPCodesNo500InternalServerError error:[OCMArg any]])
.andForwardToRealObject()
.andDo(^(NSInvocation *invocation) {
[responseReceivedExcpectation fulfill];
});
self.sut.pendingCalls[containerId] = mockedCall;
// Respond with a retryable error.
[MSHttpTestUtil stubHttp500Response];
// Send the call.
[self.sut sendCallAsync:mockedCall];
[self waitForExpectationsWithTimeout:kMSTestTimeout
handler:^(NSError *error) {
/**
* When
*/
// Suspend now that the call is retrying.
[self.sut suspend];
/**
* Then
*/
// Retry must be stopped.
assertThat(((MSSenderCall *)self.sut.pendingCalls[@"1"]).timerSource, nilValue());
// No call submitted to the session.
assertThatBool(self.sut.pendingCalls[@"1"].submitted, isFalse());
if (error) {
XCTFail(@"Expectation Failed with error: %@", error);
}
}];
}
- (void)testInvalidContainer {
MSMockLog *log1 = [[MSMockLog alloc] init];
log1.sid = MS_UUID_STRING;
log1.toffset = [NSNumber numberWithLongLong:[MSUtil nowInMilliseconds]];
// Log does not have device info, therefore, it's an invalid log
MSLogContainer *container = [[MSLogContainer alloc] initWithBatchId:@"1" andLogs:(NSArray<MSLog> *)@[ log1 ]];
[self.sut sendAsync:container
completionHandler:^(NSString *batchId, NSError *error, NSUInteger statusCode) {
XCTAssertEqual(error.domain, kMSMCErrorDomain);
XCTAssertEqual(error.code, kMSMCLogInvalidContainerErrorCode);
}];
XCTAssertEqual([self.sut.pendingCalls count], 0);
}
- (void)testNilContainer {
MSLogContainer *container = nil;
__weak XCTestExpectation *expectation = [self expectationWithDescription:@"HTTP Network Down"];
[self.sut sendAsync:container
completionHandler:^(NSString *batchId, NSError *error, NSUInteger statusCode) {
XCTAssertNotNil(error);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:kMSTestTimeout
handler:^(NSError *_Nullable error) {
if (error) {
XCTFail(@"Expectation Failed with error: %@", error);
}
}];
}
- (void)testAddDelegate {
// If.
id delegateMock = OCMProtocolMock(@protocol(MSSenderDelegate));
// When.
[self.sut addDelegate:delegateMock];
// Then.
assertThatBool([self.sut.delegates containsObject:delegateMock], isTrue());
}
- (void)testAddMultipleDelegates {
// If.
id delegateMock1 = OCMProtocolMock(@protocol(MSSenderDelegate));
id delegateMock2 = OCMProtocolMock(@protocol(MSSenderDelegate));
// When.
[self.sut addDelegate:delegateMock1];
[self.sut addDelegate:delegateMock2];
// Then.
assertThatBool([self.sut.delegates containsObject:delegateMock1], isTrue());
assertThatBool([self.sut.delegates containsObject:delegateMock2], isTrue());
}
- (void)testAddTwiceSameDelegate {
// If.
id delegateMock = OCMProtocolMock(@protocol(MSSenderDelegate));
// When.
[self.sut addDelegate:delegateMock];
[self.sut addDelegate:delegateMock];
// Then.
assertThatBool([self.sut.delegates containsObject:delegateMock], isTrue());
assertThatUnsignedLong(self.sut.delegates.count, equalToInt(1));
}
- (void)testRemoveDelegate {
// If.
id delegateMock = OCMProtocolMock(@protocol(MSSenderDelegate));
[self.sut addDelegate:delegateMock];
// When.
[self.sut removeDelegate:delegateMock];
// Then.
assertThatBool([self.sut.delegates containsObject:delegateMock], isFalse());
}
- (void)testRemoveTwiceSameDelegate {
// If.
id delegateMock1 = OCMProtocolMock(@protocol(MSSenderDelegate));
id delegateMock2 = OCMProtocolMock(@protocol(MSSenderDelegate));
[self.sut addDelegate:delegateMock1];
[self.sut addDelegate:delegateMock2];
// When.
[self.sut removeDelegate:delegateMock1];
[self.sut removeDelegate:delegateMock1];
// Then.
assertThatBool([self.sut.delegates containsObject:delegateMock1], isFalse());
assertThatBool([self.sut.delegates containsObject:delegateMock2], isTrue());
assertThatUnsignedLong(self.sut.delegates.count, equalToInt(1));
}
- (void)testNullifiedDelegate {
// If.
@autoreleasepool {
__weak id delegateMock = OCMProtocolMock(@protocol(MSSenderDelegate));
[self.sut addDelegate:delegateMock];
// When.
delegateMock = nil;
}
// Then.
// There is a bug somehow in NSHashTable where the count on the table itself is not decremented while an object is
// deallocated and auto removed from the table. The NSHashtable allObjects: is used instead to remediate.
assertThatUnsignedLong(self.sut.delegates.allObjects.count, equalToInt(0));
}
- (void)testCallDelegatesOnSuspended {
// If.
id delegateMock1 = OCMProtocolMock(@protocol(MSSenderDelegate));
id delegateMock2 = OCMProtocolMock(@protocol(MSSenderDelegate));
[self.sut resume];
[self.sut addDelegate:delegateMock1];
[self.sut addDelegate:delegateMock2];
// When.
[self.sut suspend];
// Then.
OCMVerify([delegateMock1 senderDidSuspend:self.sut]);
OCMVerify([delegateMock2 senderDidSuspend:self.sut]);
}
- (void)testCallDelegatesOnResumed {
// If.
id delegateMock1 = OCMProtocolMock(@protocol(MSSenderDelegate));
id delegateMock2 = OCMProtocolMock(@protocol(MSSenderDelegate));
[self.sut suspend];
[self.sut addDelegate:delegateMock1];
[self.sut addDelegate:delegateMock2];
// When.
[self.sut suspend];
[self.sut resume];
// Then.
OCMVerify([delegateMock1 senderDidResume:self.sut]);
OCMVerify([delegateMock2 senderDidResume:self.sut]);
}
- (void)testLargeSecret {
// If.
NSString *secret = @"shhhh-its-a-secret";
NSString *hiddenSecret;
// When.
hiddenSecret = [self.sut hideSecret:secret];
// Then.
NSString *fullyHiddenSecret =
[@"" stringByPaddingToLength:hiddenSecret.length withString:kMSHidingStringForAppSecret startingAtIndex:0];
NSString *appSecretHiddenPart = [hiddenSecret commonPrefixWithString:fullyHiddenSecret options:0];
NSString *appSecretVisiblePart = [hiddenSecret substringFromIndex:appSecretHiddenPart.length];
assertThatInteger(secret.length - appSecretHiddenPart.length, equalToShort(kMSMaxCharactersDisplayedForAppSecret));
assertThat(appSecretVisiblePart, is([secret substringFromIndex:appSecretHiddenPart.length]));
}
- (void)testShortSecret {
// If.
NSString *secret = @"";
for (short i = 1; i <= kMSMaxCharactersDisplayedForAppSecret - 1; i++)
secret = [NSString stringWithFormat:@"%@%hd", secret, i];
NSString *hiddenSecret;
// When.
hiddenSecret = [self.sut hideSecret:secret];
// Then.
NSString *fullyHiddenSecret =
[@"" stringByPaddingToLength:hiddenSecret.length withString:kMSHidingStringForAppSecret startingAtIndex:0];
assertThatInteger(hiddenSecret.length, equalToInteger(secret.length));
assertThat(hiddenSecret, is(fullyHiddenSecret));
}
#pragma mark - Test Helpers
- (void)simulateReachabilityChangedNotification:(NetworkStatus)status {
self.currentNetworkStatus = status;
[[NSNotificationCenter defaultCenter] postNotificationName:kMSReachabilityChangedNotification
object:self.reachabilityMock];
}
- (MSLogContainer *)createLogContainerWithId:(NSString *)batchId {
MSDevice *device = [[MSDevice alloc] init];
device.sdkVersion = @"1.0.0";
MSMockLog *log1 = [[MSMockLog alloc] init];
log1.sid = MS_UUID_STRING;
log1.toffset = [NSNumber numberWithLongLong:[MSUtil nowInMilliseconds]];
log1.device = device;
MSMockLog *log2 = [[MSMockLog alloc] init];
log2.sid = MS_UUID_STRING;
log2.toffset = [NSNumber numberWithLongLong:[MSUtil nowInMilliseconds]];
log2.device = device;
MSLogContainer *logContainer =
[[MSLogContainer alloc] initWithBatchId:batchId andLogs:(NSArray<MSLog> *)@[ log1, log2 ]];
return logContainer;
}
@end