|
| 1 | +#if os(iOS) || os(tvOS) |
| 2 | + |
| 3 | +@_spi(Private) @testable import Sentry |
| 4 | +@_spi(Private) import SentryTestUtils |
| 5 | +import XCTest |
| 6 | + |
| 7 | +/// Integration tests that verify the completion-handler swizzling for replay's |
| 8 | +/// network detail capture actually works end-to-end. |
| 9 | +/// |
| 10 | +/// Unlike the unit tests in SentryNetworkTrackerTests (which call tracker |
| 11 | +/// methods directly), these tests start the SDK, make real HTTP requests, |
| 12 | +/// and assert that the swizzled completion handler fires and populates |
| 13 | +/// network details on the resulting breadcrumb. |
| 14 | +/// |
| 15 | +/// Uses postman-echo.com so no local test server is required. |
| 16 | +class SentryNetworkDetailSwizzlingTests: XCTestCase { |
| 17 | + |
| 18 | + private let echoURL = URL(string: "https://postman-echo.com/get")! |
| 19 | + |
| 20 | + override func setUp() { |
| 21 | + super.setUp() |
| 22 | + |
| 23 | + let options = Options() |
| 24 | + options.dsn = TestConstants.dsnAsString(username: "SentryNetworkDetailSwizzlingTests") |
| 25 | + options.tracesSampleRate = 1.0 |
| 26 | + options.enableNetworkBreadcrumbs = true |
| 27 | + options.sessionReplay.networkDetailAllowUrls = ["postman-echo.com"] |
| 28 | + options.sessionReplay.networkCaptureBodies = true |
| 29 | + SentrySDK.start(options: options) |
| 30 | + } |
| 31 | + |
| 32 | + override func tearDown() { |
| 33 | + super.tearDown() |
| 34 | + clearTestState() |
| 35 | + } |
| 36 | + |
| 37 | + // MARK: - Tests |
| 38 | + |
| 39 | + /// Verifies the swizzle of `-[NSURLSession dataTaskWithRequest:completionHandler:]` |
| 40 | + /// captures response details into the breadcrumb. |
| 41 | + func testDataTaskWithRequest_completionHandler_capturesNetworkDetails() throws { |
| 42 | + let transaction = SentrySDK.startTransaction( |
| 43 | + name: "Test", operation: "test", bindToScope: true |
| 44 | + ) |
| 45 | + |
| 46 | + let expect = expectation(description: "Request completed") |
| 47 | + expect.assertForOverFulfill = false |
| 48 | + |
| 49 | + let session = URLSession(configuration: .default) |
| 50 | + let request = URLRequest(url: echoURL) |
| 51 | + |
| 52 | + var receivedData: Data? |
| 53 | + var receivedResponse: URLResponse? |
| 54 | + var receivedError: Error? |
| 55 | + |
| 56 | + let task = session.dataTask(with: request) { data, response, error in |
| 57 | + receivedData = data |
| 58 | + receivedResponse = response |
| 59 | + receivedError = error |
| 60 | + expect.fulfill() |
| 61 | + } |
| 62 | + defer { task.cancel() } |
| 63 | + |
| 64 | + task.resume() |
| 65 | + wait(for: [expect], timeout: 5) |
| 66 | + |
| 67 | + transaction.finish() |
| 68 | + |
| 69 | + // Original completion handler received valid data |
| 70 | + XCTAssertNil(receivedError, "Request should succeed") |
| 71 | + XCTAssertNotNil(receivedData, "Should receive response data") |
| 72 | + let httpResponse = try XCTUnwrap(receivedResponse as? HTTPURLResponse) |
| 73 | + XCTAssertEqual(httpResponse.statusCode, 200) |
| 74 | + |
| 75 | + // Network details were captured via the swizzled completion handler |
| 76 | + let breadcrumb = try lastHTTPBreadcrumb(for: echoURL) |
| 77 | + let details = try XCTUnwrap( |
| 78 | + breadcrumb.data?[SentryReplayNetworkDetails.replayNetworkDetailsKey] as? SentryReplayNetworkDetails, |
| 79 | + "Swizzled completion handler should have populated network details on the breadcrumb" |
| 80 | + ) |
| 81 | + let serialized = details.serialize() |
| 82 | + XCTAssertEqual(serialized["statusCode"] as? Int, 200) |
| 83 | + XCTAssertNotNil(serialized["response"], "Response details should be captured") |
| 84 | + } |
| 85 | + |
| 86 | + /// Verifies the swizzle of `-[NSURLSession dataTaskWithURL:completionHandler:]` |
| 87 | + /// captures response details into the breadcrumb. |
| 88 | + func testDataTaskWithURL_completionHandler_capturesNetworkDetails() throws { |
| 89 | + let transaction = SentrySDK.startTransaction( |
| 90 | + name: "Test", operation: "test", bindToScope: true |
| 91 | + ) |
| 92 | + |
| 93 | + let expect = expectation(description: "Request completed") |
| 94 | + expect.assertForOverFulfill = false |
| 95 | + |
| 96 | + let session = URLSession(configuration: .default) |
| 97 | + |
| 98 | + var receivedData: Data? |
| 99 | + var receivedResponse: URLResponse? |
| 100 | + var receivedError: Error? |
| 101 | + |
| 102 | + let task = session.dataTask(with: echoURL) { data, response, error in |
| 103 | + receivedData = data |
| 104 | + receivedResponse = response |
| 105 | + receivedError = error |
| 106 | + expect.fulfill() |
| 107 | + } |
| 108 | + defer { task.cancel() } |
| 109 | + |
| 110 | + task.resume() |
| 111 | + wait(for: [expect], timeout: 5) |
| 112 | + |
| 113 | + transaction.finish() |
| 114 | + |
| 115 | + // Original completion handler received valid data |
| 116 | + XCTAssertNil(receivedError, "Request should succeed") |
| 117 | + XCTAssertNotNil(receivedData, "Should receive response data") |
| 118 | + let httpResponse = try XCTUnwrap(receivedResponse as? HTTPURLResponse) |
| 119 | + XCTAssertEqual(httpResponse.statusCode, 200) |
| 120 | + |
| 121 | + // Network details were captured via the swizzled completion handler |
| 122 | + let breadcrumb = try lastHTTPBreadcrumb(for: echoURL) |
| 123 | + let details = try XCTUnwrap( |
| 124 | + breadcrumb.data?[SentryReplayNetworkDetails.replayNetworkDetailsKey] as? SentryReplayNetworkDetails, |
| 125 | + "Swizzled completion handler should have populated network details on the breadcrumb" |
| 126 | + ) |
| 127 | + let serialized = details.serialize() |
| 128 | + XCTAssertEqual(serialized["statusCode"] as? Int, 200) |
| 129 | + XCTAssertNotNil(serialized["response"], "Response details should be captured") |
| 130 | + } |
| 131 | + |
| 132 | + // MARK: - Helpers |
| 133 | + |
| 134 | + /// Finds the most recent HTTP breadcrumb whose URL matches the given URL. |
| 135 | + private func lastHTTPBreadcrumb(for url: URL) throws -> Breadcrumb { |
| 136 | + let scope = SentrySDKInternal.currentHub().scope |
| 137 | + let breadcrumbs = try XCTUnwrap( |
| 138 | + Dynamic(scope).breadcrumbArray as [Breadcrumb]?, |
| 139 | + "Scope should contain breadcrumbs" |
| 140 | + ) |
| 141 | + let matching = breadcrumbs.filter { |
| 142 | + $0.category == "http" && ($0.data?["url"] as? String)?.contains(url.host ?? "") == true |
| 143 | + } |
| 144 | + return try XCTUnwrap(matching.last, "Should find an HTTP breadcrumb for \(url)") |
| 145 | + } |
| 146 | +} |
| 147 | + |
| 148 | +#endif // os(iOS) || os(tvOS) |
0 commit comments