blob: 14ed06c78c351fff85360b957cbb77907fe8ec9d [file] [log] [blame]
// 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)testAddPaymentFailure {
XCTestExpectation* expectation =
[self expectationWithDescription:@"result should return failed state"];
FlutterMethodCall* call =
[FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]"
arguments:@{
@"productIdentifier" : @"123",
@"quantity" : @(1),
@"simulatesAskToBuyInSandbox" : @YES,
}];
SKPaymentQueueStub* queue = [SKPaymentQueueStub new];
queue.testState = SKPaymentTransactionStateFailed;
__block SKPaymentTransaction* transactionForUpdateBlock;
self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue
transactionsUpdated:^(NSArray<SKPaymentTransaction*>* _Nonnull transactions) {
SKPaymentTransaction* transaction = transactions[0];
if (transaction.transactionState == SKPaymentTransactionStateFailed) {
transactionForUpdateBlock = transaction;
[expectation fulfill];
}
}
transactionRemoved:nil
restoreTransactionFailed:nil
restoreCompletedTransactionsFinished:nil
shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) {
return YES;
}
updatedDownloads:nil];
[queue addTransactionObserver:self.plugin.paymentQueueHandler];
[self.plugin handleMethodCall:call
result:^(id r){
}];
[self waitForExpectations:@[ expectation ] timeout:5];
XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStateFailed);
}
- (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,
}];
SKPaymentQueueStub* queue = [SKPaymentQueueStub new];
queue.testState = SKPaymentTransactionStatePurchased;
__block SKPaymentTransaction* transactionForUpdateBlock;
self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue
transactionsUpdated:^(NSArray<SKPaymentTransaction*>* _Nonnull transactions) {
SKPaymentTransaction* transaction = transactions[0];
if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
transactionForUpdateBlock = transaction;
if (@available(iOS 12.2, *)) {
XCTAssertNil(transaction.payment.paymentDiscount);
}
[expectation fulfill];
}
}
transactionRemoved:nil
restoreTransactionFailed:nil
restoreCompletedTransactionsFinished:nil
shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) {
return YES;
}
updatedDownloads:nil];
[queue addTransactionObserver:self.plugin.paymentQueueHandler];
[self.plugin handleMethodCall:call
result:^(id r){
}];
[self waitForExpectations:@[ expectation ] timeout:5];
XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased);
}
- (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),
}
}];
SKPaymentQueueStub* queue = [SKPaymentQueueStub new];
queue.testState = SKPaymentTransactionStatePurchased;
__block SKPaymentTransaction* transactionForUpdateBlock;
self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue
transactionsUpdated:^(NSArray<SKPaymentTransaction*>* _Nonnull transactions) {
SKPaymentTransaction* transaction = transactions[0];
if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
transactionForUpdateBlock = transaction;
if (@available(iOS 12.2, *)) {
SKPaymentDiscount* paymentDiscount = transaction.payment.paymentDiscount;
XCTAssertEqual(paymentDiscount.identifier, @"test_identifier");
XCTAssertEqual(paymentDiscount.keyIdentifier, @"test_key_identifier");
XCTAssertEqualObjects(
paymentDiscount.nonce,
[[NSUUID alloc] initWithUUIDString:@"4a11a9cc-3bc3-11ec-8d3d-0242ac130003"]);
XCTAssertEqual(paymentDiscount.signature, @"test_signature");
XCTAssertEqual(paymentDiscount.timestamp, @(1635847102));
}
[expectation fulfill];
}
}
transactionRemoved:nil
restoreTransactionFailed:nil
restoreCompletedTransactionsFinished:nil
shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) {
return YES;
}
updatedDownloads:nil];
[queue addTransactionObserver:self.plugin.paymentQueueHandler];
[self.plugin handleMethodCall:call
result:^(id r){
}];
[self waitForExpectations:@[ expectation ] timeout:5];
XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased);
}
- (void)testAddPaymentFailureWithInvalidPaymentDiscount {
XCTestExpectation* expectation =
[self expectationWithDescription:@"result should return success state"];
FlutterMethodCall* call =
[FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]"
arguments:@{
@"productIdentifier" : @"123",
@"quantity" : @(1),
@"simulatesAskToBuyInSandbox" : @YES,
@"paymentDiscount" : @{
@"keyIdentifier" : @"test_key_identifier",
@"nonce" : @"4a11a9cc-3bc3-11ec-8d3d-0242ac130003",
@"signature" : @"test_signature",
@"timestamp" : @(1635847102),
}
}];
[self.plugin
handleMethodCall:call
result:^(id r) {
XCTAssertTrue([r isKindOfClass:FlutterError.class]);
FlutterError* result = r;
XCTAssertEqualObjects(result.code, @"storekit_invalid_payment_discount_object");
XCTAssertEqualObjects(result.message,
@"You have requested a payment and specified a payment "
@"discount with invalid properties. When specifying a "
@"payment discount the 'identifier' field is mandatory.");
XCTAssertEqualObjects(result.details, call.arguments);
[expectation fulfill];
}];
[self waitForExpectations:@[ expectation ] timeout:5];
}
- (void)testAddPaymentWithNullSandboxArgument {
XCTestExpectation* expectation =
[self expectationWithDescription:@"result should return success state"];
XCTestExpectation* simulatesAskToBuyInSandboxExpectation =
[self expectationWithDescription:@"payment isn't simulatesAskToBuyInSandbox"];
FlutterMethodCall* call =
[FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]"
arguments:@{
@"productIdentifier" : @"123",
@"quantity" : @(1),
@"simulatesAskToBuyInSandbox" : [NSNull null],
}];
SKPaymentQueueStub* queue = [SKPaymentQueueStub new];
queue.testState = SKPaymentTransactionStatePurchased;
__block SKPaymentTransaction* transactionForUpdateBlock;
self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue
transactionsUpdated:^(NSArray<SKPaymentTransaction*>* _Nonnull transactions) {
SKPaymentTransaction* transaction = transactions[0];
if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
transactionForUpdateBlock = transaction;
[expectation fulfill];
}
if (!transaction.payment.simulatesAskToBuyInSandbox) {
[simulatesAskToBuyInSandboxExpectation fulfill];
}
}
transactionRemoved:nil
restoreTransactionFailed:nil
restoreCompletedTransactionsFinished:nil
shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) {
return YES;
}
updatedDownloads:nil];
[queue addTransactionObserver:self.plugin.paymentQueueHandler];
[self.plugin handleMethodCall:call
result:^(id r){
}];
[self waitForExpectations:@[ expectation, simulatesAskToBuyInSandboxExpectation ] timeout:5];
XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased);
}
- (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];
[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)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];
[self.plugin handleMethodCall:call
result:^(id r) {
resultArray = r;
[expectation fulfill];
}];
[self waitForExpectations:@[ expectation ] timeout:5];
XCTAssertEqualObjects(resultArray, @[ transactionMap ]);
}
- (void)testStartAndStopObservingPaymentQueue {
FlutterMethodCall* startCall = [FlutterMethodCall
methodCallWithMethodName:@"-[SKPaymentQueue startObservingTransactionQueue]"
arguments:nil];
FlutterMethodCall* stopCall =
[FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue stopObservingTransactionQueue]"
arguments:nil];
SKPaymentQueueStub* queue = [SKPaymentQueueStub new];
self.plugin.paymentQueueHandler =
[[FIAPaymentQueueHandler alloc] initWithQueue:queue
transactionsUpdated:nil
transactionRemoved:nil
restoreTransactionFailed:nil
restoreCompletedTransactionsFinished:nil
shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment,
SKProduct* _Nonnull product) {
return YES;
}
updatedDownloads:nil];
// Check that there is no observer to start with.
XCTAssertNil(queue.observer);
// Start observing
[self.plugin handleMethodCall:startCall
result:^(id r){
}];
// Observer should be set
XCTAssertNotNil(queue.observer);
// Stop observing
[self.plugin handleMethodCall:stopCall
result:^(id r){
}];
// No observer should be set
XCTAssertNil(queue.observer);
}
- (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];
// 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);
}
}
- (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];
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);
}
}
- (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
}
@end