diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 1a16dc73233..96a6c17367a 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -2185,6 +2185,7 @@ D4AF00242D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzlingTests.m; sourceTree = ""; }; D4AF7D212E93FFCA004F0F59 /* SentryUIRedactBuilderTests+ReactNative.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryUIRedactBuilderTests+ReactNative.swift"; sourceTree = ""; }; D4AF7D252E9401EB004F0F59 /* SentryUIRedactBuilderTests+UIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryUIRedactBuilderTests+UIKit.swift"; sourceTree = ""; }; + D4AF7D272E9402AC004F0F59 /* SentryUIRedactBuilderTests+SpecialViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryUIRedactBuilderTests+SpecialViews.swift"; sourceTree = ""; }; D4AF7D292E940492004F0F59 /* SentryUIRedactBuilderTests+Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryUIRedactBuilderTests+Common.swift"; sourceTree = ""; }; D4AF7D2B2E9404ED004F0F59 /* SentryUIRedactBuilderTests+EdgeCases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryUIRedactBuilderTests+EdgeCases.swift"; sourceTree = ""; }; D4B0DC7E2DA9257200DE61B6 /* SentryRenderVideoResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRenderVideoResult.swift; sourceTree = ""; }; @@ -6336,6 +6337,7 @@ D80694C42B7CC9AE00B820E6 /* SentryReplayEventTests.swift in Sources */, 7B34721728086A9D0041F047 /* SentrySwizzleWrapperTests.swift in Sources */, 8EC4CF5025C3A0070093DEE9 /* SentrySpanContextTests.swift in Sources */, + D4AF7D282E9402AC004F0F59 /* SentryUIRedactBuilderTests+SpecialViews.swift in Sources */, 6281C5742D3E50DF009D0978 /* ArbitraryDataTests.swift in Sources */, 7BE0DC2F272ABAF6004FA8B7 /* SentryAutoBreadcrumbTrackingIntegrationTests.swift in Sources */, 7B869EBE249B964D004F4FDB /* SentryThreadEquality.swift in Sources */, diff --git a/Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests+SpecialViews.swift b/Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests+SpecialViews.swift new file mode 100644 index 00000000000..1b412b0bc39 --- /dev/null +++ b/Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests+SpecialViews.swift @@ -0,0 +1,379 @@ +#if os(iOS) && !targetEnvironment(macCatalyst) +import AVKit +import Foundation +import PDFKit +import SafariServices +@_spi(Private) @testable import Sentry +import SentryTestUtils +import SnapshotTesting +import SwiftUI +import UIKit +import WebKit +import XCTest + +/// See `SentryUIRedactBuilderTests.swift` for more information on how to print the internal view hierarchy of a view. +class SentryUIRedactBuilderTests_SpecialViews: SentryUIRedactBuilderTests { // swiftlint:disable:this type_name + private func getSut(maskAllText: Bool, maskAllImages: Bool) -> SentryUIRedactBuilder { + return SentryUIRedactBuilder(options: TestRedactOptions( + maskAllText: maskAllText, + maskAllImages: maskAllImages + )) + } + + // MARK: - PDF View + + private func setupPDFViewFixture() -> UIView { + let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + + let pdfView = PDFView(frame: CGRect(x: 20, y: 20, width: 40, height: 40)) + rootView.addSubview(pdfView) + + return rootView + + // View Hierarchy: + // --------------- + // > + // | ; backgroundColor = ; layer = > + // | | ; layer = ; contentOffset: {0, 0}; contentSize: {0, 0}; adjustedContentInset: {0, 0, 0, 0}> + } + + func testRedact_withPDFView_withMaskingEnabled_shouldBeRedacted() throws { + // -- Arrange -- + let rootView = setupPDFViewFixture() + + // -- Act -- + let sut = getSut(maskAllText: true, maskAllImages: true) + let result = sut.redactRegionsFor(view: rootView) + + // -- Assert -- + let pdfRegion = try XCTUnwrap(result.element(at: 0)) + XCTAssertEqual(pdfRegion.size, CGSize(width: 40, height: 40)) + XCTAssertEqual(pdfRegion.type, .redact) + XCTAssertEqual(pdfRegion.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 20, ty: 20)) + XCTAssertNil(pdfRegion.color) + + let pdfScrollViewRegion = try XCTUnwrap(result.element(at: 1)) + XCTAssertEqual(pdfScrollViewRegion.size, CGSize(width: 40, height: 40)) + XCTAssertEqual(pdfScrollViewRegion.type, .redact) + XCTAssertEqual(pdfScrollViewRegion.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 20, ty: 20)) + XCTAssertNil(pdfScrollViewRegion.color) + + // Assert no additional regions + XCTAssertEqual(result.count, 2) + } + + func testRedact_withPDFView_withMaskingDisabled_shouldBeRedacted() throws { + // -- Arrange -- + let rootView = setupPDFViewFixture() + + // -- Act -- + let sut = getSut(maskAllText: false, maskAllImages: false) + let result = sut.redactRegionsFor(view: rootView) + + // -- Assert -- + let pdfRegion = try XCTUnwrap(result.element(at: 0)) + XCTAssertEqual(pdfRegion.size, CGSize(width: 40, height: 40)) + XCTAssertEqual(pdfRegion.type, .redact) + XCTAssertEqual(pdfRegion.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 20, ty: 20)) + XCTAssertNil(pdfRegion.color) + + let pdfScrollViewRegion = try XCTUnwrap(result.element(at: 1)) + XCTAssertEqual(pdfScrollViewRegion.size, CGSize(width: 40, height: 40)) + XCTAssertEqual(pdfScrollViewRegion.type, .redact) + XCTAssertEqual(pdfScrollViewRegion.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 20, ty: 20)) + XCTAssertNil(pdfScrollViewRegion.color) + + // Assert no additional regions + XCTAssertEqual(result.count, 2) + } + + // MARK: - WKWebView + + private func setupWKWebViewFixture() -> UIView { + let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + + let webView = WKWebView(frame: .init(x: 20, y: 20, width: 40, height: 40), configuration: .init()) + rootView.addSubview(webView) + + return rootView + + // View Hierarchy: + // --------------- + // > + // | > + // | | ; backgroundColor = kCGColorSpaceModelRGB 1 1 1 1; layer = ; contentOffset: {0, 0}; contentSize: {0, 0}; adjustedContentInset: {0, 0, 0, 0}> + // | | | > + // | | | | > + // | | | | | > + // | | | | > + // | | | <_UIScrollViewScrollIndicator: 0x105c33170; frame = (34 30; 3 7); alpha = 0; autoresize = LM; layer = > + // | | | | > + // | | | <_UIScrollViewScrollIndicator: 0x105c3eb90; frame = (30 34; 7 3); alpha = 0; autoresize = TM; layer = > + // | | | | > + } + + func testRedact_withWKWebView_withMaskingEnabled_shouldRedactView() throws { + // -- Arrange -- + let rootView = setupWKWebViewFixture() + + // -- Act -- + let sut = getSut(maskAllText: true, maskAllImages: true) + let result = sut.redactRegionsFor(view: rootView) + + // -- Assert -- + let region = try XCTUnwrap(result.element(at: 0)) // WKWebView + XCTAssertNil(region.color) + XCTAssertEqual(region.size, CGSize(width: 40, height: 40)) + XCTAssertEqual(region.type, .redact) + XCTAssertEqual(region.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 20, ty: 20)) + + let region2 = try XCTUnwrap(result.element(at: 1)) // WKScrollView + XCTAssertNil(region2.color) + XCTAssertEqual(region2.size, CGSize(width: 40, height: 40)) + XCTAssertEqual(region2.type, .redact) + XCTAssertEqual(region2.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 20, ty: 20)) + + // Assert no additional regions + XCTAssertEqual(result.count, 2) + } + + func testRedact_withWKWebView_withMaskingDisabled_shouldRedactView() throws { + // -- Arrange -- + let rootView = setupWKWebViewFixture() + + // -- Act -- + let sut = getSut(maskAllText: false, maskAllImages: false) + let result = sut.redactRegionsFor(view: rootView) + + // -- Assert -- + let region = try XCTUnwrap(result.element(at: 0)) + XCTAssertNil(region.color) + XCTAssertEqual(region.size, CGSize(width: 40, height: 40)) // WKWebView + XCTAssertEqual(region.type, .redact) + XCTAssertEqual(region.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 20, ty: 20)) + + let region2 = try XCTUnwrap(result.element(at: 1)) + XCTAssertNil(region2.color) + XCTAssertEqual(region2.size, CGSize(width: 40, height: 40)) // WKScrollView + XCTAssertEqual(region2.type, .redact) + XCTAssertEqual(region2.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 20, ty: 20)) + + // Assert no additional regions + XCTAssertEqual(result.count, 2) + } + + // MARK: - UIWebView + + private func setupUIWebViewFixture() throws -> UIView { + let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + + // The UIWebView initializers are marked as unavailable, so we use createFakeView. + // Note: All fake views are kept alive to prevent dealloc crashes (see createFakeView docs). + let webView = try XCTUnwrap(createFakeView( + type: UIView.self, + name: "UIWebView", + frame: .init(x: 20, y: 20, width: 40, height: 40) + )) + rootView.addSubview(webView) + + return rootView + + // View Hierarchy: + // --------------- + // > + // | > + } + + func testRedact_withUIWebView_withMaskingEnabled_shouldRedactView() throws { + // -- Arrange -- + let rootView = try setupUIWebViewFixture() + + // -- Act -- + let sut = getSut(maskAllText: true, maskAllImages: true) + let result = sut.redactRegionsFor(view: rootView) + + // -- Assert -- + let region = try XCTUnwrap(result.element(at: 0)) + XCTAssertNil(region.color) + XCTAssertEqual(region.size, CGSize(width: 40, height: 40)) + XCTAssertEqual(region.type, .redact) + XCTAssertEqual(region.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 20, ty: 20)) + + // Assert no additional regions + XCTAssertEqual(result.count, 1) + } + + func testRedact_withUIWebView_withMaskingDisabled_shouldRedactView() throws { + // -- Arrange -- + let rootView = try setupUIWebViewFixture() + + // -- Act -- + let sut = getSut(maskAllText: false, maskAllImages: false) + let result = sut.redactRegionsFor(view: rootView) + + // -- Assert -- + let region = try XCTUnwrap(result.element(at: 0)) + XCTAssertNil(region.color) + XCTAssertEqual(region.size, CGSize(width: 40, height: 40)) + XCTAssertEqual(region.type, .redact) + XCTAssertEqual(region.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 20, ty: 20)) + + // Assert no additional regions + XCTAssertEqual(result.count, 1) + } + + // MARK: - SFSafariView Redaction + + private func setupSFSafariViewControllerFixture() throws -> UIView { + let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + + let safariViewController = SFSafariViewController(url: URL(string: "https://example.com")!) + let safariView = try XCTUnwrap(safariViewController.view) + safariView.frame = CGRect(x: 20, y: 20, width: 40, height: 40) + rootView.addSubview(safariView) + + return rootView + + // View Hierarchy: + // --------------- + // == iOS 26 & 18 & 17 == + // > + // | > + // + // == iOS 16 & 15 == + // > + // | > + // | | ; layer = > + // | | | > delegate=0x12e60f600 no-scroll-edge-support + // | | | > + } + + private func assertSFSafariViewControllerRegions(regions: [SentryRedactRegion]) throws { + if #available(iOS 17, *) { // iOS 26 & 18 & 17 + let region = try XCTUnwrap(regions.element(at: 0)) + XCTAssertNil(region.color) + XCTAssertEqual(region.size, CGSize(width: 40, height: 40)) + XCTAssertEqual(region.type, .redact) + XCTAssertEqual(region.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 20, ty: 20)) + + // Assert that there are no other regions + XCTAssertEqual(regions.count, 1) + } else if #available(iOS 15, *) { // iOS 16 & 15 + let toolbarRegion = try XCTUnwrap(regions.element(at: 0)) // UIToolbar + XCTAssertNil(toolbarRegion.color) + XCTAssertEqual(toolbarRegion.size, CGSize(width: 0, height: 0)) + XCTAssertEqual(toolbarRegion.type, .redact) + XCTAssertEqual(toolbarRegion.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 20, ty: 20)) + + let navigationBarRegion = try XCTUnwrap(regions.element(at: 1)) // UINavigationBar + XCTAssertNil(navigationBarRegion.color) + XCTAssertEqual(navigationBarRegion.size, CGSize(width: 0, height: 0)) + XCTAssertEqual(navigationBarRegion.type, .redact) + XCTAssertEqual(navigationBarRegion.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 20, ty: 20)) + + let placeholderRegion = try XCTUnwrap(regions.element(at: 2)) // SFSafariLaunchPlaceholderView + XCTAssertNil(placeholderRegion.color) + XCTAssertEqual(placeholderRegion.size, CGSize(width: 40, height: 40)) + XCTAssertEqual(placeholderRegion.type, .redact) + XCTAssertEqual(placeholderRegion.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 20, ty: 20)) + + let safariViewRegion = try XCTUnwrap(regions.element(at: 3)) // SFSafariView + XCTAssertNil(safariViewRegion.color) + XCTAssertEqual(safariViewRegion.size, CGSize(width: 40, height: 40)) + XCTAssertEqual(safariViewRegion.type, .redact) + XCTAssertEqual(safariViewRegion.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 20, ty: 20)) + + // Assert that there are no other regions + XCTAssertEqual(regions.count, 4) + } else { + throw XCTSkip("Redaction of SFSafariViewController is not tested on iOS versions below 15") + } + } + + func testRedact_withSFSafariView_withMaskingEnabled_shouldRedactViewHierarchy() throws { +#if targetEnvironment(macCatalyst) + throw XCTSkip("SFSafariViewController opens system browser on macOS, nothing to redact, skipping test") +#else + // -- Arrange -- + let rootView = try setupSFSafariViewControllerFixture() + + // -- Act -- + let sut = getSut(maskAllText: true, maskAllImages: true) + let result = sut.redactRegionsFor(view: rootView) + + // -- Assert -- + try assertSFSafariViewControllerRegions(regions: result) +#endif + } + + func testRedact_withSFSafariView_withMaskingDisabled_shouldRedactView() throws { +#if targetEnvironment(macCatalyst) + throw XCTSkip("SFSafariViewController opens system browser on macOS, nothing to redact, skipping test") +#else + // -- Arrange -- + let rootView = try setupSFSafariViewControllerFixture() + + // -- Act -- + let sut = getSut(maskAllText: false, maskAllImages: false) + let result = sut.redactRegionsFor(view: rootView) + + // -- Assert -- + try assertSFSafariViewControllerRegions(regions: result) +#endif + } + + // MARK: - AVPlayer Redaction + + private func setupAVPlayerViewControllerFixture() throws -> UIView { + let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + + let avPlayerViewController = AVPlayerViewController() + let avPlayerView = try XCTUnwrap(avPlayerViewController.view) + avPlayerView.frame = CGRect(x: 20, y: 20, width: 40, height: 40) + rootView.addSubview(avPlayerView) + + return rootView + + // View Hierarchy: + // --------------- + // > + // | > + } + + private func assertAVPlayerViewControllerRegions(regions: [SentryRedactRegion]) throws { + let region = try XCTUnwrap(regions.element(at: 0)) + XCTAssertEqual(region.size, CGSize(width: 40, height: 40)) + XCTAssertEqual(region.type, .redact) + XCTAssertEqual(region.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 20, ty: 20)) + XCTAssertNil(region.color) + + // Assert that there are no other regions + XCTAssertEqual(regions.count, 1) + } + + func testRedact_withAVPlayerViewController_shouldBeRedacted() throws { + // -- Arrange -- + let rootView = try setupAVPlayerViewControllerFixture() + + // -- Act -- + let sut = getSut(maskAllText: true, maskAllImages: true) + let result = sut.redactRegionsFor(view: rootView) + + // -- Assert -- + try assertAVPlayerViewControllerRegions(regions: result) + } + + func testRedact_withAVPlayerViewControllerEvenWithMaskingDisabled_shouldBeRedacted() throws { + // -- Arrange -- + let rootView = try setupAVPlayerViewControllerFixture() + + // -- Act -- + let sut = getSut(maskAllText: false, maskAllImages: false) + let result = sut.redactRegionsFor(view: rootView) + + // -- Assert -- + try assertAVPlayerViewControllerRegions(regions: result) + } +} + +#endif // os(iOS) && !targetEnvironment(macCatalyst)