mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-11 22:55:55 +05:45
* Add trace_id to the checkpoint hit event The id is created when the checkpoint is evaluated and handed to the workflow it starts, so the hit, the workflow's step and paywall events and the receipt's paywall trace_id all share it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Drop Android references from trace id comments Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Remove remaining Android references in workflow event comments Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
188 lines
7.5 KiB
Swift
188 lines
7.5 KiB
Swift
//
|
|
// Copyright RevenueCat Inc. All Rights Reserved.
|
|
//
|
|
// Licensed under the MIT License (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// https://opensource.org/licenses/MIT
|
|
//
|
|
// CheckpointEventsRequestTests.swift
|
|
|
|
import Foundation
|
|
import Nimble
|
|
import XCTest
|
|
|
|
@_spi(Internal) @testable import RevenueCat
|
|
|
|
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
|
|
class CheckpointEventsRequestTests: TestCase {
|
|
|
|
private static let userID = "app_user_id"
|
|
private static let appSessionID = UUID(uuidString: "315107f4-98bf-4b68-a582-eb27bcb6e111")!
|
|
|
|
private let id = UUID(uuidString: "498207f4-87af-4b57-a581-eb27bcc6e009")!
|
|
private let date = Date(timeIntervalSince1970: 1_699_270_688.995)
|
|
|
|
override func setUpWithError() throws {
|
|
try super.setUpWithError()
|
|
try AvailabilityChecks.iOS15APIAvailableOrSkipTest()
|
|
}
|
|
|
|
func testWireFormatCarriesExpectedFields() throws {
|
|
let request = try XCTUnwrap(FeatureEventsRequest.CheckpointEvent(storedEvent: try self.storedEvent()))
|
|
|
|
expect(request.id) == self.id.uuidString
|
|
expect(request.version) == 1
|
|
expect(request.type) == "checkpoint_hit"
|
|
expect(request.identifier) == "onboarding_complete"
|
|
expect(request.checkpointType) == "custom"
|
|
expect(request.appUserID) == Self.userID
|
|
expect(request.appSessionID) == Self.appSessionID.uuidString
|
|
expect(request.timestamp) == self.date.millisecondsSince1970
|
|
expect(request.result) == "present_ui"
|
|
expect(request.workflowID) == "wf_123"
|
|
expect(request.offeringID) == "offering_id"
|
|
expect(request.checkpointRuleID) == "rule_123"
|
|
expect(request.traceID) == "trace_123"
|
|
}
|
|
|
|
func testKhepriCompatibleShape() throws {
|
|
let json = try self.encodedJSON()
|
|
|
|
expect(json).to(contain("\"id\":\"\(self.id.uuidString)\""))
|
|
expect(json).to(contain("\"version\":1"))
|
|
expect(json).to(contain("\"type\":\"checkpoint_hit\""))
|
|
expect(json).to(contain("\"identifier\":\"onboarding_complete\""))
|
|
expect(json).to(contain("\"checkpoint_type\":\"custom\""))
|
|
expect(json).to(contain("\"app_user_id\":\"\(Self.userID)\""))
|
|
expect(json).to(contain("\"app_session_id\":\"\(Self.appSessionID.uuidString)\""))
|
|
expect(json).to(contain("\"timestamp\":\(self.date.millisecondsSince1970)"))
|
|
expect(json).to(contain("\"result\":\"present_ui\""))
|
|
expect(json).to(contain("\"workflow_id\":\"wf_123\""))
|
|
expect(json).to(contain("\"offering_id\":\"offering_id\""))
|
|
expect(json).to(contain("\"checkpoint_rule_id\":\"rule_123\""))
|
|
expect(json).to(contain("\"trace_id\":\"trace_123\""))
|
|
}
|
|
|
|
func testEachResultIsEncodedWithItsWireValue() throws {
|
|
let expected: [CheckpointHitResult: String] = [
|
|
.presentUI: "present_ui",
|
|
.returnData: "return_data",
|
|
.noMatch: "no_match",
|
|
.configurationUnavailable: "configuration_unavailable",
|
|
.unknownCheckpoint: "unknown_checkpoint"
|
|
]
|
|
|
|
for (result, wireValue) in expected {
|
|
let json = try self.encodedJSON(data: .init(id: self.id,
|
|
identifier: "onboarding_complete",
|
|
date: self.date,
|
|
result: result))
|
|
|
|
expect(json).to(contain("\"result\":\"\(wireValue)\""))
|
|
}
|
|
}
|
|
|
|
/// `workflow_id` comes back from the store as `workflowId`, so without explicit coding keys the ids
|
|
/// decode as nil and the hit reaches the backend with no outcome.
|
|
func testOutcomeIdsSurviveTheStoreRoundTrip() throws {
|
|
let request = try XCTUnwrap(FeatureEventsRequest.CheckpointEvent(storedEvent: try self.storedEvent()))
|
|
|
|
expect(request.workflowID) == "wf_123"
|
|
expect(request.offeringID) == "offering_id"
|
|
expect(request.checkpointRuleID) == "rule_123"
|
|
expect(request.traceID) == "trace_123"
|
|
}
|
|
|
|
/// Hits stored by an SDK version without trace ids have no such key and must still be sent.
|
|
func testDecodesAHitStoredWithoutATraceId() throws {
|
|
let stored = try self.storedEvent(data: .init(id: self.id,
|
|
identifier: "onboarding_complete",
|
|
date: self.date,
|
|
result: .noMatch))
|
|
expect(stored.encodedEvent).toNot(contain("trace"))
|
|
|
|
let request = try XCTUnwrap(FeatureEventsRequest.CheckpointEvent(storedEvent: stored))
|
|
|
|
expect(request.result) == "no_match"
|
|
expect(request.traceID).to(beNil())
|
|
}
|
|
|
|
func testOmitsIdsThatDidNotResolve() throws {
|
|
let json = try self.encodedJSON(data: .init(id: self.id,
|
|
identifier: "onboarding_complete",
|
|
date: self.date,
|
|
result: .noMatch))
|
|
|
|
expect(json).to(contain("\"result\":\"no_match\""))
|
|
expect(json).toNot(contain("workflow_id"))
|
|
expect(json).toNot(contain("offering_id"))
|
|
expect(json).toNot(contain("checkpoint_rule_id"))
|
|
expect(json).toNot(contain("trace_id"))
|
|
}
|
|
|
|
/// khepri discriminates the events union on `type`, so nothing downstream reads a `discriminator` key.
|
|
func testDiscriminatorAbsentFromJSON() throws {
|
|
let json = try self.encodedJSON()
|
|
|
|
expect(json).toNot(contain("discriminator"))
|
|
}
|
|
|
|
func testReturnsNilWhenAppSessionIDIsMissing() throws {
|
|
let stored = try self.storedEvent(appSessionID: nil)
|
|
|
|
expect(FeatureEventsRequest.CheckpointEvent(storedEvent: stored)).to(beNil())
|
|
}
|
|
|
|
func testReturnsNilForNonCheckpointStoredEvent() throws {
|
|
let event = CheckpointEvent.hit(self.resolvedData)
|
|
let stored = try XCTUnwrap(StoredFeatureEvent(
|
|
event: event,
|
|
userID: Self.userID,
|
|
feature: .paywalls,
|
|
appSessionID: Self.appSessionID,
|
|
eventDiscriminator: nil
|
|
))
|
|
|
|
expect(FeatureEventsRequest.CheckpointEvent(storedEvent: stored)).to(beNil())
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private var resolvedData: CheckpointEvent.Data {
|
|
.init(id: self.id,
|
|
identifier: "onboarding_complete",
|
|
date: self.date,
|
|
result: .presentUI,
|
|
workflowID: "wf_123",
|
|
offeringID: "offering_id",
|
|
checkpointRuleID: "rule_123",
|
|
traceID: "trace_123")
|
|
}
|
|
|
|
private func storedEvent(appSessionID: UUID? = CheckpointEventsRequestTests.appSessionID,
|
|
data: CheckpointEvent.Data? = nil) throws
|
|
-> StoredFeatureEvent {
|
|
let event = CheckpointEvent.hit(data ?? self.resolvedData)
|
|
|
|
return try XCTUnwrap(StoredFeatureEvent(
|
|
event: event,
|
|
userID: Self.userID,
|
|
feature: .checkpoints,
|
|
appSessionID: appSessionID,
|
|
eventDiscriminator: nil
|
|
))
|
|
}
|
|
|
|
private func encodedJSON(data: CheckpointEvent.Data? = nil) throws -> String {
|
|
let stored = try self.storedEvent(data: data)
|
|
let request = try XCTUnwrap(FeatureEventsRequest.CheckpointEvent(storedEvent: stored))
|
|
let encoder = JSONEncoder()
|
|
encoder.outputFormatting = .sortedKeys
|
|
|
|
return try XCTUnwrap(String(data: encoder.encode(request), encoding: .utf8))
|
|
}
|
|
|
|
}
|