import Foundation import CoreFoundation @main struct AuthVerificationMockCheck { static let mock = AuthVerificationMock.shared static let token = "synthetic-session-a" static let otherToken = "synthetic-session-b" static let member = Data(#"{"success":true,"message":null,"extra":{"trace":17},"data":{"can":137,"point":29,"isAuth":false,"gender":"FEMALE","signupDate":"synthetic-date","chargeCount":3,"role":"CREATOR","messageNotice":true,"followingChannelLiveNotice":false,"followingChannelUploadContentNotice":true,"auditionNotice":false,"countryCode":" kr ","isAdultContentVisible":false,"contentType":"MALE","extra":{"nested":[1,true,null,"keep"]}}}"#.utf8) static let mypage = Data(#"{"success":true,"data":{"nickname":"synthetic-name","profileUrl":"synthetic-profile","chargeCan":71,"rewardCan":42,"point":19,"youtubeUrl":null,"instagramUrl":"keep","liveReservationCount":4,"isAuth":false,"extra":{"untouched":true}}}"#.utf8) static func request(visible: Bool = true, type: ContentType = .FEMALE) -> AuthVerifyRequest { AuthVerifyRequest(receiptId: "synthetic-check-receipt", isAdultContentVisible: visible, contentType: type) } static func object(_ data: Data) -> NSDictionary { try! JSONSerialization.jsonObject(with: data) as! NSDictionary } static func overlay(_ data: Data = member, status: Int = 200, requestToken: String = token) -> Data { mock.overlayMemberResponse(data, statusCode: status, requestToken: requestToken, session: mock.session(for: requestToken), as: GetMemberInfoResponse.self) } static func begin() { UserDefaults.reset() UserDefaults.set(token, forKey: .token) } static func confirm(visible: Bool = true, type: ContentType = .FEMALE) { assert(mock.confirm(request: request(visible: visible, type: type), requestToken: token) != nil) } static func checkLatePreConfirmationResponse() { begin() let beforeConfirmation = mock.session(for: token) assert(mock.overlayMemberResponse(member, statusCode: 200, requestToken: token, session: beforeConfirmation, as: GetMemberInfoResponse.self) == member) confirm() let delayed = mock.overlayMemberResponse(member, statusCode: 200, requestToken: token, session: beforeConfirmation, as: GetMemberInfoResponse.self) let decoded = try! JSONDecoder().decode(ApiResponse.self, from: delayed).data! assert(decoded.isAuth && decoded.isAdultContentVisible && decoded.contentType == "FEMALE", "Late pre-confirmation GET must use true/FEMALE, got \(decoded.isAuth)/\(decoded.contentType)") assert(decoded.can == 137 && decoded.point == 29 && decoded.role == .CREATOR) confirm(visible: false, type: .MALE) let updated = mock.overlayMemberResponse(member, statusCode: 200, requestToken: token, session: beforeConfirmation, as: GetMemberInfoResponse.self) let latest = try! JSONDecoder().decode(ApiResponse.self, from: updated).data! assert(latest.isAuth && !latest.isAdultContentVisible && latest.contentType == "ALL", "Confirmation must not replace the login epoch; delayed GET uses latest confirmed choice") print("PASS: late pre-confirmation GET uses current confirmed choice within the same login session") } static func checkConfirmationAndThreeFieldOverlay() { begin() UserDefaults.set(false, forKey: .auth) UserDefaults.set(false, forKey: .isAdultContentVisible) UserDefaults.set("MALE", forKey: .contentPreference) assert(overlay() == member, "No override before confirmation") let response = mock.confirm(request: request(), requestToken: token)! let auth = try! JSONDecoder().decode(ApiResponse.self, from: response) assert(auth.success && auth.data?.gender == 0) assert(!UserDefaults.bool(forKey: .auth)) assert(!UserDefaults.bool(forKey: .isAdultContentVisible)) assert(UserDefaults.string(forKey: .contentPreference) == "MALE") let result = overlay() let envelope = object(result) let data = envelope["data"] as! NSDictionary assert(data["isAuth"] as! Bool && data["isAdultContentVisible"] as! Bool) assert(data["contentType"] as! String == "FEMALE") assert(CFGetTypeID(data["isAuth"] as! NSNumber) == CFBooleanGetTypeID()) assert(CFGetTypeID(data["isAdultContentVisible"] as! NSNumber) == CFBooleanGetTypeID()) let unchanged = data.mutableCopy() as! NSMutableDictionary unchanged["isAuth"] = false unchanged["isAdultContentVisible"] = false unchanged["contentType"] = "MALE" let restored = envelope.mutableCopy() as! NSMutableDictionary restored["data"] = unchanged assert(restored == object(member), "Only three fields may change, including unknown fields") let decoded = try! JSONDecoder().decode(ApiResponse.self, from: result).data! assert(decoded.can == 137 && decoded.point == 29 && decoded.role == .CREATOR) assert(decoded.gender == "FEMALE" && decoded.countryCode == "KR" && decoded.auditionNotice == false) let mypageResult = mock.overlayMemberResponse(mypage, statusCode: 200, requestToken: token, session: mock.session(for: token), as: MyPageResponse.self) let page = try! JSONDecoder().decode(ApiResponse.self, from: mypageResult).data! assert(page.isAuth && page.chargeCan == 71 && page.rewardCan == 42 && page.point == 19) let originalPage = (object(mypage)["data"] as! NSDictionary).mutableCopy() as! NSMutableDictionary originalPage["isAuth"] = true originalPage["isAdultContentVisible"] = true originalPage["contentType"] = "FEMALE" assert(originalPage == object(mypageResult)["data"] as! NSDictionary) for (visible, type, expected) in [(true, ContentType.ALL, "ALL"), (true, .MALE, "MALE"), (true, .FEMALE, "FEMALE"), (false, .FEMALE, "ALL")] { confirm(visible: visible, type: type) let data = object(overlay())["data"] as! NSDictionary assert(data["isAdultContentVisible"] as! Bool == visible) assert(data["contentType"] as! String == expected) } print("PASS: confirmation-only state, existing auth DTO, three-field self-member/mypage overlay, enum and OFF normalization") } static func checkInvalidResponses() { begin() confirm() let invalid = ["not-json", "[]", #"{"success":false,"data":null,"message":"keep-error"}"#, #"{"success":true,"data":null}"#, #"{"success":true,"data":{}}"#, #"{"success":"true","data":{}}"#] for json in invalid { let data = Data(json.utf8) assert(overlay(data) == data, "Invalid envelope must stay byte-identical") assert(mock.overlayMemberResponse(data, statusCode: 200, requestToken: token, session: mock.session(for: token), as: MyPageResponse.self) == data) } let unsuccessful = object(member).mutableCopy() as! NSMutableDictionary unsuccessful["success"] = false let unsuccessfulData = try! JSONSerialization.data(withJSONObject: unsuccessful) assert(overlay(unsuccessfulData) == unsuccessfulData) for status in [199, 300, 401, 403, 500] { assert(overlay(status: status) == member, "HTTP failure must not be repaired") } for (key, value) in [("isAuth", "false" as Any), ("can", "137" as Any), ("role", "UNKNOWN" as Any), ("isAdultContentVisible", "false" as Any), ("contentType", 7 as Any)] { let envelope = object(member).mutableCopy() as! NSMutableDictionary let data = (envelope["data"] as! NSDictionary).mutableCopy() as! NSMutableDictionary data[key] = value envelope["data"] = data let malformed = try! JSONSerialization.data(withJSONObject: envelope) assert(overlay(malformed) == malformed, "DTO type failures must not be repaired") } print("PASS: malformed, non-success, null/missing data, DTO type and non-2xx failures are byte-identical") } static func checkSessionLifetimeAndIsolation() { UserDefaults.reset() for blank in ["", " \n "] { UserDefaults.set(blank, forKey: .token) assert(mock.confirm(request: request(), requestToken: blank) == nil) assert(overlay(requestToken: blank) == member) } begin() assert(mock.confirm(request: request(), requestToken: otherToken) == nil) confirm() let oldSession = mock.session(for: token) assert(overlay(requestToken: otherToken) == member) assert(mock.overlayMemberResponse(member, statusCode: 200, requestToken: token, session: nil, as: GetMemberInfoResponse.self) == member) UserDefaults.set("synthetic-other-name", forKey: .nickname) assert((object(overlay())["data"] as! NSDictionary)["isAuth"] as! Bool) assert(AuthVerificationMock.shared === mock, "Process-wide state survives owner reconstruction/Splash") UserDefaults.reset() assert(overlay() == member) UserDefaults.set(token, forKey: .token) assert(overlay() == member, "Reset must not resurrect old state") confirm() UserDefaults.set(token, forKey: .token) assert(overlay() == member, "Same-token relogin must reset state") confirm() assert(mock.overlayMemberResponse(member, statusCode: 200, requestToken: token, session: oldSession, as: GetMemberInfoResponse.self) == member) let activeSession = mock.session(for: token) UserDefaults.set(otherToken, forKey: .token) assert(overlay(requestToken: otherToken) == member) assert(mock.confirm(request: request(type: .MALE), requestToken: otherToken) != nil) assert(mock.overlayMemberResponse(member, statusCode: 200, requestToken: token, session: activeSession, as: GetMemberInfoResponse.self) == member) assert(mock.confirm(request: request(), requestToken: token) == nil) assert((object(overlay(requestToken: otherToken))["data"] as! NSDictionary)["contentType"] as! String == "MALE") print("PASS: blank/mismatch/stale tokens, reset/replacement/same-token relogin and in-flight account isolation") } static func checkLaterPreferences() { begin() let update = Data(#"{"success":true,"data":{"isAdultContentVisible":true,"contentType":" male "}}"#.utf8) mock.updateContentPreference(from: update, statusCode: 200, requestToken: token, session: nil) mock.updateContentPreference(from: update, statusCode: 200, requestToken: token, session: mock.session(for: token)) assert(overlay() == member, "Real preference success alone must not activate mock auth") confirm() let session = mock.session(for: token) mock.updateContentPreference(from: update, statusCode: 200, requestToken: token, session: session) assert((object(overlay())["data"] as! NSDictionary)["contentType"] as! String == "MALE") let failures = [("bad-json", 200), (#"{"success":false,"data":{"isAdultContentVisible":false,"contentType":"ALL"}}"#, 200), (#"{"success":true,"data":null}"#, 200), (#"{"success":true,"data":{"isAdultContentVisible":"false","contentType":"ALL"}}"#, 200), (#"{"success":true,"data":{"isAdultContentVisible":false,"contentType":"ALL"}}"#, 401)] for (json, status) in failures { mock.updateContentPreference(from: Data(json.utf8), statusCode: status, requestToken: token, session: session) assert((object(overlay())["data"] as! NSDictionary)["contentType"] as! String == "MALE") } let off = Data(#"{"success":true,"data":{"isAdultContentVisible":false,"contentType":"FEMALE"}}"#.utf8) mock.updateContentPreference(from: off, statusCode: 200, requestToken: token, session: session) let data = object(overlay())["data"] as! NSDictionary assert(!(data["isAdultContentVisible"] as! Bool) && data["contentType"] as! String == "ALL") UserDefaults.set(token, forKey: .token) confirm() mock.updateContentPreference(from: update, statusCode: 200, requestToken: token, session: session) assert((object(overlay())["data"] as! NSDictionary)["contentType"] as! String == "FEMALE") print("PASS: real later preference success updates active snapshot; failure/inactive/stale requests do not") } static func checkConcurrentCallbacks() { begin() confirm() DispatchQueue.concurrentPerform(iterations: 100) { _ in let session = mock.session(for: token) let data = mock.overlayMemberResponse(member, statusCode: 200, requestToken: token, session: session, as: GetMemberInfoResponse.self) assert((object(data)["data"] as! NSDictionary)["isAuth"] as! Bool) let update = Data(#"{"success":true,"data":{"isAdultContentVisible":true,"contentType":"FEMALE"}}"#.utf8) mock.updateContentPreference(from: update, statusCode: 200, requestToken: token, session: session) } print("PASS: concurrent member/preference callbacks") } static func main() { defer { UserDefaults.reset() } checkLatePreConfirmationResponse() checkConfirmationAndThreeFieldOverlay() checkInvalidResponses() checkSessionLifetimeAndIsolation() checkLaterPreferences() checkConcurrentCallbacks() print("PASS: actual Foundation-only AuthVerificationMock/DTO/UserDefaults sources") } }