247 lines
13 KiB
Bash
247 lines
13 KiB
Bash
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
root="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
mode="${1:-privacy}"
|
|
tmp="$(mktemp -d)"
|
|
trap 'rm -rf "$tmp"' EXIT
|
|
|
|
models=(
|
|
"$root/SodaLive/Sources/V2/Gift/Models/GiftModels.swift"
|
|
"$root/SodaLive/Sources/V2/Gift/Models/GiftDisplayModels.swift"
|
|
"$root/SodaLive/Sources/V2/Gift/Models/GiftDisplayPolicy.swift"
|
|
)
|
|
|
|
cat > "$tmp/DateParser.swift" <<'EOF'
|
|
import Foundation
|
|
enum DateParser { static func parse(_ text: String) -> Date? { nil } }
|
|
EOF
|
|
|
|
if [[ "$mode" == "privacy" ]]; then
|
|
swiftc "${models[@]}" "$tmp/DateParser.swift" "$root/work/tests/ReceivedGiftDetailCheck.swift" -o "$tmp/privacy"
|
|
"$tmp/privacy"
|
|
|
|
cat > "$tmp/Allowed.swift" <<'EOF'
|
|
@main
|
|
enum AllowedProbe { static func main() {
|
|
func detailProbe() -> GiftDetailResponse { fatalError() }
|
|
let model = GiftDisplayPolicy.receivedDetail(from: detailProbe())
|
|
_ = model?.status
|
|
_ = model?.statusName
|
|
_ = model?.undeliverableReason
|
|
_ = model?.senderNickname
|
|
_ = model?.sizeName
|
|
_ = model?.categoryName
|
|
_ = model?.shippingRequestedDateText
|
|
_ = model?.recipientAddressRequired
|
|
_ = model?.recipientAddress
|
|
_ = model?.progress
|
|
|
|
} }
|
|
EOF
|
|
swiftc "${models[@]}" "$tmp/DateParser.swift" "$tmp/Allowed.swift" -o "$tmp/allowed"
|
|
echo "PASS allowed-member probe"
|
|
|
|
for member in senderInfo mailbox applicationNo paidCan tracking; do
|
|
cat > "$tmp/Forbidden.swift" <<EOF
|
|
@main
|
|
enum ForbiddenProbe { static func main() {
|
|
func detailProbe() -> GiftDetailResponse { fatalError() }
|
|
let model = GiftDisplayPolicy.receivedDetail(from: detailProbe())
|
|
_ = model?.$member
|
|
} }
|
|
EOF
|
|
if swiftc "${models[@]}" "$tmp/DateParser.swift" "$tmp/Forbidden.swift" -o "$tmp/forbidden" 2> "$tmp/error"; then
|
|
echo "FAIL forbidden member probe: $member unexpectedly compiled" >&2
|
|
exit 1
|
|
fi
|
|
if ! grep -q "has no member" "$tmp/error"; then
|
|
echo "FAIL forbidden member probe: $member failed for another reason" >&2
|
|
cat "$tmp/error" >&2
|
|
exit 1
|
|
fi
|
|
echo "PASS forbidden member probe: $member has no member"
|
|
done
|
|
exit 0
|
|
fi
|
|
|
|
if [[ "$mode" == "foundation" ]]; then
|
|
component="$root/SodaLive/Sources/V2/Gift/Detail/Components/ReceivedGiftDetailContentView.swift"
|
|
if [[ -f "$component" ]]; then
|
|
if ! xcrun swiftc -parse "$component"; then
|
|
echo "FAIL foundation: component does not parse" >&2
|
|
exit 1
|
|
fi
|
|
if ! grep -Eq 'let[[:space:]]+model:[[:space:]]*ReceivedGiftDetailDisplayModel' "$component"; then
|
|
echo "FAIL foundation: component does not declare ReceivedGiftDetailDisplayModel model" >&2
|
|
exit 1
|
|
fi
|
|
else
|
|
echo "RED foundation: missing ReceivedGiftDetailContentView"
|
|
exit 1
|
|
fi
|
|
if ! grep -q 'case \.received(let model)' "$root/SodaLive/Sources/V2/Gift/Detail/GiftDetailView.swift" || ! grep -Fq 'ReceivedGiftDetailContentView(model: model)' "$root/SodaLive/Sources/V2/Gift/Detail/GiftDetailView.swift"; then
|
|
echo "FAIL foundation RED: received branch integration probe failed" >&2
|
|
exit 1
|
|
fi
|
|
echo "PASS foundation: component and received-branch integration"
|
|
exit 0
|
|
fi
|
|
|
|
if [[ "$mode" == "recipient-form" ]]; then
|
|
form="$root/SodaLive/Sources/V2/Gift/RecipientAddress/GiftRecipientAddressFormView.swift"
|
|
view_model="$root/SodaLive/Sources/V2/Gift/RecipientAddress/GiftRecipientAddressViewModel.swift"
|
|
if [[ ! -f "$form" ]]; then
|
|
echo "RED recipient-form: missing GiftRecipientAddressFormView.swift"
|
|
exit 1
|
|
fi
|
|
if [[ ! -f "$view_model" ]]; then
|
|
echo "RED recipient-form: missing GiftRecipientAddressViewModel.swift"
|
|
exit 1
|
|
fi
|
|
xcrun swiftc -parse "$form"
|
|
xcrun swiftc -parse "$view_model"
|
|
for input in recipientName recipientPhoneNumber recipientZipCode recipientAddress recipientAddressDetail; do
|
|
if ! grep -Eq "var[[:space:]]+$input[[:space:]]*:[[:space:]]*String" "$view_model"; then
|
|
echo "FAIL recipient-form: missing String input state $input" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
for agreement in recipientTermsAgreed recipientPrivacyAgreed; do
|
|
if ! grep -Eq "var[[:space:]]+$agreement[[:space:]]*:[[:space:]]*Bool" "$view_model"; then
|
|
echo "FAIL recipient-form: missing Bool agreement state $agreement" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
if ! grep -Eq 'let[[:space:]]+model:[[:space:]]*ReceivedGiftDetailDisplayModel' "$form"; then
|
|
echo "FAIL recipient-form: form does not declare ReceivedGiftDetailDisplayModel model" >&2
|
|
exit 1
|
|
fi
|
|
detail_view="$root/SodaLive/Sources/V2/Gift/Detail/GiftDetailView.swift"
|
|
if ! grep -q 'case \.received(let model)' "$detail_view" || \
|
|
! grep -Fq 'model.recipientAddressRequired' "$detail_view" || \
|
|
! grep -Fq 'GiftRecipientAddressFormView(model: model, viewModel: recipientAddressViewModel)' "$detail_view" || \
|
|
! grep -Fq 'ReceivedGiftDetailContentView(model: model)' "$detail_view"; then
|
|
echo "FAIL recipient-form: received branch form/fallback integration probe failed" >&2
|
|
exit 1
|
|
fi
|
|
for probe in \
|
|
'@ObservedObject var viewModel: GiftRecipientAddressViewModel' \
|
|
'viewModel.submit()' \
|
|
'isActive: viewModel.canSubmit' \
|
|
'.disabled(!viewModel.canSubmit)' \
|
|
'.disabled(viewModel.isSubmitting || viewModel.hasSubmitted)'; do
|
|
grep -Fq "$probe" "$form" || { echo "FAIL recipient-form: missing $probe" >&2; exit 1; }
|
|
done
|
|
for probe in \
|
|
'@StateObject private var recipientAddressViewModel: GiftRecipientAddressViewModel' \
|
|
'_viewModel = StateObject(wrappedValue: detailViewModel)' \
|
|
'GiftRecipientAddressViewModel(applicationNo: applicationNo, onSubmitted:' \
|
|
'detailViewModel.fetch()' \
|
|
'.sodaToast(isPresented: $recipientAddressViewModel.isShowPopup, message: recipientAddressViewModel.errorMessage)' \
|
|
'.sodaToast(isPresented: $recipientAddressViewModel.isShowSuccessToast, message: recipientAddressViewModel.successMessage)' \
|
|
'.onChange(of: recipientAddressViewModel.isShowSuccessToast)' \
|
|
'if !isPresented && isActive' \
|
|
'recipientAddressViewModel.successToastDidDismiss()' \
|
|
'isActive = true' \
|
|
'isActive = false' \
|
|
'if recipientAddressViewModel.hasSubmitted && !viewModel.isLoading && !viewModel.errorMessage.isEmpty' \
|
|
'Button(action: viewModel.fetch)' \
|
|
'recipientAddressViewModel.isSubmitting || recipientAddressViewModel.isShowSuccessToast' \
|
|
'(recipientAddressViewModel.hasSubmitted && viewModel.isLoading)'; do
|
|
grep -Fq "$probe" "$detail_view" || { echo "FAIL recipient-form: missing $probe" >&2; exit 1; }
|
|
done
|
|
if grep -Fq 'recipientAddressViewModel.submit()' "$detail_view"; then
|
|
echo "FAIL recipient-form: detail retry must not POST" >&2
|
|
exit 1
|
|
fi
|
|
echo "PASS recipient-form: injection, submission, toast lifecycle, GET-only retry and fallback"
|
|
exit 0
|
|
fi
|
|
|
|
if [[ "$mode" == "received-states" ]]; then
|
|
component="$root/SodaLive/Sources/V2/Gift/Detail/Components/ReceivedGiftDetailContentView.swift"
|
|
detail="$root/SodaLive/Sources/V2/Gift/Detail/GiftDetailView.swift"
|
|
xcrun swiftc -parse "$component" "$detail"
|
|
python3 - "$component" "$detail" <<'PY'
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
component, detail = (Path(path).read_text() for path in sys.argv[1:])
|
|
|
|
def require(condition, message):
|
|
if not condition:
|
|
sys.exit("FAIL received-states: " + message)
|
|
print("PASS received-states: " + message)
|
|
|
|
def block(source, marker):
|
|
require(marker in source, "structure " + marker)
|
|
start = source.index("{", source.index(marker))
|
|
depth = 1
|
|
for end in range(start + 1, len(source)):
|
|
depth += (source[end] == "{") - (source[end] == "}")
|
|
if depth == 0:
|
|
return source[start + 1:end], source[end + 1:]
|
|
sys.exit("FAIL received-states: unbalanced block " + marker)
|
|
|
|
content, _ = block(component, "struct ReceivedGiftDetailContentView: View")
|
|
title, _ = block(content, "private var heroTitle: String")
|
|
label, _ = block(content, "private var statusLabel: String?")
|
|
for state in ("trackingRegistered", "arrivedAtMailbox", "inspectionCompleted", "delivered", "undeliverable"):
|
|
require(re.search(r"case [^:]*\." + state + r"\b[^:]*:", title), "hero state " + state)
|
|
for text in ("배송지 입력 완료", "전달 완료", "전달 불가"):
|
|
require('"' + text + '"' in label, "label " + text)
|
|
for text in ("배송지 입력이 완료되었어요", "선물을 전달받았어요", "전달할 수 없는 품목입니다"):
|
|
require('"' + text + '"' in title, "title " + text)
|
|
for state in ("arrivedAtMailbox", "inspectionCompleted"):
|
|
require(re.search(r"case [^:]*\." + state + r"\b[^:]*:\s*return model.statusName", title), "backend copy " + state)
|
|
body, _ = block(content, "var body: some View")
|
|
stack, _ = block(body, "VStack")
|
|
require(stack.strip().startswith("hero"), "hero precedes exclusive content branch")
|
|
unavailable, remainder = block(stack, "if model.status == .undeliverable")
|
|
require(unavailable.strip() == "undeliverableReason", "undeliverable branch contains only reason/warning")
|
|
require(remainder.strip().startswith("else {"), "normal content is exclusive else")
|
|
normal, trailing = block(remainder, "else")
|
|
require(not trailing.strip(), "no content after exclusive branch")
|
|
require("GiftProgressView(progress: model.progress)" in normal and "giftInfo" in normal and "recipientAddress" in normal, "normal progress, gift and address")
|
|
reason, _ = block(content, "private var undeliverableReason: some View")
|
|
require("model.undeliverableReason?.trimmingCharacters" in reason and "!reason.isEmpty" in reason, "optional nonblank reason")
|
|
require("해당 선물은 보관기간(입고일 포함 최대 7일) 내에 반송 등 필요한 조치가 완료되지 않을 경우, 사전에 동의한 약관 및 폐기 등의 내용에 따라 폐기될 수 있습니다. 자세한 내용은 고객센터로 문의해 주세요." in reason, "disposal warning")
|
|
require(not re.search(r"GiftProgressView|giftInfo|recipientAddress|GiftRecipientAddressFormView", unavailable + reason), "no progress/gift/address/form in undeliverable content")
|
|
require("보내주신 선물은 소다라이브 선물 정책에 따라 크리에이터에게 전달할 수 없는 품목으로 확인되었습니다." in content, "existing policy explanation")
|
|
address, _ = block(content, "private var recipientAddress: some View")
|
|
optional, rest = block(address, "if let address = model.recipientAddress")
|
|
require(not rest.strip(), "nil address renders no section or placeholder")
|
|
for probe in ('GiftInfoSectionView(title: "받는 주소")', 'GiftInfoRow(title: "이름", value: address.name)', 'GiftInfoRow(title: "휴대폰 번호", value: address.phoneNumber)', 'GiftInfoRow(title: "주소", value: address.address)'):
|
|
require(probe in optional, "address row " + probe)
|
|
require(not re.search(r"model\.(senderInfo|mailbox|applicationNo|paidCan|tracking)\b", component), "privacy-safe component")
|
|
actions, _ = block(component, "struct ReceivedGiftDetailActionBar: View")
|
|
require("guard !isNavigating else { return }" in actions and "isNavigating = true" in actions and ".disabled(isNavigating)" in actions, "inquiry repeat guard")
|
|
require("AppState.shared.setAppStep(step: GiftConstants.inquiryStep)" in actions and '"문의하기"' in actions, "existing inquiry route")
|
|
appear, _ = block(actions, ".onAppear")
|
|
require("isNavigating = false" in appear, "navigation guard resets")
|
|
require("let onPolicy: () -> Void" in actions and "onPolicy()" in actions and '"선물 정책 확인"' in actions, "policy callback")
|
|
require(not re.search(r"https?://|openURL|URL\(", component), "no invented URL")
|
|
received = detail.split("case .received(let model):", 1)[1].split("case .canceled(let model):", 1)[0]
|
|
form, fallback = block(received, "if model.recipientAddressRequired")
|
|
require("GiftRecipientAddressFormView(model: model, viewModel: recipientAddressViewModel)" in form, "form remains required-flag controlled")
|
|
require(fallback.strip().startswith("else {"), "content remains form fallback")
|
|
inset, _ = block(fallback, ".safeAreaInset(edge: .bottom, spacing: 0)")
|
|
only_unavailable, trailing = block(inset, "if model.status == .undeliverable")
|
|
require(not trailing.strip() and inset.strip().startswith("if model.status == .undeliverable"), "bottom actions only for received undeliverable")
|
|
require("ReceivedGiftDetailActionBar(onPolicy: openGiftTerms)" in only_unavailable, "received policy action opens gift terms")
|
|
require(detail.count("ReceivedGiftDetailActionBar") == 1, "no sent/canceled action insertion")
|
|
sent = detail.split("case .sent(let model):", 1)[1].split("case .received(let model):", 1)[0]
|
|
require("onPolicy: openGiftTerms" in sent, "sent policy action opens gift terms")
|
|
require('@Environment(\\.openURL) private var openURL' in detail, "system URL opener")
|
|
open_terms, _ = block(detail, "private func openGiftTerms()")
|
|
require("GiftConstants.termsURL" in open_terms and "openURL(url)" in open_terms, "confirmed gift terms URL")
|
|
require("isShowingPolicyToast" not in detail and "GiftConstants.unavailableActionMessage" not in detail, "policy toast fallback removed")
|
|
PY
|
|
exit 0
|
|
fi
|
|
|
|
echo "usage: $0 [privacy|foundation|recipient-form|received-states]" >&2
|
|
exit 2
|