diff --git a/Sources/Misc/SandboxEnvironmentDetector.swift b/Sources/Misc/SandboxEnvironmentDetector.swift index d96345e335..bdf101d409 100644 --- a/Sources/Misc/SandboxEnvironmentDetector.swift +++ b/Sources/Misc/SandboxEnvironmentDetector.swift @@ -50,7 +50,14 @@ final class BundleSandboxEnvironmentDetector: SandboxEnvironmentDetector { } #if os(macOS) || targetEnvironment(macCatalyst) - return !self.isProductionReceipt || !self.isMacAppStore + // this relies on an undocumented field in the receipt that provides the Environment. + // if it's not present, we go to a secondary check. + if let isProductionReceipt = self.isProductionReceipt { + return !isProductionReceipt + } else { + return !self.isMacAppStore + } + #else return path.contains("sandboxReceipt") #endif @@ -73,12 +80,14 @@ extension BundleSandboxEnvironmentDetector: Sendable {} private extension BundleSandboxEnvironmentDetector { - var isProductionReceipt: Bool { + var isProductionReceipt: Bool? { do { - return try self.receiptFetcher.fetchAndParseLocalReceipt().environment == .production + let receiptEnvironment = try self.receiptFetcher.fetchAndParseLocalReceipt().environment + guard receiptEnvironment != .unknown else { return nil } // don't make assumptions if we're not sure + return receiptEnvironment == .production } catch { Logger.error(Strings.receipt.parse_receipt_locally_error(error: error)) - return false + return nil } } diff --git a/Tests/UnitTests/Misc/SandboxEnvironmentDetectorTests.swift b/Tests/UnitTests/Misc/SandboxEnvironmentDetectorTests.swift index b78ca0f95a..3934e82307 100644 --- a/Tests/UnitTests/Misc/SandboxEnvironmentDetectorTests.swift +++ b/Tests/UnitTests/Misc/SandboxEnvironmentDetectorTests.swift @@ -45,7 +45,7 @@ class SandboxEnvironmentDetectorTests: TestCase { // `macOS` sandbox detection does not rely on receipt path class SandboxEnvironmentDetectorTests: TestCase { - func testIsNotSandboxIfReceiptIsProductionAndMAS() throws { + func testIsNotSandboxIfReceiptIsProduction() throws { expect( SystemInfo.with( macAppStore: true, @@ -54,41 +54,85 @@ class SandboxEnvironmentDetectorTests: TestCase { ) == false } - func testIsSandboxIfReceiptIsProductionAndNotMAS() throws { + func testIsSandboxIfReceiptIsNotProduction() throws { expect( SystemInfo.with( macAppStore: false, - receiptEnvironment: .production + receiptEnvironment: .sandbox ).isSandbox ) == true } - func testIsSandboxIfReceiptIsNotProductionAndNotMAS() throws { - expect( - SystemInfo.with( - macAppStore: false, - receiptEnvironment: .sandbox - ).isSandbox - ) == true + func testIsSandboxWhenReceiptEnvironmentIsUnknownDefaultToMacAppStoreDetector() throws { + var isSandbox = false + var macAppStoreDetector = MockMacAppStoreDetector(isMacAppStore: !isSandbox) + var detector = SystemInfo.with( + macAppStore: !isSandbox, + receiptEnvironment: .unknown, + macAppStoreDetector: macAppStoreDetector + ) + + expect(detector.isSandbox) == isSandbox + expect(macAppStoreDetector.isMacAppStoreCalled) == true + + isSandbox = !isSandbox + + macAppStoreDetector = MockMacAppStoreDetector(isMacAppStore: !isSandbox) + detector = SystemInfo.with( + macAppStore: !isSandbox, + receiptEnvironment: .unknown, + macAppStoreDetector: macAppStoreDetector + ) + + expect(detector.isSandbox) == isSandbox } - func testIsSandboxIfReceiptIsNotProductionAndMAS() throws { - expect( - SystemInfo.with( - macAppStore: true, - receiptEnvironment: .sandbox - ).isSandbox - ) == true + func testIsSandboxWhenReceiptParsingFailsDefaultsToMacAppStoreDetector() throws { + var isSandbox = false + var macAppStoreDetector = MockMacAppStoreDetector(isMacAppStore: !isSandbox) + var detector = SystemInfo.with( + macAppStore: !isSandbox, + failReceiptParsing: true, + macAppStoreDetector: macAppStoreDetector + ) + + expect(detector.isSandbox) == isSandbox + expect(macAppStoreDetector.isMacAppStoreCalled) == true + + isSandbox = !isSandbox + + macAppStoreDetector = MockMacAppStoreDetector(isMacAppStore: !isSandbox) + detector = SystemInfo.with( + macAppStore: !isSandbox, + failReceiptParsing: true, + macAppStoreDetector: macAppStoreDetector + ) + + expect(detector.isSandbox) == isSandbox } - func testIsSandboxIfReceiptParsingFailsAndBundleSignatureIsNotMAS() throws { - expect( - SystemInfo.with( - macAppStore: false, - receiptEnvironment: .production, - failReceiptParsing: true - ).isSandbox - ) == true + func testIsSandboxWhenReceiptIsProductionReturnsProductionAndDoesntHitMacAppStoreDetector() throws { + let macAppStoreDetector = MockMacAppStoreDetector(isMacAppStore: false) + let detector = SystemInfo.with( + macAppStore: false, + receiptEnvironment: .production, + macAppStoreDetector: macAppStoreDetector + ) + + expect(detector.isSandbox) == false + expect(macAppStoreDetector.isMacAppStoreCalled) == false + } + + func testIsSandboxWhenReceiptIsSandboxReturnsSandboxAndDoesntHitMacAppStoreDetector() throws { + let macAppStoreDetector = MockMacAppStoreDetector(isMacAppStore: false) + let detector = SystemInfo.with( + macAppStore: false, + receiptEnvironment: .sandbox, + macAppStoreDetector: macAppStoreDetector + ) + + expect(detector.isSandbox) == true + expect(macAppStoreDetector.isMacAppStoreCalled) == false } } @@ -104,7 +148,8 @@ private extension SandboxEnvironmentDetector { inSimulator: Bool = false, macAppStore: Bool = false, receiptEnvironment: AppleReceipt.Environment = .production, - failReceiptParsing: Bool = false + failReceiptParsing: Bool = false, + macAppStoreDetector: MockMacAppStoreDetector? = nil ) -> SandboxEnvironmentDetector { let bundle = MockBundle() bundle.receiptURLResult = result @@ -126,7 +171,7 @@ private extension SandboxEnvironmentDetector { isRunningInSimulator: inSimulator, receiptFetcher: MockLocalReceiptFetcher(mockReceipt: mockReceipt, failReceiptParsing: failReceiptParsing), - macAppStoreDetector: MockMacAppStoreDetector(isMacAppStore: macAppStore) + macAppStoreDetector: macAppStoreDetector ?? MockMacAppStoreDetector(isMacAppStore: macAppStore) ) } @@ -151,12 +196,17 @@ private final class MockLocalReceiptFetcher: LocalReceiptFetcherType { } -private struct MockMacAppStoreDetector: MacAppStoreDetector { +private final class MockMacAppStoreDetector: MacAppStoreDetector, @unchecked Sendable { - let isMacAppStore: Bool + let isMacAppStoreValue: Bool + private(set) var isMacAppStoreCalled = false init(isMacAppStore: Bool) { - self.isMacAppStore = isMacAppStore + self.isMacAppStoreValue = isMacAppStore } + var isMacAppStore: Bool { + isMacAppStoreCalled = true + return isMacAppStoreValue + } }