Files
purchases-ios/Tests/BackendIntegrationTests/SubscriberAttributesManagerIntegrationTests.swift
T
NachoSoto e182a6ad53 IntegrationTests: replaced Purchases.shared with a throwing property (#2867)
This is a similar change to #2532.

I noticed yet another source of crashes in our integration tests.
This is what [the
result](https://app.circleci.com/pipelines/github/RevenueCat/purchases-ios/13247/workflows/52fd2014-7a19-442f-a572-64116393abec/jobs/94728)
was:
```
failed - Message 'Finishing transaction' should not have been logged
expected to not find object in collection that satisfies predicate
```

That came from this code:
```swift
self.logger.verifyMessageWasNotLogged("Finishing transaction")

let info1 = try await Purchases.shared.customerInfo()
```

After that assertion failure, `XCTest` continued execution, while it had
already tear down `Purchases`, which lead to this crash:
```
Thread 0 Crashed:
0   libswiftCore.dylib            	       0x103b84e81 _assertionFailure(_:_:file:line:flags:) + 353
1   RevenueCat                    	       0x1332516d0 static Purchases.shared.getter + 352 (Purchases.swift:65)
2   BackendIntegrationTests       	       0x130f7048a OfflineStoreKit1IntegrationTests.testPurchaseWhileServerIsDownPostsReceiptWhenForegroundingApp() + 362 (OfflineStoreKitIntegrationTests.swift:289)
3   BackendIntegrationTests       	       0x130f75ee1 @objc closure #1 in OfflineStoreKit1IntegrationTests.testPurchaseWhileServerIsDownPostsReceiptWhenForegroundingApp() + 1
4   BackendIntegrationTests       	       0x130f77d91 partial apply for @objc closure #1 in OfflineStoreKit1IntegrationTests.testPurchaseWhileServerIsDownPostsReceiptWhenForegroundingApp() + 1
```

To prevent this, instead of using `Purchases.shared` this replaces that
with a throwing `self.purchases`.
Just like we don't use force-unwraps in tests and instead use
`XCTUwnrap`, this now will fail the test instead of crashing it.
2023-07-24 13:20:00 +02:00

288 lines
8.9 KiB
Swift

//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// SubscriberAttributesManagerIntegrationTests.swift
//
// Created by Nacho Soto on 4/1/22.
@testable import RevenueCat
import Nimble
import XCTest
// swiftlint:disable:next type_name
class SubscriberAttributesManagerIntegrationTests: BaseBackendIntegrationTests {
private var attribution: Attribution!
private var userID: String!
private var syncedAttributes: [(userID: String, attributes: [String: String])] = []
private static let testEmail = "test@revenuecat.com"
@MainActor
override func setUp() {
super.setUp()
self.attribution = Purchases.shared.attribution
self.attribution.delegate = self
self.userID = Purchases.shared.appUserID
self.syncedAttributes = []
}
// MARK: -
func testNothingToSync() {
waitUntil { completion in
let parameters = Purchases.shared.syncSubscriberAttributes(completion: {
completion()
})
expect(parameters) == 0
}
}
func testSyncOneAttribute() async throws {
self.attribution.setEmail(Self.testEmail)
let errors = try await self.syncAttributes()
self.verifyAttributesSyncedWithNoErrors(errors, 1)
self.verifySyncedAttribute(self.userID, [reserved(.email): Self.testEmail])
}
func testSettingTheSameAttributeDoesNotNeedToChangeIt() async throws {
self.attribution.setEmail(Self.testEmail)
var errors = try await self.syncAttributes()
self.verifyAttributesSyncedWithNoErrors(errors, 1)
self.verifySyncedAttribute(self.userID, [reserved(.email): Self.testEmail])
self.attribution.setEmail(Self.testEmail)
errors = try await self.syncAttributes()
self.verifyAttributesSyncedWithNoErrors(errors, 0)
expect(self.syncedAttributes)
.to(
haveCount(1),
description: "Attribute should not have synced again"
)
}
func testChangingEmailSyncsIt() async throws {
self.attribution.setEmail(Self.testEmail)
var errors = try await self.syncAttributes()
self.verifyAttributesSyncedWithNoErrors(errors, 1)
self.verifySyncedAttribute(self.userID, [reserved(.email): Self.testEmail])
let newEmail = "test2@revenuecat.com"
self.attribution.setEmail(newEmail)
errors = try await self.syncAttributes()
self.verifyAttributesSyncedWithNoErrors(errors, 1)
self.verifySyncedAttribute(self.userID, [reserved(.email): newEmail])
}
func testSyncInvalidEmail() async throws {
let invalidEmail = "invalid @ email @.com"
self.attribution.setEmail(invalidEmail)
let errors = try await self.syncAttributes()
let error = try XCTUnwrap(errors.onlyElement ?? nil) as NSError
self.verifySyncedAttribute(self.userID, [reserved(.email): invalidEmail])
expect(error.domain) == RCPurchasesErrorCodeDomain
expect(error.code) == ErrorCode.invalidSubscriberAttributesError.rawValue
expect(error.subscriberAttributesErrors) == [
"$email": "Email address is not a valid email."
]
}
func testLogInGetsNewAttributes() async throws {
self.attribution.setEmail(Self.testEmail)
var errors = try await self.syncAttributes()
self.verifyAttributesSyncedWithNoErrors(errors, 1)
self.verifySyncedAttribute(self.userID, [reserved(.email): Self.testEmail])
let newUserID = UUID().uuidString
_ = try await self.purchases.logIn(newUserID)
self.attribution.setEmail(Self.testEmail)
errors = try await self.syncAttributes()
self.verifyAttributesSyncedWithNoErrors(errors, 1)
self.verifySyncedAttribute(newUserID, [reserved(.email): Self.testEmail])
}
func testPushTokenWithInvalidTokenDoesNotFail() async throws {
let token = "invalid token".asData
self.attribution.setPushToken(token)
let errors = try await self.syncAttributes()
self.verifyAttributesSyncedWithNoErrors(errors, 1)
self.verifySyncedAttribute(self.userID, [reserved(.pushToken): token.asString])
}
func testSetCustomAttributes() async throws {
let attributes = [
"custom_key": "random value",
"locale": Locale.current.identifier
]
self.attribution.setAttributes(attributes)
let errors = try await self.syncAttributes()
self.verifyAttributesSyncedWithNoErrors(errors, 1) // 1 user with 2 attributes
self.verifySyncedAttribute(self.userID, attributes)
}
func testSetMultipleAttributes() async throws {
let name = "Tom Hanks"
let phone = "4157689215"
self.attribution.setDisplayName(name)
self.attribution.setPhoneNumber(phone)
let errors = try await self.syncAttributes()
// 1 user with 2 attributes:
self.verifyAttributesSyncedWithNoErrors(errors, 1)
// 1 request with 2 attributes:
self.verifySyncedAttribute(self.userID, [
reserved(.displayName): name,
reserved(.phoneNumber): phone
])
}
func testLogInPostsUnsyncedAttributes() async throws {
let user1 = UUID().uuidString
let name1 = "User 1"
let user2 = UUID().uuidString
let name2 = "User 2"
_ = try await self.purchases.logIn(user1)
self.attribution.setDisplayName(name1)
_ = try await self.purchases.logIn(user2)
// Log in forced previous unsynced attributes to be synced
self.verifySyncedAttribute(user1, [reserved(.displayName): name1])
self.attribution.setDisplayName(name2)
let errors = try await self.syncAttributes()
self.verifyAttributesSyncedWithNoErrors(errors, 1)
self.verifySyncedAttribute(user2, [reserved(.displayName): name2])
}
func testLogOutPostsUnsyncedAttributes() async throws {
let user = UUID().uuidString
let name = "User 1"
_ = try await self.purchases.logIn(user)
expect(try self.purchases.isAnonymous) == false
self.attribution.setDisplayName(name)
_ = try await self.purchases.logOut()
expect(try self.purchases.isAnonymous) == true
// Log out should post unsynced attributes
self.verifySyncedAttribute(user, [reserved(.displayName): name])
let anonUser = try self.purchases.appUserID
let anonName = "User 2"
expect(anonUser) != user
self.attribution.setDisplayName(anonName)
let errors = try await self.syncAttributes()
self.verifyAttributesSyncedWithNoErrors(errors, 1)
self.verifySyncedAttribute(anonUser, [reserved(.displayName): anonName])
}
}
extension SubscriberAttributesManagerIntegrationTests: AttributionDelegate {
func attribution(didFinishSyncingAttributes attributes: SubscriberAttribute.Dictionary,
forUserID userID: String) {
self.syncedAttributes.append(
(userID: userID, attributes: attributes.mapValues { $0.value })
)
}
}
private extension SubscriberAttributesManagerIntegrationTests {
func reserved(_ attribute: ReservedSubscriberAttribute) -> String {
return attribute.rawValue
}
func syncAttributes() async throws -> [Error?] {
let purchases = try self.purchases
return await withCheckedContinuation { continuation in
let errors: Atomic<[Error?]> = .init([])
purchases.syncSubscriberAttributes(
syncedAttribute: { error in errors.modify { $0.append(error) } },
completion: { continuation.resume(returning: errors.value) }
)
}
}
private func verifyAttributesSyncedWithNoErrors(
_ errors: [Error?],
_ expectedCount: Int,
file: FileString = #file,
line: UInt = #line
) {
expect(
file: file, line: line,
errors
).to(
haveCount(expectedCount),
description: "Incorrect number of attributes"
)
expect(
file: file, line: line,
errors
).toNot(
containElementSatisfying { $0 != nil },
description: "Encountered errors: \(errors)"
)
}
private func verifySyncedAttribute(
_ userID: String,
_ attributes: [String: String],
file: FileString = #file,
line: UInt = #line
) {
expect(
file: file, line: line,
self.syncedAttributes
).to(
containElementSatisfying {
$0.userID == userID && $0.attributes == attributes
},
description: "Attribute request not found. Synced attributes: \(self.syncedAttributes)"
)
}
}