|
1 | 1 | #if os(iOS) |
| 2 | +import AVKit |
2 | 3 | import Foundation |
3 | 4 | import PDFKit |
4 | 5 | import SafariServices |
@@ -461,16 +462,84 @@ class SentryUIRedactBuilderTests: XCTestCase { |
461 | 462 | XCTAssertEqual(result.count, 0) |
462 | 463 | } |
463 | 464 |
|
464 | | - func testRedactList() { |
465 | | - let expectedList = ["_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView", |
| 465 | + func testDefaultRedactList_shouldContainAllPlatformSpecificClasses() { |
| 466 | + // -- Arrange -- |
| 467 | + let expectedListClassNames = [ |
| 468 | + // SwiftUI Views |
| 469 | + "_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView", |
466 | 470 | "_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView", |
467 | | - "SwiftUI._UIGraphicsView", "SwiftUI.ImageLayer", "UIWebView", "SFSafariView", "UILabel", "UITextView", "UITextField", "WKWebView", "PDFView" |
468 | | - ].compactMap { NSClassFromString($0) } |
469 | | - |
| 471 | + "SwiftUI._UIGraphicsView", "SwiftUI.ImageLayer", |
| 472 | + // Web Views |
| 473 | + "UIWebView", "SFSafariView", "WKWebView", |
| 474 | + // Text Views (incl. HybridSDK) |
| 475 | + "UILabel", "UITextView", "UITextField", "RCTTextView", "RCTParagraphComponentView", |
| 476 | + // Document Views |
| 477 | + "PDFView", |
| 478 | + // Image Views (incl. HybridSDK) |
| 479 | + "UIImageView", "RCTImageView", |
| 480 | + // Audio / Video Views |
| 481 | + "AVPlayerView" |
| 482 | + ] |
| 483 | + |
| 484 | + let expectedList = expectedListClassNames.map { className -> (String, ObjectIdentifier?) in |
| 485 | + guard let classType = NSClassFromString(className) else { |
| 486 | + print("Class \(className) not found, skipping test") |
| 487 | + return (className, nil) |
| 488 | + } |
| 489 | + return (className, ObjectIdentifier(classType)) |
| 490 | + } |
| 491 | + |
| 492 | + // -- Act -- |
470 | 493 | let sut = getSut() |
471 | | - expectedList.forEach { element in |
472 | | - XCTAssertTrue(sut.containsRedactClass(element), "\(element) not found") |
| 494 | + |
| 495 | + // -- Assert -- |
| 496 | + // Build sets of expected and actual identifiers for comparison |
| 497 | + let expectedIdentifiers = Set(expectedList.compactMap { $0.1 }) |
| 498 | + let actualIdentifiers = Set(sut.redactClassesIdentifiers) |
| 499 | + |
| 500 | + // Check for identifiers that are expected but missing in the actual result |
| 501 | + let missingIdentifiers = expectedIdentifiers.subtracting(actualIdentifiers) |
| 502 | + // Check for identifiers that are present in the actual result but not expected |
| 503 | + let unexpectedIdentifiers = actualIdentifiers.subtracting(expectedIdentifiers) |
| 504 | + |
| 505 | + // For each expected class, check that if we expect the class identifier to be nil, it is nil |
| 506 | + for (expectedClassName, expectedNullableIdentifier) in expectedList { |
| 507 | + if expectedNullableIdentifier == nil { |
| 508 | + // If we expect nil, assert that no identifier in the actual list matches the class name |
| 509 | + let found = sut.redactClassesIdentifiers.contains { $0.debugDescription.contains(expectedClassName) } |
| 510 | + XCTAssertFalse(found, "Class \(expectedClassName) not found in runtime, but it is present in the redact list") |
| 511 | + } else { |
| 512 | + // If we expect a non-nil identifier, assert that it is present in the actual list |
| 513 | + XCTAssertTrue(sut.redactClassesIdentifiers.contains(where: { $0 == expectedNullableIdentifier }), "Expected class \(expectedClassName) not found in redact list") |
| 514 | + } |
473 | 515 | } |
| 516 | + |
| 517 | + // Assert that there are no missing identifiers |
| 518 | + XCTAssertTrue(missingIdentifiers.isEmpty, "Missing expected class identifiers: \(missingIdentifiers)") |
| 519 | + |
| 520 | + // Assert that there are no unexpected identifiers |
| 521 | + for identifier in unexpectedIdentifiers { |
| 522 | + // Try to get the class name from the identifier |
| 523 | + let classCount = objc_getClassList(nil, 0) |
| 524 | + var className = "<unknown>" |
| 525 | + if classCount > 0 { |
| 526 | + let classes = UnsafeMutablePointer<AnyClass?>.allocate(capacity: Int(classCount)) |
| 527 | + defer { classes.deallocate() } |
| 528 | + let autoreleasingClasses = AutoreleasingUnsafeMutablePointer<AnyClass>(classes) |
| 529 | + let count = objc_getClassList(autoreleasingClasses, classCount) |
| 530 | + for i in 0..<Int(count) { |
| 531 | + if let cls = classes[i], ObjectIdentifier(cls) == identifier { |
| 532 | + className = NSStringFromClass(cls) |
| 533 | + break |
| 534 | + } |
| 535 | + } |
| 536 | + } |
| 537 | + XCTFail("Unexpected class identifier found: \(identifier) (\(className))") |
| 538 | + } |
| 539 | + XCTAssertTrue(unexpectedIdentifiers.isEmpty, "Unexpected class identifiers found: \(unexpectedIdentifiers)") |
| 540 | + |
| 541 | + // Assert that the sets are equal (final check) |
| 542 | + XCTAssertEqual(actualIdentifiers, expectedIdentifiers, "Mismatch between expected and actual class identifiers") |
474 | 543 | } |
475 | 544 |
|
476 | 545 | func testIgnoreList() { |
@@ -638,6 +707,67 @@ class SentryUIRedactBuilderTests: XCTestCase { |
638 | 707 | // -- Act & Assert -- |
639 | 708 | XCTAssertTrue(sut.containsRedactClass(PDFView.self), "PDFView should be in the redact class list") |
640 | 709 | } |
| 710 | + |
| 711 | + func testRedactAVPlayerViewController() throws { |
| 712 | + // -- Arrange -- |
| 713 | + let sut = getSut() |
| 714 | + let avPlayerViewController = AVPlayerViewController() |
| 715 | + let avPlayerView = try XCTUnwrap(avPlayerViewController.view) |
| 716 | + avPlayerView.frame = CGRect(x: 20, y: 20, width: 40, height: 40) |
| 717 | + rootView.addSubview(avPlayerView) |
| 718 | + |
| 719 | + // -- Act -- |
| 720 | + let result = sut.redactRegionsFor(view: rootView) |
| 721 | + |
| 722 | + // -- Assert -- |
| 723 | + // Root View |
| 724 | + // └ AVPlayerViewController.view (Public API) |
| 725 | + // └ AVPlayerView (Private API) |
| 726 | + XCTAssertGreaterThanOrEqual(result.count, 1) |
| 727 | + let avPlayerRegion = try XCTUnwrap(result.first) |
| 728 | + XCTAssertEqual(avPlayerRegion.size, CGSize(width: 40, height: 40)) |
| 729 | + XCTAssertEqual(avPlayerRegion.type, .redact) |
| 730 | + XCTAssertEqual(avPlayerRegion.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 20, ty: 20)) |
| 731 | + XCTAssertNil(avPlayerRegion.color) |
| 732 | + } |
| 733 | + |
| 734 | + func testRedactAVPlayerViewControllerEvenWithMaskingDisabled() throws { |
| 735 | + // -- Arrange -- |
| 736 | + // AVPlayerViewController should always be redacted for security reasons, |
| 737 | + // regardless of maskAllText and maskAllImages settings |
| 738 | + let sut = getSut(TestRedactOptions(maskAllText: false, maskAllImages: false)) |
| 739 | + let avPlayerViewController = AVPlayerViewController() |
| 740 | + let avPlayerView = try XCTUnwrap(avPlayerViewController.view) |
| 741 | + avPlayerView.frame = CGRect(x: 20, y: 20, width: 40, height: 40) |
| 742 | + rootView.addSubview(avPlayerView) |
| 743 | + |
| 744 | + // -- Act -- |
| 745 | + let result = sut.redactRegionsFor(view: rootView) |
| 746 | + |
| 747 | + // -- Assert -- |
| 748 | + // Root View |
| 749 | + // └ AVPlayerViewController.view (Public API) |
| 750 | + // └ AVPlayerView (Private API) |
| 751 | + XCTAssertGreaterThanOrEqual(result.count, 1) |
| 752 | + let avPlayerRegion = try XCTUnwrap(result.first) |
| 753 | + XCTAssertEqual(avPlayerRegion.size, CGSize(width: 40, height: 40)) |
| 754 | + XCTAssertEqual(avPlayerRegion.type, .redact) |
| 755 | + XCTAssertEqual(avPlayerRegion.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 20, ty: 20)) |
| 756 | + XCTAssertNil(avPlayerRegion.color) |
| 757 | + } |
| 758 | + |
| 759 | + func testAVPlayerViewInRedactList() throws { |
| 760 | + // -- Arrange -- |
| 761 | + let sut = getSut() |
| 762 | + |
| 763 | + // -- Act & Assert -- |
| 764 | + // Note: The redaction system uses "AVPlayerView" as the class name string |
| 765 | + // which should resolve to the internal view hierarchy of AVPlayerViewController |
| 766 | + guard let avPlayerViewClass = NSClassFromString("AVPlayerView") else { |
| 767 | + throw XCTSkip("AVPlayerView class not found, skipping test") |
| 768 | + } |
| 769 | + XCTAssertTrue(sut.containsRedactClass(avPlayerViewClass), "AVPlayerView should be in the redact class list") |
| 770 | + } |
641 | 771 | } |
642 | 772 |
|
643 | 773 | #endif |
0 commit comments