| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import <OCMock/OCMock.h> |
| #import <XCTest/XCTest.h> |
| #import "FIAPaymentQueueHandler.h" |
| #import "Stubs.h" |
| |
| @import in_app_purchase_storekit; |
| |
| @interface InAppPurchasePluginTest : XCTestCase |
| |
| @property(strong, nonatomic) FIAPReceiptManagerStub *receiptManagerStub; |
| @property(strong, nonatomic) InAppPurchasePlugin *plugin; |
| |
| @end |
| |
| @implementation InAppPurchasePluginTest |
| |
| - (void)setUp { |
| self.receiptManagerStub = [FIAPReceiptManagerStub new]; |
| self.plugin = [[InAppPurchasePluginStub alloc] initWithReceiptManager:self.receiptManagerStub]; |
| } |
| |
| - (void)tearDown { |
| } |
| |
| - (void)testInvalidMethodCall { |
| XCTestExpectation *expectation = |
| [self expectationWithDescription:@"expect result to be not implemented"]; |
| FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"invalid" arguments:NULL]; |
| __block id result; |
| [self.plugin handleMethodCall:call |
| result:^(id r) { |
| [expectation fulfill]; |
| result = r; |
| }]; |
| [self waitForExpectations:@[ expectation ] timeout:5]; |
| XCTAssertEqual(result, FlutterMethodNotImplemented); |
| } |
| |
| - (void)testCanMakePayments { |
| XCTestExpectation *expectation = [self expectationWithDescription:@"expect result to be YES"]; |
| FlutterMethodCall *call = |
| [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue canMakePayments:]" |
| arguments:NULL]; |
| __block id result; |
| [self.plugin handleMethodCall:call |
| result:^(id r) { |
| [expectation fulfill]; |
| result = r; |
| }]; |
| [self waitForExpectations:@[ expectation ] timeout:5]; |
| XCTAssertEqual(result, @YES); |
| } |
| |
| - (void)testGetProductResponse { |
| XCTestExpectation *expectation = |
| [self expectationWithDescription:@"expect response contains 1 item"]; |
| FlutterMethodCall *call = [FlutterMethodCall |
| methodCallWithMethodName:@"-[InAppPurchasePlugin startProductRequest:result:]" |
| arguments:@[ @"123" ]]; |
| __block id result; |
| [self.plugin handleMethodCall:call |
| result:^(id r) { |
| [expectation fulfill]; |
| result = r; |
| }]; |
| [self waitForExpectations:@[ expectation ] timeout:5]; |
| XCTAssert([result isKindOfClass:[NSDictionary class]]); |
| NSArray *resultArray = [result objectForKey:@"products"]; |
| XCTAssertEqual(resultArray.count, 1); |
| XCTAssertTrue([resultArray.firstObject[@"productIdentifier"] isEqualToString:@"123"]); |
| } |
| |
| - (void)testAddPaymentShouldReturnFlutterErrorWhenArgumentsAreInvalid { |
| XCTestExpectation *expectation = |
| [self expectationWithDescription: |
| @"Result should contain a FlutterError when invalid parameters are passed in."]; |
| NSString *argument = @"Invalid argument"; |
| FlutterMethodCall *call = |
| [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" |
| arguments:argument]; |
| [self.plugin handleMethodCall:call |
| result:^(id _Nullable result) { |
| FlutterError *error = result; |
| XCTAssertEqualObjects(@"storekit_invalid_argument", error.code); |
| XCTAssertEqualObjects(@"Argument type of addPayment is not a Dictionary", |
| error.message); |
| XCTAssertEqualObjects(argument, error.details); |
| [expectation fulfill]; |
| }]; |
| |
| [self waitForExpectations:@[ expectation ] timeout:5]; |
| } |
| |
| - (void)testAddPaymentShouldReturnFlutterErrorWhenPaymentFails { |
| NSDictionary *arguments = @{ |
| @"productIdentifier" : @"123", |
| @"quantity" : @(1), |
| @"simulatesAskToBuyInSandbox" : @YES, |
| }; |
| XCTestExpectation *expectation = |
| [self expectationWithDescription:@"Result should return failed state."]; |
| FlutterMethodCall *call = |
| [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" |
| arguments:arguments]; |
| |
| FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class); |
| OCMStub([mockHandler addPayment:[OCMArg any]]).andReturn(NO); |
| self.plugin.paymentQueueHandler = mockHandler; |
| |
| [self.plugin handleMethodCall:call |
| result:^(id _Nullable result) { |
| FlutterError *error = result; |
| XCTAssertEqualObjects(@"storekit_duplicate_product_object", error.code); |
| XCTAssertEqualObjects( |
| @"There is a pending transaction for the same product identifier. " |
| @"Please either wait for it to be finished or finish it manually " |
| @"using `completePurchase` to avoid edge cases.", |
| error.message); |
| XCTAssertEqualObjects(arguments, error.details); |
| [expectation fulfill]; |
| }]; |
| |
| [self waitForExpectations:@[ expectation ] timeout:5]; |
| OCMVerify(times(1), [mockHandler addPayment:[OCMArg any]]); |
| } |
| |
| - (void)testAddPaymentSuccessWithoutPaymentDiscount { |
| XCTestExpectation *expectation = |
| [self expectationWithDescription:@"Result should return success state"]; |
| FlutterMethodCall *call = |
| [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" |
| arguments:@{ |
| @"productIdentifier" : @"123", |
| @"quantity" : @(1), |
| @"simulatesAskToBuyInSandbox" : @YES, |
| }]; |
| FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class); |
| OCMStub([mockHandler addPayment:[OCMArg any]]).andReturn(YES); |
| self.plugin.paymentQueueHandler = mockHandler; |
| [self.plugin handleMethodCall:call |
| result:^(id _Nullable result) { |
| XCTAssertNil(result); |
| [expectation fulfill]; |
| }]; |
| [self waitForExpectations:@[ expectation ] timeout:5]; |
| } |
| |
| - (void)testAddPaymentSuccessWithPaymentDiscount { |
| XCTestExpectation *expectation = |
| [self expectationWithDescription:@"Result should return success state"]; |
| FlutterMethodCall *call = |
| [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" |
| arguments:@{ |
| @"productIdentifier" : @"123", |
| @"quantity" : @(1), |
| @"simulatesAskToBuyInSandbox" : @YES, |
| @"paymentDiscount" : @{ |
| @"identifier" : @"test_identifier", |
| @"keyIdentifier" : @"test_key_identifier", |
| @"nonce" : @"4a11a9cc-3bc3-11ec-8d3d-0242ac130003", |
| @"signature" : @"test_signature", |
| @"timestamp" : @(1635847102), |
| } |
| }]; |
| |
| FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class); |
| OCMStub([mockHandler addPayment:[OCMArg any]]).andReturn(YES); |
| self.plugin.paymentQueueHandler = mockHandler; |
| [self.plugin handleMethodCall:call |
| result:^(id _Nullable result) { |
| XCTAssertNil(result); |
| [expectation fulfill]; |
| }]; |
| [self waitForExpectations:@[ expectation ] timeout:5]; |
| OCMVerify( |
| times(1), |
| [mockHandler |
| addPayment:[OCMArg checkWithBlock:^BOOL(id obj) { |
| SKPayment *payment = obj; |
| if (@available(iOS 12.2, *)) { |
| SKPaymentDiscount *discount = payment.paymentDiscount; |
| |
| return [discount.identifier isEqual:@"test_identifier"] && |
| [discount.keyIdentifier isEqual:@"test_key_identifier"] && |
| [discount.nonce |
| isEqual:[[NSUUID alloc] |
| initWithUUIDString:@"4a11a9cc-3bc3-11ec-8d3d-0242ac130003"]] && |
| [discount.signature isEqual:@"test_signature"] && |
| [discount.timestamp isEqual:@(1635847102)]; |
| } |
| |
| return YES; |
| }]]); |
| } |
| |
| - (void)testAddPaymentFailureWithInvalidPaymentDiscount { |
| // Support for payment discount is only available on iOS 12.2 and higher. |
| if (@available(iOS 12.2, *)) { |
| XCTestExpectation *expectation = |
| [self expectationWithDescription:@"Result should return success state"]; |
| NSDictionary *arguments = @{ |
| @"productIdentifier" : @"123", |
| @"quantity" : @(1), |
| @"simulatesAskToBuyInSandbox" : @YES, |
| @"paymentDiscount" : @{ |
| @"keyIdentifier" : @"test_key_identifier", |
| @"nonce" : @"4a11a9cc-3bc3-11ec-8d3d-0242ac130003", |
| @"signature" : @"test_signature", |
| @"timestamp" : @(1635847102), |
| } |
| }; |
| FlutterMethodCall *call = |
| [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" |
| arguments:arguments]; |
| |
| FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class); |
| id translator = OCMClassMock(FIAObjectTranslator.class); |
| |
| NSString *error = @"Some error occurred"; |
| OCMStub(ClassMethod([translator |
| getSKPaymentDiscountFromMap:[OCMArg any] |
| withError:(NSString __autoreleasing **)[OCMArg setTo:error]])) |
| .andReturn(nil); |
| self.plugin.paymentQueueHandler = mockHandler; |
| [self.plugin |
| handleMethodCall:call |
| result:^(id _Nullable result) { |
| FlutterError *error = result; |
| XCTAssertEqualObjects(@"storekit_invalid_payment_discount_object", error.code); |
| XCTAssertEqualObjects( |
| @"You have requested a payment and specified a " |
| @"payment discount with invalid properties. Some error occurred", |
| error.message); |
| XCTAssertEqualObjects(arguments, error.details); |
| [expectation fulfill]; |
| }]; |
| [self waitForExpectations:@[ expectation ] timeout:5]; |
| OCMVerify(never(), [mockHandler addPayment:[OCMArg any]]); |
| } |
| } |
| |
| - (void)testAddPaymentWithNullSandboxArgument { |
| XCTestExpectation *expectation = |
| [self expectationWithDescription:@"result should return success state"]; |
| FlutterMethodCall *call = |
| [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" |
| arguments:@{ |
| @"productIdentifier" : @"123", |
| @"quantity" : @(1), |
| @"simulatesAskToBuyInSandbox" : [NSNull null], |
| }]; |
| FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class); |
| OCMStub([mockHandler addPayment:[OCMArg any]]).andReturn(YES); |
| self.plugin.paymentQueueHandler = mockHandler; |
| [self.plugin handleMethodCall:call |
| result:^(id _Nullable result) { |
| XCTAssertNil(result); |
| [expectation fulfill]; |
| }]; |
| [self waitForExpectations:@[ expectation ] timeout:5]; |
| OCMVerify(times(1), [mockHandler addPayment:[OCMArg checkWithBlock:^BOOL(id obj) { |
| SKPayment *payment = obj; |
| return !payment.simulatesAskToBuyInSandbox; |
| }]]); |
| } |
| |
| - (void)testRestoreTransactions { |
| XCTestExpectation *expectation = |
| [self expectationWithDescription:@"result successfully restore transactions"]; |
| FlutterMethodCall *call = [FlutterMethodCall |
| methodCallWithMethodName:@"-[InAppPurchasePlugin restoreTransactions:result:]" |
| arguments:nil]; |
| SKPaymentQueueStub *queue = [SKPaymentQueueStub new]; |
| queue.testState = SKPaymentTransactionStatePurchased; |
| __block BOOL callbackInvoked = NO; |
| self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue |
| transactionsUpdated:^(NSArray<SKPaymentTransaction *> *_Nonnull transactions) { |
| } |
| transactionRemoved:nil |
| restoreTransactionFailed:nil |
| restoreCompletedTransactionsFinished:^() { |
| callbackInvoked = YES; |
| [expectation fulfill]; |
| } |
| shouldAddStorePayment:nil |
| updatedDownloads:nil |
| transactionCache:OCMClassMock(FIATransactionCache.class)]; |
| [queue addTransactionObserver:self.plugin.paymentQueueHandler]; |
| [self.plugin handleMethodCall:call |
| result:^(id r){ |
| }]; |
| [self waitForExpectations:@[ expectation ] timeout:5]; |
| XCTAssertTrue(callbackInvoked); |
| } |
| |
| - (void)testRetrieveReceiptDataSuccess { |
| XCTestExpectation *expectation = [self expectationWithDescription:@"receipt data retrieved"]; |
| FlutterMethodCall *call = [FlutterMethodCall |
| methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" |
| arguments:nil]; |
| __block NSDictionary *result; |
| [self.plugin handleMethodCall:call |
| result:^(id r) { |
| result = r; |
| [expectation fulfill]; |
| }]; |
| [self waitForExpectations:@[ expectation ] timeout:5]; |
| XCTAssertNotNil(result); |
| XCTAssert([result isKindOfClass:[NSString class]]); |
| } |
| |
| - (void)testRetrieveReceiptDataNil { |
| NSBundle *mockBundle = OCMPartialMock([NSBundle mainBundle]); |
| OCMStub(mockBundle.appStoreReceiptURL).andReturn(nil); |
| XCTestExpectation *expectation = [self expectationWithDescription:@"nil receipt data retrieved"]; |
| FlutterMethodCall *call = [FlutterMethodCall |
| methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" |
| arguments:nil]; |
| __block NSDictionary *result; |
| [self.plugin handleMethodCall:call |
| result:^(id r) { |
| result = r; |
| [expectation fulfill]; |
| }]; |
| [self waitForExpectations:@[ expectation ] timeout:5]; |
| XCTAssertNil(result); |
| } |
| |
| - (void)testRetrieveReceiptDataError { |
| XCTestExpectation *expectation = [self expectationWithDescription:@"receipt data retrieved"]; |
| FlutterMethodCall *call = [FlutterMethodCall |
| methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" |
| arguments:nil]; |
| __block NSDictionary *result; |
| self.receiptManagerStub.returnError = YES; |
| [self.plugin handleMethodCall:call |
| result:^(id r) { |
| result = r; |
| [expectation fulfill]; |
| }]; |
| [self waitForExpectations:@[ expectation ] timeout:5]; |
| XCTAssertNotNil(result); |
| XCTAssert([result isKindOfClass:[FlutterError class]]); |
| NSDictionary *details = ((FlutterError *)result).details; |
| XCTAssertNotNil(details[@"error"]); |
| NSNumber *errorCode = (NSNumber *)details[@"error"][@"code"]; |
| XCTAssertEqual(errorCode, [NSNumber numberWithInteger:99]); |
| } |
| |
| - (void)testRefreshReceiptRequest { |
| XCTestExpectation *expectation = [self expectationWithDescription:@"expect success"]; |
| FlutterMethodCall *call = |
| [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin refreshReceipt:result:]" |
| arguments:nil]; |
| __block BOOL result = NO; |
| [self.plugin handleMethodCall:call |
| result:^(id r) { |
| result = YES; |
| [expectation fulfill]; |
| }]; |
| [self waitForExpectations:@[ expectation ] timeout:5]; |
| XCTAssertTrue(result); |
| } |
| |
| - (void)testPresentCodeRedemptionSheet { |
| XCTestExpectation *expectation = |
| [self expectationWithDescription:@"expect successfully present Code Redemption Sheet"]; |
| FlutterMethodCall *call = [FlutterMethodCall |
| methodCallWithMethodName:@"-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]" |
| arguments:nil]; |
| __block BOOL callbackInvoked = NO; |
| [self.plugin handleMethodCall:call |
| result:^(id r) { |
| callbackInvoked = YES; |
| [expectation fulfill]; |
| }]; |
| [self waitForExpectations:@[ expectation ] timeout:5]; |
| XCTAssertTrue(callbackInvoked); |
| } |
| |
| - (void)testGetPendingTransactions { |
| XCTestExpectation *expectation = [self expectationWithDescription:@"expect success"]; |
| FlutterMethodCall *call = |
| [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue transactions]" arguments:nil]; |
| SKPaymentQueue *mockQueue = OCMClassMock(SKPaymentQueue.class); |
| NSDictionary *transactionMap = @{ |
| @"transactionIdentifier" : [NSNull null], |
| @"transactionState" : @(SKPaymentTransactionStatePurchasing), |
| @"payment" : [NSNull null], |
| @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" |
| code:123 |
| userInfo:@{}]], |
| @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), |
| @"originalTransaction" : [NSNull null], |
| }; |
| OCMStub(mockQueue.transactions).andReturn(@[ [[SKPaymentTransactionStub alloc] |
| initWithMap:transactionMap] ]); |
| |
| __block NSArray *resultArray; |
| self.plugin.paymentQueueHandler = |
| [[FIAPaymentQueueHandler alloc] initWithQueue:mockQueue |
| transactionsUpdated:nil |
| transactionRemoved:nil |
| restoreTransactionFailed:nil |
| restoreCompletedTransactionsFinished:nil |
| shouldAddStorePayment:nil |
| updatedDownloads:nil |
| transactionCache:OCMClassMock(FIATransactionCache.class)]; |
| [self.plugin handleMethodCall:call |
| result:^(id r) { |
| resultArray = r; |
| [expectation fulfill]; |
| }]; |
| [self waitForExpectations:@[ expectation ] timeout:5]; |
| XCTAssertEqualObjects(resultArray, @[ transactionMap ]); |
| } |
| |
| - (void)testStartObservingPaymentQueue { |
| XCTestExpectation *expectation = |
| [self expectationWithDescription:@"Should return success result"]; |
| FlutterMethodCall *startCall = [FlutterMethodCall |
| methodCallWithMethodName:@"-[SKPaymentQueue startObservingTransactionQueue]" |
| arguments:nil]; |
| FIAPaymentQueueHandler *mockHandler = OCMClassMock([FIAPaymentQueueHandler class]); |
| self.plugin.paymentQueueHandler = mockHandler; |
| [self.plugin handleMethodCall:startCall |
| result:^(id _Nullable result) { |
| XCTAssertNil(result); |
| [expectation fulfill]; |
| }]; |
| |
| [self waitForExpectations:@[ expectation ] timeout:5]; |
| OCMVerify(times(1), [mockHandler startObservingPaymentQueue]); |
| } |
| |
| - (void)testStopObservingPaymentQueue { |
| XCTestExpectation *expectation = |
| [self expectationWithDescription:@"Should return success result"]; |
| FlutterMethodCall *stopCall = |
| [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue stopObservingTransactionQueue]" |
| arguments:nil]; |
| FIAPaymentQueueHandler *mockHandler = OCMClassMock([FIAPaymentQueueHandler class]); |
| self.plugin.paymentQueueHandler = mockHandler; |
| [self.plugin handleMethodCall:stopCall |
| result:^(id _Nullable result) { |
| XCTAssertNil(result); |
| [expectation fulfill]; |
| }]; |
| |
| [self waitForExpectations:@[ expectation ] timeout:5]; |
| OCMVerify(times(1), [mockHandler stopObservingPaymentQueue]); |
| } |
| |
| #if TARGET_OS_IOS |
| - (void)testRegisterPaymentQueueDelegate { |
| if (@available(iOS 13, *)) { |
| FlutterMethodCall *call = |
| [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue registerDelegate]" |
| arguments:nil]; |
| |
| self.plugin.paymentQueueHandler = |
| [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueueStub new] |
| transactionsUpdated:nil |
| transactionRemoved:nil |
| restoreTransactionFailed:nil |
| restoreCompletedTransactionsFinished:nil |
| shouldAddStorePayment:nil |
| updatedDownloads:nil |
| transactionCache:OCMClassMock(FIATransactionCache.class)]; |
| |
| // Verify the delegate is nil before we register one. |
| XCTAssertNil(self.plugin.paymentQueueHandler.delegate); |
| |
| [self.plugin handleMethodCall:call |
| result:^(id r){ |
| }]; |
| |
| // Verify the delegate is not nil after we registered one. |
| XCTAssertNotNil(self.plugin.paymentQueueHandler.delegate); |
| } |
| } |
| #endif |
| |
| - (void)testRemovePaymentQueueDelegate { |
| if (@available(iOS 13, *)) { |
| FlutterMethodCall *call = |
| [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue removeDelegate]" |
| arguments:nil]; |
| |
| self.plugin.paymentQueueHandler = |
| [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueueStub new] |
| transactionsUpdated:nil |
| transactionRemoved:nil |
| restoreTransactionFailed:nil |
| restoreCompletedTransactionsFinished:nil |
| shouldAddStorePayment:nil |
| updatedDownloads:nil |
| transactionCache:OCMClassMock(FIATransactionCache.class)]; |
| self.plugin.paymentQueueHandler.delegate = OCMProtocolMock(@protocol(SKPaymentQueueDelegate)); |
| |
| // Verify the delegate is not nil before removing it. |
| XCTAssertNotNil(self.plugin.paymentQueueHandler.delegate); |
| |
| [self.plugin handleMethodCall:call |
| result:^(id r){ |
| }]; |
| |
| // Verify the delegate is nill after removing it. |
| XCTAssertNil(self.plugin.paymentQueueHandler.delegate); |
| } |
| } |
| |
| #if TARGET_OS_IOS |
| - (void)testShowPriceConsentIfNeeded { |
| FlutterMethodCall *call = |
| [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue showPriceConsentIfNeeded]" |
| arguments:nil]; |
| |
| FIAPaymentQueueHandler *mockQueueHandler = OCMClassMock(FIAPaymentQueueHandler.class); |
| self.plugin.paymentQueueHandler = mockQueueHandler; |
| |
| [self.plugin handleMethodCall:call |
| result:^(id r){ |
| }]; |
| |
| #pragma clang diagnostic push |
| #pragma clang diagnostic ignored "-Wpartial-availability" |
| if (@available(iOS 13.4, *)) { |
| OCMVerify(times(1), [mockQueueHandler showPriceConsentIfNeeded]); |
| } else { |
| OCMVerify(never(), [mockQueueHandler showPriceConsentIfNeeded]); |
| } |
| #pragma clang diagnostic pop |
| } |
| #endif |
| |
| @end |