decdb74ade
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.
191 lines
6.5 KiB
GDScript
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 ===")
|