feat(auth): 인증 후 콘텐츠 설정과 개발용 모의 인증을 추가한다
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import Foundation
|
||||
import CoreFoundation
|
||||
|
||||
@main
|
||||
struct AuthContentSettingsCheck {
|
||||
static func main() throws {
|
||||
let receiptId = "synthetic-auth-receipt"
|
||||
let cases: [(Bool, ContentType)] = [
|
||||
(true, .ALL),
|
||||
(true, .MALE),
|
||||
(true, .FEMALE),
|
||||
(false, .ALL)
|
||||
]
|
||||
|
||||
for (isAdultContentVisible, contentType) in cases {
|
||||
let request = AuthVerifyRequest(
|
||||
receiptId: receiptId,
|
||||
isAdultContentVisible: isAdultContentVisible,
|
||||
contentType: contentType
|
||||
)
|
||||
let encoded = try JSONEncoder().encode(request)
|
||||
guard let json = try JSONSerialization.jsonObject(with: encoded) as? [String: Any],
|
||||
let encodedReceipt = json["receipt_id"] as? String,
|
||||
let encodedVisibility = json["isAdultContentVisible"] as? NSNumber,
|
||||
let encodedContentType = json["contentType"] as? String else {
|
||||
assertionFailure("Expected receipt string, visibility bool and content type string")
|
||||
return
|
||||
}
|
||||
|
||||
assert(Set(json.keys) == Set(["receipt_id", "isAdultContentVisible", "contentType"]))
|
||||
assert(encodedReceipt == receiptId)
|
||||
assert(CFGetTypeID(encodedVisibility) == CFBooleanGetTypeID())
|
||||
assert(encodedVisibility.boolValue == isAdultContentVisible)
|
||||
assert(encodedContentType == contentType.rawValue)
|
||||
}
|
||||
|
||||
print("PASS: AuthVerifyRequest exact JSON keys/types and ON ALL/MALE/FEMALE, OFF ALL (4 cases)")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
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<GetMemberInfoResponse>.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<GetMemberInfoResponse>.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<AuthResponse>.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<GetMemberInfoResponse>.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<MyPageResponse>.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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
TEMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TEMP_DIR"' EXIT
|
||||
|
||||
swiftc -swift-version 5 -D AUTH_VERIFICATION_MOCK -module-cache-path "$TEMP_DIR/module-cache" \
|
||||
"$ROOT/SodaLive/Sources/V2/Auth/AuthVerificationMock.swift" \
|
||||
"$ROOT/SodaLive/Sources/Extensions/UserDefaultsExtension.swift" \
|
||||
"$ROOT/SodaLive/Sources/Common/ApiResponse.swift" \
|
||||
"$ROOT/SodaLive/Sources/MyPage/Auth/AuthVerifyRequest.swift" \
|
||||
"$ROOT/SodaLive/Sources/MyPage/Auth/AuthResponse.swift" \
|
||||
"$ROOT/SodaLive/Sources/MyPage/MyPageResponse.swift" \
|
||||
"$ROOT/SodaLive/Sources/Settings/Content/ContentType.swift" \
|
||||
"$ROOT/SodaLive/Sources/Settings/Content/UpdateContentPreferenceResponse.swift" \
|
||||
"$ROOT/SodaLive/Sources/Settings/Notification/GetMemberInfoResponse.swift" \
|
||||
"$ROOT/work/tests/AuthVerificationMockCheck.swift" \
|
||||
-o "$TEMP_DIR/check-auth-verification-mock"
|
||||
"$TEMP_DIR/check-auth-verification-mock"
|
||||
|
||||
for configuration in Debug Release; do
|
||||
FLAGS=()
|
||||
if [[ "$configuration" == Debug ]]; then FLAGS=(-D DEBUG); fi
|
||||
swiftc -swift-version 5 -module-cache-path "$TEMP_DIR/module-cache" "${FLAGS[@]}" \
|
||||
-emit-library -module-name "AuthMockStandard$configuration" \
|
||||
"$ROOT/SodaLive/Sources/V2/Auth/AuthVerificationMock.swift" \
|
||||
"$ROOT/SodaLive/Sources/Extensions/UserDefaultsExtension.swift" \
|
||||
-o "$TEMP_DIR/standard-$configuration.dylib"
|
||||
nm -j "$TEMP_DIR/standard-$configuration.dylib" > "$TEMP_DIR/symbols-$configuration"
|
||||
if rg -q 'AuthVerificationMock' "$TEMP_DIR/symbols-$configuration"; then
|
||||
printf 'FAIL: mock symbols in standard %s\n' "$configuration" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'PASS: standard %s contains no mock symbols\n' "$configuration"
|
||||
done
|
||||
Reference in New Issue
Block a user