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 ) } }