Files
OSGKeyboard/OSGKeyboardTests/AnalyticsModelTests.swift
T
Rocky 10bc457c72
CI / Validate manifests (push) Has been cancelled
CI / SwiftLint (push) Has been cancelled
CI / iOS / Extension (push) Has been cancelled
CI / macOS (push) Has been cancelled
feat(app): add resilient Flow recovery and privacy-safe analytics
Unify PiP recovery across startup and foreground transitions, surface actionable status, and add privacy-safe analytics plus the refreshed onboarding and account experience. Update documentation assets and advance the release build to 84.
2026-08-20 20:22:20 +08:00

166 lines
6.0 KiB
Swift

// AnalyticsModelTests.swift
// OSGKeyboardTests
//
// Wire-contract and privacy invariants for the allowlisted analytics schema.
import Foundation
@testable import OSGKeyboardShared
import XCTest
final class AnalyticsModelTests: XCTestCase {
func testEventEncodingContainsOnlyAllowlistedKeysAndNoFreeTextContainer() throws {
let event = try AnalyticsEvent(
installationId: analyticsTestUUID(1),
clientEventId: analyticsTestUUID(2),
eventType: .aiFeatureFailed,
occurredAt: Date(timeIntervalSince1970: 1_700_000_000.125),
surface: .keyboard,
appVersion: "2.0.0",
osVersion: "26.0",
feature: .polish,
executionMode: .managed,
failureCategory: .network,
durationBucket: .oneToThreeSeconds
)
let data = try JSONEncoder().encode(event)
let object = try XCTUnwrap(
JSONSerialization.jsonObject(with: data) as? [String: Any]
)
XCTAssertEqual(
Set(object.keys),
[
"installationId",
"clientEventId",
"eventType",
"occurredAt",
"surface",
"appVersion",
"osVersion",
"feature",
"executionMode",
"failureCategory",
"durationBucket"
]
)
XCTAssertFalse(object.keys.contains("properties"))
XCTAssertFalse(object.keys.contains("text"))
XCTAssertEqual(
Set(Mirror(reflecting: event).children.compactMap(\.label)),
Set(object.keys).union(["acquisitionChannel"])
)
}
func testUnknownFieldsAreRejectedAtEveryWireEnvelope() throws {
let event = try AnalyticsEvent(
installationId: analyticsTestUUID(1),
clientEventId: analyticsTestUUID(2),
eventType: .sessionStarted,
occurredAt: Date(timeIntervalSince1970: 1_700_000_000),
surface: .app,
appVersion: "2.0.0",
osVersion: "26.0"
)
var eventObject = try XCTUnwrap(
JSONSerialization.jsonObject(with: JSONEncoder().encode(event))
as? [String: Any]
)
eventObject["userText"] = "must be rejected"
assertUnknownField(
try JSONSerialization.data(withJSONObject: eventObject),
as: AnalyticsEvent.self,
expected: "userText"
)
let request = Data(#"{"events":[],"properties":{"secret":"value"}}"#.utf8)
assertUnknownField(
request,
as: AnalyticsUploadRequest.self,
expected: "properties"
)
let response = Data(#"{"accepted":1,"replayed":0,"debug":"secret"}"#.utf8)
assertUnknownField(
response,
as: AnalyticsUploadResponse.self,
expected: "debug"
)
}
func testEnvironmentFiltersAndTruncatesVersionsToSafeBound() {
let environment = AnalyticsEnvironment(
appVersion: String(repeating: "a", count: 40) + "/private",
osVersion: "26.0 (Build 23A)/用户"
)
XCTAssertEqual(environment.appVersion, String(repeating: "a", count: 32))
XCTAssertEqual(environment.appVersion.utf8.count, 32)
XCTAssertEqual(environment.osVersion, "26.0Build23A")
let empty = AnalyticsEnvironment(appVersion: "用户 /", osVersion: "")
XCTAssertEqual(empty.appVersion, "unknown")
XCTAssertEqual(empty.osVersion, "unknown")
}
func testPurchaseCancelledRequiresCancelledFailureCategory() throws {
XCTAssertNoThrow(
try AnalyticsEvent(
installationId: analyticsTestUUID(1),
clientEventId: analyticsTestUUID(2),
eventType: .purchaseCancelled,
occurredAt: Date(timeIntervalSince1970: 1_700_000_000),
surface: .app,
appVersion: "2.0.0",
osVersion: "26.0",
failureCategory: .cancelled
)
)
XCTAssertThrowsError(
try AnalyticsEvent(
installationId: analyticsTestUUID(1),
clientEventId: analyticsTestUUID(3),
eventType: .purchaseCancelled,
occurredAt: Date(timeIntervalSince1970: 1_700_000_000),
surface: .app,
appVersion: "2.0.0",
osVersion: "26.0",
failureCategory: .network
)
) { error in
guard case AnalyticsModelError.invalidDimensions(.purchaseCancelled) = error else {
return XCTFail("Unexpected error: \(error)")
}
}
}
func testDurationBucketBoundariesAreStable() {
XCTAssertEqual(AnalyticsDurationBucket(elapsedNanoseconds: 999_999_999), .lessThanOneSecond)
XCTAssertEqual(AnalyticsDurationBucket(elapsedNanoseconds: 1_000_000_000), .oneToThreeSeconds)
XCTAssertEqual(AnalyticsDurationBucket(elapsedNanoseconds: 2_999_999_999), .oneToThreeSeconds)
XCTAssertEqual(AnalyticsDurationBucket(elapsedNanoseconds: 3_000_000_000), .threeToTenSeconds)
XCTAssertEqual(AnalyticsDurationBucket(elapsedNanoseconds: 10_000_000_000), .tenToThirtySeconds)
XCTAssertEqual(AnalyticsDurationBucket(elapsedNanoseconds: 30_000_000_000), .thirtySecondsOrMore)
}
private func assertUnknownField<T: Decodable>(
_ data: Data,
as type: T.Type,
expected: String,
file: StaticString = #filePath,
line: UInt = #line
) {
XCTAssertThrowsError(
try JSONDecoder().decode(type, from: data),
file: file,
line: line
) { error in
guard case AnalyticsModelError.unknownField(let field) = error else {
return XCTFail("Unexpected error: \(error)", file: file, line: line)
}
XCTAssertEqual(field, expected, file: file, line: line)
}
}
}