# 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 ===")