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>
100 lines
3.7 KiB
Swift
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
|
|
)
|
|
}
|
|
}
|