Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LoadShedderIntegrationTests: verify requests are actually handled by load shedder #2663

Merged
merged 5 commits into from
Jun 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Sources/Logging/Strings/NetworkStrings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ enum NetworkStrings {
case blocked_network(url: URL, newHost: String?)
case api_request_redirect(from: URL, to: URL)
case operation_state(NetworkOperation.Type, state: String)
case request_handled_by_load_shedder(HTTPRequest.Path)

#if DEBUG
case api_request_forcing_server_error(HTTPRequest)
Expand Down Expand Up @@ -103,6 +104,9 @@ extension NetworkStrings: LogMessage {
case let .operation_state(operation, state):
return "\(operation): \(state)"

case let .request_handled_by_load_shedder(path):
return "Request was handled by load shedder: \(path.description)"

#if DEBUG
case let .api_request_forcing_server_error(request):
return "Returning fake HTTP 500 error for '\(request.description)'"
Expand Down
16 changes: 9 additions & 7 deletions Sources/Networking/HTTPClient/ETagManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ class ETagManager {
.compactMapValues { $0 }
}

/// - Returns: `response` if a cached response couldn't be fetched,
/// or the cached `HTTPResponse`, always including the headers in `response`.
func httpResultFromCacheOrBackend(with response: HTTPResponse<Data?>,
request: URLRequest,
retried: Bool) -> HTTPResponse<Data>? {
Expand All @@ -99,7 +101,8 @@ class ETagManager {
let newResponse = storedResponse.withUpdatedValidationTime()

self.storeIfPossible(newResponse, for: request)
return newResponse.asResponse(withRequestDate: response.requestDate)
return newResponse.asResponse(withRequestDate: response.requestDate,
headers: response.responseHeaders)
}
if retried {
Logger.warn(
Expand Down Expand Up @@ -148,10 +151,6 @@ private extension ETagManager {
}
}

func storedHTTPResponse(for request: URLRequest, withRequestDate requestDate: Date?) -> HTTPResponse<Data>? {
return self.storedETagAndResponse(for: request)?.asResponse(withRequestDate: requestDate)
}

func storeStatusCodeAndResponseIfNoError(for request: URLRequest,
response: HTTPResponse<Data?>,
eTag: String) {
Expand Down Expand Up @@ -246,10 +245,13 @@ extension ETagManager.Response {
return try? JSONEncoder.default.encode(self)
}

fileprivate func asResponse(withRequestDate requestDate: Date?) -> HTTPResponse<Data> {
fileprivate func asResponse(
withRequestDate requestDate: Date?,
headers: HTTPClient.ResponseHeaders
) -> HTTPResponse<Data> {
return HTTPResponse(
statusCode: self.statusCode,
responseHeaders: [:],
responseHeaders: headers,
body: self.data,
requestDate: requestDate,
verificationResult: self.verificationResult
Expand Down
10 changes: 10 additions & 0 deletions Sources/Networking/HTTPClient/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ extension HTTPClient {
case signature = "X-Signature"
case requestDate = "X-RevenueCat-Request-Time"
case contentType = "Content-Type"
case isLoadShedder = "X-RevenueCat-Fortress"

}

Expand Down Expand Up @@ -319,6 +320,11 @@ private extension HTTPClient {
// If that can't be extracted, get status code from the parsed response.
httpCode: urlResponse?.httpStatusCode ?? response.statusCode
))

if response.isLoadShedder {
Logger.debug(Strings.network.request_handled_by_load_shedder(request.httpRequest.path))
}

case let .failure(error):
Logger.debug(Strings.network.api_request_failed(request.httpRequest,
httpCode: urlResponse?.httpStatusCode,
Expand Down Expand Up @@ -530,6 +536,10 @@ private extension HTTPResponse {
}
}

var isLoadShedder: Bool {
return self.value(forHeaderField: HTTPClient.ResponseHeader.isLoadShedder.rawValue) == "true"
}

}

private extension HTTPResponse where Body == Data {
Expand Down
42 changes: 42 additions & 0 deletions Tests/BackendIntegrationTests/LoadShedderIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,45 @@ class LoadShedderStoreKit1IntegrationTests: BaseStoreKitIntegrationTests {
return Signing.enforcedVerificationMode()
}

// MARK: -

private var logger: TestLogHandler!

override func setUp() async throws {
self.logger = TestLogHandler(capacity: 500)

try await super.setUp()
}

override func tearDown() async throws {
self.logger = nil

try await super.tearDown()
}

func testCanGetOfferings() async throws {
let receivedOfferings = try await Purchases.shared.offerings()

expect(receivedOfferings.all).toNot(beEmpty())
assertSnapshot(matching: receivedOfferings.response, as: .formattedJson)
}

func testOfferingsComeFromLoadShedder() async throws {
self.logger.verifyMessageWasLogged(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having these kind of tests would be very useful in Android. It's difficult to test this without it

Strings.network.request_handled_by_load_shedder(
.getOfferings(appUserID: try Purchases.shared.appUserID.escapedOrError())
),
level: .debug
)
}

func testCanPurchasePackage() async throws {
try await self.purchaseMonthlyOffering()

self.logger.verifyMessageWasLogged(
Strings.network.request_handled_by_load_shedder(.postReceiptData),
level: .debug
)
}

@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
Expand All @@ -57,4 +87,16 @@ class LoadShedderStoreKit1IntegrationTests: BaseStoreKitIntegrationTests {
expect(result.entitlementsByProduct["com.revenuecat.loadShedder.monthly"]) == ["premium"]
}

@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
func testProductEntitlementMappingComesFromLoadShedder() async throws {
try AvailabilityChecks.iOS15APIAvailableOrSkipTest()

try await self.logger.verifyMessageIsEventuallyLogged(
Strings.network.request_handled_by_load_shedder(.getProductEntitlementMapping).description,
level: .debug,
timeout: .seconds(5),
pollInterval: .milliseconds(100)
)
}

}
8 changes: 8 additions & 0 deletions Tests/BackendIntegrationTests/OtherIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ class OtherIntegrationTests: BaseBackendIntegrationTests {
try await Purchases.shared.healthRequest(signatureVerification: true)
}

func testHandledByProductionServer() async throws {
let logger = TestLogHandler()

try await Purchases.shared.healthRequest(signatureVerification: false)

logger.verifyMessageWasNotLogged(Strings.network.request_handled_by_load_shedder(.health))
}

@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
func testProductEntitlementMapping() async throws {
try AvailabilityChecks.iOS15APIAvailableOrSkipTest()
Expand Down
25 changes: 15 additions & 10 deletions Tests/UnitTests/Networking/ETagManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,22 @@ class ETagManagerTests: TestCase {
let request = URLRequest(url: Self.testURL)
let cachedResponse = self.mockStoredETagResponse(for: Self.testURL, statusCode: .success, eTag: eTag)

let response = self.eTagManager.httpResultFromCacheOrBackend(
with: self.responseForTest(url: Self.testURL,
body: nil,
eTag: eTag,
statusCode: .notModified),
request: request,
retried: false
let response = try XCTUnwrap(
self.eTagManager.httpResultFromCacheOrBackend(
with: self.responseForTest(url: Self.testURL,
body: nil,
eTag: eTag,
statusCode: .notModified),
request: request,
retried: false
)
)
expect(response).toNot(beNil())
expect(response?.statusCode) == .success
expect(response?.body) == cachedResponse

expect(response.statusCode) == .success
expect(response.body) == cachedResponse
expect(response.responseHeaders).toNot(beEmpty())
expect(Set(response.responseHeaders.keys.compactMap { $0 as? String }))
== Set(self.getHeaders(eTag: eTag).keys)
}

func testValidationTimeIsUpdatedWhenUsingStoredResponse() throws {
Expand Down
57 changes: 55 additions & 2 deletions Tests/UnitTests/Networking/HTTPClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1069,11 +1069,16 @@ final class HTTPClientTests: BaseHTTPClientTests {
let eTag = "tag"
let requestDate = Date().addingTimeInterval(-1000000)

let headers: [String: String] = [
HTTPClient.ResponseHeader.contentType.rawValue: "application/json",
HTTPClient.ResponseHeader.signature.rawValue: UUID().uuidString
]

self.eTagManager.stubResponseEtag(eTag)
self.eTagManager.shouldReturnResultFromBackend = false
self.eTagManager.stubbedHTTPResultFromCacheOrBackendResult = .init(
statusCode: .success,
responseHeaders: [:],
responseHeaders: headers,
body: mockedCachedResponse,
requestDate: requestDate,
verificationResult: .notRequested
Expand All @@ -1084,7 +1089,7 @@ final class HTTPClientTests: BaseHTTPClientTests {

return .init(data: Data(),
statusCode: .notModified,
headers: nil)
headers: headers)
}

let response: HTTPResponse<Data>.Result? = waitUntilValue { completion in
Expand All @@ -1098,6 +1103,7 @@ final class HTTPClientTests: BaseHTTPClientTests {
expect(response?.value?.body) == mockedCachedResponse
expect(response?.value?.requestDate) == requestDate
expect(response?.value?.verificationResult) == .notRequested
expect(response?.value?.responseHeaders).to(haveCount(headers.count))

expect(self.eTagManager.invokedETagHeaderParametersList).to(haveCount(1))
expect(self.eTagManager.invokedETagHeaderParameters?.withSignatureVerification) == false
Expand Down Expand Up @@ -1415,6 +1421,53 @@ final class HTTPClientTests: BaseHTTPClientTests {
)
}

func testNormalResponsesAreNotDetectedAsLoadSheddder() throws {
let path: HTTPRequest.Path = .logIn

stub(condition: isPath(path)) { _ in
return HTTPStubsResponse(
data: .init(),
statusCode: .success,
headers: [:]
)
}

let logger = TestLogHandler()

let response: HTTPResponse<Data>.Result? = waitUntilValue { completion in
self.client.perform(.init(method: .get, path: path), completionHandler: completion)
}
expect(response).to(beSuccess())

logger.verifyMessageWasNotLogged(Strings.network.request_handled_by_load_shedder(path))
}

func testLoadShedderResponsesAreLogged() throws {
let path: HTTPRequest.Path = .logIn

stub(condition: isPath(path)) { _ in
return HTTPStubsResponse(
data: .init(),
statusCode: .success,
headers: [
HTTPClient.ResponseHeader.isLoadShedder.rawValue: "true"
]
)
}

let logger = TestLogHandler()

let response: HTTPResponse<Data>.Result? = waitUntilValue { completion in
self.client.perform(.init(method: .get, path: path), completionHandler: completion)
}
expect(response).to(beSuccess())

logger.verifyMessageWasLogged(
Strings.network.request_handled_by_load_shedder(path),
level: .debug
)
}

}

func isPath(_ path: HTTPRequest.Path) -> HTTPStubsTestBlock {
Expand Down