wangdl 4ba747af46 feat: iOS learning info P0 complete (IOS-INFO-000~017, 038~039)
Models: ReadingMaterialContext, ReadingEventUploadItem, AppContext
Runtime: ReadingRuntimeAdapter, SessionManager, RecoveryService
Reader: MaterialReaderView lifecycle, Heartbeat Timer, Position Adapter
Export: EventMapper, UploadQueue, export→queue→ack flow
Tests: 16 test files covering models, runtime, lifecycle, upload
Docs: ios-learning-info-architecture.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-15 21:53:36 +08:00

100 lines
3.7 KiB
Swift

import XCTest
@testable import AIStudyApp
final class AppContextTests: XCTestCase {
func test_appContext_platform_is_ios_or_ipados() {
let ctx = AppContext.testValue()
XCTAssertTrue(ctx.platform == "ios" || ctx.platform == "ipadOS",
"platform should be ios or ipadOS, got \(ctx.platform)")
}
func test_appContext_appVersion_notEmpty() {
let ctx = AppContext.testValue()
XCTAssertFalse(ctx.appVersion.isEmpty, "appVersion should not be empty")
}
func test_appContext_buildNumber_notEmpty() {
let ctx = AppContext.testValue()
XCTAssertFalse(ctx.buildNumber.isEmpty, "buildNumber should not be empty")
}
func test_appContext_timezoneOffset_isWithinRange() {
let ctx = AppContext.testValue()
// Valid range: -720 (UTC-12) to +840 (UTC+14)
XCTAssertGreaterThanOrEqual(ctx.clientTimezoneOffsetMinutes, -720)
XCTAssertLessThanOrEqual(ctx.clientTimezoneOffsetMinutes, 840)
}
func test_appContext_refreshed_updates_timezone() {
var ctx = AppContext.testValue()
ctx.clientTimezoneOffsetMinutes = 0
let refreshed = ctx.refreshed()
// refreshed should update from current timezone
let expected = TimeZone.current.secondsFromGMT() / 60
XCTAssertEqual(refreshed.clientTimezoneOffsetMinutes, expected)
}
func test_appContext_codable_roundtrip() throws {
let ctx = AppContext(
platform: "ios",
appVersion: "1.2.3",
buildNumber: "42",
clientTimezoneOffsetMinutes: 480,
locale: "zh-Hans_CN",
deviceType: "phone",
osVersion: "18.0"
)
let data = try JSONEncoder().encode(ctx)
let decoded = try JSONDecoder().decode(AppContext.self, from: data)
XCTAssertEqual(decoded.platform, "ios")
XCTAssertEqual(decoded.appVersion, "1.2.3")
XCTAssertEqual(decoded.buildNumber, "42")
XCTAssertEqual(decoded.clientTimezoneOffsetMinutes, 480)
XCTAssertEqual(decoded.locale, "zh-Hans_CN")
XCTAssertEqual(decoded.deviceType, "phone")
XCTAssertEqual(decoded.osVersion, "18.0")
}
func test_appContext_equatable() {
let a = AppContext.testValue()
let b = a
XCTAssertEqual(a, b)
}
func test_appContext_no_token_field() {
let ctx = AppContext.testValue()
let mirror = Mirror(reflecting: ctx)
let children = mirror.children.map { $0.label ?? "" }
let hasToken = children.contains { $0.lowercased().contains("token") }
XCTAssertFalse(hasToken, "AppContext should not contain token fields")
}
func test_appContext_no_privacy_field() {
let ctx = AppContext.testValue()
let mirror = Mirror(reflecting: ctx)
let children = mirror.children.map { $0.label ?? "" }
let privacyFields = ["idfa", "idfv", "advertising", "pushToken", "deviceId"]
for field in privacyFields {
let found = children.contains { $0.lowercased().contains(field.lowercased()) }
XCTAssertFalse(found, "AppContext should not contain \(field)")
}
}
}
// MARK: - Test Helper
extension AppContext {
static func testValue() -> Self {
AppContext(
platform: "ios",
appVersion: Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.0.0",
buildNumber: Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "0",
clientTimezoneOffsetMinutes: TimeZone.current.secondsFromGMT() / 60,
locale: Locale.current.identifier,
deviceType: "phone",
osVersion: UIDevice.current.systemVersion
)
}
}