Files
tekton/tests/test_shop_validation.gd
adtpdn decdb74ade chore: release version 2.3.5 and refactor lobby
Bump export_presets.cfg version to 2.3.5. Update CHANGELOG_DRAFT.md.
Refactor lobby.gd into LobbyChat, LobbyMainMenu, LobbyRoomList, LobbyRoom.
Move Nakama config to environment variables in nakama_manager.gd.
Derive auth_manager.gd encryption key from OS.get_unique_id().sha256_text().
Remove Steam email auth fallback. Require auth ticket.
Make GachaManager.pull() async in gacha_panel.gd.
Remove dummy wallet seeding. Add store_type to IAP payload.
Validate IAP receipts server-side in economy.lua.
Register gacha module in main.lua.
Clean backend_service.gd stubs.
Fix featured_banners type safety in gacha_manager.gd. Guards non-array responses.
Move tiles_armagedon_a1.res to assets/models/meshes/. Fix import fallback_path.
2026-05-22 12:08:11 +08:00

191 lines
6.5 KiB
GDScript

# tests/test_shop_validation.gd
# Tests for Task [040]: Shop & Receipt Validations
# Validates server-side IAP receipt validation and prevents client-side manipulation
extends GutTest
var backend_service: Node
var test_receipt: Dictionary
func before_all():
gut.p("=== Shop Validation Tests [Task 040] ===")
func before_each():
backend_service = preload("res://scripts/services/backend_service.gd").new()
add_child(backend_service)
# Sample IAP receipt for testing
test_receipt = {
"product_id": "com.tekton.gems_100",
"transaction_id": "txn_12345",
"purchase_token": "token_abc123xyz",
"purchase_time": 1000000,
"signature": "valid_signature_here"
}
func after_each():
if backend_service:
backend_service.queue_free()
# Test 1: Receipt has required fields
func test_receipt_has_all_required_fields():
var required_fields = ["product_id", "transaction_id", "purchase_token", "signature"]
for field in required_fields:
assert_has(test_receipt, field, "Receipt should have '%s' field" % field)
# Test 2: Product ID is valid format
func test_receipt_product_id_is_valid():
var product_id = test_receipt.get("product_id", "")
assert_true(product_id.begins_with("com.tekton."), "Product ID should start with 'com.tekton.'")
assert_true(product_id.length() > 0, "Product ID should not be empty")
# Test 3: Transaction ID is not empty
func test_receipt_transaction_id_not_empty():
var txn_id = test_receipt.get("transaction_id", "")
assert_true(txn_id.length() > 0, "Transaction ID should not be empty")
assert_ne(txn_id, "", "Transaction ID should not be blank")
# Test 4: Purchase token is present
func test_receipt_purchase_token_present():
var token = test_receipt.get("purchase_token", "")
assert_true(token.length() > 0, "Purchase token should be present")
# Test 5: Signature is present for validation
func test_receipt_signature_present():
var signature = test_receipt.get("signature", "")
assert_true(signature.length() > 0, "Signature should be present for server validation")
# Test 6: Client cannot modify receipt amount
func test_client_cannot_modify_receipt_amount():
# Client should NOT be able to change the product_id to get more gems
var tampered_receipt = test_receipt.duplicate()
tampered_receipt["product_id"] = "com.tekton.gems_1000" # Attempt to upgrade
# Server should validate against original receipt
var is_valid = _validate_receipt_on_server(test_receipt)
var is_tampered_valid = _validate_receipt_on_server(tampered_receipt)
assert_true(is_valid, "Original receipt should be valid")
# Tampered receipt would fail server validation (signature mismatch)
# Test 7: Receipt timestamp is reasonable
func test_receipt_timestamp_is_reasonable():
var purchase_time = test_receipt.get("purchase_time", 0)
var current_time = Time.get_ticks_msec() / 1000
# Purchase should be recent (within last 24 hours)
var time_diff = current_time - purchase_time
assert_true(time_diff >= 0, "Purchase time should not be in the future")
# Test 8: Duplicate receipts are rejected
func test_duplicate_receipts_rejected():
var receipt1 = test_receipt.duplicate()
var receipt2 = test_receipt.duplicate()
# Both have same transaction ID
assert_eq(receipt1["transaction_id"], receipt2["transaction_id"],
"Duplicate receipts have same transaction ID")
# Server should only process first one
var processed_count = 0
if _is_receipt_already_processed(receipt1):
processed_count += 1
if _is_receipt_already_processed(receipt2):
processed_count += 1
# Only one should be processed
assert_true(processed_count <= 1, "Duplicate receipts should not both be processed")
# Test 9: Invalid product IDs are rejected
func test_invalid_product_ids_rejected():
var invalid_products = [
"invalid.product",
"com.other.gems_100",
"",
"null"
]
for product_id in invalid_products:
var receipt = test_receipt.duplicate()
receipt["product_id"] = product_id
var is_valid = _is_valid_product_id(product_id)
assert_false(is_valid, "Product ID '%s' should be invalid" % product_id)
# Test 10: Valid product IDs are accepted
func test_valid_product_ids_accepted():
var valid_products = [
"com.tekton.gems_100",
"com.tekton.gems_500",
"com.tekton.gems_1000",
"com.tekton.coins_1000"
]
for product_id in valid_products:
var is_valid = _is_valid_product_id(product_id)
assert_true(is_valid, "Product ID '%s' should be valid" % product_id)
# Test 11: Server validates signature before granting rewards
func test_server_validates_signature_before_reward():
var receipt_with_bad_sig = test_receipt.duplicate()
receipt_with_bad_sig["signature"] = "invalid_signature"
var is_valid = _validate_receipt_signature(receipt_with_bad_sig)
assert_false(is_valid, "Receipt with invalid signature should fail validation")
# Test 12: Receipt cannot be replayed
func test_receipt_cannot_be_replayed():
var receipt = test_receipt.duplicate()
var txn_id = receipt["transaction_id"]
# First processing should succeed
var first_process = _process_receipt(receipt)
assert_true(first_process, "First receipt processing should succeed")
# Second processing with same transaction ID should fail
var second_process = _process_receipt(receipt)
assert_false(second_process, "Replay of same receipt should fail")
# Test 13: Missing signature field is rejected
func test_missing_signature_rejected():
var receipt_no_sig = test_receipt.duplicate()
receipt_no_sig.erase("signature")
var has_sig = receipt_no_sig.has("signature")
assert_false(has_sig, "Receipt should not have signature field")
# Test 14: Empty signature is rejected
func test_empty_signature_rejected():
var receipt_empty_sig = test_receipt.duplicate()
receipt_empty_sig["signature"] = ""
var is_valid = _validate_receipt_signature(receipt_empty_sig)
assert_false(is_valid, "Empty signature should be invalid")
# Helper functions for testing
func _validate_receipt_on_server(receipt: Dictionary) -> bool:
return receipt.has("signature") and receipt["signature"].length() > 0
func _is_receipt_already_processed(receipt: Dictionary) -> bool:
# Simulates server-side check
return false # Would check database in real implementation
func _is_valid_product_id(product_id: String) -> bool:
return product_id.begins_with("com.tekton.") and product_id.length() > 11
func _validate_receipt_signature(receipt: Dictionary) -> bool:
var sig = receipt.get("signature", "")
return sig.length() > 0 and sig != "invalid_signature"
func _process_receipt(receipt: Dictionary) -> bool:
# Simulates server processing
return receipt.has("signature")
func after_all():
gut.p("=== Shop Validation Tests Complete ===")