Files
sodalive-ios/work/tests/check-received-gift-detail.sh
T

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("ServiceCenterInquiry.openTalk()" in actions and '"문의하기"' in actions, "shared TALK inquiry action")
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